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. |
4 | module main |
5 | |
6 | import os |
7 | import rand |
8 | import time |
9 | import gx |
10 | import gg |
11 | // import sokol.sapp |
12 | |
13 | const ( |
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 | |
25 | const ( |
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 | |
38 | const ( |
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 } |
71 | struct Block { |
72 | mut: |
73 | x int |
74 | y int |
75 | } |
76 | |
77 | enum GameState { |
78 | paused |
79 | running |
80 | gameover |
81 | } |
82 | |
83 | struct Game { |
84 | mut: |
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 | |
126 | fn 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 ?] |
131 | fn (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 | |
145 | fn 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 | |
161 | fn 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 | |
184 | fn (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 | |
205 | fn (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 | |
215 | fn (mut g Game) update_game_state() { |
216 | if g.state == .running { |
217 | g.move_tetro() |
218 | g.delete_completed_lines() |
219 | } |
220 | } |
221 | |
222 | fn (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 | |
233 | fn (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 | |
250 | fn (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 | |
272 | fn (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 | |
287 | fn (mut g Game) delete_completed_lines() { |
288 | for y := field_height; y >= 1; y-- { |
289 | g.delete_completed_line(y) |
290 | } |
291 | } |
292 | |
293 | fn (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 |
310 | fn (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 |
320 | fn (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 |
326 | fn (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 | |
336 | fn (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 | |
343 | fn (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 | |
356 | fn (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 | |
361 | fn (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 | |
366 | fn (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 | |
376 | fn (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 | |
394 | fn (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 | |
402 | fn 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 | |
427 | fn 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 | |
440 | fn (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 | |
456 | fn (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 | |
503 | fn (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 | } |