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