diff options
author | Indrajith K L | 2022-12-03 17:00:20 +0530 |
---|---|---|
committer | Indrajith K L | 2022-12-03 17:00:20 +0530 |
commit | f5c4671bfbad96bf346bd7e9a21fc4317b4959df (patch) | |
tree | 2764fc62da58f2ba8da7ed341643fc359873142f /v_windows/v/examples/2048 | |
download | cli-tools-windows-master.tar.gz cli-tools-windows-master.tar.bz2 cli-tools-windows-master.zip |
Diffstat (limited to 'v_windows/v/examples/2048')
-rw-r--r-- | v_windows/v/examples/2048/.gitignore | 2 | ||||
-rw-r--r-- | v_windows/v/examples/2048/2048.v | 939 | ||||
-rw-r--r-- | v_windows/v/examples/2048/LICENSE | 21 | ||||
-rw-r--r-- | v_windows/v/examples/2048/README.md | 25 | ||||
-rw-r--r-- | v_windows/v/examples/2048/demo.png | bin | 0 -> 19426 bytes | |||
-rw-r--r-- | v_windows/v/examples/2048/v.mod | 7 |
6 files changed, 994 insertions, 0 deletions
diff --git a/v_windows/v/examples/2048/.gitignore b/v_windows/v/examples/2048/.gitignore new file mode 100644 index 0000000..e324642 --- /dev/null +++ b/v_windows/v/examples/2048/.gitignore @@ -0,0 +1,2 @@ +2048 +main 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() +} diff --git a/v_windows/v/examples/2048/LICENSE b/v_windows/v/examples/2048/LICENSE new file mode 100644 index 0000000..7897d4d --- /dev/null +++ b/v_windows/v/examples/2048/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Delyan Angelov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/v_windows/v/examples/2048/README.md b/v_windows/v/examples/2048/README.md new file mode 100644 index 0000000..15516fb --- /dev/null +++ b/v_windows/v/examples/2048/README.md @@ -0,0 +1,25 @@ +# V 2048 + +This is a simple 2048 game, written in [the V programming language](https://vlang.io/). + +WebAssembly demo: https://v2048.vercel.app + +![screenshot](demo.png) + +## Description: +Merge tiles by moving them. +After each move, a new random tile is added (2 or 4). +The goal of the game is to create a tile with a value of 2048. + +## Keys: +Escape - exit the game +Backspace - undo last move +n - restart the game +t - toggle the UI theme +Enter - toggle the tile text format + +UP,LEFT,DOWN,RIGHT / W,A,S,D / touchscreen swipes - move the tiles + +## Running instructions: +Compile & run the game with `./v run examples/2048` + diff --git a/v_windows/v/examples/2048/demo.png b/v_windows/v/examples/2048/demo.png Binary files differnew file mode 100644 index 0000000..fcd4684 --- /dev/null +++ b/v_windows/v/examples/2048/demo.png diff --git a/v_windows/v/examples/2048/v.mod b/v_windows/v/examples/2048/v.mod new file mode 100644 index 0000000..5c146f8 --- /dev/null +++ b/v_windows/v/examples/2048/v.mod @@ -0,0 +1,7 @@ +Module { + name: 'v2048', + description: 'A simple 2048 game written in V.', + version: '0.0.2', + repo_url: 'https://github.com/spytheman/v2048', + dependencies: [] +} |