examples/2048: add a simple Monte Carlo player on `a`
parent
624f22e27e
commit
46be0710ac
|
@ -19,6 +19,7 @@ mut:
|
|||
tile_format TileFormat = .normal
|
||||
moves int
|
||||
perf &Perf = 0
|
||||
is_ai_mode bool
|
||||
}
|
||||
|
||||
struct Ui {
|
||||
|
@ -279,6 +280,44 @@ fn (b Board) to_left() Board {
|
|||
return res
|
||||
}
|
||||
|
||||
fn (b Board) move(d Direction) (Board, bool) {
|
||||
new := match d {
|
||||
.left { b.to_left() }
|
||||
.right { b.hmirror().to_left().hmirror() }
|
||||
.up { b.transpose().to_left().transpose() }
|
||||
.down { b.transpose().hmirror().to_left().hmirror().transpose() }
|
||||
}
|
||||
// If the board hasn't changed, it's an illegal move, don't allow it.
|
||||
for x in 0..4 {
|
||||
for y in 0..4 {
|
||||
if b.field[x][y] != new.field[x][y] {
|
||||
return new, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return new, false
|
||||
}
|
||||
|
||||
fn (mut b Board) is_game_over() bool {
|
||||
for y in 0..4 {
|
||||
for x in 0..4 {
|
||||
fidx := b.field[y][x]
|
||||
if fidx == 0 {
|
||||
// there are remaining zeros
|
||||
return false
|
||||
}
|
||||
if (x > 0 && fidx == b.field[y][x - 1])
|
||||
|| (x < 4 - 1 && fidx == b.field[y][x + 1])
|
||||
|| (y > 0 && fidx == b.field[y - 1][x])
|
||||
|| (y < 4 - 1 && fidx == b.field[y + 1][x]) {
|
||||
// there are remaining merges
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn (mut app App) update_tickers() {
|
||||
for y in 0..4 {
|
||||
for x in 0..4 {
|
||||
|
@ -300,7 +339,7 @@ fn (mut app App) new_game() {
|
|||
}
|
||||
}
|
||||
app.state = .play
|
||||
app.undo = []
|
||||
app.undo = []Board{cap:4096}
|
||||
app.moves = 0
|
||||
app.new_random_tile()
|
||||
app.new_random_tile()
|
||||
|
@ -319,37 +358,19 @@ fn (mut app App) check_for_victory() {
|
|||
}
|
||||
|
||||
fn (mut app App) check_for_game_over() {
|
||||
mut zeros := 0
|
||||
mut remaining_merges := 0
|
||||
for y in 0..4 {
|
||||
for x in 0..4 {
|
||||
fidx := app.board.field[y][x]
|
||||
if fidx == 0 {
|
||||
zeros++
|
||||
continue
|
||||
}
|
||||
if (x > 0 && fidx == app.board.field[y][x - 1])
|
||||
|| (x < 4 - 1 && fidx == app.board.field[y][x + 1])
|
||||
|| (y > 0 && fidx == app.board.field[y - 1][x])
|
||||
|| (y < 4 - 1 && fidx == app.board.field[y + 1][x]) {
|
||||
remaining_merges++
|
||||
}
|
||||
}
|
||||
}
|
||||
if remaining_merges == 0 && zeros == 0 {
|
||||
if app.board.is_game_over() {
|
||||
app.game_over()
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut app App) new_random_tile() {
|
||||
fn (mut b Board) place_random_tile() (Pos, int) {
|
||||
mut etiles := [16]Pos{}
|
||||
mut empty_tiles_max := 0
|
||||
for y in 0..4 {
|
||||
for x in 0..4 {
|
||||
fidx := app.board.field[y][x]
|
||||
fidx := b.field[y][x]
|
||||
if fidx == 0 {
|
||||
etiles[empty_tiles_max] = Pos{x, y}
|
||||
app.atickers[y][x] = 0
|
||||
empty_tiles_max++
|
||||
}
|
||||
}
|
||||
|
@ -357,9 +378,25 @@ fn (mut app App) new_random_tile() {
|
|||
if empty_tiles_max > 0 {
|
||||
new_random_tile_index := rand.intn(empty_tiles_max)
|
||||
empty_pos := etiles[new_random_tile_index]
|
||||
// 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
|
||||
// 10% chance of getting a `4` tile
|
||||
random_value := if rand.f64n(1.0) < 0.9 { 1 } else { 2 }
|
||||
b.field[empty_pos.y][empty_pos.x] = random_value
|
||||
return empty_pos, random_value
|
||||
}
|
||||
return Pos{}, 0
|
||||
}
|
||||
|
||||
fn (mut app App) new_random_tile() {
|
||||
for y in 0..4 {
|
||||
for x in 0..4 {
|
||||
fidx := app.board.field[y][x]
|
||||
if fidx == 0 {
|
||||
app.atickers[y][x] = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
empty_pos, random_value := app.board.place_random_tile()
|
||||
if random_value > 0 {
|
||||
app.atickers[empty_pos.y][empty_pos.x] = animation_length
|
||||
}
|
||||
app.check_for_victory()
|
||||
|
@ -374,26 +411,82 @@ fn (mut app App) game_over() {
|
|||
app.state = .over
|
||||
}
|
||||
|
||||
fn (mut app App) move(d Direction) {
|
||||
fn (mut app App) apply_new_board(new Board) {
|
||||
old := app.board
|
||||
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() }
|
||||
app.moves++
|
||||
app.board = new
|
||||
app.undo << old
|
||||
app.new_random_tile()
|
||||
}
|
||||
|
||||
fn (mut app App) move(d Direction) {
|
||||
new, is_valid := app.board.move(d)
|
||||
if !is_valid {
|
||||
return
|
||||
}
|
||||
// If the board hasn't changed, it's an illegal move, don't allow it.
|
||||
for x in 0..4 {
|
||||
for y in 0..4 {
|
||||
if old.field[x][y] != new.field[x][y] {
|
||||
app.moves++
|
||||
app.board = new
|
||||
app.undo << old
|
||||
app.new_random_tile()
|
||||
return
|
||||
app.apply_new_board(new)
|
||||
}
|
||||
|
||||
const (
|
||||
possible_moves = [Direction.up, .right, .down, .left]
|
||||
predictions_per_move = 200
|
||||
prediction_depth = 100
|
||||
)
|
||||
struct Prediction {
|
||||
mut:
|
||||
move Direction
|
||||
mpoints f64
|
||||
}
|
||||
|
||||
fn (p Prediction) str() string {
|
||||
return 'Prediction{move: ${p.move:5} | mpoints: ${p.mpoints:6.2f} }'
|
||||
}
|
||||
fn (mut app App) ai_move() {
|
||||
mut predictions := [4]Prediction{}
|
||||
mut nboard := app.board
|
||||
mut is_valid := false
|
||||
think_watch := time.new_stopwatch({})
|
||||
for move in possible_moves {
|
||||
move_idx := int(move)
|
||||
predictions[move_idx].move = move
|
||||
mut mpoints := 0
|
||||
mut mshifts := 0
|
||||
for i := 0; i < predictions_per_move; i++ {
|
||||
mut cboard := app.board
|
||||
cboard, is_valid = cboard.move(move)
|
||||
if !is_valid {
|
||||
continue
|
||||
}
|
||||
if cboard.is_game_over() {
|
||||
continue
|
||||
}
|
||||
mpoints += cboard.points
|
||||
cboard.place_random_tile()
|
||||
mut cmoves := 0
|
||||
for !cboard.is_game_over() {
|
||||
nmove := possible_moves[rand.intn(possible_moves.len)]
|
||||
nboard, is_valid = cboard.move(nmove)
|
||||
if !is_valid {
|
||||
continue
|
||||
}
|
||||
cboard = nboard
|
||||
cboard.place_random_tile()
|
||||
cmoves++
|
||||
}
|
||||
mpoints += cboard.points
|
||||
mshifts += cboard.shifts
|
||||
}
|
||||
predictions[move_idx].mpoints = f64(mpoints)/predictions_per_move
|
||||
}
|
||||
think_time := think_watch.elapsed().microseconds()
|
||||
mut bestprediction := Prediction{mpoints:-1}
|
||||
for move_idx in 0..possible_moves.len {
|
||||
if bestprediction.mpoints < predictions[move_idx].mpoints {
|
||||
bestprediction = predictions[move_idx]
|
||||
}
|
||||
}
|
||||
eprintln('Simulation time: ${think_time:6}µs | best $bestprediction')
|
||||
app.move( bestprediction.move )
|
||||
}
|
||||
|
||||
fn (app &App) label_format(kind LabelKind) gx.TextCfg {
|
||||
|
@ -483,7 +576,7 @@ fn (app &App) draw() {
|
|||
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))
|
||||
|
@ -522,13 +615,13 @@ fn (app &App) draw_tiles() {
|
|||
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)
|
||||
|
@ -558,7 +651,7 @@ fn (mut app App) handle_swipe(start, end Pos) {
|
|||
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
|
||||
|
||||
|
@ -572,7 +665,9 @@ fn (mut app App) handle_swipe(start, end Pos) {
|
|||
fn (mut app App) on_key_down(key sapp.KeyCode) {
|
||||
// these keys are independent from the game state:
|
||||
match key {
|
||||
.escape {
|
||||
.a {
|
||||
app.is_ai_mode = !app.is_ai_mode
|
||||
} .escape {
|
||||
exit(0)
|
||||
} .n {
|
||||
app.new_game()
|
||||
|
@ -595,7 +690,7 @@ fn (mut app App) on_key_down(key sapp.KeyCode) {
|
|||
} else {}
|
||||
}
|
||||
|
||||
if app.state == .play {
|
||||
if app.state in [.play, .victory] {
|
||||
match key {
|
||||
.w, .up { app.move(.up) }
|
||||
.a, .left { app.move(.left) }
|
||||
|
@ -633,8 +728,12 @@ fn frame(mut app App) {
|
|||
app.gg.begin()
|
||||
app.update_tickers()
|
||||
app.draw()
|
||||
app.gg.end()
|
||||
app.perf.frame++
|
||||
if app.is_ai_mode && app.perf.frame % 15 == 0 {
|
||||
app.ai_move()
|
||||
}
|
||||
$if showfps? { app.showfps() }
|
||||
app.gg.end()
|
||||
}
|
||||
|
||||
fn init(mut app App) {
|
||||
|
@ -647,7 +746,6 @@ fn init(mut app App) {
|
|||
|
||||
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()
|
||||
|
@ -685,9 +783,7 @@ fn main() {
|
|||
window_title = 'canvas'
|
||||
}
|
||||
|
||||
$if showfps? {
|
||||
app.perf = &Perf{}
|
||||
}
|
||||
app.perf = &Perf{}
|
||||
|
||||
app.gg = gg.new_context({
|
||||
bg_color: app.theme.bg_color
|
||||
|
|
Loading…
Reference in New Issue