Skip to content

Commit 9a2c878

Browse files
committed
Rincala: New approach live testing
1 parent f17a338 commit 9a2c878

File tree

3 files changed

+200
-93
lines changed

3 files changed

+200
-93
lines changed

src/games/arimaa.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ export class ArimaaGame extends GameBase {
534534
} else {
535535
if (result.autocomplete !== undefined) {
536536
const automove = result.autocomplete;
537-
result = this.validateMove(result.autocomplete) as IClickResult;
537+
result = this.validateMove(automove) as IClickResult;
538538
result.move = automove;
539539
} else {
540540
result.move = newmove;

src/games/rincala.ts

Lines changed: 146 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export class RincalaGame extends GameBase {
9393
},
9494
],
9595
categories: ["goal>score>eog", "mechanic>move>sow", "mechanic>capture", "board>shape>circle", "board>connect>linear", "components>simple>4c"],
96-
flags: ["automove", "scores", "random-start", "experimental"]
96+
flags: ["no-moves", "custom-randomization", "scores", "random-start", "experimental"]
9797
};
9898

9999
public static value(pc: Colour): number {
@@ -168,52 +168,52 @@ export class RincalaGame extends GameBase {
168168
return this;
169169
}
170170

171-
public moves(): string[] {
172-
if (this.gameover) { return []; }
173-
174-
const moves: string[] = [];
175-
176-
// for first move of the game, just do gatherMoves and leave it at that
177-
if (this.stack.length === 1) {
178-
moves.push(...this.gatherMoves().map(({move}) => move));
179-
}
180-
// otherwise, recurse
181-
else {
182-
this.recurseMoves(moves, null);
183-
}
184-
185-
return [...moves].sort((a,b) => {
186-
if (a.length === b.length) {
187-
return a.localeCompare(b);
188-
} else {
189-
return a.length - b.length;
190-
}
191-
});
192-
}
193-
194-
public recurseMoves(moves: string[], working: string[]|null): void {
195-
// null means very first time
196-
if (working === null) {
197-
const results = this.gatherMoves();
198-
// store all terminal moves
199-
moves.push(...results.filter(({terminal}) => terminal).map(({move}) => move));
200-
// recurse with any nonterminal moves
201-
this.recurseMoves(moves, results.filter(({terminal}) => !terminal).map(({move}) => move));
202-
}
203-
// otherwise we have some starting moves
204-
else {
205-
for (const mv of working) {
206-
const cloned = this.clone();
207-
cloned.move(mv, {partial: true, trusted: true});
208-
const results = cloned.gatherMoves();
209-
// store all terminal moves
210-
moves.push(...results.filter(({terminal}) => terminal).map(({move}) => move).map(m => `${mv},${m}`));
211-
// recurse with any nonterminal moves
212-
const nonterminal = results.filter(({terminal}) => !terminal).map(({move}) => move).map(m => `${mv},${m}`);
213-
this.recurseMoves(moves, nonterminal);
214-
}
215-
}
216-
}
171+
// public moves(): string[] {
172+
// if (this.gameover) { return []; }
173+
174+
// const moves: string[] = [];
175+
176+
// // for first move of the game, just do gatherMoves and leave it at that
177+
// if (this.stack.length === 1) {
178+
// moves.push(...this.gatherMoves().map(({move}) => move));
179+
// }
180+
// // otherwise, recurse
181+
// else {
182+
// this.recurseMoves(moves, null);
183+
// }
184+
185+
// return [...moves].sort((a,b) => {
186+
// if (a.length === b.length) {
187+
// return a.localeCompare(b);
188+
// } else {
189+
// return a.length - b.length;
190+
// }
191+
// });
192+
// }
193+
194+
// public recurseMoves(moves: string[], working: string[]|null): void {
195+
// // null means very first time
196+
// if (working === null) {
197+
// const results = this.gatherMoves();
198+
// // store all terminal moves
199+
// moves.push(...results.filter(({terminal}) => terminal).map(({move}) => move));
200+
// // recurse with any nonterminal moves
201+
// this.recurseMoves(moves, results.filter(({terminal}) => !terminal).map(({move}) => move));
202+
// }
203+
// // otherwise we have some starting moves
204+
// else {
205+
// for (const mv of working) {
206+
// const cloned = this.clone();
207+
// cloned.move(mv, {partial: true, trusted: true});
208+
// const results = cloned.gatherMoves();
209+
// // store all terminal moves
210+
// moves.push(...results.filter(({terminal}) => terminal).map(({move}) => move).map(m => `${mv},${m}`));
211+
// // recurse with any nonterminal moves
212+
// const nonterminal = results.filter(({terminal}) => !terminal).map(({move}) => move).map(m => `${mv},${m}`);
213+
// this.recurseMoves(moves, nonterminal);
214+
// }
215+
// }
216+
// }
217217

218218
// gets a list of all single legal moves from a given position,
219219
// including whether the move was terminal (capture or empty hollow)
@@ -289,8 +289,28 @@ export class RincalaGame extends GameBase {
289289
}
290290

291291
public randomMove(): string {
292-
const moves = this.moves();
293-
return moves[Math.floor(Math.random() * moves.length)];
292+
const steps: string[] = [];
293+
let step: {move: string; terminal: boolean};
294+
let cloned = this.clone();
295+
const onlyOne = this.stack.length === 1;
296+
do {
297+
const moves = cloned.gatherMoves();
298+
if (moves.length > 0) {
299+
step = moves[Math.floor(Math.random() * moves.length)];
300+
steps.push(step.move);
301+
cloned = this.clone();
302+
cloned.move(steps.join(","), {partial: true, trusted: true});
303+
if (onlyOne) break;
304+
} else {
305+
return "pass";
306+
}
307+
} while (!step.terminal);
308+
const combined = steps.join(",");
309+
const result = this.validateMove(combined);
310+
if (!result.valid || result.complete !== 1) {
311+
throw new Error(`The move ${combined} was generated but is not valid.`);
312+
}
313+
return steps.join(",");
294314
}
295315

296316
public getDirection(first: number, second: number): Direction|undefined {
@@ -359,30 +379,59 @@ export class RincalaGame extends GameBase {
359379
return result;
360380
}
361381

362-
const allmoves = this.moves();
363382
const steps = m.split(",");
364-
if (allmoves.filter(mv => mv.startsWith(m)).length > 0) {
365-
// if the exact move is found, we're done
366-
if (allmoves.includes(m)) {
367-
result.valid = true;
368-
result.complete = 1;
369-
result.message = i18next.t("apgames:validation._general.VALID_MOVE");
383+
const last = steps.pop()!;
384+
let cloned = this.clone();
385+
// validate each step
386+
for (let i = 0; i < steps.length; i++) {
387+
cloned = this.clone();
388+
const moves = cloned.gatherMoves();
389+
const found = moves.find(({move}) => move === steps[i]);
390+
if (found === undefined || found.terminal) {
391+
result.valid = false;
392+
result.message = i18next.t("apgames:validation._general.INVALID_MOVE", {move: steps.slice(0, i+1).join(",")});
370393
return result;
371394
}
372-
// if the last step is incomplete, then partial
373-
else if (steps[steps.length - 1].length === 1) {
374-
result.valid = true;
375-
result.complete = -1;
376-
result.canrender = true;
377-
result.message = i18next.t("apgames:validation.rincala.PARTIAL");
378-
return result;
395+
cloned.move(steps.slice(0, i+1).join(","), {partial: true, trusted: true});
396+
}
397+
// validate very last step
398+
const moves = cloned.gatherMoves();
399+
if (moves.filter(({move}) => move.startsWith(last)).length > 0) {
400+
const found = moves.find(({move}) => move === last);
401+
// exact match
402+
if (found !== undefined) {
403+
// if first move of the game, only one step is allowed, so ignore terminal
404+
if (this.stack.length === 1) {
405+
result.valid = true;
406+
result.complete = 1;
407+
result.message = i18next.t("apgames:validation._general.VALID_MOVE");
408+
return result;
409+
}
410+
// every other time
411+
else {
412+
// terminal
413+
if (found.terminal) {
414+
result.valid = true;
415+
result.complete = 1;
416+
result.message = i18next.t("apgames:validation._general.VALID_MOVE");
417+
return result;
418+
}
419+
// must continue
420+
else {
421+
result.valid = true;
422+
result.complete = -1;
423+
result.canrender = true;
424+
result.message = i18next.t("apgames:validation.rincala.CONTINUE");
425+
return result;
426+
}
427+
}
379428
}
380-
// otherwise, the last step was not terminal so you just need to keep going
429+
// if the last step is incomplete, then partial
381430
else {
382431
result.valid = true;
383432
result.complete = -1;
384433
result.canrender = true;
385-
result.message = i18next.t("apgames:validation.rincala.CONTINUE");
434+
result.message = i18next.t("apgames:validation.rincala.PARTIAL");
386435
return result;
387436
}
388437
} else {
@@ -394,8 +443,6 @@ export class RincalaGame extends GameBase {
394443
}
395444
}
396445

397-
// The partial flag enabled dynamic connection checking.
398-
// It leaves the object in an invalid state, so only use it on cloned objects, or call `load()` before submitting again.
399446
public move(m: string, {partial = false, trusted = false} = {}): RincalaGame {
400447
if (this.gameover) {
401448
throw new UserFacingError("MOVES_GAMEOVER", i18next.t("apgames:MOVES_GAMEOVER"));
@@ -409,9 +456,12 @@ export class RincalaGame extends GameBase {
409456
if (! result.valid) {
410457
throw new UserFacingError("VALIDATION_GENERAL", result.message)
411458
}
412-
if (!partial && !this.moves().includes(m)) {
413-
throw new UserFacingError("VALIDATION_FAILSAFE", i18next.t("apgames:validation._general.FAILSAFE", {move: m}))
459+
if (!partial && result.complete !== 1) {
460+
throw new UserFacingError("VALIDATION_GENERAL", result.message)
414461
}
462+
// if (!partial && !this.moves().includes(m)) {
463+
// throw new UserFacingError("VALIDATION_FAILSAFE", i18next.t("apgames:validation._general.FAILSAFE", {move: m}))
464+
// }
415465
}
416466

417467
this.results = [];
@@ -423,13 +473,18 @@ export class RincalaGame extends GameBase {
423473

424474
const steps = m.split(",").filter(Boolean);
425475
for (const step of steps) {
426-
// skip incomplete steps
427-
if (step.length < 2) break;
476+
// skip incomplete steps when partial
477+
if (partial && step.length < 2) {
478+
break;
479+
}
480+
// otherwise throw
481+
else if (step.length < 2) {
482+
throw new Error("Incomplete move somehow made it through.");
483+
}
428484
const results: APMoveResult[] = [];
429485
const [startLbl, dirstr] = step.split("");
430486
const dir: Direction = dirstr === ">" ? "CW" : "CCW";
431487
const start = RincalaGame.lbl2col(startLbl);
432-
// console.log(JSON.stringify({startLbl, dirstr, dir, start}));
433488
const pits = this.mv2pits(start, dir);
434489
const stack = [...this.board[start]];
435490
this.board[start] = [];
@@ -484,7 +539,7 @@ export class RincalaGame extends GameBase {
484539
// game ends if there is only 4 pieces left on the board
485540
// (by definition, this would be one of each colour)
486541
// or if there are no moves available
487-
if (this.board.flat().length === 4 || this.moves().length === 0) {
542+
if (this.board.flat().length === 4 || this.gatherMoves().length === 0) {
488543
this.gameover = true;
489544
const score1 = this.getPlayerScore(1);
490545
const score2 = this.getPlayerScore(2);
@@ -691,6 +746,24 @@ export class RincalaGame extends GameBase {
691746
return this.hands[player - 1].map(pc => RincalaGame.value(pc)).reduce((a, b) => a + b, 0);
692747
}
693748

749+
public chat(node: string[], player: string, results: APMoveResult[], r: APMoveResult): boolean {
750+
if (r.type === "_group") {
751+
let resolved = true;
752+
for (const nested of r.results) {
753+
if (nested.type === "sow") {
754+
node.push(i18next.t("apresults:SOW.rincala", {player, count: nested.pieces!.length, pieces: nested.pieces!.join(","), from: nested.from, to: nested.to!.join(",")}));
755+
} else if (nested.type === "capture") {
756+
node.push(i18next.t("apresults:CAPTURE.complete", {player, where: nested.where, what: nested.what}));
757+
} else {
758+
resolved = false;
759+
break;
760+
}
761+
}
762+
return resolved;
763+
}
764+
return false;
765+
}
766+
694767
public sameMove(move1: string, move2: string): boolean {
695768
// if either move contains an open parenthesis (showing captures),
696769
// only compare everything up to that parenthesis.

test/games/rincala.test.ts

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -74,25 +74,25 @@ describe("Rincala", () => {
7474
]);
7575
});
7676

77-
it ("recurseMoves", () => {
78-
const g = new RincalaGame();
79-
g.move(g.randomMove());
80-
g.board = toBoard("B,YGY,R,R,,,,");
81-
const moves = g.moves();
82-
expect(moves).to.deep.equal([
83-
"B<",
84-
"B>",
85-
"C>",
86-
"D<",
87-
"A>,B>",
88-
"A>,C>",
89-
"A>,D<",
90-
"C<,B<",
91-
"C<,B>",
92-
"A>,C<,B>",
93-
"C<,A>,B>",
94-
]);
95-
});
77+
// it ("recurseMoves", () => {
78+
// const g = new RincalaGame();
79+
// g.move(g.randomMove());
80+
// g.board = toBoard("B,YGY,R,R,,,,");
81+
// const moves = g.moves();
82+
// expect(moves).to.deep.equal([
83+
// "B<",
84+
// "B>",
85+
// "C>",
86+
// "D<",
87+
// "A>,B>",
88+
// "A>,C>",
89+
// "A>,D<",
90+
// "C<,B<",
91+
// "C<,B>",
92+
// "A>,C<,B>",
93+
// "C<,A>,B>",
94+
// ]);
95+
// });
9696

9797
it ("handleClick", () => {
9898
const g = new RincalaGame();
@@ -103,5 +103,39 @@ describe("Rincala", () => {
103103
expect(result.complete).to.equal(-1);
104104
expect(result.move).to.equal("A>,B");
105105
});
106+
107+
it ("moveValidation", () => {
108+
const g = new RincalaGame();
109+
g.move(g.randomMove());
110+
g.board = toBoard("B,YGY,R,R,,,,");
111+
let result = g.validateMove("A");
112+
expect(result.valid).to.be.true;
113+
expect(result.complete).to.equal(-1);
114+
result = g.validateMove("A>");
115+
expect(result.valid).to.be.true;
116+
expect(result.complete).to.equal(-1);
117+
result = g.validateMove("A<");
118+
expect(result.valid).to.be.false;
119+
result = g.validateMove("A>,B");
120+
expect(result.valid).to.be.true;
121+
expect(result.complete).to.equal(-1);
122+
result = g.validateMove("A>,B>");
123+
expect(result.valid).to.be.true;
124+
expect(result.complete).to.equal(1);
125+
result = g.validateMove("A>,C<");
126+
expect(result.valid).to.be.true;
127+
expect(result.complete).to.equal(-1);
128+
result = g.validateMove("A>,C<,B>");
129+
expect(result.valid).to.be.true;
130+
expect(result.complete).to.equal(1);
131+
});
132+
133+
it ("checkEOG", () => {
134+
const g = new RincalaGame();
135+
g.move(g.randomMove());
136+
g.board = toBoard("R,,B,,G,Y,Y,");
137+
g.move("F>");
138+
expect(g.gameover).to.be.true;
139+
});
106140
});
107141

0 commit comments

Comments
 (0)