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
- return Date.now() >= this.deadline
 
 
 
 
 
 
 
 
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 {