minimax

minimax.git
git clone git://git.lenczewski.org/minimax.git
Log | Files | Refs

commit 94020bc7570a018b87e0e03c582bd130a0724ea2
Author: MikoĊ‚aj Lenczewski <mblenczewski@gmail.com>
Date:   Sun,  5 Jan 2025 01:35:30 +0000

Initial commit

Diffstat:
A.editorconfig | 18++++++++++++++++++
A.gitignore | 4++++
Abuild.sh | 5+++++
Aclean.sh | 5+++++
Adebug.sh | 3+++
Aminimax.c | 685+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 720 insertions(+), 0 deletions(-)

diff --git a/.editorconfig b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +# visual studio editorconfig plugin specific settings +guidelines = 80, 120, 160 + +[*.{c,h}] +indent_style = tab +indent_size = 8 + +[*.sh] +indent_style = tab +indent_size = 8 diff --git a/.gitignore b/.gitignore @@ -0,0 +1,4 @@ +minimax + +imgui.ini +**/.*.swp diff --git a/build.sh b/build.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +set -ex + +cc -o minimax minimax.c -Wall -Wextra -Wpedantic -g -O0 -std=c11 -lm diff --git a/clean.sh b/clean.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +set -ex + +rm -rf minimax diff --git a/debug.sh b/debug.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +lldbgui ./minimax diff --git a/minimax.c b/minimax.c @@ -0,0 +1,685 @@ +#ifdef _XOPEN_SOURCE +# undef _XOPEN_SOURCE +#endif + +#define _XOPEN_SOURCE 700 + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE 1 +#endif + +#ifndef _BSD_SOURCE +# define _BSD_SOURCE 1 +#endif + +#ifndef _DEFAULT_SOURCE +# define _DEFAULT_SOURCE 1 +#endif + +#include <alloca.h> +#include <assert.h> +#include <stdalign.h> +#include <stddef.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <math.h> + +#include <sys/mman.h> + +/* configuration + * =========================================================================== + */ + +#define KiB 1024ULL +#define MiB (1024 * KiB) +#define GiB (1024 * MiB) + +#define ARENA_SIZE 1 * GiB + +// length of one side of the game board +#define BOARD_SIZE 3 + +// maximum depth at which to search using minimax +// NOTE: this is reduced if the memory arena would not allow for this depth +#define MAX_SEARCH_DEPTH 11 + +// test minimax algorithm on first move of game +#define MINIMAX_DEBUG 0 + +/* utils + * =========================================================================== + */ + +#define true 1 +#define false 0 + +#define MAX(a, b) (((a) > (b)) ? (a) : (b)) +#define MIN(a, b) (((a) < (b)) ? (a) : (b)) + +struct arena { + void *ptr; + uint64_t cap, len; +}; + +static inline void +arena_reset(struct arena *arena) +{ + arena->len = 0; +} + +#define ALIGN_PREV(v, align) ((v) & ~((align) - 1)) +#define ALIGN_NEXT(v, align) ALIGN_PREV((v) + ((align) - 1), (align)) + +static inline void * +arena_alloc(struct arena *arena, size_t size, size_t align) +{ + uint64_t aligned_len = ALIGN_NEXT(arena->len, align); + if (aligned_len + size > arena->cap) + return NULL; + + void *ptr = (void *) ((uintptr_t) arena->ptr + aligned_len); + arena->len = aligned_len + size; + + return ptr; +} + +#define ALLOC_ARRAY(arena, T, n) arena_alloc((arena), sizeof(T) * (n), alignof(T)) +#define ALLOC_SIZED(arena, T) ALLOC_ARRAY((arena), T, 1) + +struct list_node; +struct list_node { + struct list_node *prev, *next; +}; + +#define FROM_NODE(node, T, member) \ + ((T *) ((uintptr_t) node - offsetof(T, member))) + +#define list_node_iter(node) \ + for (struct list_node *it = node; it; it = it->next) + +struct list { + struct list_node *head; +}; + +#define list_iter(list) \ + list_node_iter(list->head) + +static inline void +list_push(struct list *restrict list, struct list_node *restrict node) +{ + if (list->head) + list->head->prev = node; + + node->next = list->head; + list->head = node; +} + +/* tic-tac-toe definitions + * =========================================================================== + */ + +enum player { + EMPTY = 0, + BLACK = +1, + WHITE = -1, +}; + +static inline enum player +get_opponent(enum player player) +{ + switch (player) { + case EMPTY: return EMPTY; + case BLACK: return WHITE; + case WHITE: return BLACK; + } +} + +static inline char +get_player_char(enum player player) +{ + switch (player) { + case EMPTY: return '.'; + case BLACK: return 'B'; + case WHITE: return 'W'; + } +} + +static inline char const * +get_player_str(enum player player) +{ + switch (player) { + case EMPTY: return "EMPTY"; + case BLACK: return "BLACK"; + case WHITE: return "WHITE"; + } +} + +struct cell { + int8_t state; +}; + +struct board { + struct cell cells[BOARD_SIZE * BOARD_SIZE]; +}; + +static inline struct cell * +get_cell_ptr(struct board *board, size_t i, size_t j) +{ + return &board->cells[j * BOARD_SIZE + i]; +} + +struct move { + uint8_t i, j; +}; + +static inline int +try_play_move(struct board *board, struct move move, enum player player) +{ + if (BOARD_SIZE <= move.i || BOARD_SIZE <= move.j) + return false; + + struct cell *cell = get_cell_ptr(board, move.i, move.j); + if (cell->state != EMPTY) + return false; + + cell->state = player; + + return true; +} + +static inline void +undo_move(struct board *board, struct move move) +{ + struct cell *cell = get_cell_ptr(board, move.i, move.j); + cell->state = EMPTY; +} + +static size_t +get_available_moves(struct board *restrict board, struct move *restrict buf) +{ + size_t moves = 0; + for (size_t j = 0; j < BOARD_SIZE; j++) { + for (size_t i = 0; i < BOARD_SIZE; i++) { + if (get_cell_ptr(board, i, j)->state == EMPTY) { + if (buf) + *buf++ = (struct move) {i, j}; + + moves++; + } + } + } + + return moves; +} + +static enum player +get_winner(struct board *board) +{ + for (size_t j = 0; j < BOARD_SIZE; j++) { /* check rows */ + enum player winner = get_cell_ptr(board, 0, j)->state; + + int contiguous = true; + for (size_t i = 0; i < BOARD_SIZE; i++) { + if (get_cell_ptr(board, i, j)->state != winner) + contiguous = false; + } + + if (contiguous) + return winner; + } + + for (size_t i = 0; i < BOARD_SIZE; i++) { /* check cols */ + enum player winner = get_cell_ptr(board, i, 0)->state; + + int contiguous = true; + for (size_t j = 0; j < BOARD_SIZE; j++) { + if (get_cell_ptr(board, i, j)->state != winner) + contiguous = false; + } + + if (contiguous) + return winner; + } + + { + enum player winner = get_cell_ptr(board, 0, 0)->state; + + int contiguous = true; + for (size_t k = 0; k < BOARD_SIZE; k++) { /* check (k,k) diagonal */ + if (get_cell_ptr(board, k, k)->state != winner) + contiguous = false; + } + + if (contiguous) + return winner; + } + + { + enum player winner = get_cell_ptr(board, 0, BOARD_SIZE - 1)->state; + + int contiguous = true; + for (size_t k = 0; k < BOARD_SIZE; k++) { /* check (k,k) diagonal */ + if (get_cell_ptr(board, k, BOARD_SIZE - 1 - k)->state != winner) + contiguous = false; + } + + if (contiguous) + return winner; + } + + return EMPTY; +} + +static int +game_over(struct board *board, enum player *winner) +{ + size_t moves = get_available_moves(board, NULL); + + if ((*winner = get_winner(board)) || !moves) /* winner, or a draw */ + return true; + + return false; +} + +static inline void +draw_board_row_divider(void) +{ + printf("-"); + for (size_t i = 0; i < BOARD_SIZE; i++) + printf("----"); + printf("\n"); +} + +static void +print_board(struct board *board) +{ + draw_board_row_divider(); + + for (size_t j = 0; j < BOARD_SIZE; j++) { + printf("|"); + for (size_t i = 0; i < BOARD_SIZE; i++) { + enum player cell = get_cell_ptr(board, i, j)->state; + printf(" %c |", get_player_char(cell)); + } + printf("\n"); + + draw_board_row_divider(); + } +} + +/* minimax definitions + * =========================================================================== + */ + +struct minimax { + struct list possible_moves; +}; + +struct minimax_node { + // NOTE: we can use this to avoid false-aliasing of cachelines + //alignas(64) + + struct move move; + int8_t player; + + struct list children; + struct list_node list_node; +}; + +static inline void +minimax_node_init(struct minimax_node *node, struct move move, enum player player) +{ + node->move = move; + node->player = player; + node->children.head = NULL; + node->list_node.prev = node->list_node.next = NULL; +} + +#define IS_TERMINAL_NODE(node) ((node)->children.head == NULL) + +#if 0 + +static void +dump_tree(FILE *fp, struct minimax_node const *node, size_t depth, size_t max_depth) +{ +#define LEADER(fp, depth) \ + for (size_t i = 0; i < depth; i++) fprintf(fp, " "); + + LEADER(fp, depth); + fprintf(fp, "node: {move: {i: %u, j: %u}, player: %s}\n", + node->move.i, node->move.j, get_player_str(node->player)); + + if (depth + 1 == max_depth) { + LEADER(fp, depth + 1); + fprintf(fp, "...\n"); + return; + } + + struct list const *list = &node->children; + list_iter(list) { + struct minimax_node *child = FROM_NODE(it, struct minimax_node, list_node); + dump_tree(fp, child, depth + 1, max_depth); + } +#undef LEADER +} + +static void +minimax_dump_trees(struct minimax *minimax, FILE *fp, size_t max_depth) +{ + struct list *list = &minimax->possible_moves; + list_iter(list) { + struct minimax_node *tree = FROM_NODE(it, struct minimax_node, list_node); + dump_tree(fp, tree, 0, max_depth); + } +} + +#endif + +static struct minimax_node * +create_tree(struct board *board, struct move move, enum player player, + struct arena *arena, size_t depth, size_t max_depth) +{ + struct minimax_node *node = ALLOC_SIZED(arena, struct minimax_node); + assert(node); + + minimax_node_init(node, move, player); + + int res = try_play_move(board, node->move, node->player); + assert(res); + + if (depth == max_depth) + goto end; + + size_t available_moves = get_available_moves(board, NULL); + if (!available_moves) + goto end; + + struct move *moves = alloca(available_moves * sizeof *moves); + get_available_moves(board, moves); + + for (size_t i = 0; i < available_moves; i++) { + struct minimax_node *child = create_tree(board, moves[i], + get_opponent(player), + arena, depth + 1, max_depth); + + list_push(&node->children, &child->list_node); + } + +end: + undo_move(board, node->move); + + return node; +} + +static float +heuristic(struct minimax_node *node, struct board *board, enum player maximising_player) +{ + enum player winner; + if ((winner = get_winner(board))) { /* this is a winning move */ + return (winner == maximising_player) ? +INFINITY : -INFINITY; + } + + size_t moves = get_available_moves(board, NULL); + if (!moves) /* no winner and no moves left to make, draw */ + return 0; + + // TODO: is there a better heuristic than assuming a loss? + return (node->player == maximising_player) ? -INFINITY : +INFINITY; +} + +static float +minimax(struct minimax_node *node, struct board *board, enum player maximising_player, + size_t max_depth) +{ + float result; + + int res = try_play_move(board, node->move, node->player); + assert(res); + + if (!max_depth || IS_TERMINAL_NODE(node)) { + result = heuristic(node, board, maximising_player); + goto end; + } + + if (node->player == maximising_player) { + float max_value = -INFINITY; + + struct list *list = &node->children; + list_iter(list) { + struct minimax_node *child = FROM_NODE(it, struct minimax_node, list_node); + float value = minimax(child, board, maximising_player, max_depth - 1); + max_value = MAX(max_value, value); + } + + result = max_value; + } else /* node->player == minimising_player */ { + float min_value = +INFINITY; + + struct list *list = &node->children; + list_iter(list) { + struct minimax_node *child = FROM_NODE(it, struct minimax_node, list_node); + float value = minimax(child, board, maximising_player, max_depth - 1); + min_value = MIN(min_value, value); + } + + result = min_value; + } + +end: + undo_move(board, node->move); + + return result; +} + +static struct move +minimax_get_best_move(struct minimax *self, struct board *board, enum player player, + float *out, struct arena *arena, size_t max_depth) +{ + size_t available_moves = get_available_moves(board, NULL); + assert(available_moves); + + struct move moves[available_moves]; + get_available_moves(board, moves); + + self->possible_moves.head = NULL; + + float best_value = -INFINITY; + struct move best_move; + + for (size_t i = 0; i < available_moves; i++) { + arena_reset(arena); + +#if MINIMAX_DEBUG + fprintf(stderr, "starting minimax tree for root node: {i: %u, j: %u} with depth: %zu\n", + moves[i].i, moves[i].j, max_depth); +#endif + + // NOTE: we limit the depth of the minimax tree to avoid + // running out of memory, but if we could employ + // memoization of similar game states we could greatly + // reduce the need for this limit + struct minimax_node *tree = create_tree(board, moves[i], player, + arena, 0, max_depth); + assert(tree); + +#if MINIMAX_DEBUG + fprintf(stderr, "created tree with %zu/%zu nodes\n", + arena->len / sizeof(struct minimax_node), + arena->cap / sizeof(struct minimax_node)); +#endif + + + list_push(&self->possible_moves, &tree->list_node); + + float value = minimax(tree, board, player, max_depth); + if (value >= best_value) { + best_value = value; + best_move = tree->move; + } + } + + *out = best_value; + + return best_move; +} + +/* main definition + * =========================================================================== + */ + +#define BOARD_SIZE2 (BOARD_SIZE * BOARD_SIZE) + +static inline size_t +factorial(size_t n, size_t steps) +{ + size_t res = n; + + while (steps-- && --n) + res = res * n; + + return res; +} + +static inline size_t +nodecount(size_t depth) +{ + return factorial(BOARD_SIZE2, depth); +} + +static size_t +max_tree_depth_for_node_cap(size_t cap) +{ + size_t depth, nodes; + for (depth = 0; depth < BOARD_SIZE2; depth++) + if ((nodes = nodecount(depth)) > cap) + return depth; + + return depth; +} + +static size_t +get_input_coord(void) +{ + char *line = NULL; + size_t len; + while (true) { + ssize_t res = getline(&line, &len, stdin); + if (res < 0) + exit(EXIT_FAILURE); + + size_t coord; + if (sscanf(line, "%zu", &coord)) { + free(line); + return coord; + } else { + printf("invalid coordinate. try again.\n"); + } + } +} + +int +main(void) +{ + // NOTE: we use an arena to avoid expensive system allocators, and so + // that we can easily free all nodes without iterating the tree + int prot = PROT_READ|PROT_WRITE; + int flags = MAP_PRIVATE|MAP_ANONYMOUS; + struct arena arena = { + .ptr = mmap(NULL, ARENA_SIZE, prot, flags, -1, 0), + .cap = ARENA_SIZE, + .len = 0, + }; + + assert(arena.ptr != MAP_FAILED); + + madvise(arena.ptr, arena.cap, MADV_HUGEPAGE); + + size_t max_nodes = arena.cap / sizeof(struct minimax_node); + fprintf(stderr, "Arena cap: %zu bytes, node size: %zu bytes, max node count: %zu\n", + arena.cap, sizeof(struct minimax_node), max_nodes); + + size_t max_search_depth = MIN(MAX_SEARCH_DEPTH, + max_tree_depth_for_node_cap(max_nodes)); + + fprintf(stderr, "Max tree depth: %zu (%zu/%zu nodes)\n", + max_search_depth, nodecount(max_search_depth), max_nodes); + + struct minimax minimax; + memset(&minimax, 0, sizeof minimax); + + struct board board; + memset(&board, 0, sizeof board); + +#if MINIMAX_DEBUG + float value; + struct move move = minimax_get_best_move(&minimax, &board, BLACK, &value, &arena, max_search_depth); + + fprintf(stderr, "Best move for %s: {i: %u, j: %u}, value: %f\n", + get_player_str(BLACK), move.i, move.j, value); + + int res = try_play_move(&board, move, BLACK); + assert(res); + + return 0; + +#endif + + enum player winner; + while (true) { + print_board(&board); + + /* player move */ + struct move player_move; + while (true) { + printf("user x coord (between 0 and %d):\n", BOARD_SIZE - 1); + size_t x = get_input_coord(); + + printf("user y coord (between 0 and %d):\n", BOARD_SIZE - 1); + size_t y = get_input_coord(); + + player_move.i = x; + player_move.j = y; + + if (!try_play_move(&board, player_move, BLACK)) { + printf("move already made! try again.\n"); + } + + break; + } + + if (game_over(&board, &winner)) + break; + + /* ai move */ + float value; + struct move best_move = minimax_get_best_move(&minimax, &board, + WHITE, &value, + &arena, max_search_depth); + +#if MINIMAX_DEBUG + fprintf(stderr, "Best move for %s: {i: %u, j: %u}, value: %f\n", + get_player_str(WHITE), best_move.i, best_move.j, value); +#endif + + int res = try_play_move(&board, best_move, WHITE); + assert(res); + + if (game_over(&board, &winner)) + break; + } + + print_board(&board); + + if (winner == BLACK) { + printf("Player won the game.\n"); + } else if (winner == WHITE) { + printf("Player lost the game.\n"); + } else { + printf("Player drew the game.\n"); + } + + return 0; +}