f0032945378484a280a3764215b1f89827335932
[vchess.git] / client / src / variants / Gomoku.js
1 import { ChessRules, Move, PiPo } from "@/base_rules";
2 import { randInt } from "@/utils/alea";
3
4 export class GomokuRules extends ChessRules {
5
6 static get Monochrome() {
7 return true;
8 }
9
10 static get Notoodark() {
11 return true;
12 }
13
14 static get Lines() {
15 let lines = [];
16 // Draw all inter-squares lines, shifted:
17 for (let i = 0; i < V.size.x; i++)
18 lines.push([[i+0.5, 0.5], [i+0.5, V.size.y-0.5]]);
19 for (let j = 0; j < V.size.y; j++)
20 lines.push([[0.5, j+0.5], [V.size.x-0.5, j+0.5]]);
21 return lines;
22 }
23
24 static get HasFlags() {
25 return false;
26 }
27
28 static get HasEnpassant() {
29 return false;
30 }
31
32 static get ReverseColors() {
33 return true;
34 }
35
36 static IsGoodPosition(position) {
37 if (position.length == 0) return false;
38 const rows = position.split("/");
39 if (rows.length != V.size.x) return false;
40 for (let row of rows) {
41 let sumElts = 0;
42 for (let i = 0; i < row.length; i++) {
43 if (row[i].toLowerCase() == V.PAWN) sumElts++;
44 else {
45 const num = parseInt(row[i], 10);
46 if (isNaN(num) || num <= 0) return false;
47 sumElts += num;
48 }
49 }
50 if (sumElts != V.size.y) return false;
51 }
52 return true;
53 }
54
55 static get size() {
56 return { x: 19, y: 19 };
57 }
58
59 static GenRandInitFen() {
60 return [...Array(19)].map(e => "991").join('/') + " w 0";
61 }
62
63 setOtherVariables() {}
64
65 getPiece() {
66 return V.PAWN;
67 }
68
69 getPpath(b) {
70 return "Gomoku/" + b;
71 }
72
73 onlyClick() {
74 return true;
75 }
76
77 canIplay(side, [x, y]) {
78 return (side == this.turn && this.board[x][y] == V.EMPTY);
79 }
80
81 hoverHighlight([x, y], side) {
82 if (!!side && side != this.turn) return false;
83 return (this.board[x][y] == V.EMPTY);
84 }
85
86 doClick([x, y]) {
87 return (
88 new Move({
89 appear: [
90 new PiPo({ x: x, y: y, c: this.turn, p: V.PAWN })
91 ],
92 vanish: [],
93 start: { x: -1, y: -1 },
94 })
95 );
96 }
97
98 getPotentialMovesFrom([x, y]) {
99 return [this.doClick([x, y])];
100 }
101
102 getAllPotentialMoves() {
103 let moves = [];
104 for (let i = 0; i < 19; i++) {
105 for (let j=0; j < 19; j++) {
106 if (this.board[i][j] == V.EMPTY) moves.push(this.doClick(i, j));
107 }
108 }
109 return moves;
110 }
111
112 filterValid(moves) {
113 return moves;
114 }
115
116 getCheckSquares() {
117 return [];
118 }
119
120 postPlay() {}
121 postUndo() {}
122
123 countAlignedStones([x, y], color) {
124 let maxInLine = 0;
125 for (let s of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) {
126 // Skip half of steps, since we explore both directions
127 if (s[0] == -1 || (s[0] == 0 && s[1] == -1)) continue;
128 let countInLine = 1;
129 for (let dir of [-1, 1]) {
130 let [i, j] = [x + dir * s[0], y + dir * s[1]];
131 while (
132 V.OnBoard(i, j) &&
133 this.board[i][j] != V.EMPTY &&
134 this.getColor(i, j) == color
135 ) {
136 countInLine++;
137 i += dir * s[0];
138 j += dir * s[1];
139 }
140 }
141 if (countInLine > maxInLine) maxInLine = countInLine;
142 }
143 return maxInLine;
144 }
145
146 getCurrentScore() {
147 let fiveAlign = { w: false, b: false, wNextTurn: false };
148 for (let i=0; i<19; i++) {
149 for (let j=0; j<19; j++) {
150 if (this.board[i][j] == V.EMPTY) {
151 if (
152 !fiveAlign.wNextTurn &&
153 this.countAlignedStones([i, j], 'b') >= 5
154 ) {
155 fiveAlign.wNextTurn = true;
156 }
157 }
158 else {
159 const c = this.getColor(i, j);
160 if (!fiveAlign[c] && this.countAlignedStones([i, j], c) >= 5)
161 fiveAlign[c] = true;
162 }
163 }
164 }
165 if (fiveAlign['w']) {
166 if (fiveAlign['b']) return "1/2";
167 if (this.turn == 'b' && fiveAlign.wNextTurn) return "*";
168 return "1-0";
169 }
170 if (fiveAlign['b']) return "0-1";
171 return "*";
172 }
173
174 getComputerMove() {
175 const color = this.turn;
176 let candidates = [];
177 let curMax = 0;
178 for (let i=0; i<19; i++) {
179 for (let j=0; j<19; j++) {
180 if (this.board[i][j] == V.EMPTY) {
181 const nbAligned = this.countAlignedStones([i, j], color);
182 if (nbAligned >= curMax) {
183 const move = new Move({
184 appear: [
185 new PiPo({ x: i, y: j, c: color, p: V.PAWN })
186 ],
187 vanish: [],
188 start: { x: -1, y: -1 }
189 });
190 if (nbAligned > curMax) {
191 curMax = nbAligned;
192 candidates = [move];
193 }
194 else candidates.push(move);
195 }
196 }
197 }
198 }
199 // Among a priori equivalent moves, select the most central ones.
200 // Of course this is not good, but can help this ultra-basic bot.
201 let bestCentrality = 0;
202 candidates.forEach(c => {
203 const deltaX = Math.min(c.end.x, 18 - c.end.x);
204 const deltaY = Math.min(c.end.y, 18 - c.end.y);
205 c.centrality = deltaX * deltaX + deltaY * deltaY;
206 if (c.centrality > bestCentrality) bestCentrality = c.centrality;
207 });
208 const threshold = Math.min(32, bestCentrality);
209 const finalCandidates = candidates.filter(c => c.centrality >= threshold);
210 return finalCandidates[randInt(finalCandidates.length)];
211 }
212
213 getNotation(move) {
214 return V.CoordsToSquare(move.end);
215 }
216
217 };