v / examples / 2048
Raw file | 934 loc (884 sloc) | 21.44 KB | Latest commit hash 017ace6ea
1import gg
2import gx
3import math
4import os
5import rand
6import time
7
8struct App {
9mut:
10 gg &gg.Context = unsafe { nil }
11 touch TouchInfo
12 ui Ui
13 theme &Theme = themes[0]
14 theme_idx int
15 board Board
16 undo []Undo
17 atickers [4][4]int
18 state GameState = .play
19 tile_format TileFormat = .normal
20 moves int
21 perf &Perf = unsafe { nil }
22 is_ai_mode bool
23}
24
25struct Ui {
26mut:
27 dpi_scale f32
28 tile_size int
29 border_size int
30 padding_size int
31 header_size int
32 font_size int
33 window_width int
34 window_height int
35 x_padding int
36 y_padding int
37}
38
39struct Theme {
40 bg_color gx.Color
41 padding_color gx.Color
42 text_color gx.Color
43 game_over_color gx.Color
44 victory_color gx.Color
45 tile_colors []gx.Color
46}
47
48const (
49 themes = [
50 &Theme{
51 bg_color: gx.rgb(250, 248, 239)
52 padding_color: gx.rgb(143, 130, 119)
53 victory_color: gx.rgb(100, 160, 100)
54 game_over_color: gx.rgb(190, 50, 50)
55 text_color: gx.black
56 tile_colors: [
57 gx.rgb(205, 193, 180), // Empty / 0 tile
58 gx.rgb(238, 228, 218), // 2
59 gx.rgb(237, 224, 200), // 4
60 gx.rgb(242, 177, 121), // 8
61 gx.rgb(245, 149, 99), // 16
62 gx.rgb(246, 124, 95), // 32
63 gx.rgb(246, 94, 59), // 64
64 gx.rgb(237, 207, 114), // 128
65 gx.rgb(237, 204, 97), // 256
66 gx.rgb(237, 200, 80), // 512
67 gx.rgb(237, 197, 63), // 1024
68 gx.rgb(237, 194, 46),
69 ]
70 },
71 &Theme{
72 bg_color: gx.rgb(55, 55, 55)
73 padding_color: gx.rgb(68, 60, 59)
74 victory_color: gx.rgb(100, 160, 100)
75 game_over_color: gx.rgb(190, 50, 50)
76 text_color: gx.white
77 tile_colors: [
78 gx.rgb(123, 115, 108),
79 gx.rgb(142, 136, 130),
80 gx.rgb(142, 134, 120),
81 gx.rgb(145, 106, 72),
82 gx.rgb(147, 89, 59),
83 gx.rgb(147, 74, 57),
84 gx.rgb(147, 56, 35),
85 gx.rgb(142, 124, 68),
86 gx.rgb(142, 122, 58),
87 gx.rgb(142, 120, 48),
88 gx.rgb(142, 118, 37),
89 gx.rgb(142, 116, 27),
90 ]
91 },
92 &Theme{
93 bg_color: gx.rgb(38, 38, 66)
94 padding_color: gx.rgb(58, 50, 74)
95 victory_color: gx.rgb(100, 160, 100)
96 game_over_color: gx.rgb(190, 50, 50)
97 text_color: gx.white
98 tile_colors: [
99 gx.rgb(92, 86, 140),
100 gx.rgb(106, 99, 169),
101 gx.rgb(106, 97, 156),
102 gx.rgb(108, 79, 93),
103 gx.rgb(110, 66, 76),
104 gx.rgb(110, 55, 74),
105 gx.rgb(110, 42, 45),
106 gx.rgb(106, 93, 88),
107 gx.rgb(106, 91, 75),
108 gx.rgb(106, 90, 62),
109 gx.rgb(106, 88, 48),
110 gx.rgb(106, 87, 35),
111 ]
112 },
113 ]
114 window_title = 'V 2048'
115 default_window_width = 544
116 default_window_height = 560
117 animation_length = 10 // frames
118 frames_per_ai_move = 8
119 possible_moves = [Direction.up, .right, .down, .left]
120 predictions_per_move = 200
121 prediction_depth = 8
122)
123
124// Used for performance monitoring when `-d showfps` is passed, unused / optimized out otherwise
125struct Perf {
126mut:
127 frame int
128 frame_old int
129 frame_sw time.StopWatch = time.new_stopwatch()
130 second_sw time.StopWatch = time.new_stopwatch()
131}
132
133struct Pos {
134 x int = -1
135 y int = -1
136}
137
138struct Board {
139mut:
140 field [4][4]int
141 points int
142 shifts int
143}
144
145struct Undo {
146 board Board
147 state GameState
148}
149
150struct TileLine {
151 ypos int
152mut:
153 field [5]int
154 points int
155 shifts int
156}
157
158struct TouchInfo {
159mut:
160 start Touch
161 end Touch
162}
163
164struct Touch {
165mut:
166 pos Pos
167 time time.Time
168}
169
170enum TileFormat {
171 normal
172 log
173 exponent
174 shifts
175 none_
176 end_ // To know when to wrap around
177}
178
179enum GameState {
180 play
181 over
182 victory
183 freeplay
184}
185
186enum LabelKind {
187 points
188 moves
189 tile
190 victory
191 game_over
192 score_end
193}
194
195enum Direction {
196 up
197 down
198 left
199 right
200}
201
202// Utility functions
203[inline]
204fn avg(a int, b int) int {
205 return (a + b) / 2
206}
207
208fn (b Board) transpose() Board {
209 mut res := b
210 for y in 0 .. 4 {
211 for x in 0 .. 4 {
212 res.field[y][x] = b.field[x][y]
213 }
214 }
215 return res
216}
217
218fn (b Board) hmirror() Board {
219 mut res := b
220 for y in 0 .. 4 {
221 for x in 0 .. 4 {
222 res.field[y][x] = b.field[y][3 - x]
223 }
224 }
225 return res
226}
227
228fn (t TileLine) to_left() TileLine {
229 right_border_idx := 4
230 mut res := t
231 mut zeros := 0
232 mut nonzeros := 0
233 // gather meta info about the line:
234 for x in res.field {
235 if x == 0 {
236 zeros++
237 } else {
238 nonzeros++
239 }
240 }
241 if nonzeros == 0 {
242 // when all the tiles are empty, there is nothing left to do
243 return res
244 }
245 if zeros > 0 {
246 // we have some 0s, do shifts to compact them:
247 mut remaining_zeros := zeros
248 for x := 0; x < right_border_idx - 1; x++ {
249 for res.field[x] == 0 && remaining_zeros > 0 {
250 res.shifts++
251 for k := x; k < right_border_idx; k++ {
252 res.field[k] = res.field[k + 1]
253 }
254 remaining_zeros--
255 }
256 }
257 }
258 // At this point, the non 0 tiles are all on the left, with no empty spaces
259 // between them. we can safely merge them, when they have the same value:
260 for x := 0; x < right_border_idx - 1; x++ {
261 if res.field[x] == 0 {
262 break
263 }
264 if res.field[x] == res.field[x + 1] {
265 for k := x; k < right_border_idx; k++ {
266 res.field[k] = res.field[k + 1]
267 }
268 res.shifts++
269 res.field[x]++
270 res.points += 1 << res.field[x]
271 }
272 }
273 return res
274}
275
276fn (b Board) to_left() Board {
277 mut res := b
278 for y in 0 .. 4 {
279 mut hline := TileLine{
280 ypos: y
281 }
282 for x in 0 .. 4 {
283 hline.field[x] = b.field[y][x]
284 }
285 reshline := hline.to_left()
286 res.shifts += reshline.shifts
287 res.points += reshline.points
288 for x in 0 .. 4 {
289 res.field[y][x] = reshline.field[x]
290 }
291 }
292 return res
293}
294
295fn (b Board) move(d Direction) (Board, bool) {
296 new := match d {
297 .left { b.to_left() }
298 .right { b.hmirror().to_left().hmirror() }
299 .up { b.transpose().to_left().transpose() }
300 .down { b.transpose().hmirror().to_left().hmirror().transpose() }
301 }
302 // If the board hasn't changed, it's an illegal move, don't allow it.
303 for x in 0 .. 4 {
304 for y in 0 .. 4 {
305 if b.field[x][y] != new.field[x][y] {
306 return new, true
307 }
308 }
309 }
310 return new, false
311}
312
313fn (mut b Board) is_game_over() bool {
314 for y in 0 .. 4 {
315 for x in 0 .. 4 {
316 fidx := b.field[y][x]
317 if fidx == 0 {
318 // there are remaining zeros
319 return false
320 }
321 if (x > 0 && fidx == b.field[y][x - 1])
322 || (x < 4 - 1 && fidx == b.field[y][x + 1])
323 || (y > 0 && fidx == b.field[y - 1][x])
324 || (y < 4 - 1 && fidx == b.field[y + 1][x]) {
325 // there are remaining merges
326 return false
327 }
328 }
329 }
330 return true
331}
332
333fn (mut app App) update_tickers() {
334 for y in 0 .. 4 {
335 for x in 0 .. 4 {
336 mut old := app.atickers[y][x]
337 if old > 0 {
338 old--
339 app.atickers[y][x] = old
340 }
341 }
342 }
343}
344
345fn (mut app App) new_game() {
346 app.board = Board{}
347 for y in 0 .. 4 {
348 for x in 0 .. 4 {
349 app.board.field[y][x] = 0
350 app.atickers[y][x] = 0
351 }
352 }
353 app.state = .play
354 app.undo = []Undo{cap: 4096}
355 app.moves = 0
356 app.new_random_tile()
357 app.new_random_tile()
358}
359
360[inline]
361fn (mut app App) check_for_victory() {
362 for y in 0 .. 4 {
363 for x in 0 .. 4 {
364 fidx := app.board.field[y][x]
365 if fidx == 11 {
366 app.state = .victory
367 return
368 }
369 }
370 }
371}
372
373[inline]
374fn (mut app App) check_for_game_over() {
375 if app.board.is_game_over() {
376 app.state = .over
377 }
378}
379
380fn (mut b Board) place_random_tile() (Pos, int) {
381 mut etiles := [16]Pos{}
382 mut empty_tiles_max := 0
383 for y in 0 .. 4 {
384 for x in 0 .. 4 {
385 fidx := b.field[y][x]
386 if fidx == 0 {
387 etiles[empty_tiles_max] = Pos{x, y}
388 empty_tiles_max++
389 }
390 }
391 }
392 if empty_tiles_max > 0 {
393 new_random_tile_index := rand.intn(empty_tiles_max) or { 0 }
394 empty_pos := etiles[new_random_tile_index]
395 // 10% chance of getting a `4` tile
396 value := rand.f64n(1.0) or { 0.0 }
397 random_value := if value < 0.9 { 1 } else { 2 }
398 b.field[empty_pos.y][empty_pos.x] = random_value
399 return empty_pos, random_value
400 }
401 return Pos{}, 0
402}
403
404fn (mut app App) new_random_tile() {
405 for y in 0 .. 4 {
406 for x in 0 .. 4 {
407 fidx := app.board.field[y][x]
408 if fidx == 0 {
409 app.atickers[y][x] = 0
410 }
411 }
412 }
413 empty_pos, random_value := app.board.place_random_tile()
414 if random_value > 0 {
415 app.atickers[empty_pos.y][empty_pos.x] = animation_length
416 }
417 if app.state != .freeplay {
418 app.check_for_victory()
419 }
420 app.check_for_game_over()
421}
422
423fn (mut app App) apply_new_board(new Board) {
424 old := app.board
425 app.moves++
426 app.board = new
427 app.undo << Undo{old, app.state}
428 app.new_random_tile()
429}
430
431fn (mut app App) move(d Direction) {
432 new, is_valid := app.board.move(d)
433 if !is_valid {
434 return
435 }
436 app.apply_new_board(new)
437}
438
439struct Prediction {
440mut:
441 move Direction
442 mpoints f64
443 mcmoves f64
444}
445
446fn (p Prediction) str() string {
447 return '{ move: ${p.move:5}, mpoints: ${p.mpoints:6.2f}, mcmoves: ${p.mcmoves:6.2f} }'
448}
449
450fn (mut app App) ai_move() {
451 mut predictions := [4]Prediction{}
452 mut is_valid := false
453 think_watch := time.new_stopwatch()
454 for move in possible_moves {
455 move_idx := int(move)
456 predictions[move_idx].move = move
457 mut mpoints := 0
458 mut mcmoves := 0
459 for _ in 0 .. predictions_per_move {
460 mut cboard := app.board
461 cboard, is_valid = cboard.move(move)
462 if !is_valid || cboard.is_game_over() {
463 continue
464 }
465 mpoints += cboard.points
466 cboard.place_random_tile()
467 mut cmoves := 0
468 for !cboard.is_game_over() {
469 nmove := possible_moves[rand.intn(possible_moves.len) or { 0 }]
470 cboard, is_valid = cboard.move(nmove)
471 if !is_valid {
472 continue
473 }
474 cboard.place_random_tile()
475 cmoves++
476 if cmoves > prediction_depth {
477 break
478 }
479 }
480 mpoints += cboard.points
481 mcmoves += cmoves
482 }
483 predictions[move_idx].mpoints = f64(mpoints) / predictions_per_move
484 predictions[move_idx].mcmoves = f64(mcmoves) / predictions_per_move
485 }
486 think_time := think_watch.elapsed().milliseconds()
487 mut bestprediction := Prediction{
488 mpoints: -1
489 }
490 for move_idx in 0 .. possible_moves.len {
491 if bestprediction.mpoints < predictions[move_idx].mpoints {
492 bestprediction = predictions[move_idx]
493 }
494 }
495 eprintln('Simulation time: ${think_time:4}ms | best ${bestprediction}')
496 app.move(bestprediction.move)
497}
498
499fn (app &App) label_format(kind LabelKind) gx.TextCfg {
500 match kind {
501 .points {
502 return gx.TextCfg{
503 color: if app.state in [.over, .victory] { gx.white } else { app.theme.text_color }
504 align: .left
505 size: app.ui.font_size / 2
506 }
507 }
508 .moves {
509 return gx.TextCfg{
510 color: if app.state in [.over, .victory] { gx.white } else { app.theme.text_color }
511 align: .right
512 size: app.ui.font_size / 2
513 }
514 }
515 .tile {
516 return gx.TextCfg{
517 color: app.theme.text_color
518 align: .center
519 vertical_align: .middle
520 size: app.ui.font_size
521 }
522 }
523 .victory {
524 return gx.TextCfg{
525 color: app.theme.victory_color
526 align: .center
527 vertical_align: .middle
528 size: app.ui.font_size * 2
529 }
530 }
531 .game_over {
532 return gx.TextCfg{
533 color: app.theme.game_over_color
534 align: .center
535 vertical_align: .middle
536 size: app.ui.font_size * 2
537 }
538 }
539 .score_end {
540 return gx.TextCfg{
541 color: gx.white
542 align: .center
543 vertical_align: .middle
544 size: app.ui.font_size * 3 / 4
545 }
546 }
547 }
548}
549
550[inline]
551fn (mut app App) set_theme(idx int) {
552 theme := themes[idx]
553 app.theme_idx = idx
554 app.theme = theme
555 app.gg.set_bg_color(theme.bg_color)
556}
557
558fn (mut app App) resize() {
559 mut s := app.gg.scale
560 if s == 0.0 {
561 s = 1.0
562 }
563 window_size := app.gg.window_size()
564 w := window_size.width
565 h := window_size.height
566 m := f32(math.min(w, h))
567 app.ui.dpi_scale = s
568 app.ui.window_width = w
569 app.ui.window_height = h
570 app.ui.padding_size = int(m / 38)
571 app.ui.header_size = app.ui.padding_size
572 app.ui.border_size = app.ui.padding_size * 2
573 app.ui.tile_size = int((m - app.ui.padding_size * 5 - app.ui.border_size * 2) / 4)
574 app.ui.font_size = int(m / 10)
575 // If the window's height is greater than its width, center the board vertically.
576 // If not, center it horizontally
577 if w > h {
578 app.ui.y_padding = 0
579 app.ui.x_padding = (app.ui.window_width - app.ui.window_height) / 2
580 } else {
581 app.ui.y_padding = (app.ui.window_height - app.ui.window_width - app.ui.header_size) / 2
582 app.ui.x_padding = 0
583 }
584}
585
586fn (app &App) draw() {
587 xpad, ypad := app.ui.x_padding, app.ui.y_padding
588 ww := app.ui.window_width
589 wh := app.ui.window_height
590 m := math.min(ww, wh)
591 labelx := xpad + app.ui.border_size
592 labely := ypad + app.ui.border_size / 2
593 app.draw_tiles()
594 // TODO: Make transparency work in `gg`
595 if app.state == .over {
596 app.gg.draw_rect_filled(0, 0, ww, wh, gx.rgba(10, 0, 0, 180))
597 app.gg.draw_text(ww / 2, (m * 4 / 10) + ypad, 'Game Over', app.label_format(.game_over))
598 f := app.label_format(.tile)
599 msg := $if android { 'Tap to restart' } $else { 'Press `r` to restart' }
600 app.gg.draw_text(ww / 2, (m * 6 / 10) + ypad, msg, gx.TextCfg{
601 ...f
602 color: gx.white
603 size: f.size * 3 / 4
604 })
605 }
606 if app.state == .victory {
607 app.gg.draw_rect_filled(0, 0, ww, wh, gx.rgba(0, 10, 0, 180))
608 app.gg.draw_text(ww / 2, (m * 4 / 10) + ypad, 'Victory!', app.label_format(.victory))
609 // f := app.label_format(.tile)
610 msg1 := $if android { 'Tap to continue' } $else { 'Press `space` to continue' }
611 msg2 := $if android { 'Tap to restart' } $else { 'Press `r` to restart' }
612 app.gg.draw_text(ww / 2, (m * 6 / 10) + ypad, msg1, app.label_format(.score_end))
613 app.gg.draw_text(ww / 2, (m * 8 / 10) + ypad, msg2, app.label_format(.score_end))
614 }
615 // Draw at the end, so that it's on top of the victory / game over overlays
616 app.gg.draw_text(labelx, labely, 'Points: ${app.board.points}', app.label_format(.points))
617 app.gg.draw_text(ww - labelx, labely, 'Moves: ${app.moves}', app.label_format(.moves))
618}
619
620fn (app &App) draw_tiles() {
621 xstart := app.ui.x_padding + app.ui.border_size
622 ystart := app.ui.y_padding + app.ui.border_size + app.ui.header_size
623 toffset := app.ui.tile_size + app.ui.padding_size
624 tiles_size := math.min(app.ui.window_width, app.ui.window_height) - app.ui.border_size * 2
625 // Draw the padding around the tiles
626 app.gg.draw_rounded_rect_filled(xstart, ystart, tiles_size, tiles_size, tiles_size / 24,
627 app.theme.padding_color)
628 // Draw the actual tiles
629 for y in 0 .. 4 {
630 for x in 0 .. 4 {
631 tidx := app.board.field[y][x]
632 tile_color := if tidx < app.theme.tile_colors.len {
633 app.theme.tile_colors[tidx]
634 } else {
635 // If there isn't a specific color for this tile, reuse the last color available
636 app.theme.tile_colors.last()
637 }
638 anim_size := animation_length - app.atickers[y][x]
639 tw := int(f64(app.ui.tile_size) / animation_length * anim_size)
640 th := tw // square tiles, w == h
641 xoffset := xstart + app.ui.padding_size + x * toffset + (app.ui.tile_size - tw) / 2
642 yoffset := ystart + app.ui.padding_size + y * toffset + (app.ui.tile_size - th) / 2
643 app.gg.draw_rounded_rect_filled(xoffset, yoffset, tw, th, tw / 8, tile_color)
644 if tidx != 0 { // 0 == blank spot
645 xpos := xoffset + tw / 2
646 ypos := yoffset + th / 2
647 mut fmt := app.label_format(.tile)
648 fmt = gx.TextCfg{
649 ...fmt
650 size: int(f32(fmt.size - 1) / animation_length * anim_size)
651 }
652 match app.tile_format {
653 .normal {
654 app.gg.draw_text(xpos, ypos, '${1 << tidx}', fmt)
655 }
656 .log {
657 app.gg.draw_text(xpos, ypos, '${tidx}', fmt)
658 }
659 .exponent {
660 app.gg.draw_text(xpos, ypos, '2', fmt)
661 fs2 := int(f32(fmt.size) * 0.67)
662 app.gg.draw_text(xpos + app.ui.tile_size / 10, ypos - app.ui.tile_size / 8,
663 '${tidx}', gx.TextCfg{
664 ...fmt
665 size: fs2
666 align: gx.HorizontalAlign.left
667 })
668 }
669 .shifts {
670 fs2 := int(f32(fmt.size) * 0.6)
671 app.gg.draw_text(xpos, ypos, '2<<${tidx - 1}', gx.TextCfg{
672 ...fmt
673 size: fs2
674 })
675 }
676 .none_ {} // Don't draw any text here, colors only
677 .end_ {} // Should never get here
678 }
679 }
680 }
681 }
682}
683
684fn (mut app App) handle_touches() {
685 s, e := app.touch.start, app.touch.end
686 adx, ady := math.abs(e.pos.x - s.pos.x), math.abs(e.pos.y - s.pos.y)
687 if math.max(adx, ady) < 10 {
688 app.handle_tap()
689 } else {
690 app.handle_swipe()
691 }
692}
693
694fn (mut app App) handle_tap() {
695 _, ypad := app.ui.x_padding, app.ui.y_padding
696 w, h := app.ui.window_width, app.ui.window_height
697 m := math.min(w, h)
698 s, e := app.touch.start, app.touch.end
699 avgx, avgy := avg(s.pos.x, e.pos.x), avg(s.pos.y, e.pos.y)
700 // TODO: Replace "touch spots" with actual buttons
701 // bottom left -> change theme
702 if avgx < 50 && h - avgy < 50 {
703 app.next_theme()
704 }
705 // bottom right -> change tile format
706 if w - avgx < 50 && h - avgy < 50 {
707 app.next_tile_format()
708 }
709 if app.state == .victory {
710 if avgy > (m / 2) + ypad {
711 if avgy < (m * 7 / 10) + ypad {
712 app.state = .freeplay
713 } else if avgy < (m * 9 / 10) + ypad {
714 app.new_game()
715 } else {
716 // TODO remove and implement an actual way to toggle themes on mobile
717 }
718 }
719 } else if app.state == .over {
720 if avgy > (m / 2) + ypad && avgy < (m * 7 / 10) + ypad {
721 app.new_game()
722 }
723 }
724}
725
726fn (mut app App) handle_swipe() {
727 // Currently, swipes are only used to move the tiles.
728 // If the user's not playing, exit early to avoid all the unnecessary calculations
729 if app.state !in [.play, .freeplay] {
730 return
731 }
732 s, e := app.touch.start, app.touch.end
733 w, h := app.ui.window_width, app.ui.window_height
734 dx, dy := e.pos.x - s.pos.x, e.pos.y - s.pos.y
735 adx, ady := math.abs(dx), math.abs(dy)
736 dmin := if math.min(adx, ady) > 0 { math.min(adx, ady) } else { 1 }
737 dmax := if math.max(adx, ady) > 0 { math.max(adx, ady) } else { 1 }
738 tdiff := int(e.time.unix_time_milli() - s.time.unix_time_milli())
739 // TODO: make this calculation more accurate (don't use arbitrary numbers)
740 min_swipe_distance := int(math.sqrt(math.min(w, h) * tdiff / 100)) + 20
741 if dmax < min_swipe_distance {
742 return
743 }
744 // Swipe was too short
745 if dmax / dmin < 2 {
746 return
747 }
748 // Swiped diagonally
749 if adx > ady {
750 if dx < 0 {
751 app.move(.left)
752 } else {
753 app.move(.right)
754 }
755 } else {
756 if dy < 0 {
757 app.move(.up)
758 } else {
759 app.move(.down)
760 }
761 }
762}
763
764[inline]
765fn (mut app App) next_theme() {
766 app.set_theme(if app.theme_idx == themes.len - 1 { 0 } else { app.theme_idx + 1 })
767}
768
769[inline]
770fn (mut app App) next_tile_format() {
771 app.tile_format = unsafe { TileFormat(int(app.tile_format) + 1) }
772 if app.tile_format == .end_ {
773 app.tile_format = .normal
774 }
775}
776
777[inline]
778fn (mut app App) undo() {
779 if app.undo.len > 0 {
780 undo := app.undo.pop()
781 app.board = undo.board
782 app.state = undo.state
783 app.moves--
784 }
785}
786
787fn (mut app App) on_key_down(key gg.KeyCode) {
788 // these keys are independent from the game state:
789 match key {
790 .c { app.is_ai_mode = !app.is_ai_mode }
791 .escape { app.gg.quit() }
792 .n, .r { app.new_game() }
793 .backspace { app.undo() }
794 .enter { app.next_tile_format() }
795 .j { app.state = .over }
796 .t { app.next_theme() }
797 else {}
798 }
799 if app.state in [.play, .freeplay] {
800 if !app.is_ai_mode {
801 match key {
802 .w, .up { app.move(.up) }
803 .a, .left { app.move(.left) }
804 .s, .down { app.move(.down) }
805 .d, .right { app.move(.right) }
806 else {}
807 }
808 }
809 }
810 if app.state == .victory {
811 if key == .space {
812 app.state = .freeplay
813 }
814 }
815}
816
817fn on_event(e &gg.Event, mut app App) {
818 match e.typ {
819 .key_down {
820 app.on_key_down(e.key_code)
821 }
822 .resized, .restored, .resumed {
823 app.resize()
824 }
825 .touches_began {
826 if e.num_touches > 0 {
827 t := e.touches[0]
828 app.touch.start = Touch{
829 pos: Pos{
830 x: int(t.pos_x / app.ui.dpi_scale)
831 y: int(t.pos_y / app.ui.dpi_scale)
832 }
833 time: time.now()
834 }
835 }
836 }
837 .touches_ended {
838 if e.num_touches > 0 {
839 t := e.touches[0]
840 app.touch.end = Touch{
841 pos: Pos{
842 x: int(t.pos_x / app.ui.dpi_scale)
843 y: int(t.pos_y / app.ui.dpi_scale)
844 }
845 time: time.now()
846 }
847 app.handle_touches()
848 }
849 }
850 .mouse_down {
851 app.touch.start = Touch{
852 pos: Pos{
853 x: int(e.mouse_x / app.ui.dpi_scale)
854 y: int(e.mouse_y / app.ui.dpi_scale)
855 }
856 time: time.now()
857 }
858 }
859 .mouse_up {
860 app.touch.end = Touch{
861 pos: Pos{
862 x: int(e.mouse_x / app.ui.dpi_scale)
863 y: int(e.mouse_y / app.ui.dpi_scale)
864 }
865 time: time.now()
866 }
867 app.handle_touches()
868 }
869 else {}
870 }
871}
872
873fn frame(mut app App) {
874 $if showfps ? {
875 app.perf.frame_sw.restart()
876 }
877 app.gg.begin()
878 app.update_tickers()
879 app.draw()
880 app.perf.frame++
881 if app.is_ai_mode && app.state in [.play, .freeplay] && app.perf.frame % frames_per_ai_move == 0 {
882 app.ai_move()
883 }
884 $if showfps ? {
885 app.showfps()
886 }
887 app.gg.end()
888}
889
890fn init(mut app App) {
891 app.resize()
892 $if showfps ? {
893 app.perf.frame_sw.restart()
894 app.perf.second_sw.restart()
895 }
896}
897
898fn (mut app App) showfps() {
899 println(app.perf.frame_sw.elapsed().microseconds())
900 f := app.perf.frame
901 if (f & 127) == 0 {
902 last_frame_us := app.perf.frame_sw.elapsed().microseconds()
903 ticks := f64(app.perf.second_sw.elapsed().milliseconds())
904 fps := f64(app.perf.frame - app.perf.frame_old) * ticks / 1000 / 4.5
905 last_fps := 128000.0 / ticks
906 eprintln('frame ${f:-5} | avg. fps: ${fps:-5.1f} | avg. last 128 fps: ${last_fps:-5.1f} | last frame time: ${last_frame_us:-4}µs')
907 app.perf.second_sw.restart()
908 app.perf.frame_old = f
909 }
910}
911
912fn main() {
913 mut app := &App{}
914 app.new_game()
915 mut font_path := os.resource_abs_path(os.join_path('..', 'assets', 'fonts', 'RobotoMono-Regular.ttf'))
916 $if android {
917 font_path = 'fonts/RobotoMono-Regular.ttf'
918 }
919 app.perf = &Perf{}
920 app.gg = gg.new_context(
921 bg_color: app.theme.bg_color
922 width: default_window_width
923 height: default_window_height
924 sample_count: 4 // higher quality curves
925 create_window: true
926 window_title: 'V 2048'
927 frame_fn: frame
928 event_fn: on_event
929 init_fn: init
930 user_data: app
931 font_path: font_path
932 )
933 app.gg.run()
934}