v2 / examples / tetris / tetris.js.v
489 lines · 450 sloc · 10.95 KB · 8e35f4d9848f7ad35d857a187dddbfd2eca5e19d
Raw
1// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module main
5
6import rand
7import time
8import gg
9// import sokol.sapp
10
11const block_size = 20 // virtual pixels
12
13const field_height = 20 // # of blocks
14
15const field_width = 10
16const tetro_size = 4
17const win_width = block_size * field_width
18const win_height = block_size * field_height
19const text_size = 24
20
21const text_cfg = gg.TextCfg{
22 align: .left
23 size: text_size
24 color: gg.rgb(0, 0, 0)
25}
26const over_cfg = gg.TextCfg{
27 align: .left
28 size: text_size
29 color: gg.white
30}
31
32// Tetros' 4 possible states are encoded in binaries
33// 0000 0 0000 0 0000 0 0000 0 0000 0 0000 0
34// 0000 0 0000 0 0000 0 0000 0 0011 3 0011 3
35// 0110 6 0010 2 0011 3 0110 6 0001 1 0010 2
36// 0110 6 0111 7 0110 6 0011 3 0001 1 0010 2
37// There is a special case 1111, since 15 can't be used.
38const b_tetros = [
39 [66, 66, 66, 66],
40 [27, 131, 72, 232],
41 [36, 231, 36, 231],
42 [63, 132, 63, 132],
43 [311, 17, 223, 74],
44 [322, 71, 113, 47],
45 [1111, 9, 1111, 9],
46]
47// Each tetro has its unique color
48const colors = [
49 gg.rgb(0, 0, 0), // unused ?
50 gg.rgb(255, 242, 0), // yellow quad
51 gg.rgb(174, 0, 255), // purple triple
52 gg.rgb(60, 255, 0), // green short topright
53 gg.rgb(255, 0, 0), // red short topleft
54 gg.rgb(255, 180, 31), // orange long topleft
55 gg.rgb(33, 66, 255), // blue long topright
56 gg.rgb(74, 198, 255), // lightblue longest
57 gg.rgb(0, 170, 170),
58]
59const ui_color = gg.rgba(255, 0, 0, 210)
60
61// TODO: type Tetro [tetro_size]struct{ x, y int }
62struct Block {
63mut:
64 x int
65 y int
66}
67
68enum GameState {
69 paused
70 running
71 gameover
72}
73
74struct Game {
75mut:
76 // Score of the current game
77 score int
78 // Lines of the current game
79 lines int
80 // State of the current game
81 state GameState
82 // Block size in screen dimensions
83 block_size int = block_size
84 // Field margin
85 margin int
86 // Position of the current tetro
87 pos_x int
88 pos_y int
89 // field[y][x] contains the color of the block with (x,y) coordinates
90 // "-1" border is to avoid bounds checking.
91 // -1 -1 -1 -1
92 // -1 0 0 -1
93 // -1 0 0 -1
94 // -1 -1 -1 -1
95 field [][]int
96 // TODO: tetro Tetro
97 tetro []Block
98 // TODO: tetros_cache []Tetro
99 tetros_cache []Block
100 // Index of the current tetro. Refers to its color.
101 tetro_idx int
102 // Idem for the next tetro
103 next_tetro_idx int
104 // Index of the rotation (0-3)
105 rotation_idx int
106 // gg context for drawing
107 gg &gg.Context = unsafe { nil }
108 font_loaded bool
109 show_ghost bool = true
110 // frame/time counters:
111 frame int
112 frame_old int
113 frame_sw time.StopWatch = time.new_stopwatch()
114 second_sw time.StopWatch = time.new_stopwatch()
115}
116
117fn remap(v f32, min f32, max f32, new_min f32, new_max f32) f32 {
118 return (((v - min) * (new_max - new_min)) / (max - min)) + new_min
119}
120
121@[if showfps ?]
122fn (mut game Game) showfps() {
123 game.frame++
124 last_frame_ms := f64(game.frame_sw.elapsed().microseconds()) / 1000.0
125 ticks := f64(game.second_sw.elapsed().microseconds()) / 1000.0
126 if ticks > 999.0 {
127 fps := f64(game.frame - game.frame_old) * ticks / 1000.0
128 $if debug {
129 eprintln('fps: ${fps:5.1f} | last frame took: ${last_frame_ms:6.3f}ms | frame: ${game.frame:6} ')
130 }
131 game.second_sw.restart()
132 game.frame_old = game.frame
133 }
134}
135
136fn frame(mut game Game) {
137 if game.gg.frame & 15 == 0 {
138 game.update_game_state()
139 }
140 ws := gg.window_size()
141 bs := remap(block_size, 0, win_height, 0, ws.height)
142 m := (f32(ws.width) - bs * field_width) * 0.5
143 game.block_size = int(bs)
144 game.margin = int(m)
145 game.frame_sw.restart()
146 game.gg.begin()
147 game.draw_scene()
148 game.showfps()
149 game.gg.end()
150}
151
152fn main() {
153 mut game := &Game{
154 gg: &gg.Context{}
155 }
156
157 game.gg = gg.new_context(
158 bg_color: gg.white
159 width: win_width
160 height: win_height
161 create_window: true
162 window_title: 'V Tetris' //
163 user_data: game
164 frame_fn: frame
165 event_fn: on_event
166 html5_canvas_name: 'canvas'
167 )
168 game.init_game()
169 game.gg.run() // Run the render loop in the main thread
170}
171
172fn (mut g Game) init_game() {
173 g.parse_tetros()
174 g.next_tetro_idx = rand.intn(b_tetros.len) or { 0 } // generate initial "next"
175 g.generate_tetro()
176 g.field = []
177 // Generate the field, fill it with 0's, add -1's on each edge
178 for _ in 0 .. field_height + 2 {
179 mut row := [0].repeat(field_width + 2)
180 row[0] = -1
181 row[field_width + 1] = -1
182 g.field << row
183 }
184 for j in 0 .. field_width + 2 {
185 g.field[0][j] = -1
186 g.field[field_height + 1][j] = -1
187 }
188 g.score = 0
189 g.lines = 0
190 g.state = .running
191}
192
193fn (mut g Game) parse_tetros() {
194 for b_tetros0 in b_tetros {
195 for b_tetro in b_tetros0 {
196 for t in parse_binary_tetro(b_tetro) {
197 g.tetros_cache << t
198 }
199 }
200 }
201}
202
203fn (mut g Game) update_game_state() {
204 if g.state == .running {
205 g.move_tetro()
206 g.delete_completed_lines()
207 }
208}
209
210fn (mut g Game) draw_ghost() {
211 if g.state != .gameover && g.show_ghost {
212 pos_y := g.move_ghost()
213 for i in 0 .. tetro_size {
214 tetro := g.tetro[i]
215 g.draw_block_color(pos_y + tetro.y, g.pos_x + tetro.x, gg.rgba(125, 125, 225, 40))
216 }
217 }
218}
219
220fn (g Game) move_ghost() int {
221 mut pos_y := g.pos_y
222 mut end := false
223 for !end {
224 for block in g.tetro {
225 y := block.y + pos_y + 1
226 x := block.x + g.pos_x
227 if g.field[y][x] != 0 {
228 end = true
229 break
230 }
231 }
232 pos_y++
233 }
234 return pos_y - 1
235}
236
237fn (mut g Game) move_tetro() bool {
238 // Check each block in current tetro
239 for block in g.tetro {
240 y := block.y + g.pos_y + 1
241 x := block.x + g.pos_x
242 // Reached the bottom of the screen or another block?
243 if g.field[y][x] != 0 {
244 // The new tetro has no space to drop => end of the game
245 if g.pos_y < 2 {
246 g.state = .gameover
247 return false
248 }
249 // Drop it and generate a new one
250 g.drop_tetro()
251 g.generate_tetro()
252 return false
253 }
254 }
255 g.pos_y++
256 return true
257}
258
259fn (mut g Game) move_right(dx int) bool {
260 // Reached left/right edge or another tetro?
261 for i in 0 .. tetro_size {
262 tetro := g.tetro[i]
263 y := tetro.y + g.pos_y
264 x := tetro.x + g.pos_x + dx
265 if g.field[y][x] != 0 {
266 // Do not move
267 return false
268 }
269 }
270 g.pos_x += dx
271 return true
272}
273
274fn (mut g Game) delete_completed_lines() {
275 for y := field_height; y >= 1; y-- {
276 g.delete_completed_line(y)
277 }
278}
279
280fn (mut g Game) delete_completed_line(y int) {
281 for x := 1; x <= field_width; x++ {
282 if g.field[y][x] == 0 {
283 return
284 }
285 }
286 g.score += 10
287 g.lines++
288 // Move everything down by 1 position
289 for yy := y - 1; yy >= 1; yy-- {
290 for x := 1; x <= field_width; x++ {
291 g.field[yy + 1][x] = g.field[yy][x]
292 }
293 }
294}
295
296// Place a new tetro on top
297fn (mut g Game) generate_tetro() {
298 g.pos_y = 0
299 g.pos_x = field_width / 2 - tetro_size / 2
300 g.tetro_idx = g.next_tetro_idx
301 g.next_tetro_idx = rand.intn(b_tetros.len) or { 0 }
302 g.rotation_idx = 0
303 g.get_tetro()
304}
305
306// Get the right tetro from cache
307fn (mut g Game) get_tetro() {
308 idx := g.tetro_idx * tetro_size * tetro_size + g.rotation_idx * tetro_size
309 mut tetros := []Block{}
310 for tetro in g.tetros_cache[idx..idx + tetro_size] {
311 tetros << Block{tetro.x, tetro.y}
312 }
313 g.tetro = tetros
314 // g.tetro = g.tetros_cache[idx..idx + tetro_size].clone()
315}
316
317// TODO: mut
318fn (mut g Game) drop_tetro() {
319 for i in 0 .. tetro_size {
320 tetro := g.tetro[i]
321 x := tetro.x + g.pos_x
322 y := tetro.y + g.pos_y
323 // Remember the color of each block
324 g.field[y][x] = g.tetro_idx + 1
325 }
326}
327
328fn (mut g Game) draw_tetro() {
329 for i in 0 .. tetro_size {
330 tetro := g.tetro[i]
331 g.draw_block(g.pos_y + tetro.y, g.pos_x + tetro.x, g.tetro_idx + 1)
332 }
333}
334
335fn (mut g Game) draw_next_tetro() {
336 if g.state != .gameover {
337 idx := g.next_tetro_idx * tetro_size * tetro_size
338 next_tetro := g.tetros_cache[idx..idx + tetro_size]
339 pos_y := 0
340 pos_x := field_width / 2 - tetro_size / 2
341 for i in 0 .. tetro_size {
342 block := next_tetro[i]
343 g.draw_block_color(pos_y + block.y, pos_x + block.x, gg.rgb(220, 220, 220))
344 }
345 }
346}
347
348fn (mut g Game) draw_block_color(i int, j int, color gg.Color) {
349 g.gg.draw_rect(f32((j - 1) * g.block_size) + g.margin, f32((i - 1) * g.block_size),
350 f32(g.block_size - 1), f32(g.block_size - 1), color)
351}
352
353fn (mut g Game) draw_block(i int, j int, color_idx int) {
354 color := if g.state == .gameover { gg.gray } else { colors[color_idx] }
355 g.draw_block_color(i, j, color)
356}
357
358fn (mut g Game) draw_field() {
359 for i := 1; i < field_height + 1; i++ {
360 for j := 1; j < field_width + 1; j++ {
361 if g.field[i][j] > 0 {
362 g.draw_block(i, j, g.field[i][j])
363 }
364 }
365 }
366}
367
368fn (mut g Game) draw_ui() {
369 ws := gg.window_size()
370 textsize := int(remap(text_size, 0, win_width, 0, ws.width))
371 g.gg.draw_text(1, 10, g.score.str(), text_cfg)
372 lines := g.lines.str()
373 g.gg.draw_text(ws.width - lines.len * textsize, 10, lines, text_cfg)
374 if g.state == .gameover {
375 g.gg.draw_rect(0, ws.height / 2 - textsize, ws.width, 5 * textsize, ui_color)
376 g.gg.draw_text(1, ws.height / 2 + 0 * textsize, 'Game Over', over_cfg)
377 g.gg.draw_text(1, ws.height / 2 + 2 * textsize, 'Space to restart', over_cfg)
378 } else if g.state == .paused {
379 g.gg.draw_rect(0, ws.height / 2 - textsize, ws.width, 5 * textsize, ui_color)
380 g.gg.draw_text(1, ws.height / 2 + 0 * textsize, 'Game Paused', text_cfg)
381 g.gg.draw_text(1, ws.height / 2 + 2 * textsize, 'SPACE to resume', text_cfg)
382 }
383 // g.gg.draw_rect(0, block_size, win_width, limit_thickness, ui_color)
384}
385
386fn (mut g Game) draw_scene() {
387 g.draw_ghost()
388 g.draw_next_tetro()
389 g.draw_tetro()
390 g.draw_field()
391 g.draw_ui()
392}
393
394fn parse_binary_tetro(t_ int) []Block {
395 mut t := t_
396 mut res := [Block{}, Block{}, Block{}, Block{}]
397 mut cnt := 0
398 horizontal := t == 9 // special case for the horizontal line
399 ten_powers := [1000, 100, 10, 1]
400 for i := 0; i <= 3; i++ {
401 // Get ith digit of t
402 p := ten_powers[i]
403 mut digit := t / p
404 t %= p
405
406 // Convert the digit to binary
407 for j := 3; j >= 0; j-- {
408 bin := digit % 2
409 digit /= 2
410 if bin == 1 || (horizontal && i == tetro_size - 1) {
411 res[cnt].x = j
412 res[cnt].y = i
413 cnt++
414 }
415 }
416 }
417 return res
418}
419
420fn on_event(e &gg.Event, mut game Game) {
421 // println('code=${e.char_code}')
422 if e.typ == .key_down {
423 game.key_down(e.key_code)
424 }
425}
426
427fn (mut game Game) rotate_tetro() {
428 old_rotation_idx := game.rotation_idx
429 game.rotation_idx++
430 if game.rotation_idx == tetro_size {
431 game.rotation_idx = 0
432 }
433 game.get_tetro()
434 if !game.move_right(0) {
435 game.rotation_idx = old_rotation_idx
436 game.get_tetro()
437 }
438 if game.pos_x < 0 {
439 // game.pos_x = 1
440 }
441}
442
443fn (mut game Game) key_down(key gg.KeyCode) {
444 // global keys
445 match key {
446 .escape {
447 game.gg.quit()
448 }
449 .space {
450 if game.state == .running {
451 game.state = .paused
452 } else if game.state == .paused {
453 game.state = .running
454 } else if game.state == .gameover {
455 game.init_game()
456 game.state = .running
457 }
458 }
459 else {}
460 }
461
462 if game.state != .running {
463 return
464 }
465 // keys while game is running
466 match key {
467 .up {
468 // Rotate the tetro
469 game.rotate_tetro()
470 }
471 .left {
472 game.move_right(-1)
473 }
474 .right {
475 game.move_right(1)
476 }
477 .down {
478 game.move_tetro() // drop faster when the player presses <down>
479 }
480 .d {
481 for game.move_tetro() {
482 }
483 }
484 .g {
485 game.show_ghost = !game.show_ghost
486 }
487 else {}
488 }
489}
490