Spaces:
Runtime error
Runtime error
Pedro de Carvalho commited on
Commit ·
77cf2a4
1
Parent(s): b9b8e2c
Update dragons
Browse files
shared/bot/BotDefinitions.ts
CHANGED
|
@@ -105,9 +105,9 @@ export function getSelfPlayBotIds(): DatasetBotId[] {
|
|
| 105 |
export function createDatasetBot(id: DatasetBotId, options: DatasetBotOptions = {}): DatasetBot {
|
| 106 |
if (id === 'random') return new RandomBot()
|
| 107 |
if (id === 'greedy') return new GreedyBot()
|
| 108 |
-
if (id === 'minimax_weak') return new MinimaxDatasetBot(id, { maxDepth: 1, timeBudgetMs: 80, candidateRadius: 1 })
|
| 109 |
-
if (id === 'minimax_balanced') return new MinimaxDatasetBot(id, { maxDepth: 2, timeBudgetMs: 180, candidateRadius: 2 })
|
| 110 |
-
if (id === 'minimax_strong') return new MinimaxDatasetBot(id, { maxDepth: 3, timeBudgetMs: 320, candidateRadius: 2 })
|
| 111 |
if (id === 'mcts_weak') return new MctsDatasetBot(id, { simulations: 48, rolloutDepth: 12, exploration: 1.4, candidateRadius: 1, earlyTemperature: 1, lateTemperature: 0.15 }, options)
|
| 112 |
if (id === 'mcts_balanced') return new MctsDatasetBot(id, { simulations: 96, rolloutDepth: 18, exploration: 1.2, candidateRadius: 2, earlyTemperature: 0.75, lateTemperature: 0.08 }, options)
|
| 113 |
if (id === 'mcts_strong') return new MctsDatasetBot(id, { simulations: 192, rolloutDepth: 24, exploration: 1.05, candidateRadius: 2, earlyTemperature: 0.6, lateTemperature: 0.03 }, options)
|
|
@@ -177,7 +177,7 @@ class MinimaxDatasetBot implements DatasetBot {
|
|
| 177 |
readonly params: Record<string, number | string | boolean>
|
| 178 |
private readonly bot: MinimaxAlphaBetaBot
|
| 179 |
|
| 180 |
-
constructor(id: DatasetBotId, options: { maxDepth: number; timeBudgetMs: number; candidateRadius: number }) {
|
| 181 |
this.id = id
|
| 182 |
this.params = { strategy: 'minimax', attitude: minimaxAttitude(id), aggressionWeight: 0.65, defensiveBias: 0.85, ...options }
|
| 183 |
this.bot = new MinimaxAlphaBetaBot({ ...options, enableThreatSpaceSearch: false })
|
|
|
|
| 105 |
export function createDatasetBot(id: DatasetBotId, options: DatasetBotOptions = {}): DatasetBot {
|
| 106 |
if (id === 'random') return new RandomBot()
|
| 107 |
if (id === 'greedy') return new GreedyBot()
|
| 108 |
+
if (id === 'minimax_weak') return new MinimaxDatasetBot(id, { maxDepth: 1, timeBudgetMs: 80, candidateRadius: 1, panicPly: 1 })
|
| 109 |
+
if (id === 'minimax_balanced') return new MinimaxDatasetBot(id, { maxDepth: 2, timeBudgetMs: 180, candidateRadius: 2, panicPly: 1 })
|
| 110 |
+
if (id === 'minimax_strong') return new MinimaxDatasetBot(id, { maxDepth: 3, timeBudgetMs: 320, candidateRadius: 2, panicPly: 2 })
|
| 111 |
if (id === 'mcts_weak') return new MctsDatasetBot(id, { simulations: 48, rolloutDepth: 12, exploration: 1.4, candidateRadius: 1, earlyTemperature: 1, lateTemperature: 0.15 }, options)
|
| 112 |
if (id === 'mcts_balanced') return new MctsDatasetBot(id, { simulations: 96, rolloutDepth: 18, exploration: 1.2, candidateRadius: 2, earlyTemperature: 0.75, lateTemperature: 0.08 }, options)
|
| 113 |
if (id === 'mcts_strong') return new MctsDatasetBot(id, { simulations: 192, rolloutDepth: 24, exploration: 1.05, candidateRadius: 2, earlyTemperature: 0.6, lateTemperature: 0.03 }, options)
|
|
|
|
| 177 |
readonly params: Record<string, number | string | boolean>
|
| 178 |
private readonly bot: MinimaxAlphaBetaBot
|
| 179 |
|
| 180 |
+
constructor(id: DatasetBotId, options: { maxDepth: number; timeBudgetMs: number; candidateRadius: number; panicPly?: number }) {
|
| 181 |
this.id = id
|
| 182 |
this.params = { strategy: 'minimax', attitude: minimaxAttitude(id), aggressionWeight: 0.65, defensiveBias: 0.85, ...options }
|
| 183 |
this.bot = new MinimaxAlphaBetaBot({ ...options, enableThreatSpaceSearch: false })
|
shared/bot/MinimaxAlphaBetaBot.ts
CHANGED
|
@@ -27,6 +27,7 @@ export interface MinimaxAlphaBetaOptions {
|
|
| 27 |
enableKillerMoves?: boolean
|
| 28 |
enableIterativeDeepening?: boolean
|
| 29 |
enableThreatSpaceSearch?: boolean
|
|
|
|
| 30 |
}
|
| 31 |
|
| 32 |
interface ThreatSummary {
|
|
@@ -61,7 +62,10 @@ export class MinimaxAlphaBetaBot {
|
|
| 61 |
private readonly enableKillerMoves: boolean
|
| 62 |
private readonly enableIterativeDeepening: boolean
|
| 63 |
private readonly enableThreatSpaceSearch: boolean
|
|
|
|
| 64 |
private deadline = 0
|
|
|
|
|
|
|
| 65 |
private hasher = new ZobristHasher(BOARD_SIZE)
|
| 66 |
private transpositionTable = new Map<bigint, TranspositionEntry>()
|
| 67 |
private killerMoves: BotMove[][] = []
|
|
@@ -76,6 +80,7 @@ export class MinimaxAlphaBetaBot {
|
|
| 76 |
this.enableKillerMoves = options.enableKillerMoves ?? true
|
| 77 |
this.enableIterativeDeepening = options.enableIterativeDeepening ?? true
|
| 78 |
this.enableThreatSpaceSearch = options.enableThreatSpaceSearch ?? true
|
|
|
|
| 79 |
}
|
| 80 |
|
| 81 |
selectMove(context: BotContext): BotMove {
|
|
@@ -84,6 +89,8 @@ export class MinimaxAlphaBetaBot {
|
|
| 84 |
const player = context.currentPlayer
|
| 85 |
const opponent = otherPlayer(player)
|
| 86 |
this.deadline = Date.now() + this.timeBudgetMs
|
|
|
|
|
|
|
| 87 |
this.hasher = new ZobristHasher(boardSize)
|
| 88 |
this.transpositionTable.clear()
|
| 89 |
this.killerMoves = []
|
|
@@ -120,6 +127,7 @@ export class MinimaxAlphaBetaBot {
|
|
| 120 |
let ordered = this.orderMoves(board, candidates, player, context, 0)
|
| 121 |
let best = ordered[0]
|
| 122 |
let bestScore = -Infinity
|
|
|
|
| 123 |
const maxDepth = this.enableIterativeDeepening ? this.maxDepth : this.maxDepth
|
| 124 |
const startDepth = this.enableIterativeDeepening ? 1 : this.maxDepth
|
| 125 |
|
|
@@ -130,12 +138,11 @@ export class MinimaxAlphaBetaBot {
|
|
| 130 |
ordered = this.orderMoves(board, ordered, player, context, 0, best)
|
| 131 |
|
| 132 |
for (const move of ordered) {
|
| 133 |
-
if (this.outOfTime())
|
| 134 |
-
completedDepth = false
|
| 135 |
-
break
|
| 136 |
-
}
|
| 137 |
const next = this.withMove(board, move, player, context.playerSymbols)
|
| 138 |
const score = this.minimax(next, depth - 1, -Infinity, Infinity, false, opponent, player, context, 1)
|
|
|
|
|
|
|
| 139 |
if (score > depthBestScore || (score === depthBestScore && compareMove(move, depthBest) < 0)) {
|
| 140 |
depthBest = move
|
| 141 |
depthBestScore = score
|
|
@@ -149,9 +156,99 @@ export class MinimaxAlphaBetaBot {
|
|
| 149 |
if (!this.enableIterativeDeepening) break
|
| 150 |
}
|
| 151 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
return best
|
| 153 |
}
|
| 154 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
private minimax(
|
| 156 |
board: Map<string, Cell>,
|
| 157 |
depth: number,
|
|
@@ -659,7 +756,15 @@ export class MinimaxAlphaBetaBot {
|
|
| 659 |
}
|
| 660 |
|
| 661 |
private outOfTime(): boolean {
|
| 662 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 663 |
}
|
| 664 |
|
| 665 |
private recordKillerMove(ply: number, move: BotMove): void {
|
|
|
|
| 27 |
enableKillerMoves?: boolean
|
| 28 |
enableIterativeDeepening?: boolean
|
| 29 |
enableThreatSpaceSearch?: boolean
|
| 30 |
+
panicPly?: number
|
| 31 |
}
|
| 32 |
|
| 33 |
interface ThreatSummary {
|
|
|
|
| 62 |
private readonly enableKillerMoves: boolean
|
| 63 |
private readonly enableIterativeDeepening: boolean
|
| 64 |
private readonly enableThreatSpaceSearch: boolean
|
| 65 |
+
private readonly panicPly: number
|
| 66 |
private deadline = 0
|
| 67 |
+
private hardDeadline = 0
|
| 68 |
+
private panicked = false
|
| 69 |
private hasher = new ZobristHasher(BOARD_SIZE)
|
| 70 |
private transpositionTable = new Map<bigint, TranspositionEntry>()
|
| 71 |
private killerMoves: BotMove[][] = []
|
|
|
|
| 80 |
this.enableKillerMoves = options.enableKillerMoves ?? true
|
| 81 |
this.enableIterativeDeepening = options.enableIterativeDeepening ?? true
|
| 82 |
this.enableThreatSpaceSearch = options.enableThreatSpaceSearch ?? true
|
| 83 |
+
this.panicPly = options.panicPly ?? 1
|
| 84 |
}
|
| 85 |
|
| 86 |
selectMove(context: BotContext): BotMove {
|
|
|
|
| 89 |
const player = context.currentPlayer
|
| 90 |
const opponent = otherPlayer(player)
|
| 91 |
this.deadline = Date.now() + this.timeBudgetMs
|
| 92 |
+
this.hardDeadline = Date.now() + Math.max(this.timeBudgetMs * 1.5, this.timeBudgetMs + 200)
|
| 93 |
+
this.panicked = false
|
| 94 |
this.hasher = new ZobristHasher(boardSize)
|
| 95 |
this.transpositionTable.clear()
|
| 96 |
this.killerMoves = []
|
|
|
|
| 127 |
let ordered = this.orderMoves(board, candidates, player, context, 0)
|
| 128 |
let best = ordered[0]
|
| 129 |
let bestScore = -Infinity
|
| 130 |
+
let searchedAny = false
|
| 131 |
const maxDepth = this.enableIterativeDeepening ? this.maxDepth : this.maxDepth
|
| 132 |
const startDepth = this.enableIterativeDeepening ? 1 : this.maxDepth
|
| 133 |
|
|
|
|
| 138 |
ordered = this.orderMoves(board, ordered, player, context, 0, best)
|
| 139 |
|
| 140 |
for (const move of ordered) {
|
| 141 |
+
if (this.outOfTime()) break
|
|
|
|
|
|
|
|
|
|
| 142 |
const next = this.withMove(board, move, player, context.playerSymbols)
|
| 143 |
const score = this.minimax(next, depth - 1, -Infinity, Infinity, false, opponent, player, context, 1)
|
| 144 |
+
if (this.outOfTime()) break
|
| 145 |
+
searchedAny = true
|
| 146 |
if (score > depthBestScore || (score === depthBestScore && compareMove(move, depthBest) < 0)) {
|
| 147 |
depthBest = move
|
| 148 |
depthBestScore = score
|
|
|
|
| 156 |
if (!this.enableIterativeDeepening) break
|
| 157 |
}
|
| 158 |
|
| 159 |
+
if (!searchedAny || this.panicked) {
|
| 160 |
+
const panicMove = this.panicMove(board, player, opponent, context, boardSize)
|
| 161 |
+
if (panicMove) return panicMove
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
return best
|
| 165 |
}
|
| 166 |
|
| 167 |
+
private panicMove(
|
| 168 |
+
board: Map<string, Cell>,
|
| 169 |
+
player: number,
|
| 170 |
+
opponent: number,
|
| 171 |
+
context: BotContext,
|
| 172 |
+
boardSize: number,
|
| 173 |
+
): BotMove | null {
|
| 174 |
+
const candidates = this.generateCandidates(board, boardSize)
|
| 175 |
+
if (candidates.length === 0) return null
|
| 176 |
+
|
| 177 |
+
const ownWin = this.findWinningMove(board, candidates, player, context.playerSymbols, context.rules)
|
| 178 |
+
if (ownWin) return ownWin
|
| 179 |
+
|
| 180 |
+
const blockWin = this.findWinningMove(board, candidates, opponent, context.playerSymbols, context.rules)
|
| 181 |
+
if (blockWin) return blockWin
|
| 182 |
+
|
| 183 |
+
const ordered = this.orderMoves(board, candidates, player, context, 0).slice(0, this.maxCandidates)
|
| 184 |
+
let best = ordered[0]
|
| 185 |
+
let bestScore = -Infinity
|
| 186 |
+
|
| 187 |
+
for (const move of ordered) {
|
| 188 |
+
if (this.hardOutOfTime()) break
|
| 189 |
+
const next = this.withMove(board, move, player, context.playerSymbols)
|
| 190 |
+
const score = this.panicMinimax(next, this.panicPly - 1, -Infinity, Infinity, false, opponent, player, context, 1)
|
| 191 |
+
if (score > bestScore || (score === bestScore && compareMove(move, best) < 0)) {
|
| 192 |
+
best = move
|
| 193 |
+
bestScore = score
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
return best
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
private panicMinimax(
|
| 201 |
+
board: Map<string, Cell>,
|
| 202 |
+
depth: number,
|
| 203 |
+
alpha: number,
|
| 204 |
+
beta: number,
|
| 205 |
+
maximizing: boolean,
|
| 206 |
+
turn: number,
|
| 207 |
+
rootPlayer: number,
|
| 208 |
+
context: BotContext,
|
| 209 |
+
ply: number,
|
| 210 |
+
): number {
|
| 211 |
+
if (depth <= 0 || this.hardOutOfTime()) {
|
| 212 |
+
return this.evaluate(board, rootPlayer, context)
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
const boardSize = context.boardSize ?? context.rules.boardSize ?? BOARD_SIZE
|
| 216 |
+
const candidates = this.orderMoves(board, this.generateCandidates(board, boardSize), turn, {
|
| 217 |
+
...context,
|
| 218 |
+
board: Object.fromEntries(board),
|
| 219 |
+
currentPlayer: turn,
|
| 220 |
+
}, ply).slice(0, this.maxCandidates)
|
| 221 |
+
|
| 222 |
+
if (candidates.length === 0) return this.evaluate(board, rootPlayer, context)
|
| 223 |
+
|
| 224 |
+
if (maximizing) {
|
| 225 |
+
let value = -Infinity
|
| 226 |
+
for (const move of candidates) {
|
| 227 |
+
const next = this.withMove(board, move, turn, context.playerSymbols)
|
| 228 |
+
if (checkWin(next, move.x, move.y, String(turn), context.rules.noOverlines).won) {
|
| 229 |
+
return WIN_SCORE - (this.panicPly - depth)
|
| 230 |
+
}
|
| 231 |
+
value = Math.max(value, this.panicMinimax(next, depth - 1, alpha, beta, false, otherPlayer(turn), rootPlayer, context, ply + 1))
|
| 232 |
+
alpha = Math.max(alpha, value)
|
| 233 |
+
if (alpha >= beta || this.hardOutOfTime()) break
|
| 234 |
+
}
|
| 235 |
+
return value
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
let value = Infinity
|
| 239 |
+
for (const move of candidates) {
|
| 240 |
+
const next = this.withMove(board, move, turn, context.playerSymbols)
|
| 241 |
+
if (checkWin(next, move.x, move.y, String(turn), context.rules.noOverlines).won) {
|
| 242 |
+
return -WIN_SCORE + (this.panicPly - depth)
|
| 243 |
+
}
|
| 244 |
+
value = Math.min(value, this.panicMinimax(next, depth - 1, alpha, beta, true, otherPlayer(turn), rootPlayer, context, ply + 1))
|
| 245 |
+
beta = Math.min(beta, value)
|
| 246 |
+
if (alpha >= beta || this.hardOutOfTime()) break
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
return value
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
private minimax(
|
| 253 |
board: Map<string, Cell>,
|
| 254 |
depth: number,
|
|
|
|
| 756 |
}
|
| 757 |
|
| 758 |
private outOfTime(): boolean {
|
| 759 |
+
if (Date.now() >= this.deadline) {
|
| 760 |
+
this.panicked = true
|
| 761 |
+
return true
|
| 762 |
+
}
|
| 763 |
+
return false
|
| 764 |
+
}
|
| 765 |
+
|
| 766 |
+
private hardOutOfTime(): boolean {
|
| 767 |
+
return Date.now() >= this.hardDeadline
|
| 768 |
}
|
| 769 |
|
| 770 |
private recordKillerMove(ply: number, move: BotMove): void {
|