examples/2048: some updates and improvements (#6343)
|
@ -3,11 +3,125 @@ import gx
|
||||||
import os
|
import os
|
||||||
import rand
|
import rand
|
||||||
import sokol.sapp
|
import sokol.sapp
|
||||||
|
import time
|
||||||
|
|
||||||
struct Tile {
|
struct App {
|
||||||
id int
|
mut:
|
||||||
points int
|
gg &gg.Context = 0
|
||||||
picname string
|
touch TouchInfo
|
||||||
|
ui Ui
|
||||||
|
theme &Theme = themes[0]
|
||||||
|
theme_idx int
|
||||||
|
board Board
|
||||||
|
undo []Board
|
||||||
|
atickers [4][4]int
|
||||||
|
state GameState = .play
|
||||||
|
tile_format TileFormat = .normal
|
||||||
|
moves int
|
||||||
|
perf &Perf = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Ui {
|
||||||
|
mut:
|
||||||
|
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) // 2048
|
||||||
|
]
|
||||||
|
},
|
||||||
|
&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
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 {
|
struct Pos {
|
||||||
|
@ -15,74 +129,6 @@ struct Pos {
|
||||||
y 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: .left
|
|
||||||
size: 24
|
|
||||||
color: gx.rgb(0, 0, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
moves_label = TextLabel{
|
|
||||||
text: 'Moves: '
|
|
||||||
pos: Pos{window_width - 160, 5}
|
|
||||||
cfg: gx.TextCfg{
|
|
||||||
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: .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 {
|
struct Board {
|
||||||
mut:
|
mut:
|
||||||
field [4][4]int
|
field [4][4]int
|
||||||
|
@ -90,36 +136,6 @@ mut:
|
||||||
shifts 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 {
|
struct TileLine {
|
||||||
ypos int
|
ypos int
|
||||||
mut:
|
mut:
|
||||||
|
@ -128,6 +144,78 @@ mut:
|
||||||
shifts int
|
shifts int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TouchInfo {
|
||||||
|
mut:
|
||||||
|
start_pos Pos
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TileFormat {
|
||||||
|
normal
|
||||||
|
log
|
||||||
|
exponent
|
||||||
|
shifts
|
||||||
|
none_
|
||||||
|
end_ // To know when to wrap around
|
||||||
|
}
|
||||||
|
|
||||||
|
enum GameState {
|
||||||
|
play
|
||||||
|
over
|
||||||
|
victory
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LabelKind {
|
||||||
|
points
|
||||||
|
moves
|
||||||
|
tile
|
||||||
|
victory
|
||||||
|
game_over
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Direction {
|
||||||
|
up
|
||||||
|
down
|
||||||
|
left
|
||||||
|
right
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility functions, to avoid importing `math`
|
||||||
|
[inline]
|
||||||
|
fn min(a, b int) int {
|
||||||
|
if a < b { return a } else { return b }
|
||||||
|
}
|
||||||
|
|
||||||
|
[inline]
|
||||||
|
fn max(a, b int) int {
|
||||||
|
if a > b { return a } else { return b }
|
||||||
|
}
|
||||||
|
|
||||||
|
[inline]
|
||||||
|
fn abs(a int) int {
|
||||||
|
if a < 0 { return -a } else { return a }
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// GCC optimization bug; inlining fails when compiled with -prod
|
||||||
[no_inline]
|
[no_inline]
|
||||||
fn (t TileLine) to_left() TileLine {
|
fn (t TileLine) to_left() TileLine {
|
||||||
right_border_idx := 5
|
right_border_idx := 5
|
||||||
|
@ -135,12 +223,8 @@ fn (t TileLine) to_left() TileLine {
|
||||||
mut zeros := 0
|
mut zeros := 0
|
||||||
mut nonzeros := 0
|
mut nonzeros := 0
|
||||||
// gather meta info about the line:
|
// gather meta info about the line:
|
||||||
for x := 0; x < 4; x++ {
|
for x in res.field {
|
||||||
if res.field[x] == 0 {
|
if x == 0 { zeros++ } else { nonzeros++ }
|
||||||
zeros++
|
|
||||||
} else {
|
|
||||||
nonzeros++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if nonzeros == 0 {
|
if nonzeros == 0 {
|
||||||
// when all the tiles are empty, there is nothing left to do
|
// when all the tiles are empty, there is nothing left to do
|
||||||
|
@ -171,7 +255,7 @@ fn (t TileLine) to_left() TileLine {
|
||||||
}
|
}
|
||||||
res.shifts++
|
res.shifts++
|
||||||
res.field[x]++
|
res.field[x]++
|
||||||
res.points += all_tiles[res.field[x]].points
|
res.points += 1 << res.field[x]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
|
@ -179,65 +263,24 @@ fn (t TileLine) to_left() TileLine {
|
||||||
|
|
||||||
fn (b Board) to_left() Board {
|
fn (b Board) to_left() Board {
|
||||||
mut res := b
|
mut res := b
|
||||||
for y := 0; y < 4; y++ {
|
for y in 0..4 {
|
||||||
mut hline := TileLine{
|
mut hline := TileLine{ypos: y}
|
||||||
ypos: y
|
for x in 0..4 {
|
||||||
}
|
|
||||||
for x := 0; x < 4; x++ {
|
|
||||||
hline.field[x] = b.field[y][x]
|
hline.field[x] = b.field[y][x]
|
||||||
}
|
}
|
||||||
reshline := hline.to_left()
|
reshline := hline.to_left()
|
||||||
res.shifts += reshline.shifts
|
res.shifts += reshline.shifts
|
||||||
res.points += reshline.points
|
res.points += reshline.points
|
||||||
for x := 0; x < 4; x++ {
|
for x in 0..4 {
|
||||||
res.field[y][x] = reshline.field[x]
|
res.field[y][x] = reshline.field[x]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res
|
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() {
|
fn (mut app App) update_tickers() {
|
||||||
for y := 0; y < 4; y++ {
|
for y in 0..4 {
|
||||||
for x := 0; x < 4; x++ {
|
for x in 0..4 {
|
||||||
mut old := app.atickers[y][x]
|
mut old := app.atickers[y][x]
|
||||||
if old > 0 {
|
if old > 0 {
|
||||||
old--
|
old--
|
||||||
|
@ -247,57 +290,10 @@ fn (mut app App) update_tickers() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
fn (mut app App) new_game() {
|
||||||
app.board = Board{}
|
app.board = Board{}
|
||||||
for y := 0; y < 4; y++ {
|
for y in 0..4 {
|
||||||
for x := 0; x < 4; x++ {
|
for x in 0..4 {
|
||||||
app.board.field[y][x] = 0
|
app.board.field[y][x] = 0
|
||||||
app.atickers[y][x] = 0
|
app.atickers[y][x] = 0
|
||||||
}
|
}
|
||||||
|
@ -310,8 +306,8 @@ fn (mut app App) new_game() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn (mut app App) check_for_victory() {
|
fn (mut app App) check_for_victory() {
|
||||||
for y := 0; y < 4; y++ {
|
for y in 0..4 {
|
||||||
for x := 0; x < 4; x++ {
|
for x in 0..4 {
|
||||||
fidx := app.board.field[y][x]
|
fidx := app.board.field[y][x]
|
||||||
if fidx == 11 {
|
if fidx == 11 {
|
||||||
app.victory()
|
app.victory()
|
||||||
|
@ -324,23 +320,17 @@ fn (mut app App) check_for_victory() {
|
||||||
fn (mut app App) check_for_game_over() {
|
fn (mut app App) check_for_game_over() {
|
||||||
mut zeros := 0
|
mut zeros := 0
|
||||||
mut remaining_merges := 0
|
mut remaining_merges := 0
|
||||||
for y := 0; y < 4; y++ {
|
for y in 0..4 {
|
||||||
for x := 0; x < 4; x++ {
|
for x in 0..4 {
|
||||||
fidx := app.board.field[y][x]
|
fidx := app.board.field[y][x]
|
||||||
if fidx == 0 {
|
if fidx == 0 {
|
||||||
zeros++
|
zeros++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if x > 0 && fidx == app.board.field[y][x - 1] {
|
if (x > 0 && fidx == app.board.field[y][x - 1])
|
||||||
remaining_merges++
|
|| (x < 4 - 1 && fidx == app.board.field[y][x + 1])
|
||||||
}
|
|| (y > 0 && fidx == app.board.field[y - 1][x])
|
||||||
if x < 4 - 1 && fidx == app.board.field[y][x + 1] {
|
|| (y < 4 - 1 && fidx == app.board.field[y + 1][x]) {
|
||||||
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++
|
remaining_merges++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -353,11 +343,12 @@ fn (mut app App) check_for_game_over() {
|
||||||
fn (mut app App) new_random_tile() {
|
fn (mut app App) new_random_tile() {
|
||||||
mut etiles := [16]Pos{}
|
mut etiles := [16]Pos{}
|
||||||
mut empty_tiles_max := 0
|
mut empty_tiles_max := 0
|
||||||
for y := 0; y < 4; y++ {
|
for y in 0..4 {
|
||||||
for x := 0; x < 4; x++ {
|
for x in 0..4 {
|
||||||
fidx := app.board.field[y][x]
|
fidx := app.board.field[y][x]
|
||||||
if fidx == 0 {
|
if fidx == 0 {
|
||||||
etiles[empty_tiles_max] = Pos{x, y}
|
etiles[empty_tiles_max] = Pos{x, y}
|
||||||
|
app.atickers[y][x] = 0
|
||||||
empty_tiles_max++
|
empty_tiles_max++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -365,9 +356,10 @@ fn (mut app App) new_random_tile() {
|
||||||
if empty_tiles_max > 0 {
|
if empty_tiles_max > 0 {
|
||||||
new_random_tile_index := rand.intn(empty_tiles_max)
|
new_random_tile_index := rand.intn(empty_tiles_max)
|
||||||
empty_pos := etiles[new_random_tile_index]
|
empty_pos := etiles[new_random_tile_index]
|
||||||
random_value := 1 + rand.intn(2)
|
// 1/8 chance of creating a `4` tile
|
||||||
|
random_value := if rand.intn(8) == 0 { 2 } else { 1 }
|
||||||
app.board.field[empty_pos.y][empty_pos.x] = random_value
|
app.board.field[empty_pos.y][empty_pos.x] = random_value
|
||||||
app.atickers[empty_pos.y][empty_pos.x] = 30
|
app.atickers[empty_pos.y][empty_pos.x] = animation_length
|
||||||
}
|
}
|
||||||
app.check_for_victory()
|
app.check_for_victory()
|
||||||
app.check_for_game_over()
|
app.check_for_game_over()
|
||||||
|
@ -381,11 +373,14 @@ fn (mut app App) game_over() {
|
||||||
app.state = .over
|
app.state = .over
|
||||||
}
|
}
|
||||||
|
|
||||||
type BoardMoveFN = fn (b Board) Board
|
fn (mut app App) move(d Direction) {
|
||||||
|
|
||||||
fn (mut app App) move(move_fn BoardMoveFN) {
|
|
||||||
old := app.board
|
old := app.board
|
||||||
new := move_fn(old)
|
new := match d {
|
||||||
|
.left { old.to_left() }
|
||||||
|
.right { old.hmirror().to_left().hmirror() }
|
||||||
|
.up { old.transpose().to_left().transpose() }
|
||||||
|
.down { old.transpose().hmirror().to_left().hmirror().transpose() }
|
||||||
|
}
|
||||||
if old.shifts != new.shifts {
|
if old.shifts != new.shifts {
|
||||||
app.moves++
|
app.moves++
|
||||||
app.board = new
|
app.board = new
|
||||||
|
@ -394,78 +389,301 @@ fn (mut app App) move(move_fn BoardMoveFN) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn (app &App) label_format(kind LabelKind) gx.TextCfg {
|
||||||
|
match kind {
|
||||||
|
.points {
|
||||||
|
return {
|
||||||
|
color: app.theme.text_color
|
||||||
|
align: .left
|
||||||
|
size: app.ui.font_size / 2
|
||||||
|
}
|
||||||
|
} .moves {
|
||||||
|
return {
|
||||||
|
color: app.theme.text_color
|
||||||
|
align: .right
|
||||||
|
size: app.ui.font_size / 2
|
||||||
|
}
|
||||||
|
} .tile {
|
||||||
|
return {
|
||||||
|
color: app.theme.text_color
|
||||||
|
align: .center
|
||||||
|
vertical_align: .middle
|
||||||
|
size: app.ui.font_size
|
||||||
|
}
|
||||||
|
} .victory {
|
||||||
|
return {
|
||||||
|
color: app.theme.victory_color
|
||||||
|
align: .center
|
||||||
|
vertical_align: .middle
|
||||||
|
size: app.ui.font_size * 2
|
||||||
|
}
|
||||||
|
} .game_over {
|
||||||
|
return {
|
||||||
|
color: app.theme.game_over_color
|
||||||
|
align: .center
|
||||||
|
vertical_align: .middle
|
||||||
|
size: app.ui.font_size * 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 := sapp.dpi_scale() || 1
|
||||||
|
w := int(sapp.width() / s)
|
||||||
|
h := int(sapp.height() / s)
|
||||||
|
m := f32(min(w, h))
|
||||||
|
|
||||||
|
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() {
|
||||||
|
ww := app.ui.window_width
|
||||||
|
wh := app.ui.window_height
|
||||||
|
labelx := app.ui.x_padding + app.ui.border_size
|
||||||
|
labely := app.ui.y_padding + app.ui.border_size / 2
|
||||||
|
|
||||||
|
app.draw_tiles()
|
||||||
|
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))
|
||||||
|
|
||||||
|
// TODO: Make transparency work in `gg`
|
||||||
|
if app.state == .over {
|
||||||
|
app.gg.draw_rect(0, 0, ww, wh, gx.rgba(15, 0, 0, 44))
|
||||||
|
app.gg.draw_text(ww / 2, wh / 2, 'Game Over', app.label_format(.game_over))
|
||||||
|
}
|
||||||
|
if app.state == .victory {
|
||||||
|
app.gg.draw_rect(0, 0, ww, wh, gx.rgba(0, 15, 0, 44))
|
||||||
|
app.gg.draw_text(ww / 2, wh / 2, 'Victory!', app.label_format(.victory))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 := min(app.ui.window_width, app.ui.window_height) - app.ui.border_size * 2
|
||||||
|
|
||||||
|
// Draw the padding around the tiles
|
||||||
|
app.gg.draw_rect(xstart, ystart, tiles_size, tiles_size, 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_rect(xoffset, yoffset, tw, th, tile_color)
|
||||||
|
|
||||||
|
if tidx != 0 { // 0 == blank spot
|
||||||
|
xpos := xoffset + tw / 2
|
||||||
|
ypos := yoffset + th / 2
|
||||||
|
mut fmt := app.label_format(.tile)
|
||||||
|
fmt = { 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', { fmt | size: fs2, align: gx.HorizontalAlign.left })
|
||||||
|
} .shifts {
|
||||||
|
fs2 := int(f32(fmt.size) * 0.6)
|
||||||
|
app.gg.draw_text(xpos, ypos, '2<<${tidx - 1}', { fmt | size: fs2 })
|
||||||
|
} .none_ {} // Don't draw any text here, colors only
|
||||||
|
.end_ {} // Should never get here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn (mut app App) handle_swipe(start, end Pos) {
|
||||||
|
min_swipe_distance := min(app.ui.window_width, app.ui.window_height) / 10
|
||||||
|
dx := end.x - start.x
|
||||||
|
dy := end.y - start.y
|
||||||
|
adx := abs(dx)
|
||||||
|
ady := abs(dy)
|
||||||
|
dmax := max(adx, ady)
|
||||||
|
dmin := min(adx, ady)
|
||||||
|
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn (mut app App) on_key_down(key sapp.KeyCode) {
|
fn (mut app App) on_key_down(key sapp.KeyCode) {
|
||||||
// these keys are independent from the game state:
|
// these keys are independent from the game state:
|
||||||
match key {
|
match key {
|
||||||
.escape {
|
.escape {
|
||||||
exit(0)
|
exit(0)
|
||||||
}
|
} .n {
|
||||||
.n {
|
|
||||||
app.new_game()
|
app.new_game()
|
||||||
}
|
} .backspace {
|
||||||
// .t {/* fast setup for a victory situation: */ app.board = new_board(['JJ@@', '@@@@', '@@@@', '@@@@'])}
|
|
||||||
.backspace {
|
|
||||||
if app.undo.len > 0 {
|
if app.undo.len > 0 {
|
||||||
app.state = .play
|
app.state = .play
|
||||||
app.board = app.undo.pop()
|
app.board = app.undo.pop()
|
||||||
app.moves--
|
app.moves--
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
} .enter {
|
||||||
|
app.tile_format = int(app.tile_format) + 1
|
||||||
|
if app.tile_format == .end_ {
|
||||||
|
app.tile_format = .normal
|
||||||
}
|
}
|
||||||
else {}
|
} .j {
|
||||||
|
app.game_over()
|
||||||
|
} .t {
|
||||||
|
app.set_theme(if app.theme_idx == themes.len - 1 { 0 } else { app.theme_idx + 1 })
|
||||||
|
} else {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if app.state == .play {
|
if app.state == .play {
|
||||||
match key {
|
match key {
|
||||||
.up, .w { app.move(fn (b Board) Board {
|
.w, .up { app.move(.up) }
|
||||||
return b.transpose().to_left().transpose()
|
.a, .left { app.move(.left) }
|
||||||
}) }
|
.s, .down { app.move(.down) }
|
||||||
.left, .a { app.move(fn (b Board) Board {
|
.d, .right { app.move(.right) }
|
||||||
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 {}
|
else {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
fn on_event(e &sapp.Event, mut app App) {
|
fn on_event(e &sapp.Event, mut app App) {
|
||||||
if e.typ == .key_down {
|
match e.typ {
|
||||||
|
.key_down {
|
||||||
app.on_key_down(e.key_code)
|
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_pos = { x: int(t.pos_x), y: int(t.pos_y) }
|
||||||
|
}
|
||||||
|
} .touches_ended {
|
||||||
|
if e.num_touches > 0 {
|
||||||
|
t := e.touches[0]
|
||||||
|
end_pos := Pos{ x: int(t.pos_x), y: int(t.pos_y) }
|
||||||
|
app.handle_swipe(app.touch.start_pos, end_pos)
|
||||||
|
}
|
||||||
|
} else {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn frame(mut app App) {
|
fn frame(mut app App) {
|
||||||
app.update_tickers()
|
$if showfps? { app.perf.frame_sw.restart() }
|
||||||
app.gg.begin()
|
app.gg.begin()
|
||||||
|
app.update_tickers()
|
||||||
app.draw()
|
app.draw()
|
||||||
app.gg.end()
|
app.gg.end()
|
||||||
|
$if showfps? { app.showfps() }
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
app.perf.frame++
|
||||||
|
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 := 128_000.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Move this somewhere else (vlib?) once Android support is merged
|
||||||
|
$if android {
|
||||||
|
#include <android/log.h>
|
||||||
|
#define LOG_TAG "v_logcat_test"
|
||||||
|
#define printf(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
||||||
|
#define fprintf(a, ...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
mut app := &App{
|
mut app := &App{}
|
||||||
gg: 0
|
|
||||||
state: .play
|
|
||||||
}
|
|
||||||
app.new_game()
|
app.new_game()
|
||||||
|
|
||||||
|
mut font_path := os.resource_abs_path(os.join_path('../assets/fonts/', 'RobotoMono-Regular.ttf'))
|
||||||
|
$if android {
|
||||||
|
font_path = 'assets/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'
|
||||||
|
}
|
||||||
|
|
||||||
|
$if showfps? {
|
||||||
|
app.perf = &Perf{}
|
||||||
|
}
|
||||||
|
|
||||||
app.gg = gg.new_context({
|
app.gg = gg.new_context({
|
||||||
bg_color: gx.white
|
bg_color: app.theme.bg_color
|
||||||
width: window_width
|
width: default_window_width
|
||||||
height: window_height
|
height: default_window_height
|
||||||
use_ortho: true
|
|
||||||
create_window: true
|
create_window: true
|
||||||
window_title: window_title
|
window_title: window_title
|
||||||
frame_fn: frame
|
frame_fn: frame
|
||||||
event_fn: on_event
|
event_fn: on_event
|
||||||
|
init_fn: init
|
||||||
user_data: app
|
user_data: app
|
||||||
font_path: os.resource_abs_path('../assets/fonts/RobotoMono-Regular.ttf')
|
font_path: font_path
|
||||||
})
|
})
|
||||||
app.load_tiles()
|
|
||||||
app.victory_image = app.new_image('victory.png')
|
|
||||||
app.gg.run()
|
app.gg.run()
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
# V 2048
|
# V 2048
|
||||||
|
|
||||||
This is a simple [2048 game](https://play2048.co/), written in [the V programming language](https://vlang.io/).
|
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)
|
![screenshot](demo.png)
|
||||||
|
|
||||||
## Description:
|
## Description:
|
||||||
Merge tiles by moving them.
|
Merge tiles by moving them.
|
||||||
|
@ -12,8 +12,10 @@ The goal of the game is to create a tile with a value of 2048.
|
||||||
Escape - exit the game
|
Escape - exit the game
|
||||||
Backspace - undo last move
|
Backspace - undo last move
|
||||||
n - restart the game
|
n - restart the game
|
||||||
|
t - toggle the UI theme
|
||||||
|
Enter - toggle the tile text format
|
||||||
|
|
||||||
UP,LEFT,DOWN,RIGHT or W,A,S,D - move the tiles
|
UP,LEFT,DOWN,RIGHT / W,A,S,D / touchscreen swipes - move the tiles
|
||||||
|
|
||||||
## Running instructions:
|
## Running instructions:
|
||||||
Compile & run the game with `./v run examples/2048`
|
Compile & run the game with `./v run examples/2048`
|
||||||
|
|
Before Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 9.6 KiB |
After Width: | Height: | Size: 25 KiB |