diff --git a/examples/2048/.gitignore b/examples/2048/.gitignore new file mode 100644 index 0000000000..e324642a4c --- /dev/null +++ b/examples/2048/.gitignore @@ -0,0 +1,2 @@ +2048 +main diff --git a/examples/2048/LICENSE b/examples/2048/LICENSE new file mode 100644 index 0000000000..7897d4df1e --- /dev/null +++ b/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/examples/2048/README.md b/examples/2048/README.md new file mode 100644 index 0000000000..d5371def92 --- /dev/null +++ b/examples/2048/README.md @@ -0,0 +1,20 @@ +# V 2048 + +This is a simple [2048 game](https://play2048.co/), written in [the V programming language](https://vlang.io/). +![2048 Game Screenshot](https://url4e.com/gyazo/images/1ad829cf.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 + +UP,LEFT,DOWN,RIGHT or W,A,S,D - move the tiles + +## Running instructions: +Compile & run the game with `./v run examples/2048` + diff --git a/examples/2048/assets/1.png b/examples/2048/assets/1.png new file mode 100644 index 0000000000..e4cd6d7497 Binary files /dev/null and b/examples/2048/assets/1.png differ diff --git a/examples/2048/assets/1024.png b/examples/2048/assets/1024.png new file mode 100644 index 0000000000..2b8da8d5b2 Binary files /dev/null and b/examples/2048/assets/1024.png differ diff --git a/examples/2048/assets/128.png b/examples/2048/assets/128.png new file mode 100644 index 0000000000..811a5b967e Binary files /dev/null and b/examples/2048/assets/128.png differ diff --git a/examples/2048/assets/16.png b/examples/2048/assets/16.png new file mode 100644 index 0000000000..7b77193cc7 Binary files /dev/null and b/examples/2048/assets/16.png differ diff --git a/examples/2048/assets/2.png b/examples/2048/assets/2.png new file mode 100644 index 0000000000..e1de4367f0 Binary files /dev/null and b/examples/2048/assets/2.png differ diff --git a/examples/2048/assets/2048.png b/examples/2048/assets/2048.png new file mode 100644 index 0000000000..5cc1469cee Binary files /dev/null and b/examples/2048/assets/2048.png differ diff --git a/examples/2048/assets/256.png b/examples/2048/assets/256.png new file mode 100644 index 0000000000..afe02713c5 Binary files /dev/null and b/examples/2048/assets/256.png differ diff --git a/examples/2048/assets/32.png b/examples/2048/assets/32.png new file mode 100644 index 0000000000..7d02f06e74 Binary files /dev/null and b/examples/2048/assets/32.png differ diff --git a/examples/2048/assets/4.png b/examples/2048/assets/4.png new file mode 100644 index 0000000000..faa6476104 Binary files /dev/null and b/examples/2048/assets/4.png differ diff --git a/examples/2048/assets/512.png b/examples/2048/assets/512.png new file mode 100644 index 0000000000..ed25482b39 Binary files /dev/null and b/examples/2048/assets/512.png differ diff --git a/examples/2048/assets/64.png b/examples/2048/assets/64.png new file mode 100644 index 0000000000..28d5d4af36 Binary files /dev/null and b/examples/2048/assets/64.png differ diff --git a/examples/2048/assets/8.png b/examples/2048/assets/8.png new file mode 100644 index 0000000000..6e69a1e954 Binary files /dev/null and b/examples/2048/assets/8.png differ diff --git a/examples/2048/assets/victory.png b/examples/2048/assets/victory.png new file mode 100644 index 0000000000..6d0f49b585 Binary files /dev/null and b/examples/2048/assets/victory.png differ diff --git a/examples/2048/main.v b/examples/2048/main.v new file mode 100644 index 0000000000..795e046d0a --- /dev/null +++ b/examples/2048/main.v @@ -0,0 +1,468 @@ +import gg +import gx +import os +import rand +import sokol.sapp + +struct Tile { + id int + points int + picname string +} + +struct Pos { + x int = -1 + y int = -1 +} + +struct ImageLabel { + pos Pos + dim Pos +} + +struct TextLabel { + text string + pos Pos + cfg gx.TextCfg +} + +const ( + window_title = 'V 2048' + window_width = 562 + window_height = 580 + points_label = TextLabel{ + text: 'Points: ' + pos: Pos{10, 5} + cfg: gx.TextCfg{ + align: gx.align_left + size: 24 + color: gx.rgb(0, 0, 0) + } + } + moves_label = TextLabel{ + text: 'Moves: ' + pos: Pos{window_width - 160, 5} + cfg: gx.TextCfg{ + align: gx.align_left + size: 24 + color: gx.rgb(0, 0, 0) + } + } + game_over_label = TextLabel{ + text: 'Game Over' + pos: Pos{80, 220} + cfg: gx.TextCfg{ + align: gx.align_left + size: 100 + color: gx.rgb(0, 0, 255) + } + } + victory_image_label = ImageLabel{ + pos: Pos{80, 220} + dim: Pos{430, 130} + } + all_tiles = [ + Tile{0, 0, '1.png'}, + Tile{1, 2, '2.png'}, + Tile{2, 4, '4.png'}, + Tile{3, 8, '8.png'}, + Tile{4, 16, '16.png'}, + Tile{5, 32, '32.png'}, + Tile{6, 64, '64.png'}, + Tile{7, 128, '128.png'}, + Tile{8, 256, '256.png'}, + Tile{9, 512, '512.png'}, + Tile{10, 1024, '1024.png'}, + Tile{11, 2048, '2048.png'}, + ] +) + +struct TileImage { + tile Tile +mut: + image gg.Image +} + +struct Board { +mut: + field [4][4]int + points int + shifts int +} + +fn new_board(sb []string) Board { + mut b := Board{} + for y := 0; y < 4; y++ { + for x := 0; x < 4; x++ { + b.field[y][x] = sb[y][x] - 64 + } + } + return b +} + +fn (b Board) transpose() Board { + mut res := b + for y := 0; y < 4; y++ { + for x := 0; x < 4; x++ { + res.field[y][x] = b.field[x][y] + } + } + return res +} + +fn (b Board) hmirror() Board { + mut res := b + for y := 0; y < 4; y++ { + for x := 0; x < 4; x++ { + res.field[y][x] = b.field[y][4 - x - 1] + } + } + return res +} + +struct TileLine { + ypos int +mut: + field [5]int + points int + shifts int +} + +// +fn (t TileLine) to_left() TileLine { + right_border_idx := 5 + mut res := t + mut zeros := 0 + mut nonzeros := 0 + // gather meta info about the line: + for x := 0; x < 4; x++ { + if res.field[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 += all_tiles[res.field[x]].points + } + } + return res +} + +fn (b Board) to_left() Board { + mut res := b + for y := 0; y < 4; y++ { + mut hline := TileLine{y} + for x := 0; x < 4; x++ { + hline.field[x] = b.field[y][x] + } + reshline := hline.to_left() + res.shifts += reshline.shifts + res.points += reshline.points + for x := 0; x < 4; x++ { + res.field[y][x] = reshline.field[x] + } + } + return res +} + +// +enum GameState { + play + over + victory +} + +struct App { +mut: + gg &gg.Context + tiles []TileImage + victory_image gg.Image + // + board Board + undo []Board + atickers [4][4]int + state GameState = .play + moves int +} + +fn (mut app App) new_image(imagename string) gg.Image { + ipath := os.resource_abs_path(os.join_path('assets', imagename)) + return app.gg.create_image(ipath) +} + +fn (mut app App) new_tile(t Tile) TileImage { + mut timage := TileImage{ + tile: t + } + timage.image = app.new_image(t.picname) + return timage +} + +fn (mut app App) load_tiles() { + for t in all_tiles { + app.tiles << app.new_tile(t) + } +} + +fn (mut app App) update_tickers() { + for y := 0; y < 4; y++ { + for x := 0; x < 4; x++ { + mut old := app.atickers[y][x] + if old > 0 { + old-- + app.atickers[y][x] = old + } + } + } +} + +fn (app &App) draw() { + app.draw_background() + app.draw_tiles() + plabel := '$points_label.text ${app.board.points:08}' + mlabel := '$moves_label.text ${app.moves:5d}' + app.gg.draw_text(points_label.pos.x, points_label.pos.y, plabel, points_label.cfg) + app.gg.draw_text(moves_label.pos.x, moves_label.pos.y, mlabel, moves_label.cfg) + if app.state == .over { + app.gg.draw_text(game_over_label.pos.x, game_over_label.pos.y, game_over_label.text, + game_over_label.cfg) + } + if app.state == .victory { + app.gg.draw_image(victory_image_label.pos.x, victory_image_label.pos.y, victory_image_label.dim.x, + victory_image_label.dim.y, app.victory_image) + } +} + +fn (app &App) draw_background() { + tw, th := 128, 128 + for y := 30; y <= window_height; y+=tw { + for x := 0; x <= window_width; x+=th { + app.gg.draw_image(x, y, tw, th, app.tiles[0].image) + } + } +} + +fn (app &App) draw_tiles() { + border := 10 + xstart := 10 + ystart := 30 + tsize := 128 + for y := 0; y < 4; y++ { + for x := 0; x < 4; x++ { + tidx := app.board.field[y][x] + if tidx == 0 { + continue + } + tile := app.tiles[tidx] + tw := tsize - 10 * app.atickers[y][x] + th := tsize - 10 * app.atickers[y][x] + tx := xstart + x * (tsize + border) + (tsize - tw) / 2 + ty := ystart + y * (tsize + border) + (tsize - th) / 2 + app.gg.draw_image(tx, ty, tw, th, tile.image) + } + } +} + +fn (mut app App) new_game() { + app.board = Board{} + for y := 0; y < 4; y++ { + for x := 0; x < 4; x++ { + app.board.field[y][x] = 0 + app.atickers[y][x] = 0 + } + } + app.state = .play + app.undo = [] + app.moves = 0 + app.new_random_tile() + app.new_random_tile() +} + +fn (mut app App) check_for_victory() { + for y := 0; y < 4; y++ { + for x := 0; x < 4; x++ { + fidx := app.board.field[y][x] + if fidx == 11 { + app.victory() + return + } + } + } +} + +fn (mut app App) check_for_game_over() { + mut zeros := 0 + mut remaining_merges := 0 + for y := 0; y < 4; y++ { + for x := 0; x < 4; x++ { + fidx := app.board.field[y][x] + if fidx == 0 { + zeros++ + continue + } + if x > 0 && fidx == app.board.field[y][x - 1] { + remaining_merges++ + } + if x < 4 - 1 && fidx == app.board.field[y][x + 1] { + remaining_merges++ + } + if y > 0 && fidx == app.board.field[y - 1][x] { + remaining_merges++ + } + if y < 4 - 1 && fidx == app.board.field[y + 1][x] { + remaining_merges++ + } + } + } + if remaining_merges == 0 && zeros == 0 { + app.game_over() + } +} + +fn (mut app App) new_random_tile() { + mut etiles := [16]Pos{} + mut empty_tiles_max := 0 + for y := 0; y < 4; y++ { + for x := 0; x < 4; x++ { + fidx := app.board.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] + random_value := 1 + rand.intn(2) + app.board.field[empty_pos.y][empty_pos.x] = random_value + app.atickers[empty_pos.y][empty_pos.x] = 30 + } + app.check_for_victory() + app.check_for_game_over() +} + +fn (mut app App) victory() { + app.state = .victory +} + +fn (mut app App) game_over() { + app.state = .over +} + +type BoardMoveFN fn(b Board) Board +fn (mut app App) move(move_fn BoardMoveFN) { + old := app.board + new := move_fn(old) + if old.shifts != new.shifts { + app.moves++ + app.board = new + app.undo << old + app.new_random_tile() + } +} + +fn (mut app App) on_key_down(key sapp.KeyCode) { + // these keys are independent from the game state: + match key { + .escape { + exit(0) + } + .n { + app.new_game() + } + //.t {/* fast setup for a victory situation: */ app.board = new_board(['JJ@@', '@@@@', '@@@@', '@@@@'])} + .backspace { + if app.undo.len > 0 { + app.state = .play + app.board = app.undo.pop() + app.moves-- + return + } + } + else {} + } + if app.state == .play { + match key { + .up, .w { app.move(fn (b Board) Board { + return b.transpose().to_left().transpose() + }) } + .left, .a { app.move(fn (b Board) Board { + return b.to_left() + }) } + .down, .s { app.move(fn (b Board) Board { + return b.transpose().hmirror().to_left().hmirror().transpose() + }) } + .right, .d { app.move(fn (b Board) Board { + return b.hmirror().to_left().hmirror() + }) } + else {} + } + } +} + +// +fn on_event(e &sapp.Event, mut app App) { + if e.typ == .key_down { + app.on_key_down(e.key_code) + } +} + +fn frame(mut app App) { + app.update_tickers() + app.gg.begin() + app.draw() + app.gg.end() +} + +fn main() { + mut app := &App{ + gg: 0 + state: .play + } + app.new_game() + app.gg = gg.new_context({ + bg_color: gx.white + width: window_width + height: window_height + use_ortho: true + create_window: true + window_title: window_title + frame_fn: frame + event_fn: on_event + user_data: app + font_path: os.resource_abs_path('../assets/fonts/RobotoMono-Regular.ttf') + }) + app.load_tiles() + app.victory_image = app.new_image('victory.png') + app.gg.run() +} diff --git a/examples/2048/v.mod b/examples/2048/v.mod new file mode 100644 index 0000000000..5c146f8d47 --- /dev/null +++ b/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: [] +}