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 rand |
7 | import time |
8 | import gx |
9 | import gg |
10 | // import sokol.sapp |
11 | |
12 | const ( |
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 | |
24 | const ( |
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 | |
37 | const ( |
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 } |
70 | struct Block { |
71 | mut: |
72 | x int |
73 | y int |
74 | } |
75 | |
76 | enum GameState { |
77 | paused |
78 | running |
79 | gameover |
80 | } |
81 | |
82 | struct Game { |
83 | mut: |
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 | |
125 | fn 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 ?] |
130 | fn (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 | |
144 | fn 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 | |
160 | fn 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 | |
180 | fn (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 | |
201 | fn (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 | |
211 | fn (mut g Game) update_game_state() { |
212 | if g.state == .running { |
213 | g.move_tetro() |
214 | g.delete_completed_lines() |
215 | } |
216 | } |
217 | |
218 | fn (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 | |
229 | fn (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 | |
246 | fn (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 | |
268 | fn (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 | |
283 | fn (mut g Game) delete_completed_lines() { |
284 | for y := field_height; y >= 1; y-- { |
285 | g.delete_completed_line(y) |
286 | } |
287 | } |
288 | |
289 | fn (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 |
306 | fn (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 |
316 | fn (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 |
327 | fn (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 | |
337 | fn (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 | |
344 | fn (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 | |
357 | fn (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 | |
362 | fn (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 | |
367 | fn (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 | |
377 | fn (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 | |
395 | fn (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 | |
403 | fn 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 | |
429 | fn 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 | |
436 | fn (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 | |
452 | fn (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 | } |