2048: Evil AI logic: select new tile in the worst position

Not very smart: the worst position is defined such that after the
player's next move the fewest tiles remain empty.
This commit is contained in:
Dmitrii Ubskii 2021-05-15 17:37:29 +03:00 committed by Linus Groh
parent 4434e900af
commit a93d0fe8c2
Notes: sideshowbarker 2024-07-18 17:52:33 +09:00
3 changed files with 83 additions and 7 deletions

View file

@ -5,11 +5,14 @@
*/
#include "Game.h"
#include <AK/Array.h>
#include <AK/NumericLimits.h>
#include <AK/String.h>
#include <stdlib.h>
Game::Game(size_t grid_size, size_t target_tile)
Game::Game(size_t grid_size, size_t target_tile, bool evil_ai)
: m_grid_size(grid_size)
, m_evil_ai(evil_ai)
{
if (target_tile == 0)
m_target_tile = 2048;
@ -25,8 +28,8 @@ Game::Game(size_t grid_size, size_t target_tile)
row.append(0);
}
add_random_tile();
add_random_tile();
add_tile();
add_tile();
}
void Game::add_random_tile()
@ -161,6 +164,16 @@ static bool is_stalled(const Game::Board& board)
return true;
}
static size_t get_number_of_free_cells(const Game::Board& board)
{
size_t accumulator = 0;
for (auto& row : board) {
for (auto& cell : row)
accumulator += cell == 0;
}
return accumulator;
}
bool Game::slide_tiles(Direction direction)
{
size_t successful_merge_score = 0;
@ -195,7 +208,7 @@ Game::MoveOutcome Game::attempt_move(Direction direction)
bool moved = slide_tiles(direction);
if (moved) {
m_turns++;
add_random_tile();
add_tile();
}
if (is_complete(m_board, m_target_tile))
@ -207,6 +220,57 @@ Game::MoveOutcome Game::attempt_move(Direction direction)
return MoveOutcome::InvalidMove;
}
void Game::add_evil_tile()
{
size_t worst_row = 0;
size_t worst_column = 0;
u32 worst_value = 2;
size_t most_free_cells = NumericLimits<size_t>::max();
for (size_t row = 0; row < m_grid_size; row++) {
for (size_t column = 0; column < m_grid_size; column++) {
if (m_board[row][column] != 0)
continue;
for (u32 value : Array { 2, 4 }) {
Game saved_state = *this;
saved_state.m_board[row][column] = value;
if (is_stalled(saved_state.m_board)) {
// We can stall the board now, instant game over.
worst_row = row;
worst_column = column;
worst_value = value;
goto found_worst_tile;
}
// This is the best outcome the player can achieve in one move.
// We want this to be as low as possible.
size_t best_outcome = 0;
for (auto direction : Array { Direction::Down, Direction::Left, Direction::Right, Direction::Up }) {
Game moved_state = saved_state;
bool moved = moved_state.slide_tiles(direction);
if (!moved) // invalid move
continue;
best_outcome = max(best_outcome, get_number_of_free_cells(moved_state.board()));
}
if (best_outcome < most_free_cells) {
worst_row = row;
worst_column = column;
worst_value = value;
most_free_cells = best_outcome;
}
}
}
}
found_worst_tile:
m_board[worst_row][worst_column] = worst_value;
}
u32 Game::largest_tile() const
{
u32 tile = 0;

View file

@ -10,8 +10,9 @@
class Game final {
public:
Game(size_t board_size, size_t target_tile = 0);
Game(size_t grid_size, size_t target_tile, bool evil_ai);
Game(const Game&) = default;
Game& operator=(const Game&) = default;
enum class MoveOutcome {
OK,
@ -49,11 +50,22 @@ public:
private:
bool slide_tiles(Direction);
void add_tile()
{
if (m_evil_ai)
add_evil_tile();
else
add_random_tile();
}
void add_random_tile();
void add_evil_tile();
size_t m_grid_size { 0 };
u32 m_target_tile { 0 };
bool m_evil_ai { false };
Board m_board;
size_t m_score { 0 };
size_t m_turns { 0 };

View file

@ -76,7 +76,7 @@ int main(int argc, char** argv)
main_widget.set_layout<GUI::VerticalBoxLayout>();
main_widget.set_fill_with_background_color(true);
Game game { board_size, target_tile };
Game game { board_size, target_tile, evil_ai };
auto& board_view = main_widget.add<BoardView>(&game.board());
board_view.set_focus(true);
@ -123,7 +123,7 @@ int main(int argc, char** argv)
undo_stack.clear();
redo_stack.clear();
game = Game(board_size, target_tile);
game = Game(board_size, target_tile, evil_ai);
// This ensures that the sizes are correct.
board_view.set_board(nullptr);