v2 / examples / tetris / tetris.v
503 lines · 465 sloc · 11.31 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 os.asset
7import rand
8import time
9import gg
10// import sokol.sapp
11
12const block_size = 20 // virtual pixels
13
14const field_height = 20 // # of blocks
15
16const field_width = 10
17const tetro_size = 4
18const win_width = block_size * field_width
19const win_height = block_size * field_height
20const text_size = 24
21
22const text_cfg = gg.TextCfg{
23 align: .left
24 size: text_size
25 color: gg.rgb(0, 0, 0)
26}
27const over_cfg = gg.TextCfg{
28 align: .left
29 size: text_size
30 color: gg.white
31}
32
33// Tetros' 4 possible states are encoded in binaries
34// 0000 0 0000 0 0000 0 0000 0 0000 0 0000 0
35// 0000 0 0000 0 0000 0 0000 0 0011 3 0011 3
36// 0110 6 0010 2 0011 3 0110 6 0001 1 0010 2
37// 0110 6 0111 7 0110 6 0011 3 0001 1 0010 2
38// There is a special case 1111, since 15 can't be used.
39const b_tetros = [
40 [66, 66, 66, 66],
41 [27, 131, 72, 232],
42 [36, 231, 36, 231],
43 [63, 132, 63, 132],
44 [311, 17, 223, 74],
45 [322, 71, 113, 47],
46 [1111, 9, 1111, 9],
47]
48// Each tetro has its unique color
49const colors = [
50 gg.rgb(0, 0, 0), // unused ?
51 gg.rgb(255, 242, 0), // yellow quad
52 gg.rgb(174, 0, 255), // purple triple
53 gg.rgb(60, 255, 0), // green short topright
54 gg.rgb(255, 0, 0), // red short topleft
55 gg.rgb(255, 180, 31), // orange long topleft
56 gg.rgb(33, 66, 255), // blue long topright
57 gg.rgb(74, 198, 255), // lightblue longest
58 gg.rgb(0, 170, 170),
59]
60const ui_color = gg.rgba(255, 0, 0, 210)
61
62// TODO: type Tetro [tetro_size]struct{ x, y int }
63struct Block {
64mut:
65 x int
66 y int
67}
68
69enum GameState {
70 paused
71 running
72 gameover
73}
74
75struct Game {
76mut:
77 // Score of the current game
78 score int
79 // Lines of the current game
80 lines int
81 // State of the current game
82 state GameState
83 // Block size in screen dimensions
84 block_size int = block_size
85 // Field margin
86 margin int
87 // Position of the current tetro
88 pos_x int
89 pos_y int
90 // field[y][x] contains the color of the block with (x,y) coordinates
91 // "-1" border is to avoid bounds checking.
92 // -1 -1 -1 -1
93 // -1 0 0 -1
94 // -1 0 0 -1
95 // -1 -1 -1 -1
96 field [][]int
97 // TODO: tetro Tetro
98 tetro []Block
99 // TODO: tetros_cache []Tetro
100 tetros_cache []Block
101 // Index of the current tetro. Refers to its color.
102 tetro_idx int
103 // Idem for the next tetro
104 next_tetro_idx int
105 // Index of the rotation (0-3)
106 rotation_idx int
107 // gg context for drawing
108 gg &gg.Context = unsafe { nil }
109 font_loaded bool
110 show_ghost bool
111 // frame/time counters:
112 frame int
113 frame_old int
114 frame_sw time.StopWatch = time.new_stopwatch()
115 second_sw time.StopWatch = time.new_stopwatch()
116}
117
118fn remap(v f32, min f32, max f32, new_min f32, new_max f32) f32 {
119 return (((v - min) * (new_max - new_min)) / (max - min)) + new_min
120}
121
122@[if showfps ?]
123fn (mut game Game) showfps() {
124 game.frame++
125 last_frame_ms := f64(game.frame_sw.elapsed().microseconds()) / 1000.0
126 ticks := f64(game.second_sw.elapsed().microseconds()) / 1000.0
127 if ticks > 999.0 {
128 fps := f64(game.frame - game.frame_old) * ticks / 1000.0
129 $if debug {
130 eprintln('fps: ${fps:5.1f} | last frame took: ${last_frame_ms:6.3f}ms | frame: ${game.frame:6} ')
131 }
132 game.second_sw.restart()
133 game.frame_old = game.frame
134 }
135}
136
137fn frame(mut game Game) {
138 if game.gg.timer.elapsed().milliseconds() > 264 {
139 game.gg.timer.restart()
140 game.update_game_state()
141 }
142 ws := gg.window_size()
143 bs := remap(block_size, 0, win_height, 0, ws.height)
144 m := (f32(ws.width) - bs * field_width) * 0.5
145 game.block_size = int(bs)
146 game.margin = int(m)
147 game.frame_sw.restart()
148 game.gg.begin()
149 game.draw_scene()
150 game.showfps()
151 game.gg.end()
152}
153
154fn main() {
155 mut game := &Game{}
156 game.gg = gg.new_context(
157 bg_color: gg.white
158 width: win_width
159 height: win_height
160 create_window: true
161 window_title: 'V Tetris'
162 user_data: game
163 frame_fn: frame
164 event_fn: on_event
165 font_path: asset.get_path('../assets', 'fonts/RobotoMono-Regular.ttf')
166 // wait_events: true
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 := []int{len: field_width + 2}
180 row[0] = -1
181 row[field_width + 1] = -1
182 g.field << row.clone()
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 (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 g.tetro = g.tetros_cache[idx..idx + tetro_size].clone()
310}
311
312fn (mut g Game) drop_tetro() {
313 for i in 0 .. tetro_size {
314 tetro := g.tetro[i]
315 x := tetro.x + g.pos_x
316 y := tetro.y + g.pos_y
317 // Remember the color of each block
318 g.field[y][x] = g.tetro_idx + 1
319 }
320}
321
322fn (g &Game) draw_tetro() {
323 for i in 0 .. tetro_size {
324 tetro := g.tetro[i]
325 g.draw_block(g.pos_y + tetro.y, g.pos_x + tetro.x, g.tetro_idx + 1)
326 }
327}
328
329fn (g &Game) draw_next_tetro() {
330 if g.state != .gameover {
331 idx := g.next_tetro_idx * tetro_size * tetro_size
332 next_tetro := g.tetros_cache[idx..idx + tetro_size].clone()
333 pos_y := 0
334 pos_x := field_width / 2 - tetro_size / 2
335 for i in 0 .. tetro_size {
336 block := next_tetro[i]
337 g.draw_block_color(pos_y + block.y, pos_x + block.x, gg.rgb(220, 220, 220))
338 }
339 }
340}
341
342fn (g &Game) draw_block_color(i int, j int, color gg.Color) {
343 g.gg.draw_rect_filled(f32((j - 1) * g.block_size) + g.margin, f32((i - 1) * g.block_size),
344 f32(g.block_size - 1), f32(g.block_size - 1), color)
345}
346
347fn (g &Game) draw_block(i int, j int, color_idx int) {
348 color := if g.state == .gameover { gg.gray } else { colors[color_idx] }
349 g.draw_block_color(i, j, color)
350}
351
352fn (g &Game) draw_field() {
353 for i := 1; i < field_height + 1; i++ {
354 for j := 1; j < field_width + 1; j++ {
355 if g.field[i][j] > 0 {
356 g.draw_block(i, j, g.field[i][j])
357 }
358 }
359 }
360}
361
362fn (mut g Game) draw_ui() {
363 ws := gg.window_size()
364 textsize := int(remap(text_size, 0, win_width, 0, ws.width))
365 g.gg.draw_text(1, 3, g.score.str(), text_cfg)
366 lines := g.lines.str()
367 g.gg.draw_text(ws.width - lines.len * textsize, 3, lines, text_cfg)
368 if g.state == .gameover {
369 g.gg.draw_rect_filled(0, ws.height / 2 - textsize, ws.width, 5 * textsize, ui_color)
370 g.gg.draw_text(1, ws.height / 2 + 0 * textsize, 'Game Over', over_cfg)
371 g.gg.draw_text(1, ws.height / 2 + 2 * textsize, 'Space to restart', over_cfg)
372 } else if g.state == .paused {
373 g.gg.draw_rect_filled(0, ws.height / 2 - textsize, ws.width, 5 * textsize, ui_color)
374 g.gg.draw_text(1, ws.height / 2 + 0 * textsize, 'Game Paused', text_cfg)
375 g.gg.draw_text(1, ws.height / 2 + 2 * textsize, 'SPACE to resume', text_cfg)
376 }
377 // g.gg.draw_rect(0, block_size, win_width, limit_thickness, ui_color)
378}
379
380fn (mut g Game) draw_scene() {
381 g.draw_ghost()
382 g.draw_next_tetro()
383 g.draw_tetro()
384 g.draw_field()
385 g.draw_ui()
386}
387
388fn parse_binary_tetro(t_ int) []Block {
389 mut t := t_
390 mut res := []Block{len: 4}
391 mut cnt := 0
392 horizontal := t == 9 // special case for the horizontal line
393 ten_powers := [1000, 100, 10, 1]!
394 for i := 0; i <= 3; i++ {
395 // Get ith digit of t
396 p := ten_powers[i]
397 mut digit := t / p
398 t %= p
399 // Convert the digit to binary
400 for j := 3; j >= 0; j-- {
401 bin := digit % 2
402 digit /= 2
403 if bin == 1 || (horizontal && i == tetro_size - 1) {
404 res[cnt].x = j
405 res[cnt].y = i
406 cnt++
407 }
408 }
409 }
410 return res
411}
412
413fn on_event(e &gg.Event, mut game Game) {
414 // println('code=${e.char_code}')
415 if e.typ == .key_down {
416 game.key_down(e.key_code)
417 }
418 if e.typ == .touches_began || e.typ == .touches_moved {
419 if e.num_touches > 0 {
420 touch_point := e.touches[0]
421 game.touch_event(touch_point)
422 }
423 }
424}
425
426fn (mut game Game) rotate_tetro() {
427 old_rotation_idx := game.rotation_idx
428 game.rotation_idx++
429 if game.rotation_idx == tetro_size {
430 game.rotation_idx = 0
431 }
432 game.get_tetro()
433 if !game.move_right(0) {
434 game.rotation_idx = old_rotation_idx
435 game.get_tetro()
436 }
437 if game.pos_x < 0 {
438 // game.pos_x = 1
439 }
440}
441
442fn (mut game Game) key_down(key gg.KeyCode) {
443 // global keys
444 match key {
445 .escape {
446 game.gg.quit()
447 }
448 .space {
449 if game.state == .running {
450 game.state = .paused
451 } else if game.state == .paused {
452 game.state = .running
453 } else if game.state == .gameover {
454 game.init_game()
455 game.state = .running
456 }
457 }
458 else {}
459 }
460
461 if game.state != .running {
462 return
463 }
464 // keys while game is running
465 match key {
466 .up {
467 // Rotate the tetro
468 game.rotate_tetro()
469 }
470 .left {
471 game.move_right(-1)
472 }
473 .right {
474 game.move_right(1)
475 }
476 .down {
477 game.move_tetro() // drop faster when the player presses <down>
478 }
479 .d {
480 for game.move_tetro() {
481 }
482 }
483 .g {
484 game.show_ghost = !game.show_ghost
485 }
486 else {}
487 }
488}
489
490fn (mut game Game) touch_event(touch_point gg.TouchPoint) {
491 ws := gg.window_size()
492 tx := touch_point.pos_x
493 ty := touch_point.pos_y
494 if ty < f32(ws.height) * 0.5 {
495 game.rotate_tetro()
496 } else {
497 if tx <= f32(ws.width) * 0.5 {
498 game.move_right(-1)
499 } else {
500 game.move_right(1)
501 }
502 }
503}
504