Skip to content

Commit

Permalink
Separate agents from game Logic (#30)
Browse files Browse the repository at this point in the history
The C source file is split into several ones to facilitate the addition
of new agents. Several changes were implemented to achieve this:
1. 'move_t' was introduced to transform negamax into a dedicated func.
2. qsort was relocated from available_moves to incorporate it into the
   game logic.

The readability of the code also increased with the following changes:
1. 'table' and 'move_record' were declared as static arrays.
2. The condition is_consecutive(score, -1) was replaced with score < 0,
   and vice versa.
  • Loading branch information
paul90317 authored Feb 7, 2024
1 parent 4642950 commit b87649d
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 229 deletions.
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
PROG = ttt
CFLAGS = -Wall -Wextra -std=c11
CFLAGS = -Wall -Wextra -std=c11 -I .

GIT_HOOKS := .git/hooks/applied

Expand All @@ -9,7 +9,7 @@ $(GIT_HOOKS):
@scripts/install-git-hooks
@echo

$(PROG): $(PROG).c
$(PROG): $(PROG).c game.c agents/negamax.c
gcc $(CFLAGS) -o $@ $^

clean:
Expand Down
125 changes: 125 additions & 0 deletions agents/negamax.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#include <assert.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "game.h"
#include "negamax.h"

#define MAX_SEARCH_DEPTH 6

static int history_score_sum[N_GRIDS];
static int history_count[N_GRIDS];

static int cmp_moves(const void *a, const void *b)
{
int *_a = (int *) a, *_b = (int *) b;
int score_a = 0, score_b = 0;

if (history_count[*_a])
score_a = history_score_sum[*_a] / history_count[*_a];
if (history_count[*_b])
score_b = history_score_sum[*_b] / history_count[*_b];
return score_b - score_a;
}

static int eval_line_segment_score(const char *table,
char player,
int i,
int j,
line_t line)
{
int score = 0;
for (int k = 0; k < GOAL; k++) {
char curr =
table[GET_INDEX(i + k * line.i_shift, j + k * line.j_shift)];
if (curr == player) {
if (score < 0)
return 0;
if (score)
score *= 10;
else
score = 1;
} else if (curr != ' ') {
if (score > 0)
return 0;
if (score)
score *= 10;
else
score = -1;
}
}
return score;
}

static int get_score(const char *table, char player)
{
int score = 0;
for (int i_line = 0; i_line < 4; ++i_line) {
line_t line = lines[i_line];
for (int i = line.i_lower_bound; i < line.i_upper_bound; ++i) {
for (int j = line.j_lower_bound; j < line.j_upper_bound; ++j) {
score += eval_line_segment_score(table, player, i, j, line);
}
}
}
return score;
}

static move_t negamax(char *table, int depth, char player, int alpha, int beta)
{
if (check_win(table) != ' ' || depth == 0) {
move_t result = {get_score(table, player), -1};
return result;
}

int score;
move_t best_move = {-10000, -1};
int *moves = available_moves(table);
int n_moves = 0;
while (n_moves < N_GRIDS && moves[n_moves] != -1)
++n_moves;
qsort(moves, n_moves, sizeof(int), cmp_moves);
for (int i = 0; i < n_moves; i++) {
table[moves[i]] = player;
if (!i) // do a full search on the first move
score = -negamax(table, depth - 1, player == 'X' ? 'O' : 'X', -beta,
-alpha)
.score;
else {
// do a null-window search on the rest of the moves
score = -negamax(table, depth - 1, player == 'X' ? 'O' : 'X',
-alpha - 1, -alpha)
.score;
if (alpha < score && score < beta) // do a full re-search
score = -negamax(table, depth - 1, player == 'X' ? 'O' : 'X',
-beta, -score)
.score;
}
history_count[moves[i]]++;
history_score_sum[moves[i]] += score;
if (score > best_move.score) {
best_move.score = score;
best_move.move = moves[i];
}
table[moves[i]] = ' ';
if (score > alpha)
alpha = score;
if (alpha >= beta)
break;
}

free((char *) moves);
return best_move;
}

move_t negamax_predict(char *table, char player)
{
memset(history_score_sum, 0, sizeof(history_score_sum));
memset(history_count, 0, sizeof(history_count));
move_t result;
for (int depth = 2; depth <= MAX_SEARCH_DEPTH; depth += 2)
result = negamax(table, depth, player, -100000, 100000);
return result;
}
7 changes: 7 additions & 0 deletions agents/negamax.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#pragma once

typedef struct {
int score, move;
} move_t;

move_t negamax_predict(char *table, char player);
75 changes: 75 additions & 0 deletions game.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#include <assert.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "game.h"

#define LOOKUP(table, i, j, else_value) \
((i) < 0 || (j) < 0 || (i) > BOARD_SIZE || (j) > BOARD_SIZE \
? (else_value) \
: (table)[GET_INDEX(i, j)])

_Static_assert(BOARD_SIZE <= 26, "Board size must not be greater than 26");
_Static_assert(BOARD_SIZE > 0, "Board size must be greater than 0");
_Static_assert(GOAL <= BOARD_SIZE, "Goal must not be greater than board size");
_Static_assert(GOAL > 0, "Goal must be greater than 0");
_Static_assert(ALLOW_EXCEED == 0 || ALLOW_EXCEED == 1,
"ALLOW_EXCEED must be a boolean that is 0 or 1");

const line_t lines[4] = {
{1, 0, 0, 0, BOARD_SIZE - GOAL + 1, BOARD_SIZE}, // ROW
{0, 1, 0, 0, BOARD_SIZE, BOARD_SIZE - GOAL + 1}, // COL
{1, 1, 0, 0, BOARD_SIZE - GOAL + 1, BOARD_SIZE - GOAL + 1}, // PRIMARY
{1, -1, 0, GOAL - 1, BOARD_SIZE - GOAL + 1, BOARD_SIZE}, // SECONDARY
};

static char check_line_segment_win(const char *t, int i, int j, line_t line)
{
char last = t[GET_INDEX(i, j)];
if (last == ' ')
return ' ';
for (int k = 1; k < GOAL; k++) {
if (last != t[GET_INDEX(i + k * line.i_shift, j + k * line.j_shift)]) {
return ' ';
}
}
#if !ALLOW_EXCEED
if (last == LOOKUP(t, i - line.i_shift, j - line.j_shift, ' ') ||
last ==
LOOKUP(t, i + GOAL * line.i_shift, j + GOAL * line.j_shift, ' '))
return ' ';
#endif
return last;
}

char check_win(char *t)
{
for (int i_line = 0; i_line < 4; ++i_line) {
line_t line = lines[i_line];
for (int i = line.i_lower_bound; i < line.i_upper_bound; ++i) {
for (int j = line.j_lower_bound; j < line.j_upper_bound; ++j) {
char win = check_line_segment_win(t, i, j, line);
if (win != ' ')
return win;
}
}
}
for (int i = 0; i < N_GRIDS; i++)
if (t[i] == ' ')
return ' ';
return 'D';
}

int *available_moves(char *table)
{
int *moves = malloc(N_GRIDS * sizeof(int));
int m = 0;
for (int i = 0; i < N_GRIDS; i++)
if (table[i] == ' ')
moves[m++] = i;
if (m < N_GRIDS)
moves[m] = -1;
return moves;
}
19 changes: 19 additions & 0 deletions game.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#pragma once

#define BOARD_SIZE 4
#define GOAL 3
#define ALLOW_EXCEED 1
#define N_GRIDS (BOARD_SIZE * BOARD_SIZE)
#define GET_INDEX(i, j) ((i) * (BOARD_SIZE) + (j))
#define GET_COL(x) ((x) % BOARD_SIZE)
#define GET_ROW(x) ((x) / BOARD_SIZE)

typedef struct {
int i_shift, j_shift;
int i_lower_bound, j_lower_bound, i_upper_bound, j_upper_bound;
} line_t;

extern const line_t lines[4];

int *available_moves(char *table);
char check_win(char *t);
Loading

0 comments on commit b87649d

Please sign in to comment.