Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions config/tron.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
[base]
env_name = tron
async = 0
checkpoint_interval = 500
eval_agents = 2048

[vec]
total_agents = 4096
num_buffers = 2
num_threads = 8
num_policies = 2
hist_policy_percent = 0.15
hist_policy_hidden_size = 256
hist_policy_num_layers = 2

[env]
num_agents = 2
bot_difficulty = 2 # 0 random, 1 survives, 2 space, 3 minimax
opening_steps = 4
reward_territory = 0.12672
territory_gamma = 0.995 # Keep equal to train.gamma

[selfplay]
enabled = 1
max_size = 16
seed = 42
opp_timeout_steps = 100_000_000
eval_pool_size = 8
eval_games = 4096

[policy]
hidden_size = 256
num_layers = 2
expansion_factor = 1

[train]
total_timesteps = 1_000_000_000
learning_rate = 0.001
gamma = 0.995
gae_lambda = 0.96917
ent_coef = 0.03
anneal_ent_coef = 1
min_ent_coef_ratio = 0.1
minibatch_size = 8192
horizon = 128
91 changes: 91 additions & 0 deletions ocean/tron/bots.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// 3 checks every immediate response
enum {
BOT_RANDOM,
BOT_SURVIVAL,
BOT_SPACE,
BOT_MINIMAX,
BOT_LEVELS
};

static const TronAction BOT_ACTIONS[3] = {STRAIGHT, LEFT, RIGHT};

TronAction bot_action(const TronGame *game, int player, int level,
unsigned int *rng, BotCache *cache) {
if (game->outcome != PLAYING) return STRAIGHT;
if (level == BOT_RANDOM) return BOT_ACTIONS[rand_r(rng) % 3];

int opp = other_player(player);
TronAction best[3];
int best_n = 0;
// Level 1 avoids an immediate crash against a straight opponent
if (level == BOT_SURVIVAL) {
for (int i = 0; i < 3; i++) {
TronGame sim = *game;
TronActions atn = {.player = {
[PLAYER_CYAN] = STRAIGHT,
[PLAYER_RED] = STRAIGHT,
}};
atn.player[player] = BOT_ACTIONS[i];
step(&sim, atn);
if (sim.outcome == PLAYING || sim.outcome == player_win(player)) {
best[best_n++] = BOT_ACTIONS[i];
}
}
if (best_n == 0) return BOT_ACTIONS[rand_r(rng) % 3];
return best[rand_r(rng) % best_n];
}

// One minimax call consumes at most 3 candidates * 3 replies * 2 fills = 18
// marks. Reset above 230 so uint8_t cannot wrap during a call
if (cache->mark > 230) {
memset(cache->seen, 0, sizeof(cache->seen));
cache->mark = 0;
}
uint16_t queue[CELLS];
int best_value = -(CELLS + 2);
// Level 2 assumes straight; level 3 checks all replies
int responses = level == BOT_SPACE ? 1 : 3;
for (int i = 0; i < 3; i++) {
int worst = CELLS + 2;
for (int j = 0; j < responses; j++) {
TronGame sim = *game;
TronActions atn = {.player = {
[PLAYER_CYAN] = STRAIGHT,
[PLAYER_RED] = STRAIGHT,
}};
atn.player[player] = BOT_ACTIONS[i];
atn.player[opp] = BOT_ACTIONS[j];
step(&sim, atn);

int value = 0;
if (sim.outcome != DRAW) {
if (sim.outcome != PLAYING) {
value = sim.outcome == player_win(player)
? CELLS + 1
: -(CELLS + 1);
} else {
int own = trail_index(sim.x[player], sim.y[player]);
int opponent = trail_index(sim.x[opp], sim.y[opp]);
int own_size = flood(&sim, own, opponent, cache->seen, queue,
++cache->mark);
int opponent_size = flood(
&sim, opponent, own,
cache->seen, queue, ++cache->mark);
value = own_size - opponent_size;
}
}
if (value < worst) worst = value;

if (worst < best_value) break;
}
if (worst > best_value) {
best_value = worst;
best[0] = BOT_ACTIONS[i];
best_n = 1;
} else if (worst == best_value) {
best[best_n++] = BOT_ACTIONS[i];
}
}
// avoids a straight/left/right bias
return best[rand_r(rng) % best_n];
}
60 changes: 60 additions & 0 deletions ocean/tron/tron.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#define _POSIX_C_SOURCE 200809L

#include "tron.h"

int main(void) {
const float tick_dt = 1.0f / RENDER_TICKS_PER_SECOND;
const float reset_delay = 0.6f;
TronGame game = {0};
TronRenderer renderer = {0};
BotCache cache[PLAYERS] = {0};
unsigned int rng = 0x6d2b79f5u;
float elapsed = 0.0f;
float crash_age = 1.0f;

SetConfigFlags(FLAG_MSAA_4X_HINT);
InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "PufferLib // Tron");
SetExitKey(KEY_NULL);
SetTargetFPS(RENDER_FPS);
reset(&game);
renderer_init(&renderer, &game);

while (!WindowShouldClose()) {
float dt = GetFrameTime();
if (game.outcome != PLAYING) crash_age += dt;
if (game.outcome != PLAYING && crash_age >= reset_delay) {
reset(&game);
renderer.previous = game;
trails_reset(renderer.trail, &game);
elapsed = 0.0f;
crash_age = 1.0f;
}

if (game.outcome == PLAYING) elapsed += dt < tick_dt ? dt : tick_dt;
if (elapsed >= tick_dt) {
renderer.previous = game;
step(&game,
(TronActions){.player = {
[PLAYER_CYAN] = bot_action(
&game, PLAYER_CYAN, BOT_MINIMAX,
&rng, &cache[PLAYER_CYAN]),
[PLAYER_RED] = bot_action(
&game, PLAYER_RED, BOT_MINIMAX,
&rng, &cache[PLAYER_RED]),
}});
trails_record(renderer.trail, &game);
elapsed -= tick_dt;
if (game.outcome != PLAYING) crash_age = 0.0f;
}

float lerp = game.tick && game.outcome == PLAYING
? elapsed / tick_dt
: 1.0f;
renderer_draw(&renderer, &game, lerp, crash_age);
}

UnloadTexture(renderer.cycle);
UnloadTexture(renderer.puffer);
UnloadTexture(renderer.crash);
CloseWindow();
}
Loading