examples: add 2048 game

pull/6163/head
Delyan Angelov 2020-08-18 09:07:06 +03:00
parent b5b53a5311
commit ed156838e6
18 changed files with 518 additions and 0 deletions

2
examples/2048/.gitignore vendored 100644
View File

@ -0,0 +1,2 @@
2048
main

View File

@ -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.

View File

@ -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`

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -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()
}

View File

@ -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: []
}