Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions locales/en/apgames.json
Original file line number Diff line number Diff line change
Expand Up @@ -4153,6 +4153,7 @@
"INVALID_HOP_FORWARD": "When advancing, your frog must land on the first available spot of a given suit.",
"INVALID_HOP_FORWARD_ADVANCED": "Under the advanced rules, when playing a two-suited card from your hand, your frog must land on the first available spot of either suit (not your choice). If both suits are present on the same board card (that is, in the same column), you may choose either spot.",
"INVALID_HOP_BACKWARD": "When hopping back, your frog must land on the nearest available card. If there is more than one spot available in that column, you may choose any one of them.",
"INVALID_HOP_BACKWARD_EXCUSE": "Your frog cannot hop backward from the Excuse.",
"INVALID_MOVE": "The move '{{move}}' could not be parsed.",
"INVALID_NON-MOVE": "Unless you're blocked, you must move a frog on each of your moves.",
"LABEL_MARKET": "Draw pool",
Expand Down
64 changes: 54 additions & 10 deletions src/games/frogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class FroggerGame extends GameBase {
{
type: "designer",
name: "José Carlos de Diego Guerrero",
urls: ["http://www.labsk.net"],
urls: ["https://labsk.net/wkr/autor/"],
},
{
type: "coder",
Expand Down Expand Up @@ -420,7 +420,8 @@ export class FroggerGame extends GameBase {
const fromX = this.algebraic2coords(from)[0];

if ( fromX === 0 ) {
throw new Error("Could not back up from the Excuse. This should never happen.");
//This can happen now so don't throw it never happens.
return [];
}

for (let c = fromX - 1; c > 0; c--) {
Expand Down Expand Up @@ -1044,6 +1045,9 @@ export class FroggerGame extends GameBase {
}
}
} else {
//It would help to be able to check the market for the card
//(${piece!.substring(1)}), b/c it may start a new submove,
//but that would require cloning.
newmove = `${move},${piece!.substring(1)}/`;
}
}
Expand Down Expand Up @@ -1101,11 +1105,18 @@ export class FroggerGame extends GameBase {
}
}

const result = this.validateMove(newmove) as IClickResult;
let result = this.validateMove(newmove) as IClickResult;
if (! result.valid) {
result.move = move;
} else {
result.move = newmove;
if (result.autocomplete !== undefined) {
//Internal autocompletion:
const automove = result.autocomplete;
result = this.validateMove(automove) as IClickResult;
result.move = automove;
} else {
result.move = newmove;
}
}
return result;
} catch (e) {
Expand Down Expand Up @@ -1250,16 +1261,29 @@ export class FroggerGame extends GameBase {
//Check cards.
//(The case remaining with no card is falling back at no profit.)
if (subIFM.card) {
if (subIFM.forward && (cloned.closedhands[cloned.currplayer - 1].concat(cloned.hands[cloned.currplayer - 1])).indexOf(subIFM.card!) < 0 ) {
if (subIFM.forward && (cloned.closedhands[cloned.currplayer - 1].concat(cloned.hands[cloned.currplayer - 1])).indexOf(subIFM.card) < 0 ) {
//Bad hand card.
result.valid = false;
result.message = i18next.t("apgames:validation.frogger.NO_SUCH_HAND_CARD", {card: subIFM.card});
return result;
} else if (!subIFM.forward && cloned.market.indexOf(subIFM.card) < 0 ) {
//Bad card.
result.valid = false;
result.message = i18next.t("apgames:validation.frogger.NO_SUCH_MARKET_CARD", {card: subIFM.card});
return result;
//Bad card? Unless...
if ( (cloned.closedhands[cloned.currplayer - 1].concat(cloned.hands[cloned.currplayer - 1])).indexOf(subIFM.card) > -1 ) {
//The player clicked on a hand card to start the next move.
//We use autocompletion to patch up this case.
result.valid = true;
result.complete = -1;
result.canrender = true;

//Trim the card off the end of the submone to start another.
result.autocomplete = m.substring(0,m.lastIndexOf(subIFM.card) - 1) + "/" + subIFM.card + ":";

return result;
} else {
result.valid = false;
result.message = i18next.t("apgames:validation.frogger.NO_SUCH_MARKET_CARD", {card: subIFM.card});
return result;
}
}
}

Expand All @@ -1271,6 +1295,13 @@ export class FroggerGame extends GameBase {
result.complete = -1;
result.canrender = true;
result.message = i18next.t("apgames:validation.frogger.PIECE_NEXT");

//Internal autocompletion:
if (subIFM.card && (cloned.countColumnFrogs() + cloned.countColumnFrogs(true) === 6)) {
//If no frogs are on the road, the choice of frog is clear.
result.autocomplete = m + cloned.coords2algebraic(0, cloned.currplayer) + "-";
}

return result;
} else {
//Reachable if an unblocked player submits the blocked move.
Expand Down Expand Up @@ -1299,6 +1330,10 @@ export class FroggerGame extends GameBase {
result.valid = false;
result.message = i18next.t("apgames:validation.frogger.NO_RETURN");
return result;
} else if (fromX === 0 && !subIFM.forward) {
result.valid = false;
result.message = i18next.t("apgames:validation.frogger.INVALID_HOP_BACKWARD_EXCUSE");
return result;
}

if ( ! subIFM.to ) {
Expand All @@ -1307,6 +1342,13 @@ export class FroggerGame extends GameBase {
result.complete = -1;
result.canrender = true;
result.message = i18next.t("apgames:validation.frogger.PLACE_NEXT");

//Internal autocompletion:
const targets:string[] = subIFM.forward ? cloned.getNextForwardsForCard(subIFM.from, subIFM.card!) : this.getNextBack(subIFM.from);
if (targets.length === 1) {
result.autocomplete = m + targets[0] + (subIFM.forward ? "/" : ",");
}

return result;
} else {
//malformed, no longer reachable.
Expand Down Expand Up @@ -1364,6 +1406,7 @@ export class FroggerGame extends GameBase {
}
} else if (!complete && cloned.market.length > 0) {
// No card. May be a partial move, or can back up without a card.
// We don't autocomplete market picks because it's always optional.
result.valid = true;
result.complete = 0;
result.canrender = true;
Expand Down Expand Up @@ -1786,7 +1829,8 @@ export class FroggerGame extends GameBase {
{
name: "piece",
colour: player,
scale: 0.75
scale: 0.75,
opacity: 0.75
},
{
text: count.toString(),
Expand Down
22 changes: 22 additions & 0 deletions test/games/frogger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,5 +378,27 @@ describe("Frogger", () => {
expect(g.validateMove("NY:a2-c2/c2-b2,NK/b2-a2,9LK/")).to.have.deep.property("valid", true);
expect(g.validateMove("NY:a2-c2/c2-b2,9LK/b2-a2,NK/")).to.have.deep.property("valid", true);
});

it ("Autocompletes a la Arimaa", () => {
const g = new FroggerGame(`{"game":"frogger","numplayers":2,"variants":["advanced","courts"],"gameover":false,"winner":[],"stack":[{"_version":"20251220","_results":[],"_timestamp":"2025-12-29T04:01:17.728Z","currplayer":1,"board":{"dataType":"Map","value":[["b4","PSVK"],["c4","PMYK"],["d4","9LK"],["e4","7VY"],["f4","9VY"],["g4","NL"],["h4","PMSL"],["i4","9MS"],["j4","5ML"],["k4","PVLY"],["a3","X1-6"],["a2","X2-6"]]},"closedhands":[["6LK","8YK","TMLY","1L"],["8MS","7SK","NM","5SV"]],"hands":[[],[]],"market":["NK","1Y","2VL","NV","NS","1K"],"discards":[],"nummoves":3}]}`);

//Can move to first occurrence of only suit. (Ace/Crown rule unchanged.)
expect(g.validateMove("1L:")).to.have.deep.property("autocomplete", "1L:a3-");
expect(g.validateMove("1L:a3-")).to.have.deep.property("autocomplete", "1L:a3-d3/");

//Can move to first occurrence of the first occuring suit.
expect(g.validateMove("6LK:a3-")).to.have.deep.property("autocomplete", "6LK:a3-b1/");

//Return valid false when user tries to move back from the Excuse.
expect(g.validateMove("a3-")).to.have.deep.property("valid", false);

//Special autocompletion case to reparse a "bad" handleClick result.
expect(g.validateMove("TMLY:a3-d3/d3-c2,6LK")).to.have.deep.property("autocomplete", "TMLY:a3-d3/d3-c2/6LK:");
expect(g.validateMove("TMLY:a3-d3/d3-c2/c2-b3,6LK")).to.have.deep.property("autocomplete", "TMLY:a3-d3/d3-c2/c2-b3/6LK:");
//OK to autocorrect to a bad value b/c during the game it gets revalidated.
expect(g.validateMove("TMLY:a3-d3/d3-c2/c2-b3/6LK:")).to.have.deep.property("valid", false);

});


});