11import { analyzeMove , index , inside , SIZE , type Board , type Color , type Point } from './rules' ;
22
3- export type Difficulty = 'easy' | 'normal' | 'hard' | 'master' ;
3+ export type Difficulty = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 ;
4+
5+ const LEVELS : Record < Difficulty , { picks : number ; depth : number ; beam : number ; time : number } > = {
6+ 1 : { picks : 8 , depth : 0 , beam : 0 , time : 0 } ,
7+ 2 : { picks : 5 , depth : 0 , beam : 0 , time : 0 } ,
8+ 3 : { picks : 3 , depth : 0 , beam : 0 , time : 0 } ,
9+ 4 : { picks : 1 , depth : 0 , beam : 0 , time : 0 } ,
10+ 5 : { picks : 1 , depth : 2 , beam : 4 , time : 300 } ,
11+ 6 : { picks : 1 , depth : 3 , beam : 5 , time : 850 } ,
12+ 7 : { picks : 1 , depth : 5 , beam : 7 , time : 2800 } ,
13+ 8 : { picks : 1 , depth : 7 , beam : 8 , time : 5000 } ,
14+ 9 : { picks : 1 , depth : 9 , beam : 10 , time : 8500 } ,
15+ } ;
16+
17+ export function normalizeDifficulty ( value : unknown ) : Difficulty {
18+ const number = Number ( value ) ;
19+ return Number . isInteger ( number ) && number >= 1 && number <= 9 ? ( number as Difficulty ) : 5 ;
20+ }
421const directions = [
522 [ 1 , 0 ] ,
623 [ 0 , 1 ] ,
@@ -72,7 +89,7 @@ function lineScore(board: Board, point: Point, color: Color): number {
7289 if ( board [ index ( x , y ) ] === color ) mine ++ ;
7390 else blank ++ ;
7491 }
75- if ( ! blocked && blank && mine >= 3 ) score += mine === 4 ? 1400 : 90 ;
92+ if ( ! blocked && blank && mine >= 3 ) score += mine === 4 ? 5000 : 500 ;
7693 }
7794 }
7895 board [ index ( point . x , point . y ) ] = 0 ;
@@ -94,16 +111,25 @@ function ranked(board: Board, color: Color, search?: Search): Candidate[] {
94111 const cached = search ?. positions . get ( key ) ;
95112 if ( cached ) return cached ;
96113 const result : Candidate [ ] = [ ] ;
97- for ( const point of candidates ( board ) ) {
98- const verdict = analyzeMove ( board , point . x , point . y , color ) ;
114+ const legality = new Map < string , ReturnType < typeof analyzeMove > > ( ) ;
115+ const scored = candidates ( board )
116+ . map ( ( point ) => {
117+ const offense = lineScore ( board , point , color ) ;
118+ const defense = lineScore ( board , point , other ( color ) ) ;
119+ const center = ( 7 - Math . abs ( 7 - point . x ) + 7 - Math . abs ( 7 - point . y ) ) * 2 ;
120+ return { point, offense, defense, center, score : offense + defense * 0.82 + center } ;
121+ } )
122+ . sort ( ( a , b ) => b . score - a . score ) ;
123+ // Deep search spends its time on promising local moves while immediate wins
124+ // and blocks stay at the front of the list through their five-in-a-row score.
125+ const shortlist = search ? scored . slice ( 0 , Math . max ( 18 , search . beam * 2 ) ) : scored ;
126+ for ( const { point, offense, defense, center } of shortlist ) {
127+ const verdict = analyzeMove ( board , point . x , point . y , color , legality ) ;
99128 if ( ! verdict . legal ) continue ;
100- const offense = lineScore ( board , point , color ) ;
101- const defense = analyzeMove ( board , point . x , point . y , other ( color ) ) . legal
102- ? lineScore ( board , point , other ( color ) )
103- : 0 ;
129+ const legalDefense = analyzeMove ( board , point . x , point . y , other ( color ) , legality ) . legal ? defense : 0 ;
104130 result . push ( {
105131 point,
106- score : offense + defense * 0.82 + ( 7 - Math . abs ( 7 - point . x ) + 7 - Math . abs ( 7 - point . y ) ) * 2 ,
132+ score : offense + legalDefense * 0.82 + center ,
107133 offense,
108134 win : verdict . win ,
109135 } ) ;
@@ -179,26 +205,27 @@ function searchPosition(
179205}
180206
181207export function chooseAiMove ( board : Board , color : Color , difficulty : Difficulty ) : Point | null {
208+ const level = LEVELS [ normalizeDifficulty ( difficulty ) ] ;
182209 const choices = ranked ( board , color ) ;
183210 if ( ! choices . length ) return null ;
184211 if ( choices . length === 1 ) return choices [ 0 ] . point ;
185212 const winning = choices . find ( ( choice ) => choice . win ) ;
186213 if ( winning ) return winning . point ;
187- if ( difficulty === 'easy' ) return choices [ Math . floor ( Math . random ( ) * Math . min ( 6 , choices . length ) ) ] . point ;
214+ if ( level . picks > 1 ) return choices [ Math . floor ( Math . random ( ) * Math . min ( level . picks , choices . length ) ) ] . point ;
188215 const threats = ranked ( board , other ( color ) ) . filter ( ( choice ) => choice . win ) ;
189216 const threatCells = new Set ( threats . map ( ( move ) => index ( move . point . x , move . point . y ) ) ) ;
190217 const roots = threats . length ? choices . filter ( ( move ) => threatCells . has ( index ( move . point . x , move . point . y ) ) ) : choices ;
191218 if ( ! roots . length ) return choices [ 0 ] . point ;
192219 if ( roots . length === 1 ) return roots [ 0 ] . point ;
193- if ( difficulty === 'normal' ) return roots [ 0 ] . point ;
220+ if ( ! level . depth ) return roots [ 0 ] . point ;
194221 const search : Search = {
195- deadline : Date . now ( ) + ( difficulty === 'master' ? 2800 : 850 ) ,
196- beam : difficulty === 'master' ? 7 : 5 ,
222+ deadline : Date . now ( ) + level . time ,
223+ beam : level . beam ,
197224 positions : new Map ( ) ,
198225 transpositions : new Map ( ) ,
199226 } ;
200227 let best = roots [ 0 ] ;
201- const maximumDepth = difficulty === 'master' ? 5 : 3 ;
228+ const maximumDepth = level . depth ;
202229 for ( let depth = 1 ; depth <= maximumDepth ; depth ++ ) {
203230 let iteration = best ,
204231 bestScore = - Infinity ,
0 commit comments