aboutsummaryrefslogtreecommitdiff
path: root/v_windows/v/examples/2048/2048.v
diff options
context:
space:
mode:
Diffstat (limited to 'v_windows/v/examples/2048/2048.v')
-rw-r--r--v_windows/v/examples/2048/2048.v939
1 files changed, 939 insertions, 0 deletions
diff --git a/v_windows/v/examples/2048/2048.v b/v_windows/v/examples/2048/2048.v
new file mode 100644
index 0000000..d237226
--- /dev/null
+++ b/v_windows/v/examples/2048/2048.v
@@ -0,0 +1,939 @@
+import gg
+import gx
+import math
+import math.mathutil as mu
+import os
+import rand
+import time
+
+struct App {
+mut:
+ gg &gg.Context = 0
+ touch TouchInfo
+ ui Ui
+ theme &Theme = themes[0]
+ theme_idx int
+ board Board
+ undo []Undo
+ atickers [4][4]int
+ state GameState = .play
+ tile_format TileFormat = .normal
+ moves int
+ perf &Perf = 0
+ is_ai_mode bool
+}
+
+struct Ui {
+mut:
+ dpi_scale f32
+ tile_size int
+ border_size int
+ padding_size int
+ header_size int
+ font_size int
+ window_width int
+ window_height int
+ x_padding int
+ y_padding int
+}
+
+struct Theme {
+ bg_color gx.Color
+ padding_color gx.Color
+ text_color gx.Color
+ game_over_color gx.Color
+ victory_color gx.Color
+ tile_colors []gx.Color
+}
+
+const (
+ themes = [
+ &Theme{
+ bg_color: gx.rgb(250, 248, 239)
+ padding_color: gx.rgb(143, 130, 119)
+ victory_color: gx.rgb(100, 160, 100)
+ game_over_color: gx.rgb(190, 50, 50)
+ text_color: gx.black
+ tile_colors: [
+ gx.rgb(205, 193, 180), /* Empty / 0 tile */
+ gx.rgb(238, 228, 218), /* 2 */
+ gx.rgb(237, 224, 200), /* 4 */
+ gx.rgb(242, 177, 121), /* 8 */
+ gx.rgb(245, 149, 99), /* 16 */
+ gx.rgb(246, 124, 95), /* 32 */
+ gx.rgb(246, 94, 59), /* 64 */
+ gx.rgb(237, 207, 114), /* 128 */
+ gx.rgb(237, 204, 97), /* 256 */
+ gx.rgb(237, 200, 80), /* 512 */
+ gx.rgb(237, 197, 63), /* 1024 */
+ gx.rgb(237, 194, 46),
+ ]
+ },
+ &Theme{
+ bg_color: gx.rgb(55, 55, 55)
+ padding_color: gx.rgb(68, 60, 59)
+ victory_color: gx.rgb(100, 160, 100)
+ game_over_color: gx.rgb(190, 50, 50)
+ text_color: gx.white
+ tile_colors: [
+ gx.rgb(123, 115, 108),
+ gx.rgb(142, 136, 130),
+ gx.rgb(142, 134, 120),
+ gx.rgb(145, 106, 72),
+ gx.rgb(147, 89, 59),
+ gx.rgb(147, 74, 57),
+ gx.rgb(147, 56, 35),
+ gx.rgb(142, 124, 68),
+ gx.rgb(142, 122, 58),
+ gx.rgb(142, 120, 48),
+ gx.rgb(142, 118, 37),
+ gx.rgb(142, 116, 27),
+ ]
+ },
+ &Theme{
+ bg_color: gx.rgb(38, 38, 66)
+ padding_color: gx.rgb(58, 50, 74)
+ victory_color: gx.rgb(100, 160, 100)
+ game_over_color: gx.rgb(190, 50, 50)
+ text_color: gx.white
+ tile_colors: [
+ gx.rgb(92, 86, 140),
+ gx.rgb(106, 99, 169),
+ gx.rgb(106, 97, 156),
+ gx.rgb(108, 79, 93),
+ gx.rgb(110, 66, 76),
+ gx.rgb(110, 55, 74),
+ gx.rgb(110, 42, 45),
+ gx.rgb(106, 93, 88),
+ gx.rgb(106, 91, 75),
+ gx.rgb(106, 90, 62),
+ gx.rgb(106, 88, 48),
+ gx.rgb(106, 87, 35),
+ ]
+ },
+ ]
+ window_title = 'V 2048'
+ default_window_width = 544
+ default_window_height = 560
+ animation_length = 10 // frames
+ frames_per_ai_move = 8
+ possible_moves = [Direction.up, .right, .down, .left]
+ predictions_per_move = 200
+ prediction_depth = 8
+)
+
+// Used for performance monitoring when `-d showfps` is passed, unused / optimized out otherwise
+struct Perf {
+mut:
+ frame int
+ frame_old int
+ frame_sw time.StopWatch = time.new_stopwatch()
+ second_sw time.StopWatch = time.new_stopwatch()
+}
+
+struct Pos {
+ x int = -1
+ y int = -1
+}
+
+struct Board {
+mut:
+ field [4][4]int
+ points int
+ shifts int
+}
+
+struct Undo {
+ board Board
+ state GameState
+}
+
+struct TileLine {
+ ypos int
+mut:
+ field [5]int
+ points int
+ shifts int
+}
+
+struct TouchInfo {
+mut:
+ start Touch
+ end Touch
+}
+
+struct Touch {
+mut:
+ pos Pos
+ time time.Time
+}
+
+enum TileFormat {
+ normal
+ log
+ exponent
+ shifts
+ none_
+ end_ // To know when to wrap around
+}
+
+enum GameState {
+ play
+ over
+ victory
+ freeplay
+}
+
+enum LabelKind {
+ points
+ moves
+ tile
+ victory
+ game_over
+ score_end
+}
+
+enum Direction {
+ up
+ down
+ left
+ right
+}
+
+// Utility functions
+[inline]
+fn avg(a int, b int) int {
+ return (a + b) / 2
+}
+
+fn (b Board) transpose() Board {
+ mut res := b
+ for y in 0 .. 4 {
+ for x in 0 .. 4 {
+ res.field[y][x] = b.field[x][y]
+ }
+ }
+ return res
+}
+
+fn (b Board) hmirror() Board {
+ mut res := b
+ for y in 0 .. 4 {
+ for x in 0 .. 4 {
+ res.field[y][x] = b.field[y][3 - x]
+ }
+ }
+ return res
+}
+
+fn (t TileLine) to_left() TileLine {
+ right_border_idx := 4
+ mut res := t
+ mut zeros := 0
+ mut nonzeros := 0
+ // gather meta info about the line:
+ for x in res.field {
+ if x == 0 {
+ zeros++
+ } else {
+ nonzeros++
+ }
+ }
+ if nonzeros == 0 {
+ // when all the tiles are empty, there is nothing left to do
+ return res
+ }
+ if zeros > 0 {
+ // we have some 0s, do shifts to compact them:
+ mut remaining_zeros := zeros
+ for x := 0; x < right_border_idx - 1; x++ {
+ for res.field[x] == 0 && remaining_zeros > 0 {
+ res.shifts++
+ for k := x; k < right_border_idx; k++ {
+ res.field[k] = res.field[k + 1]
+ }
+ remaining_zeros--
+ }
+ }
+ }
+ // At this point, the non 0 tiles are all on the left, with no empty spaces
+ // between them. we can safely merge them, when they have the same value:
+ for x := 0; x < right_border_idx - 1; x++ {
+ if res.field[x] == 0 {
+ break
+ }
+ if res.field[x] == res.field[x + 1] {
+ for k := x; k < right_border_idx; k++ {
+ res.field[k] = res.field[k + 1]
+ }
+ res.shifts++
+ res.field[x]++
+ res.points += 1 << res.field[x]
+ }
+ }
+ return res
+}
+
+fn (b Board) to_left() Board {
+ mut res := b
+ for y in 0 .. 4 {
+ mut hline := TileLine{
+ ypos: y
+ }
+ for x in 0 .. 4 {
+ hline.field[x] = b.field[y][x]
+ }
+ reshline := hline.to_left()
+ res.shifts += reshline.shifts
+ res.points += reshline.points
+ for x in 0 .. 4 {
+ res.field[y][x] = reshline.field[x]
+ }
+ }
+ return res
+}
+
+fn (b Board) move(d Direction) (Board, bool) {
+ new := match d {
+ .left { b.to_left() }
+ .right { b.hmirror().to_left().hmirror() }
+ .up { b.transpose().to_left().transpose() }
+ .down { b.transpose().hmirror().to_left().hmirror().transpose() }
+ }
+ // If the board hasn't changed, it's an illegal move, don't allow it.
+ for x in 0 .. 4 {
+ for y in 0 .. 4 {
+ if b.field[x][y] != new.field[x][y] {
+ return new, true
+ }
+ }
+ }
+ return new, false
+}
+
+fn (mut b Board) is_game_over() bool {
+ for y in 0 .. 4 {
+ for x in 0 .. 4 {
+ fidx := b.field[y][x]
+ if fidx == 0 {
+ // there are remaining zeros
+ return false
+ }
+ if (x > 0 && fidx == b.field[y][x - 1])
+ || (x < 4 - 1 && fidx == b.field[y][x + 1])
+ || (y > 0 && fidx == b.field[y - 1][x])
+ || (y < 4 - 1 && fidx == b.field[y + 1][x]) {
+ // there are remaining merges
+ return false
+ }
+ }
+ }
+ return true
+}
+
+fn (mut app App) update_tickers() {
+ for y in 0 .. 4 {
+ for x in 0 .. 4 {
+ mut old := app.atickers[y][x]
+ if old > 0 {
+ old--
+ app.atickers[y][x] = old
+ }
+ }
+ }
+}
+
+fn (mut app App) new_game() {
+ app.board = Board{}
+ for y in 0 .. 4 {
+ for x in 0 .. 4 {
+ app.board.field[y][x] = 0
+ app.atickers[y][x] = 0
+ }
+ }
+ app.state = .play
+ app.undo = []Undo{cap: 4096}
+ app.moves = 0
+ app.new_random_tile()
+ app.new_random_tile()
+}
+
+[inline]
+fn (mut app App) check_for_victory() {
+ for y in 0 .. 4 {
+ for x in 0 .. 4 {
+ fidx := app.board.field[y][x]
+ if fidx == 11 {
+ app.state = .victory
+ return
+ }
+ }
+ }
+}
+
+[inline]
+fn (mut app App) check_for_game_over() {
+ if app.board.is_game_over() {
+ app.state = .over
+ }
+}
+
+fn (mut b Board) place_random_tile() (Pos, int) {
+ mut etiles := [16]Pos{}
+ mut empty_tiles_max := 0
+ for y in 0 .. 4 {
+ for x in 0 .. 4 {
+ fidx := b.field[y][x]
+ if fidx == 0 {
+ etiles[empty_tiles_max] = Pos{x, y}
+ empty_tiles_max++
+ }
+ }
+ }
+ if empty_tiles_max > 0 {
+ new_random_tile_index := rand.intn(empty_tiles_max)
+ empty_pos := etiles[new_random_tile_index]
+ // 10% chance of getting a `4` tile
+ random_value := if rand.f64n(1.0) < 0.9 { 1 } else { 2 }
+ b.field[empty_pos.y][empty_pos.x] = random_value
+ return empty_pos, random_value
+ }
+ return Pos{}, 0
+}
+
+fn (mut app App) new_random_tile() {
+ for y in 0 .. 4 {
+ for x in 0 .. 4 {
+ fidx := app.board.field[y][x]
+ if fidx == 0 {
+ app.atickers[y][x] = 0
+ }
+ }
+ }
+ empty_pos, random_value := app.board.place_random_tile()
+ if random_value > 0 {
+ app.atickers[empty_pos.y][empty_pos.x] = animation_length
+ }
+ if app.state != .freeplay {
+ app.check_for_victory()
+ }
+ app.check_for_game_over()
+}
+
+fn (mut app App) apply_new_board(new Board) {
+ old := app.board
+ app.moves++
+ app.board = new
+ app.undo << Undo{old, app.state}
+ app.new_random_tile()
+}
+
+fn (mut app App) move(d Direction) {
+ new, is_valid := app.board.move(d)
+ if !is_valid {
+ return
+ }
+ app.apply_new_board(new)
+}
+
+struct Prediction {
+mut:
+ move Direction
+ mpoints f64
+ mcmoves f64
+}
+
+fn (p Prediction) str() string {
+ return '{ move: ${p.move:5}, mpoints: ${p.mpoints:6.2f}, mcmoves: ${p.mcmoves:6.2f} }'
+}
+
+fn (mut app App) ai_move() {
+ mut predictions := [4]Prediction{}
+ mut is_valid := false
+ think_watch := time.new_stopwatch()
+ for move in possible_moves {
+ move_idx := int(move)
+ predictions[move_idx].move = move
+ mut mpoints := 0
+ mut mcmoves := 0
+ for _ in 0 .. predictions_per_move {
+ mut cboard := app.board
+ cboard, is_valid = cboard.move(move)
+ if !is_valid || cboard.is_game_over() {
+ continue
+ }
+ mpoints += cboard.points
+ cboard.place_random_tile()
+ mut cmoves := 0
+ for !cboard.is_game_over() {
+ nmove := possible_moves[rand.intn(possible_moves.len)]
+ cboard, is_valid = cboard.move(nmove)
+ if !is_valid {
+ continue
+ }
+ cboard.place_random_tile()
+ cmoves++
+ if cmoves > prediction_depth {
+ break
+ }
+ }
+ mpoints += cboard.points
+ mcmoves += cmoves
+ }
+ predictions[move_idx].mpoints = f64(mpoints) / predictions_per_move
+ predictions[move_idx].mcmoves = f64(mcmoves) / predictions_per_move
+ }
+ think_time := think_watch.elapsed().milliseconds()
+ mut bestprediction := Prediction{
+ mpoints: -1
+ }
+ for move_idx in 0 .. possible_moves.len {
+ if bestprediction.mpoints < predictions[move_idx].mpoints {
+ bestprediction = predictions[move_idx]
+ }
+ }
+ eprintln('Simulation time: ${think_time:4}ms | best $bestprediction')
+ app.move(bestprediction.move)
+}
+
+fn (app &App) label_format(kind LabelKind) gx.TextCfg {
+ match kind {
+ .points {
+ return gx.TextCfg{
+ color: if app.state in [.over, .victory] { gx.white } else { app.theme.text_color }
+ align: .left
+ size: app.ui.font_size / 2
+ }
+ }
+ .moves {
+ return gx.TextCfg{
+ color: if app.state in [.over, .victory] { gx.white } else { app.theme.text_color }
+ align: .right
+ size: app.ui.font_size / 2
+ }
+ }
+ .tile {
+ return gx.TextCfg{
+ color: app.theme.text_color
+ align: .center
+ vertical_align: .middle
+ size: app.ui.font_size
+ }
+ }
+ .victory {
+ return gx.TextCfg{
+ color: app.theme.victory_color
+ align: .center
+ vertical_align: .middle
+ size: app.ui.font_size * 2
+ }
+ }
+ .game_over {
+ return gx.TextCfg{
+ color: app.theme.game_over_color
+ align: .center
+ vertical_align: .middle
+ size: app.ui.font_size * 2
+ }
+ }
+ .score_end {
+ return gx.TextCfg{
+ color: gx.white
+ align: .center
+ vertical_align: .middle
+ size: app.ui.font_size * 3 / 4
+ }
+ }
+ }
+}
+
+[inline]
+fn (mut app App) set_theme(idx int) {
+ theme := themes[idx]
+ app.theme_idx = idx
+ app.theme = theme
+ app.gg.set_bg_color(theme.bg_color)
+}
+
+fn (mut app App) resize() {
+ mut s := gg.dpi_scale()
+ if s == 0.0 {
+ s = 1.0
+ }
+ window_size := gg.window_size()
+ w := window_size.width
+ h := window_size.height
+ m := f32(mu.min(w, h))
+ app.ui.dpi_scale = s
+ app.ui.window_width = w
+ app.ui.window_height = h
+ app.ui.padding_size = int(m / 38)
+ app.ui.header_size = app.ui.padding_size
+ app.ui.border_size = app.ui.padding_size * 2
+ app.ui.tile_size = int((m - app.ui.padding_size * 5 - app.ui.border_size * 2) / 4)
+ app.ui.font_size = int(m / 10)
+ // If the window's height is greater than its width, center the board vertically.
+ // If not, center it horizontally
+ if w > h {
+ app.ui.y_padding = 0
+ app.ui.x_padding = (app.ui.window_width - app.ui.window_height) / 2
+ } else {
+ app.ui.y_padding = (app.ui.window_height - app.ui.window_width - app.ui.header_size) / 2
+ app.ui.x_padding = 0
+ }
+}
+
+fn (app &App) draw() {
+ xpad, ypad := app.ui.x_padding, app.ui.y_padding
+ ww := app.ui.window_width
+ wh := app.ui.window_height
+ m := mu.min(ww, wh)
+ labelx := xpad + app.ui.border_size
+ labely := ypad + app.ui.border_size / 2
+ app.draw_tiles()
+ // TODO: Make transparency work in `gg`
+ if app.state == .over {
+ app.gg.draw_rect(0, 0, ww, wh, gx.rgba(10, 0, 0, 180))
+ app.gg.draw_text(ww / 2, (m * 4 / 10) + ypad, 'Game Over', app.label_format(.game_over))
+ f := app.label_format(.tile)
+ msg := $if android { 'Tap to restart' } $else { 'Press `r` to restart' }
+ app.gg.draw_text(ww / 2, (m * 6 / 10) + ypad, msg, gx.TextCfg{
+ ...f
+ color: gx.white
+ size: f.size * 3 / 4
+ })
+ }
+ if app.state == .victory {
+ app.gg.draw_rect(0, 0, ww, wh, gx.rgba(0, 10, 0, 180))
+ app.gg.draw_text(ww / 2, (m * 4 / 10) + ypad, 'Victory!', app.label_format(.victory))
+ // f := app.label_format(.tile)
+ msg1 := $if android { 'Tap to continue' } $else { 'Press `space` to continue' }
+ msg2 := $if android { 'Tap to restart' } $else { 'Press `r` to restart' }
+ app.gg.draw_text(ww / 2, (m * 6 / 10) + ypad, msg1, app.label_format(.score_end))
+ app.gg.draw_text(ww / 2, (m * 8 / 10) + ypad, msg2, app.label_format(.score_end))
+ }
+ // Draw at the end, so that it's on top of the victory / game over overlays
+ app.gg.draw_text(labelx, labely, 'Points: $app.board.points', app.label_format(.points))
+ app.gg.draw_text(ww - labelx, labely, 'Moves: $app.moves', app.label_format(.moves))
+}
+
+fn (app &App) draw_tiles() {
+ xstart := app.ui.x_padding + app.ui.border_size
+ ystart := app.ui.y_padding + app.ui.border_size + app.ui.header_size
+ toffset := app.ui.tile_size + app.ui.padding_size
+ tiles_size := mu.min(app.ui.window_width, app.ui.window_height) - app.ui.border_size * 2
+ // Draw the padding around the tiles
+ app.gg.draw_rounded_rect(xstart, ystart, tiles_size, tiles_size, tiles_size / 24,
+ app.theme.padding_color)
+ // Draw the actual tiles
+ for y in 0 .. 4 {
+ for x in 0 .. 4 {
+ tidx := app.board.field[y][x]
+ tile_color := if tidx < app.theme.tile_colors.len {
+ app.theme.tile_colors[tidx]
+ } else {
+ // If there isn't a specific color for this tile, reuse the last color available
+ app.theme.tile_colors.last()
+ }
+ anim_size := animation_length - app.atickers[y][x]
+ tw := int(f64(app.ui.tile_size) / animation_length * anim_size)
+ th := tw // square tiles, w == h
+ xoffset := xstart + app.ui.padding_size + x * toffset + (app.ui.tile_size - tw) / 2
+ yoffset := ystart + app.ui.padding_size + y * toffset + (app.ui.tile_size - th) / 2
+ app.gg.draw_rounded_rect(xoffset, yoffset, tw, th, tw / 8, tile_color)
+ if tidx != 0 { // 0 == blank spot
+ xpos := xoffset + tw / 2
+ ypos := yoffset + th / 2
+ mut fmt := app.label_format(.tile)
+ fmt = gx.TextCfg{
+ ...fmt
+ size: int(f32(fmt.size - 1) / animation_length * anim_size)
+ }
+ match app.tile_format {
+ .normal {
+ app.gg.draw_text(xpos, ypos, '${1 << tidx}', fmt)
+ }
+ .log {
+ app.gg.draw_text(xpos, ypos, '$tidx', fmt)
+ }
+ .exponent {
+ app.gg.draw_text(xpos, ypos, '2', fmt)
+ fs2 := int(f32(fmt.size) * 0.67)
+ app.gg.draw_text(xpos + app.ui.tile_size / 10, ypos - app.ui.tile_size / 8,
+ '$tidx', gx.TextCfg{
+ ...fmt
+ size: fs2
+ align: gx.HorizontalAlign.left
+ })
+ }
+ .shifts {
+ fs2 := int(f32(fmt.size) * 0.6)
+ app.gg.draw_text(xpos, ypos, '2<<${tidx - 1}', gx.TextCfg{
+ ...fmt
+ size: fs2
+ })
+ }
+ .none_ {} // Don't draw any text here, colors only
+ .end_ {} // Should never get here
+ }
+ }
+ }
+ }
+}
+
+fn (mut app App) handle_touches() {
+ s, e := app.touch.start, app.touch.end
+ adx, ady := mu.abs(e.pos.x - s.pos.x), mu.abs(e.pos.y - s.pos.y)
+ if mu.max(adx, ady) < 10 {
+ app.handle_tap()
+ } else {
+ app.handle_swipe()
+ }
+}
+
+fn (mut app App) handle_tap() {
+ _, ypad := app.ui.x_padding, app.ui.y_padding
+ w, h := app.ui.window_width, app.ui.window_height
+ m := mu.min(w, h)
+ s, e := app.touch.start, app.touch.end
+ avgx, avgy := avg(s.pos.x, e.pos.x), avg(s.pos.y, e.pos.y)
+ // TODO: Replace "touch spots" with actual buttons
+ // bottom left -> change theme
+ if avgx < 50 && h - avgy < 50 {
+ app.next_theme()
+ }
+ // bottom right -> change tile format
+ if w - avgx < 50 && h - avgy < 50 {
+ app.next_tile_format()
+ }
+ if app.state == .victory {
+ if avgy > (m / 2) + ypad {
+ if avgy < (m * 7 / 10) + ypad {
+ app.state = .freeplay
+ } else if avgy < (m * 9 / 10) + ypad {
+ app.new_game()
+ } else {
+ // TODO remove and implement an actual way to toggle themes on mobile
+ }
+ }
+ } else if app.state == .over {
+ if avgy > (m / 2) + ypad && avgy < (m * 7 / 10) + ypad {
+ app.new_game()
+ }
+ }
+}
+
+fn (mut app App) handle_swipe() {
+ // Currently, swipes are only used to move the tiles.
+ // If the user's not playing, exit early to avoid all the unnecessary calculations
+ if app.state !in [.play, .freeplay] {
+ return
+ }
+ s, e := app.touch.start, app.touch.end
+ w, h := app.ui.window_width, app.ui.window_height
+ dx, dy := e.pos.x - s.pos.x, e.pos.y - s.pos.y
+ adx, ady := mu.abs(dx), mu.abs(dy)
+ dmin := if mu.min(adx, ady) > 0 { mu.min(adx, ady) } else { 1 }
+ dmax := if mu.max(adx, ady) > 0 { mu.max(adx, ady) } else { 1 }
+ tdiff := int(e.time.unix_time_milli() - s.time.unix_time_milli())
+ // TODO: make this calculation more accurate (don't use arbitrary numbers)
+ min_swipe_distance := int(math.sqrt(mu.min(w, h) * tdiff / 100)) + 20
+ if dmax < min_swipe_distance {
+ return
+ }
+ // Swipe was too short
+ if dmax / dmin < 2 {
+ return
+ }
+ // Swiped diagonally
+ if adx > ady {
+ if dx < 0 {
+ app.move(.left)
+ } else {
+ app.move(.right)
+ }
+ } else {
+ if dy < 0 {
+ app.move(.up)
+ } else {
+ app.move(.down)
+ }
+ }
+}
+
+[inline]
+fn (mut app App) next_theme() {
+ app.set_theme(if app.theme_idx == themes.len - 1 { 0 } else { app.theme_idx + 1 })
+}
+
+[inline]
+fn (mut app App) next_tile_format() {
+ app.tile_format = TileFormat(int(app.tile_format) + 1)
+ if app.tile_format == .end_ {
+ app.tile_format = .normal
+ }
+}
+
+[inline]
+fn (mut app App) undo() {
+ if app.undo.len > 0 {
+ undo := app.undo.pop()
+ app.board = undo.board
+ app.state = undo.state
+ app.moves--
+ }
+}
+
+fn (mut app App) on_key_down(key gg.KeyCode) {
+ // these keys are independent from the game state:
+ match key {
+ .a { app.is_ai_mode = !app.is_ai_mode }
+ .escape { exit(0) }
+ .n, .r { app.new_game() }
+ .backspace { app.undo() }
+ .enter { app.next_tile_format() }
+ .j { app.state = .over }
+ .t { app.next_theme() }
+ else {}
+ }
+ if app.state in [.play, .freeplay] {
+ match key {
+ .w, .up { app.move(.up) }
+ .a, .left { app.move(.left) }
+ .s, .down { app.move(.down) }
+ .d, .right { app.move(.right) }
+ else {}
+ }
+ }
+ if app.state == .victory {
+ if key == .space {
+ app.state = .freeplay
+ }
+ }
+}
+
+fn on_event(e &gg.Event, mut app App) {
+ match e.typ {
+ .key_down {
+ app.on_key_down(e.key_code)
+ }
+ .resized, .restored, .resumed {
+ app.resize()
+ }
+ .touches_began {
+ if e.num_touches > 0 {
+ t := e.touches[0]
+ app.touch.start = Touch{
+ pos: Pos{
+ x: int(t.pos_x / app.ui.dpi_scale)
+ y: int(t.pos_y / app.ui.dpi_scale)
+ }
+ time: time.now()
+ }
+ }
+ }
+ .touches_ended {
+ if e.num_touches > 0 {
+ t := e.touches[0]
+ app.touch.end = Touch{
+ pos: Pos{
+ x: int(t.pos_x / app.ui.dpi_scale)
+ y: int(t.pos_y / app.ui.dpi_scale)
+ }
+ time: time.now()
+ }
+ app.handle_touches()
+ }
+ }
+ .mouse_down {
+ app.touch.start = Touch{
+ pos: Pos{
+ x: int(e.mouse_x / app.ui.dpi_scale)
+ y: int(e.mouse_y / app.ui.dpi_scale)
+ }
+ time: time.now()
+ }
+ }
+ .mouse_up {
+ app.touch.end = Touch{
+ pos: Pos{
+ x: int(e.mouse_x / app.ui.dpi_scale)
+ y: int(e.mouse_y / app.ui.dpi_scale)
+ }
+ time: time.now()
+ }
+ app.handle_touches()
+ }
+ else {}
+ }
+}
+
+fn frame(mut app App) {
+ $if showfps ? {
+ app.perf.frame_sw.restart()
+ }
+ app.gg.begin()
+ app.update_tickers()
+ app.draw()
+ app.perf.frame++
+ if app.is_ai_mode && app.state in [.play, .freeplay] && app.perf.frame % frames_per_ai_move == 0 {
+ app.ai_move()
+ }
+ $if showfps ? {
+ app.showfps()
+ }
+ app.gg.end()
+}
+
+fn init(mut app App) {
+ app.resize()
+ $if showfps ? {
+ app.perf.frame_sw.restart()
+ app.perf.second_sw.restart()
+ }
+}
+
+fn (mut app App) showfps() {
+ println(app.perf.frame_sw.elapsed().microseconds())
+ f := app.perf.frame
+ if (f & 127) == 0 {
+ last_frame_us := app.perf.frame_sw.elapsed().microseconds()
+ ticks := f64(app.perf.second_sw.elapsed().milliseconds())
+ fps := f64(app.perf.frame - app.perf.frame_old) * ticks / 1000 / 4.5
+ last_fps := 128000.0 / ticks
+ 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')
+ app.perf.second_sw.restart()
+ app.perf.frame_old = f
+ }
+}
+
+fn main() {
+ mut app := &App{}
+ app.new_game()
+ mut font_path := os.resource_abs_path(os.join_path('../assets/fonts/', 'RobotoMono-Regular.ttf'))
+ $if android {
+ font_path = 'fonts/RobotoMono-Regular.ttf'
+ }
+ mut window_title_ := 'V 2048'
+ // TODO: Make emcc a real platform ifdef
+ $if emscripten ? {
+ // in emscripten, sokol uses `window_title` as the selector to the canvas it'll render to,
+ // and since `document.querySelector('V 2048')` isn't valid JS, we use `canvas` instead
+ window_title_ = 'canvas'
+ }
+ app.perf = &Perf{}
+ app.gg = gg.new_context(
+ bg_color: app.theme.bg_color
+ width: default_window_width
+ height: default_window_height
+ sample_count: 4 // higher quality curves
+ create_window: true
+ window_title: window_title_
+ frame_fn: frame
+ event_fn: on_event
+ init_fn: init
+ user_data: app
+ font_path: font_path
+ )
+ app.gg.run()
+}