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