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