examples/2048: new end screens with alpha, better touch support (#6482)

pull/6486/head
spaceface777 2020-09-26 08:54:04 +02:00 committed by GitHub
parent af37c7ac6b
commit faca9e2f06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 196 additions and 81 deletions

View File

@ -1,5 +1,6 @@
import gg import gg
import gx import gx
import math
import os import os
import rand import rand
import sokol.sapp import sokol.sapp
@ -13,7 +14,7 @@ mut:
theme &Theme = themes[0] theme &Theme = themes[0]
theme_idx int theme_idx int
board Board board Board
undo []Board undo []Undo
atickers [4][4]int atickers [4][4]int
state GameState = .play state GameState = .play
tile_format TileFormat = .normal tile_format TileFormat = .normal
@ -24,6 +25,7 @@ mut:
struct Ui { struct Ui {
mut: mut:
dpi_scale f32
tile_size int tile_size int
border_size int border_size int
padding_size int padding_size int
@ -141,6 +143,11 @@ mut:
shifts int shifts int
} }
struct Undo {
board Board
state GameState
}
struct TileLine { struct TileLine {
ypos int ypos int
mut: mut:
@ -151,7 +158,14 @@ mut:
struct TouchInfo { struct TouchInfo {
mut: mut:
start_pos Pos start Touch
end Touch
}
struct Touch {
mut:
pos Pos
time time.Time
} }
enum TileFormat { enum TileFormat {
@ -167,6 +181,7 @@ enum GameState {
play play
over over
victory victory
freeplay
} }
enum LabelKind { enum LabelKind {
@ -185,7 +200,7 @@ enum Direction {
right right
} }
// Utility functions, to avoid importing `math` // Utility functions
[inline] [inline]
fn min(a, b int) int { fn min(a, b int) int {
if a < b { return a } else { return b } if a < b { return a } else { return b }
@ -201,6 +216,11 @@ fn abs(a int) int {
if a < 0 { return -a } else { return a } if a < 0 { return -a } else { return a }
} }
[inline]
fn avg(a, b int) int {
return (a + b) / 2
}
fn (b Board) transpose() Board { fn (b Board) transpose() Board {
mut res := b mut res := b
for y in 0..4 { for y in 0..4 {
@ -343,27 +363,29 @@ fn (mut app App) new_game() {
} }
} }
app.state = .play app.state = .play
app.undo = []Board{cap:4096} app.undo = []Undo{cap:4096}
app.moves = 0 app.moves = 0
app.new_random_tile() app.new_random_tile()
app.new_random_tile() app.new_random_tile()
} }
[inline]
fn (mut app App) check_for_victory() { fn (mut app App) check_for_victory() {
for y in 0..4 { for y in 0..4 {
for x in 0..4 { 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.state = .victory
return return
} }
} }
} }
} }
[inline]
fn (mut app App) check_for_game_over() { fn (mut app App) check_for_game_over() {
if app.board.is_game_over() { if app.board.is_game_over() {
app.game_over() app.state = .over
} }
} }
@ -403,23 +425,15 @@ fn (mut app App) new_random_tile() {
if random_value > 0 { if random_value > 0 {
app.atickers[empty_pos.y][empty_pos.x] = animation_length app.atickers[empty_pos.y][empty_pos.x] = animation_length
} }
app.check_for_victory() if app.state != .freeplay { app.check_for_victory() }
app.check_for_game_over() app.check_for_game_over()
} }
fn (mut app App) victory() {
app.state = .victory
}
fn (mut app App) game_over() {
app.state = .over
}
fn (mut app App) apply_new_board(new Board) { fn (mut app App) apply_new_board(new Board) {
old := app.board old := app.board
app.moves++ app.moves++
app.board = new app.board = new
app.undo << old app.undo << Undo{ old, app.state }
app.new_random_tile() app.new_random_tile()
} }
@ -439,11 +453,11 @@ mut:
} }
fn (p Prediction) str() string { fn (p Prediction) str() string {
return 'Prediction{move: ${p.move:5} | mpoints: ${p.mpoints:6.2f} | mcmoves: ${p.mcmoves:6.2f}}' return '{ move: ${p.move:5}, mpoints: ${p.mpoints:6.2f}, mcmoves: ${p.mcmoves:6.2f} }'
} }
fn (mut app App) ai_move() { fn (mut app App) ai_move() {
mut predictions := [4]Prediction{} mut predictions := [4]Prediction{}
mut nboard := app.board
mut is_valid := false mut is_valid := false
think_watch := time.new_stopwatch({}) think_watch := time.new_stopwatch({})
for move in possible_moves { for move in possible_moves {
@ -452,60 +466,50 @@ fn (mut app App) ai_move() {
mut mpoints := 0 mut mpoints := 0
mut mshifts := 0 mut mshifts := 0
mut mcmoves := 0 mut mcmoves := 0
for i := 0; i < predictions_per_move; i++ { for _ in 0..predictions_per_move {
mut cboard := app.board mut cboard := app.board
cboard, is_valid = cboard.move(move) cboard, is_valid = cboard.move(move)
if !is_valid { if !is_valid || cboard.is_game_over() { continue }
continue
}
if cboard.is_game_over() {
continue
}
mpoints += cboard.points mpoints += cboard.points
cboard.place_random_tile() cboard.place_random_tile()
mut cmoves := 0 mut cmoves := 0
for !cboard.is_game_over() { for !cboard.is_game_over() {
nmove := possible_moves[rand.intn(possible_moves.len)] nmove := possible_moves[rand.intn(possible_moves.len)]
nboard, is_valid = cboard.move(nmove) cboard, is_valid = cboard.move(nmove)
if !is_valid { if !is_valid { continue }
continue
}
cboard = nboard
cboard.place_random_tile() cboard.place_random_tile()
cmoves++ cmoves++
if cmoves > prediction_depth { if cmoves > prediction_depth { break }
break
}
} }
mpoints += cboard.points mpoints += cboard.points
mshifts += cboard.shifts mshifts += cboard.shifts
mcmoves += cmoves mcmoves += cmoves
} }
predictions[move_idx].mpoints = f64(mpoints)/predictions_per_move predictions[move_idx].mpoints = f64(mpoints) / predictions_per_move
predictions[move_idx].mcmoves = f64(mcmoves)/predictions_per_move predictions[move_idx].mcmoves = f64(mcmoves) / predictions_per_move
} }
think_time := think_watch.elapsed().microseconds() think_time := think_watch.elapsed().milliseconds()
mut bestprediction := Prediction{mpoints:-1} mut bestprediction := Prediction{mpoints:-1}
for move_idx in 0..possible_moves.len { for move_idx in 0..possible_moves.len {
if bestprediction.mpoints < predictions[move_idx].mpoints { if bestprediction.mpoints < predictions[move_idx].mpoints {
bestprediction = predictions[move_idx] bestprediction = predictions[move_idx]
} }
} }
eprintln('Simulation time: ${think_time:6}µs | best $bestprediction') eprintln('Simulation time: ${think_time:4}ms | best $bestprediction')
app.move( bestprediction.move ) app.move(bestprediction.move)
} }
fn (app &App) label_format(kind LabelKind) gx.TextCfg { fn (app &App) label_format(kind LabelKind) gx.TextCfg {
match kind { match kind {
.points { .points {
return { return {
color: app.theme.text_color color: if app.state in [.over, .victory] { gx.white } else { app.theme.text_color }
align: .left align: .left
size: app.ui.font_size / 2 size: app.ui.font_size / 2
} }
} .moves { } .moves {
return { return {
color: app.theme.text_color color: if app.state in [.over, .victory] { gx.white } else { app.theme.text_color }
align: .right align: .right
size: app.ui.font_size / 2 size: app.ui.font_size / 2
} }
@ -532,15 +536,16 @@ fn (app &App) label_format(kind LabelKind) gx.TextCfg {
} }
} .score_end { } .score_end {
return { return {
color: app.theme.padding_color color: gx.white
align: .center align: .center
vertical_align: .middle vertical_align: .middle
size: app.ui.font_size size: app.ui.font_size * 3 / 4
} }
} }
} }
} }
[inline]
fn (mut app App) set_theme(idx int) { fn (mut app App) set_theme(idx int) {
theme := themes[idx] theme := themes[idx]
app.theme_idx = idx app.theme_idx = idx
@ -549,11 +554,13 @@ fn (mut app App) set_theme(idx int) {
} }
fn (mut app App) resize() { fn (mut app App) resize() {
mut s := if sapp.dpi_scale() == 0 { 1 } else { sapp.dpi_scale() } mut s := sapp.dpi_scale()
if s == 0.0 { s = 1.0 }
w := int(sapp.width() / s) w := int(sapp.width() / s)
h := int(sapp.height() / s) h := int(sapp.height() / s)
m := f32(min(w, h)) m := f32(min(w, h))
app.ui.dpi_scale = s
app.ui.window_width = w app.ui.window_width = w
app.ui.window_height = h app.ui.window_height = h
app.ui.padding_size = int(m / 38) app.ui.padding_size = int(m / 38)
@ -574,26 +581,38 @@ fn (mut app App) resize() {
} }
fn (app &App) draw() { fn (app &App) draw() {
xpad, ypad := app.ui.x_padding, app.ui.y_padding
ww := app.ui.window_width ww := app.ui.window_width
wh := app.ui.window_height wh := app.ui.window_height
labelx := app.ui.x_padding + app.ui.border_size m := min(ww, wh)
labely := app.ui.y_padding + app.ui.border_size / 2 labelx := xpad + app.ui.border_size
labely := ypad + app.ui.border_size / 2
app.draw_tiles() 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` // TODO: Make transparency work in `gg`
if app.state == .over { if app.state == .over {
app.gg.draw_rect(0, 0, ww, wh, gx.rgba(15, 0, 0, 44)) app.gg.draw_rect(0, 0, ww, wh, gx.rgba(10, 0, 0, 180))
app.gg.draw_text(ww / 2, wh / 3, 'Game Over', app.label_format(.game_over)) app.gg.draw_text(ww / 2, (m * 4 / 10) + ypad, 'Game Over', app.label_format(.game_over))
app.gg.draw_text(ww / 2, wh * 2 / 3, 'Score: $app.board.points', app.label_format(.score_end))
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, { f | color: gx.white, size: f.size * 3 / 4 })
} }
if app.state == .victory { if app.state == .victory {
app.gg.draw_rect(0, 0, ww, wh, gx.rgba(0, 15, 0, 44)) app.gg.draw_rect(0, 0, ww, wh, gx.rgba(0, 10, 0, 180))
app.gg.draw_text(ww / 2, wh / 3, 'Victory!', app.label_format(.victory)) app.gg.draw_text(ww / 2, (m * 4 / 10) + ypad, 'Victory!', app.label_format(.victory))
app.gg.draw_text(ww / 2, wh * 2 / 3, 'Score: $app.board.points', app.label_format(.score_end))
// 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() { fn (app &App) draw_tiles() {
@ -649,14 +668,62 @@ fn (app &App) draw_tiles() {
} }
} }
fn (mut app App) handle_swipe(start, end Pos) { fn (mut app App) handle_touches() {
min_swipe_distance := min(app.ui.window_width, app.ui.window_height) / 10 s, e := app.touch.start, app.touch.end
dx := end.x - start.x adx, ady := abs(e.pos.x - s.pos.x), abs(e.pos.y - s.pos.y)
dy := end.y - start.y
adx := abs(dx) if max(adx, ady) < 10 {
ady := abs(dy) app.handle_tap()
dmax := max(adx, ady) } else {
dmin := min(adx, ady) 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 := 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 < 200 && h - avgy < 200 { app.next_theme() }
// bottom right -> change tile format
if w - avgx < 200 && h - avgy < 200 { 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 := abs(dx), abs(dy)
dmin := if min(adx, ady) > 0 { min(adx, ady) } else { 1 }
dmax := if max(adx, ady) > 0 { 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(min(w, h) * tdiff / 60)) + 20
if dmax < min_swipe_distance { return } // Swipe was too short if dmax < min_swipe_distance { return } // Swipe was too short
if dmax / dmin < 2 { return } // Swiped diagonally if dmax / dmin < 2 { return } // Swiped diagonally
@ -668,6 +735,30 @@ fn (mut app App) handle_swipe(start, end Pos) {
} }
} }
[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 = 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 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 {
@ -675,28 +766,20 @@ fn (mut app App) on_key_down(key sapp.KeyCode) {
app.is_ai_mode = !app.is_ai_mode app.is_ai_mode = !app.is_ai_mode
} .escape { } .escape {
exit(0) exit(0)
} .n { } .n, .r {
app.new_game() app.new_game()
} .backspace { } .backspace {
if app.undo.len > 0 { app.undo()
app.state = .play
app.board = app.undo.pop()
app.moves--
return
}
} .enter { } .enter {
app.tile_format = int(app.tile_format) + 1 app.next_tile_format()
if app.tile_format == .end_ {
app.tile_format = .normal
}
} .j { } .j {
app.game_over() app.state = .over
} .t { } .t {
app.set_theme(if app.theme_idx == themes.len - 1 { 0 } else { app.theme_idx + 1 }) app.next_theme()
} else {} } else {}
} }
if app.state in [.play, .victory] { if app.state in [.play, .freeplay] {
match key { match key {
.w, .up { app.move(.up) } .w, .up { app.move(.up) }
.a, .left { app.move(.left) } .a, .left { app.move(.left) }
@ -705,6 +788,10 @@ fn (mut app App) on_key_down(key sapp.KeyCode) {
else {} else {}
} }
} }
if app.state == .victory {
if key == .space { app.state = .freeplay }
}
} }
fn on_event(e &sapp.Event, mut app App) { fn on_event(e &sapp.Event, mut app App) {
@ -716,26 +803,54 @@ fn on_event(e &sapp.Event, mut app App) {
} .touches_began { } .touches_began {
if e.num_touches > 0 { if e.num_touches > 0 {
t := e.touches[0] t := e.touches[0]
app.touch.start_pos = { x: int(t.pos_x), y: int(t.pos_y) } app.touch.start = {
pos: {
x: int(t.pos_x / app.ui.dpi_scale),
y: int(t.pos_y / app.ui.dpi_scale)
},
time: time.now()
}
} }
} .touches_ended { } .touches_ended {
if e.num_touches > 0 { if e.num_touches > 0 {
t := e.touches[0] t := e.touches[0]
end_pos := Pos{ x: int(t.pos_x), y: int(t.pos_y) } app.touch.end = {
app.handle_swipe(app.touch.start_pos, end_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 = {
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 = {
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 {} } else {}
} }
} }
fn frame(mut app App) { fn frame(mut app App) {
$if showfps? { app.perf.frame_sw.restart() } $if showfps? { app.perf.frame_sw.restart() }
app.gg.begin() app.gg.begin()
app.update_tickers() app.update_tickers()
app.draw() app.draw()
app.perf.frame++ app.perf.frame++
if app.is_ai_mode && app.perf.frame % frames_per_ai_move == 0 { if app.is_ai_mode && app.state in [.play, .freeplay] && app.perf.frame % frames_per_ai_move == 0 {
app.ai_move() app.ai_move()
} }
$if showfps? { app.showfps() } $if showfps? { app.showfps() }
@ -778,7 +893,7 @@ fn main() {
mut font_path := os.resource_abs_path(os.join_path('../assets/fonts/', 'RobotoMono-Regular.ttf')) mut font_path := os.resource_abs_path(os.join_path('../assets/fonts/', 'RobotoMono-Regular.ttf'))
$if android { $if android {
font_path = 'assets/RobotoMono-Regular.ttf' font_path = 'fonts/RobotoMono-Regular.ttf'
} }
mut window_title := 'V 2048' mut window_title := 'V 2048'