examples/2048: add a simple Monte Carlo player on `a`

pull/6441/head
Delyan Angelov 2020-09-22 11:21:20 +03:00
parent 624f22e27e
commit 46be0710ac
1 changed files with 147 additions and 51 deletions

View File

@ -19,6 +19,7 @@ mut:
tile_format TileFormat = .normal tile_format TileFormat = .normal
moves int moves int
perf &Perf = 0 perf &Perf = 0
is_ai_mode bool
} }
struct Ui { struct Ui {
@ -279,6 +280,44 @@ fn (b Board) to_left() Board {
return res 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() { fn (mut app App) update_tickers() {
for y in 0..4 { for y in 0..4 {
for x in 0..4 { for x in 0..4 {
@ -300,7 +339,7 @@ fn (mut app App) new_game() {
} }
} }
app.state = .play app.state = .play
app.undo = [] app.undo = []Board{cap:4096}
app.moves = 0 app.moves = 0
app.new_random_tile() app.new_random_tile()
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() { fn (mut app App) check_for_game_over() {
mut zeros := 0 if app.board.is_game_over() {
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 {
app.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 etiles := [16]Pos{}
mut empty_tiles_max := 0 mut empty_tiles_max := 0
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 := b.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++
} }
} }
@ -357,9 +378,25 @@ 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]
// 1/8 chance of creating a `4` tile // 10% chance of getting a `4` tile
random_value := if rand.intn(8) == 0 { 2 } else { 1 } random_value := if rand.f64n(1.0) < 0.9 { 1 } else { 2 }
app.board.field[empty_pos.y][empty_pos.x] = random_value 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.atickers[empty_pos.y][empty_pos.x] = animation_length
} }
app.check_for_victory() app.check_for_victory()
@ -374,26 +411,82 @@ fn (mut app App) game_over() {
app.state = .over app.state = .over
} }
fn (mut app App) move(d Direction) { fn (mut app App) apply_new_board(new Board) {
old := app.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() }
}
// 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.moves++
app.board = new app.board = new
app.undo << old app.undo << old
app.new_random_tile() app.new_random_tile()
}
fn (mut app App) move(d Direction) {
new, is_valid := app.board.move(d)
if !is_valid {
return 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 { fn (app &App) label_format(kind LabelKind) gx.TextCfg {
@ -572,7 +665,9 @@ fn (mut app App) handle_swipe(start, end Pos) {
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 { .a {
app.is_ai_mode = !app.is_ai_mode
} .escape {
exit(0) exit(0)
} .n { } .n {
app.new_game() app.new_game()
@ -595,7 +690,7 @@ fn (mut app App) on_key_down(key sapp.KeyCode) {
} else {} } else {}
} }
if app.state == .play { if app.state in [.play, .victory] {
match key { match key {
.w, .up { app.move(.up) } .w, .up { app.move(.up) }
.a, .left { app.move(.left) } .a, .left { app.move(.left) }
@ -633,8 +728,12 @@ fn frame(mut app App) {
app.gg.begin() app.gg.begin()
app.update_tickers() app.update_tickers()
app.draw() 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() } $if showfps? { app.showfps() }
app.gg.end()
} }
fn init(mut app App) { fn init(mut app App) {
@ -647,7 +746,6 @@ fn init(mut app App) {
fn (mut app App) showfps() { fn (mut app App) showfps() {
println(app.perf.frame_sw.elapsed().microseconds()) println(app.perf.frame_sw.elapsed().microseconds())
app.perf.frame++
f := app.perf.frame f := app.perf.frame
if (f & 127) == 0 { if (f & 127) == 0 {
last_frame_us := app.perf.frame_sw.elapsed().microseconds() last_frame_us := app.perf.frame_sw.elapsed().microseconds()
@ -685,9 +783,7 @@ fn main() {
window_title = 'canvas' window_title = 'canvas'
} }
$if showfps? {
app.perf = &Perf{} app.perf = &Perf{}
}
app.gg = gg.new_context({ app.gg = gg.new_context({
bg_color: app.theme.bg_color bg_color: app.theme.bg_color