examples: add 2048 game
|
@ -0,0 +1,2 @@
|
|||
2048
|
||||
main
|
|
@ -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.
|
|
@ -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`
|
||||
|
After Width: | Height: | Size: 8.3 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 9.6 KiB |
|
@ -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()
|
||||
}
|
|
@ -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: []
|
||||
}
|