Skip to content

Commit fdbde77

Browse files
authored
Merge pull request #286 from mcdemarco/develop
Frogger: fixes and longer but still incomplete move list
2 parents 20c8a3c + 798cf4a commit fdbde77

File tree

2 files changed

+103
-12
lines changed

2 files changed

+103
-12
lines changed

src/games/frogger.ts

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,11 @@ export class FroggerGame extends GameBase {
413413
return cards;
414414
}
415415

416+
private getFullHand(player: playerid): string[] {
417+
//Returns concatenated hidden and visible hands for player playerId.
418+
return this.hands[player - 1].concat(this.closedhands[player - 1]);
419+
}
420+
416421
private getNextBack(from: string): string[] {
417422
//Walk back through the board until we find a free column.
418423
//Return an array of all available cells in that column.
@@ -853,7 +858,8 @@ export class FroggerGame extends GameBase {
853858
}
854859

855860
public moves(player?: playerid): string[] {
856-
//Used for the autopasser. Not a full move list.
861+
//Used for the autopasser.
862+
//Not a full move list, but better than just one random single move.
857863
if (this.gameover) {
858864
return [];
859865
}
@@ -867,12 +873,65 @@ export class FroggerGame extends GameBase {
867873
return ["pass"];
868874
}
869875

870-
return [this.randomMove()];
876+
//Make a list of all possible first moves (out of the three allowed).
877+
let moves:string[] = [];
878+
if (this.checkBlocked()) {
879+
//Note the market must be populated at this point.
880+
moves = this.market.map(card => card + "//");
881+
return moves;
882+
}
883+
884+
//List frogs.
885+
let firstFrog = "";
886+
const freeFrogs: string[] = [];
887+
for (let row = 1; row < this.rows; row++) {
888+
for (let col = 0; col < this.columns - 1; col++) {
889+
const cell = this.coords2algebraic(col, row);
890+
if (this.board.has(cell)) {
891+
const frog = this.board.get(cell)!;
892+
if ( frog.charAt(1) === this.currplayer.toString() ) {
893+
if (col === 0)
894+
firstFrog = cell;
895+
else
896+
freeFrogs.push(cell);
897+
}
898+
}
899+
}
900+
}
901+
902+
//Get backward moves.
903+
freeFrogs.forEach( from => {
904+
const toArray = this.getNextBack(from);
905+
toArray.forEach( to => {
906+
moves.push(`${from}-${to}`); //the non-profit move
907+
this.getWhiteMarket(to).forEach( card => moves.push(`${from}-${to},${card}`) );
908+
});
909+
});
910+
911+
//Get forward moves.
912+
//Now we can use a first frog.
913+
if (firstFrog !== "")
914+
freeFrogs.push(firstFrog);
915+
916+
const amalgahand = this.getFullHand(this.currplayer);
917+
amalgahand.forEach( card => {
918+
freeFrogs.forEach( from => {
919+
let targets:string[] = this.getNextForwardsForCard(from, card);
920+
//Can sometimes get duplicate targets, so uniquify.
921+
targets = [...new Set(targets)];
922+
targets.forEach( to => moves.push(`${card}:${from}-${to}`) );
923+
});
924+
});
925+
926+
return moves;
871927
}
872928

873929
public randomMove(): string {
874930
//We return only one, legal move, for testing purposes.
875931

932+
//Now that there's a move list we could use it,
933+
// but these results are weighted so keeping them.
934+
876935
if (this.gameover) {
877936
throw new UserFacingError("MOVES_GAMEOVER", i18next.t("apgames:MOVES_GAMEOVER"));
878937
}
@@ -913,7 +972,7 @@ export class FroggerGame extends GameBase {
913972

914973
if ( handcard ) {
915974
//hop forward
916-
const card = this.randomElement( this.closedhands[this.currplayer - 1].concat(this.hands[this.currplayer - 1]) );
975+
const card = this.randomElement( this.getFullHand(this.currplayer) );
917976
//Card shouldn't be invisible but if it is we need to give up gracefully.
918977
if (card === "") {
919978
return "hidden";
@@ -1113,10 +1172,12 @@ export class FroggerGame extends GameBase {
11131172
//Internal autocompletion.
11141173
let automove = result.autocomplete;
11151174
result = this.validateMove(automove) as IClickResult;
1175+
//console.log("first autocomplete", automove);
11161176
//A double auto-completion may be needed.
11171177
if (result.autocomplete !== undefined) {
11181178
automove = result.autocomplete;
11191179
result = this.validateMove(automove) as IClickResult;
1180+
//console.log("double autocompleting", automove);
11201181
}
11211182
result.move = automove;
11221183
} else {
@@ -1266,14 +1327,14 @@ export class FroggerGame extends GameBase {
12661327
//Check cards.
12671328
//(The case remaining with no card is falling back at no profit.)
12681329
if (subIFM.card) {
1269-
if (subIFM.forward && (cloned.closedhands[cloned.currplayer - 1].concat(cloned.hands[cloned.currplayer - 1])).indexOf(subIFM.card) < 0 ) {
1330+
if (subIFM.forward && cloned.getFullHand(cloned.currplayer).indexOf(subIFM.card) < 0 ) {
12701331
//Bad hand card.
12711332
result.valid = false;
12721333
result.message = i18next.t("apgames:validation.frogger.NO_SUCH_HAND_CARD", {card: subIFM.card});
12731334
return result;
12741335
} else if (!subIFM.forward && cloned.market.indexOf(subIFM.card) < 0 ) {
12751336
//Bad card? Unless...
1276-
if ( (cloned.closedhands[cloned.currplayer - 1].concat(cloned.hands[cloned.currplayer - 1])).indexOf(subIFM.card) > -1 ) {
1337+
if ( cloned.getFullHand(cloned.currplayer).indexOf(subIFM.card) > -1 ) {
12771338
//The player clicked on a hand card to start the next move.
12781339
//We use autocompletion to patch up this case.
12791340
result.valid = true;
@@ -1349,7 +1410,9 @@ export class FroggerGame extends GameBase {
13491410
result.message = i18next.t("apgames:validation.frogger.PLACE_NEXT");
13501411

13511412
//Internal autocompletion:
1352-
const targets:string[] = subIFM.forward ? cloned.getNextForwardsForCard(subIFM.from, subIFM.card!) : this.getNextBack(subIFM.from);
1413+
let targets:string[] = subIFM.forward ? cloned.getNextForwardsForCard(subIFM.from, subIFM.card!) : cloned.getNextBack(subIFM.from);
1414+
//Can sometimes get duplicate targets, so uniquify.
1415+
targets = [...new Set(targets)];
13531416
if (targets.length === 1) {
13541417
result.autocomplete = m + targets[0] + (subIFM.forward ? "/" : "");
13551418
}
@@ -1794,7 +1857,7 @@ export class FroggerGame extends GameBase {
17941857
if (this._highlight.indexOf(card.uid) > -1) {
17951858
glyph = card.toGlyph({border: true, fill: {
17961859
func: "flatten",
1797-
fg: "_context_strokes",
1860+
fg: "_context_labels",
17981861
bg: "_context_background",
17991862
opacity: 0.2
18001863
}});
@@ -1843,7 +1906,7 @@ export class FroggerGame extends GameBase {
18431906
},
18441907
{
18451908
text: count.toString(),
1846-
colour: "_context_strokes",
1909+
colour: "_context_labels",
18471910
scale: 0.66
18481911
}
18491912
]
@@ -1886,7 +1949,7 @@ export class FroggerGame extends GameBase {
18861949
// build pieces areas
18871950
const areas: AreaPieces[] = [];
18881951
for (let p = 1; p <= this.numplayers; p++) {
1889-
const hand = this.closedhands[p-1].concat(this.hands[p-1]);
1952+
const hand = this.getFullHand(p as playerid);
18901953
if (hand.length > 0) {
18911954
areas.push({
18921955
type: "pieces",
@@ -1926,10 +1989,13 @@ export class FroggerGame extends GameBase {
19261989

19271990
// create an area for all invisible cards (if there are any cards left)
19281991
const hands = this.hands.map(h => [...h]);
1929-
const visibleCards = [...this.getBoardCards(), ...hands.flat(), ...this.market, ...this.discards].map(uid => Card.deserialize(uid));
1992+
const closedhands = this.closedhands.map(h => [...h]);
1993+
const visibleCards = [...this.getBoardCards(), ...hands.flat(), ...this.market, ...this.discards, ...closedhands.flat()].map(uid => Card.deserialize(uid));
1994+
19301995
if (visibleCards.includes(undefined)) {
19311996
throw new Error("Could not deserialize one of the cards. This should never happen!");
19321997
}
1998+
19331999
const remaining = allcards.sort(cardSortAsc).filter(c => visibleCards.find(cd => cd!.uid === c.uid) === undefined).map(c => "c" + c.uid) as [string, ...string[]]
19342000
if (remaining.length > 0) {
19352001
areas.push({

test/games/frogger.test.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -394,10 +394,35 @@ describe("Frogger", () => {
394394

395395
//Special autocompletion case to reparse a "bad" handleClick result.
396396
expect(g.validateMove("TMLY:a3-d3/d3-c2,6LK")).to.have.deep.property("autocomplete", "TMLY:a3-d3/d3-c2/6LK:");
397-
expect(g.validateMove("TMLY:a3-d3/d3-c2/c2-b3,6LK")).to.have.deep.property("autocomplete", "TMLY:a3-d3/d3-c2/c2-b3/6LK:");
397+
//Same result when passing through handleClick.
398+
expect(g.handleClick("TMLY:a3-d3/d3-c2", -1, -1, "c6LK")).to.have.deep.property("move", "TMLY:a3-d3/d3-c2/6LK:");
399+
398400
//OK to autocorrect to a bad value b/c during the game it gets revalidated.
401+
expect(g.validateMove("TMLY:a3-d3/d3-c2/c2-b3,6LK")).to.have.deep.property("autocomplete", "TMLY:a3-d3/d3-c2/c2-b3/6LK:");
399402
expect(g.validateMove("TMLY:a3-d3/d3-c2/c2-b3/6LK:")).to.have.deep.property("valid", false);
400-
403+
//Same result when passing through handleClick.
404+
expect(g.handleClick("TMLY:a3-d3/d3-c2/c2-b3", -1, -1, "c6LK")).to.have.deep.property("move", "TMLY:a3-d3/d3-c2/c2-b3/6LK:");
405+
406+
});
407+
408+
it ("Double autocompletes", () => {
409+
const g = new FroggerGame(`{"game":"frogger","numplayers":2,"variants":["continuous","advanced"],"gameover":false,"winner":[],"stack":[{"_version":"20251229","_results":[],"_timestamp":"2025-12-30T15:56:10.049Z","currplayer":1,"board":{"dataType":"Map","value":[["b4","PMYK"],["c4","PMSL"],["d4","1M"],["e4","PVLY"],["f4","NL"],["g4","PSVK"],["h4","NK"],["i4","1Y"],["j4","3SK"],["k4","NS"],["a3","X1-6"],["a2","X2-6"]]},"closedhands":[["","","",""],["8MS","7ML","9VY","8YK"]],"hands":[[],[]],"market":["6LK","6SY","9LK"],"discards":[],"nummoves":3},{"_version":"20251229","_results":[{"type":"move","from":"a3","to":"e3","what":"NV","how":"forward"},{"type":"move","from":"e3","to":"g3","what":"2SY","how":"forward"},{"type":"move","from":"g3","to":"l3","what":"1V","how":"forward"}],"_timestamp":"2025-12-31T20:57:34.367Z","currplayer":2,"lastmove":"NV:a3-e3/2SY:e3-g3/1V:g3-l3/","board":{"dataType":"Map","value":[["b4","PMYK"],["c4","PMSL"],["d4","1M"],["e4","PVLY"],["f4","NL"],["g4","PSVK"],["h4","NK"],["i4","1Y"],["j4","3SK"],["k4","NS"],["a3","X1-5"],["a2","X2-6"],["l3","X1-1"]]},"closedhands":[[""],["8MS","7ML","9VY","8YK"]],"hands":[[],[]],"market":["6LK","6SY","9LK"],"discards":["NV","2SY","1V"],"nummoves":3},{"_version":"20251229","_results":[{"type":"move","from":"a2","to":"b2","what":"8YK","how":"forward"},{"type":"move","from":"a2","to":"e3","what":"9VY","how":"forward"},{"type":"move","from":"e3","to":"d3","what":"6SY","how":"back"},{"type":"deckDraw"}],"_timestamp":"2026-01-01T03:11:03.580Z","currplayer":1,"lastmove":"8YK:a2-b2/9VY:a2-e3/e3-d3,6SY/","board":{"dataType":"Map","value":[["b4","PMYK"],["c4","PMSL"],["d4","1M"],["e4","PVLY"],["f4","NL"],["g4","PSVK"],["h4","NK"],["i4","1Y"],["j4","3SK"],["k4","NS"],["a3","X1-5"],["a2","X2-4"],["l3","X1-1"],["b2","X2"],["d3","X2"]]},"closedhands":[[""],["8MS","7ML"]],"hands":[[],["6SY"]],"market":["4YK","4VL","3LY"],"discards":["NV","2SY","1V","8YK","9VY"],"nummoves":3},{"_version":"20251229","_results":[{"type":"move","from":"a3","to":"b1","what":"1K","how":"forward"},{"type":"eject","from":"b2","to":"a2","what":"a Crown or Ace"},{"type":"move","from":"b1","to":"a3","what":"4VL","how":"back"},{"type":"deckDraw"}],"_timestamp":"2026-01-01T13:48:53.017Z","currplayer":2,"lastmove":"1K:a3-b1/b1-a3,4VL/","board":{"dataType":"Map","value":[["b4","PMYK"],["c4","PMSL"],["d4","1M"],["e4","PVLY"],["f4","NL"],["g4","PSVK"],["h4","NK"],["i4","1Y"],["j4","3SK"],["k4","NS"],["a3","X1-5"],["a2","X2-5"],["l3","X1-1"],["d3","X2"]]},"closedhands":[[],["8MS","7ML"]],"hands":[["4VL"],["6SY"]],"market":["NM","8VL","5YK"],"discards":["NV","2SY","1V","8YK","9VY","1K"],"nummoves":3}]}`);
410+
411+
//Setup, with testing of single completes.
412+
expect(g.handleClick("8MS:d3-g3/", -1, -1, "c7ML")).to.have.deep.property("move", "8MS:d3-g3/7ML:");
413+
414+
expect(g.validateMove("8MS:d3-g3/7ML:g3-")).to.have.deep.property("autocomplete", "8MS:d3-g3/7ML:g3-l2/");
415+
//Same result when passing through handleClick.
416+
expect(g.handleClick("8MS:d3-g3/7ML:", 1, 6, "X2")).to.have.deep.property("move", "8MS:d3-g3/7ML:g3-l2/");
417+
418+
g.move("8MS:d3-g3/7ML:g3-l2/");
419+
420+
//The actual double autocomplete.
421+
expect(g.validateMove("4VL:")).to.have.deep.property("autocomplete", "4VL:a3-");
422+
expect(g.validateMove("4VL:a3-")).to.have.deep.property("autocomplete", "4VL:a3-c1/");
423+
//The double validation happens in handleClick.
424+
expect(g.handleClick("", -1, -1, "c4VL")).to.have.deep.property("move", "4VL:a3-c1/");
425+
401426
});
402427

403428

0 commit comments

Comments
 (0)