Skip to content

Commit 2c839a4

Browse files
committed
⚡ improve: optimize gomoku
1 parent 98bbf19 commit 2c839a4

26 files changed

+408
-349
lines changed

Diff for: .vscode/settings.json

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"cSpell.words": [
3+
"ccrf",
34
"codeforces",
45
"deno",
56
"Dinic",
@@ -16,6 +17,7 @@
1617
"mcmf",
1718
"mincost",
1819
"mincut",
20+
"mwps",
1921
"nums",
2022
"Parens",
2123
"pinst",

Diff for: packages/gomoku/__test__/count-map.spec.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import fs from 'fs-extra'
22
import { locateFixtures } from 'jest.setup'
33
import type { GomokuDirectionType, IGomokuContextProps, IGomokuPiece } from '../src'
44
import { GomokuContext, GomokuCountMap, GomokuDirectionTypes } from '../src'
5-
import { PieceDataDirName, locatePieceDataFilepaths } from './util'
65

76
const { rightHalf: halfDirectionTypes } = GomokuDirectionTypes
87

@@ -103,8 +102,8 @@ describe('15x15', () => {
103102
expect(tester.candidateCouldReachFinal(1, posId)).toEqual(
104103
tester.$candidateCouldReachFinal(1, posId),
105104
)
106-
expect(Array.from(tester.mustDropPos(0)).length).toEqual(tester.$stateCouldReachFinal(0))
107-
expect(Array.from(tester.mustDropPos(1)).length).toEqual(tester.$stateCouldReachFinal(1))
105+
expect(Array.from(tester.mustWinPosSet(0)).length).toEqual(tester.$stateCouldReachFinal(0))
106+
expect(Array.from(tester.mustWinPosSet(1)).length).toEqual(tester.$stateCouldReachFinal(1))
108107
}
109108
for (let posId = 0; posId < tester.context.TOTAL_POS; ++posId) {
110109
expect(tester.candidateCouldReachFinal(0, posId)).toEqual(
@@ -113,8 +112,8 @@ describe('15x15', () => {
113112
expect(tester.candidateCouldReachFinal(1, posId)).toEqual(
114113
tester.$candidateCouldReachFinal(1, posId),
115114
)
116-
expect(Array.from(tester.mustDropPos(0)).length).toEqual(tester.$stateCouldReachFinal(0))
117-
expect(Array.from(tester.mustDropPos(1)).length).toEqual(tester.$stateCouldReachFinal(1))
115+
expect(Array.from(tester.mustWinPosSet(0)).length).toEqual(tester.$stateCouldReachFinal(0))
116+
expect(Array.from(tester.mustWinPosSet(1)).length).toEqual(tester.$stateCouldReachFinal(1))
118117
}
119118
}
120119
})

Diff for: packages/gomoku/__test__/solution.spec.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,17 @@ describe('15x15', function () {
2929
const solution = getSolution()
3030
solution.init([])
3131
solution.forward(7, 7, 0)
32-
const [r, c] = solution.minimaxSearch(1)
32+
let [r, c] = solution.minimaxSearch(1)
3333
expect(Math.abs(r - 7)).toBeLessThanOrEqual(2)
3434
expect(Math.abs(c - 7)).toBeLessThanOrEqual(2)
3535

3636
solution.forward(6, 6, 1)
37+
;[r, c] = solution.minimaxSearch(0)
3738
expect(Math.abs(r - 7)).toBeLessThanOrEqual(3)
3839
expect(Math.abs(c - 7)).toBeLessThanOrEqual(3)
3940

4041
solution.forward(6, 7, 0)
42+
;[r, c] = solution.minimaxSearch(1)
4143
expect(Math.abs(r - 7)).toBeLessThanOrEqual(3)
4244
expect(Math.abs(c - 7)).toBeLessThanOrEqual(3)
4345
})

Diff for: packages/gomoku/__test__/state.spec.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -94,14 +94,14 @@ class TestHelper extends GomokuState {
9494
if (context.hasPlacedNeighbors(id)) candidateSet.add(id)
9595
}
9696
}
97-
if (countMap.mustDropPos(nextPlayerId)) {
97+
if (countMap.mustWinPosSet(nextPlayerId).size > 0) {
9898
for (const posId of _candidateSet) {
9999
if (countMap.candidateCouldReachFinal(nextPlayerId, posId)) {
100100
return [posId]
101101
}
102102
}
103103
}
104-
if (countMap.mustDropPos(nextPlayerId ^ 1)) {
104+
if (countMap.mustWinPosSet(nextPlayerId ^ 1).size > 0) {
105105
const playerId: number = nextPlayerId ^ 1
106106
for (const posId of _candidateSet) {
107107
if (countMap.candidateCouldReachFinal(playerId, posId)) {
@@ -128,14 +128,14 @@ class TestHelper extends GomokuState {
128128
public $getCandidates(nextPlayerId: number, minMultipleOfTopScore: number): IGomokuCandidate[] {
129129
const { countMap, _candidateSet } = this
130130

131-
if (countMap.mustDropPos(nextPlayerId)) {
131+
if (countMap.mustWinPosSet(nextPlayerId).size > 0) {
132132
for (const posId of _candidateSet) {
133133
if (countMap.candidateCouldReachFinal(nextPlayerId, posId)) {
134134
return [{ posId, score: Number.MAX_VALUE }]
135135
}
136136
}
137137
}
138-
if (countMap.mustDropPos(nextPlayerId ^ 1)) {
138+
if (countMap.mustWinPosSet(nextPlayerId ^ 1).size > 0) {
139139
const playerId: number = nextPlayerId ^ 1
140140
for (const posId of _candidateSet) {
141141
if (countMap.candidateCouldReachFinal(playerId, posId)) {

Diff for: packages/gomoku/src/context.ts

+35-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { GomokuDirectionTypes, GomokuDirections } from './constant'
22
import type { GomokuDirectionType } from './constant'
3-
import type { IGomokuContext } from './context.type'
4-
import type { IDirCounter, IGomokuBoard, IGomokuPiece } from './types'
3+
import type { IGomokuContext } from './types/context'
4+
import type { IDirCounter, IGomokuBoard, IGomokuPiece } from './types/misc'
55
import { createHighDimensionArray } from './util/createHighDimensionArray'
66

77
const { full: fullDirectionTypes, rightHalf: halfDirectionTypes } = GomokuDirectionTypes
@@ -25,7 +25,6 @@ export class GomokuContext implements IGomokuContext {
2525
public readonly TOTAL_POS: number
2626
public readonly MIDDLE_POS: number
2727
public readonly board: Readonly<IGomokuBoard>
28-
public readonly idx: (r: number, c: number) => number
2928
protected readonly _idxMap: Readonly<IIdxMap>
3029
protected readonly _gomokuDirections: Readonly<IGomokuDirections>
3130
protected readonly _maxMovableMap: number[][] // [dirType][posId] => MAX_MOVABLE_STEPS
@@ -44,7 +43,6 @@ export class GomokuContext implements IGomokuContext {
4443
const _MAX_DISTANCE_OF_NEIGHBOR: number = Math.max(1, MAX_DISTANCE_OF_NEIGHBOR)
4544
const _TOTAL_PLAYER = 2
4645
const _TOTAL_POS: number = _MAX_ROW * _MAX_COL
47-
const idx = (r: number, c: number): number => r * _MAX_ROW + c
4846

4947
this.MAX_ROW = _MAX_ROW
5048
this.MAX_COL = _MAX_COL
@@ -54,12 +52,11 @@ export class GomokuContext implements IGomokuContext {
5452
this.TOTAL_POS = _TOTAL_POS
5553
this.MIDDLE_POS = _TOTAL_POS >> 1
5654
this.board = new Array(_TOTAL_POS).fill(-1)
57-
this.idx = idx
5855

5956
const _idxMap: IIdxMap = new Array(_TOTAL_POS)
6057
for (let r = 0; r < _MAX_ROW; ++r) {
6158
for (let c = 0; c < _MAX_COL; ++c) {
62-
const posId: number = idx(r, c)
59+
const posId: number = this.idx(r, c)
6360
_idxMap[posId] = [r, c]
6461
}
6562
}
@@ -93,7 +90,7 @@ export class GomokuContext implements IGomokuContext {
9390
_dirStartPosMap[revDirType][posId] = posId
9491
_dirStartPosSet[revDirType].push(posId)
9592
} else {
96-
const posId2 = idx(r2, c2)
93+
const posId2 = this.idx(r2, c2)
9794
_maxMovableMap[dirType][posId] = _maxMovableMap[dirType][posId2] + 1
9895
_dirStartPosMap[revDirType][posId] = _dirStartPosMap[revDirType][posId2]
9996
}
@@ -218,6 +215,10 @@ export class GomokuContext implements IGomokuContext {
218215
return true
219216
}
220217

218+
public idx(r: number, c: number): number {
219+
return r * this.MAX_ROW + c
220+
}
221+
221222
public revIdx(posId: number): Readonly<[r: number, c: number]> {
222223
return this._idxMap[posId]
223224
}
@@ -256,6 +257,33 @@ export class GomokuContext implements IGomokuContext {
256257
return this._neighborPlacedCount[posId] > 0
257258
}
258259

260+
public couldReachFinalInDirection(
261+
playerId: number,
262+
posId: number,
263+
dirType: GomokuDirectionType,
264+
): boolean {
265+
const { MAX_ADJACENT, board } = this
266+
const revDirType: GomokuDirectionType = dirType ^ 1
267+
268+
const maxMovableSteps0: number = this.maxMovableSteps(posId, revDirType)
269+
const maxMovableSteps2: number = this.maxMovableSteps(posId, dirType)
270+
if (maxMovableSteps0 + maxMovableSteps2 + 1 < MAX_ADJACENT) return false
271+
272+
let count = 1
273+
for (let id = posId, step = 0; step < maxMovableSteps0; ++step) {
274+
id = this.fastMoveOneStep(id, revDirType)
275+
if (board[id] !== playerId) break
276+
count += 1
277+
}
278+
279+
for (let id = posId, step = 0; step < maxMovableSteps2; ++step) {
280+
id = this.fastMoveOneStep(id, dirType)
281+
if (board[id] !== playerId) break
282+
count += 1
283+
}
284+
return count >= MAX_ADJACENT
285+
}
286+
259287
public getStartPosId(posId: number, dirType: GomokuDirectionType): number {
260288
return this._dirStartPosMap[dirType][posId]
261289
}

Diff for: packages/gomoku/src/count-map.ts

+26-50
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
import type { GomokuDirectionType } from './constant'
22
import { GomokuDirectionTypeBitset, GomokuDirectionTypes } from './constant'
3-
import type { IGomokuContext } from './context.type'
4-
import type { IGomokuCountMap } from './count-map.type'
3+
import type { IGomokuContext } from './types/context'
4+
import type { IGomokuCountMap } from './types/count-map'
55
import { createHighDimensionArray } from './util/createHighDimensionArray'
66

77
const { rightHalf: halfDirectionTypes } = GomokuDirectionTypes
88
const { rightHalf: allDirectionTypeBitset } = GomokuDirectionTypeBitset
99

1010
export class GomokuCountMap implements IGomokuCountMap {
1111
public readonly context: Readonly<IGomokuContext>
12-
protected readonly _mustDropPosSet: Array<Set<number>> // [playerId] => <must-drop position set>
12+
protected readonly _mustWinPosSet: Array<Set<number>> // [playerId] => <must-drop position set>
1313
protected readonly _candidateCouldReachFinal: number[][] // [playerId][posId]
1414

1515
constructor(context: Readonly<IGomokuContext>) {
1616
this.context = context
17-
this._mustDropPosSet = createHighDimensionArray(() => new Set<number>(), 2)
17+
this._mustWinPosSet = createHighDimensionArray(() => new Set<number>(), context.TOTAL_PLAYER)
1818
this._candidateCouldReachFinal = createHighDimensionArray(
1919
() => 0,
2020
context.TOTAL_PLAYER,
@@ -23,28 +23,30 @@ export class GomokuCountMap implements IGomokuCountMap {
2323
}
2424

2525
public init(): void {
26-
this._mustDropPosSet.forEach(set => set.clear())
2726
this._candidateCouldReachFinal.forEach(item => item.fill(0))
27+
this._mustWinPosSet.forEach(set => set.clear())
2828

29-
const { context, _mustDropPosSet, _candidateCouldReachFinal } = this
30-
const { TOTAL_POS } = context
29+
const { context } = this
30+
const { TOTAL_POS, board } = context
31+
const [ccrf0, ccrf1] = this._candidateCouldReachFinal
32+
const [mwps0, mwps1] = this._mustWinPosSet
3133

3234
// Initialize _candidateCouldReachFinal.
3335
for (let posId = 0; posId < TOTAL_POS; ++posId) {
34-
if (context.board[posId] >= 0) continue
36+
if (board[posId] >= 0) continue
3537

3638
let flag0 = 0
3739
let flag1 = 0
3840
for (const dirType of halfDirectionTypes) {
3941
const bitFlag: number = 1 << dirType
40-
if (this._couldReachFinalInDirection(0, posId, dirType)) flag0 |= bitFlag
41-
if (this._couldReachFinalInDirection(1, posId, dirType)) flag1 |= bitFlag
42+
if (context.couldReachFinalInDirection(0, posId, dirType)) flag0 |= bitFlag
43+
if (context.couldReachFinalInDirection(1, posId, dirType)) flag1 |= bitFlag
4244
}
4345

44-
if (flag0 > 0) _mustDropPosSet[0].add(posId)
45-
if (flag1 > 0) _mustDropPosSet[1].add(posId)
46-
_candidateCouldReachFinal[0][posId] = flag0
47-
_candidateCouldReachFinal[1][posId] = flag1
46+
ccrf0[posId] = flag0
47+
ccrf1[posId] = flag1
48+
if (flag0 > 0) mwps0.add(posId)
49+
if (flag1 > 0) mwps1.add(posId)
4850
}
4951
}
5052

@@ -58,8 +60,8 @@ export class GomokuCountMap implements IGomokuCountMap {
5860
this._updateRelatedCouldReachFinal(posId)
5961
}
6062

61-
public mustDropPos(playerId: number): Iterable<number> & { size: number } {
62-
return this._mustDropPosSet[playerId]
63+
public mustWinPosSet(playerId: number): Iterable<number> & { size: number } {
64+
return this._mustWinPosSet[playerId]
6365
}
6466

6567
public candidateCouldReachFinal(playerId: number, posId: number): boolean {
@@ -96,14 +98,14 @@ export class GomokuCountMap implements IGomokuCountMap {
9698

9799
protected _updateCouldReachFinal(posId: number, expiredBitset: number): void {
98100
if (expiredBitset > 0) {
99-
const { _mustDropPosSet, _candidateCouldReachFinal } = this
101+
const { context, _mustWinPosSet, _candidateCouldReachFinal } = this
100102
const prevFlag0 = _candidateCouldReachFinal[0][posId]
101103
const prevFlag1 = _candidateCouldReachFinal[1][posId]
102104

103-
_mustDropPosSet[0].delete(posId)
104-
_mustDropPosSet[1].delete(posId)
105+
_mustWinPosSet[0].delete(posId)
106+
_mustWinPosSet[1].delete(posId)
105107

106-
if (this.context.board[posId] >= 0) {
108+
if (context.board[posId] >= 0) {
107109
_candidateCouldReachFinal[0][posId] = 0
108110
_candidateCouldReachFinal[1][posId] = 0
109111
return
@@ -114,8 +116,8 @@ export class GomokuCountMap implements IGomokuCountMap {
114116
for (const dirType of halfDirectionTypes) {
115117
const bitFlag: number = 1 << dirType
116118
if (bitFlag & expiredBitset) {
117-
if (this._couldReachFinalInDirection(0, posId, dirType)) nextFlag0 |= bitFlag
118-
if (this._couldReachFinalInDirection(1, posId, dirType)) nextFlag1 |= bitFlag
119+
if (context.couldReachFinalInDirection(0, posId, dirType)) nextFlag0 |= bitFlag
120+
if (context.couldReachFinalInDirection(1, posId, dirType)) nextFlag1 |= bitFlag
119121
} else {
120122
nextFlag0 |= prevFlag0 & bitFlag
121123
nextFlag1 |= prevFlag1 & bitFlag
@@ -124,34 +126,8 @@ export class GomokuCountMap implements IGomokuCountMap {
124126

125127
_candidateCouldReachFinal[0][posId] = nextFlag0
126128
_candidateCouldReachFinal[1][posId] = nextFlag1
127-
if (nextFlag0 > 0) _mustDropPosSet[0].add(posId)
128-
if (nextFlag1 > 0) _mustDropPosSet[1].add(posId)
129-
}
130-
}
131-
132-
protected _couldReachFinalInDirection(
133-
playerId: number,
134-
posId: number,
135-
dirType: GomokuDirectionType,
136-
): boolean {
137-
const { context } = this
138-
const { MAX_ADJACENT, board } = context
139-
const revDirType: GomokuDirectionType = dirType ^ 1
140-
141-
let count = 1
142-
const maxMovableSteps0: number = context.maxMovableSteps(posId, revDirType)
143-
for (let id = posId, step = 0; step < maxMovableSteps0; ++step) {
144-
id = context.fastMoveOneStep(id, revDirType)
145-
if (board[id] !== playerId) break
146-
count += 1
147-
}
148-
149-
const maxMovableSteps2: number = context.maxMovableSteps(posId, dirType)
150-
for (let id = posId, step = 0; step < maxMovableSteps2; ++step) {
151-
id = context.fastMoveOneStep(id, dirType)
152-
if (board[id] !== playerId) break
153-
count += 1
129+
if (nextFlag0 > 0) _mustWinPosSet[0].add(posId)
130+
if (nextFlag1 > 0) _mustWinPosSet[1].add(posId)
154131
}
155-
return count >= MAX_ADJACENT
156132
}
157133
}

Diff for: packages/gomoku/src/index.ts

+16-10
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
export * from './constant'
22
export * from './context'
3-
export * from './context.type'
43
export * from './count-map'
5-
export * from './count-map.type'
4+
export * from './searcher-context'
65
export * from './solution'
76
export * from './state'
8-
export * from './state.type'
9-
export * from './state-cache'
10-
export * from './state-compressor'
11-
export * from './search/alpha-beta'
12-
export * from './search/deep'
13-
export * from './search/narrow'
7+
8+
export * from './searcher/alpha-beta'
9+
export * from './searcher/deep'
10+
export * from './searcher/narrow'
11+
12+
export * from './types/context'
13+
export * from './types/count-map'
14+
export * from './types/misc'
15+
export * from './types/searcher'
16+
export * from './types/searcher-context'
17+
export * from './types/state'
18+
1419
export * from './util/createHighDimensionArray'
20+
export * from './util/createMinimaxSearcher'
1521
export * from './util/createScoreMap'
16-
export * from './util/createMinimaxSearcherContext'
17-
export * from './types'
22+
export * from './util/state-cache'
23+
export * from './util/state-compressor'

0 commit comments

Comments
 (0)