detecte riichi sometimes ?

This commit is contained in:
Didictateur 2025-11-18 23:00:20 +01:00
parent 3333dbd011
commit e9d16e8eaa

View file

@ -250,6 +250,46 @@ export class Game {
return false; return false;
} }
/**
* Return full list of yakus for a given hand+groups (doesn't mutate state)
*/
private getYakusForHand(hand: Hand, groups: Array<Group> = []): Array<{ name: string; han: number }> {
const res: Array<{ name: string; han: number }> = [];
try {
for (const k of Object.keys(yakus)) {
const fn = (yakus as any)[k];
try {
const han = fn(hand, groups, this.windPlayer);
if (han && han > 0) res.push({ name: k, han });
} catch (e) {
// ignore individual detector errors
}
}
} catch (e) {
// ignore
}
return res;
}
// Helper: human-readable tile label for logging
private formatTile(t: Tile): string {
const fam = t.getFamily();
const val = t.getValue();
if (fam >= 1 && fam <= 3) {
const names = ["", "Man", "Pin", "Sou"];
return `${names[fam]}${val}${t.isRed() ? "(r)" : ""}`;
}
if (fam === 4) {
const winds = ["", "Ton", "Nan", "Shaa", "Pei"];
return winds[val];
}
if (fam === 5) {
const dr = ["", "Chun", "Hatsu", "Haku"];
return dr[val];
}
return `F${fam}V${val}`;
}
private initializeGame(): void { private initializeGame(): void {
this.deck.shuffle(); this.deck.shuffle();
@ -818,6 +858,18 @@ export class Game {
this.tenpaiCacheVersion[player] = this.handVersion[player]; this.tenpaiCacheVersion[player] = this.handVersion[player];
return true; return true;
} }
// also try red five
if (val === 5) {
const simR = new Hand();
for (const t of h.getTiles()) simR.push(new Tile(t.getFamily(), t.getValue(), t.isRed()));
simR.push(new Tile(fam, val, true));
simR.sort();
if (simR.toGroup() !== undefined) {
this.tenpaiCache[player] = true;
this.tenpaiCacheVersion[player] = this.handVersion[player];
return true;
}
}
} }
} }
@ -827,7 +879,7 @@ export class Game {
for (const t of h.getTiles()) { for (const t of h.getTiles()) {
sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed())); sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed()));
} }
sim.push(new Tile(4, val, false)); sim.push(new Tile(4, val, false));
sim.sort(); sim.sort();
if (sim.toGroup() !== undefined) { if (sim.toGroup() !== undefined) {
this.tenpaiCache[player] = true; this.tenpaiCache[player] = true;
@ -849,6 +901,7 @@ export class Game {
this.tenpaiCacheVersion[player] = this.handVersion[player]; this.tenpaiCacheVersion[player] = this.handVersion[player];
return true; return true;
} }
// try red dragon? dragons don't have red variants, skip
} }
// update cache // update cache
@ -866,7 +919,284 @@ export class Game {
if (!this.hasPicked || this.hasPlayed) return false; if (!this.hasPicked || this.hasPlayed) return false;
if (this.groups[player] && this.groups[player].length > 0) return false; // not closed if (this.groups[player] && this.groups[player].length > 0) return false; // not closed
if (this.declaredRiichi[player]) return false; if (this.declaredRiichi[player]) return false;
return this.isTenpai(player); // When yakus are enabled, allow Riichi either when in tenpai normally
// or when at least one winning tile would produce a yaku.
const hand = this.hands[player];
// If the player currently has 14 tiles (just drawn), check any possible
// 13-tile variant (remove one tile) for tenpai rules.
if (hand.length && hand.length() === 14) {
const tiles = hand.getTiles();
const removableLabels: string[] = [];
const perRemoval: Array<any> = [];
for (let i = 0; i < 14; i++) {
const sim = new Hand();
for (let j = 0; j < tiles.length; j++) {
if (j === i) continue;
const t = tiles[j];
sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed()));
}
sim.sort();
const isTen = this.handIsTenpai(sim);
const info: any = { removed: this.formatTile(tiles[i]), isTenpai: isTen };
let foundWinningTiles: string[] = [];
let yakusPerWin: Record<string, Array<{ name: string; han: number }>> = {};
// If yakus are enabled, search winning tiles and list yakus
if (this.enableYakus) {
// iterate all possible tile types
for (let fam = 1; fam <= 3; fam++) {
for (let val = 1; val <= 9; val++) {
const sim2 = new Hand();
for (const t of sim.getTiles()) sim2.push(new Tile(t.getFamily(), t.getValue(), t.isRed()));
sim2.push(new Tile(fam, val, false));
sim2.sort();
if (sim2.toGroup() !== undefined) {
const simGroups = sim2.toGroup() as Array<Group> | undefined;
const combined = this.groups[player].concat(simGroups ? simGroups : []);
const yakuList = this.getYakusForHand(sim2, combined);
const tileLabel = this.formatTile(new Tile(fam, val, false));
foundWinningTiles.push(tileLabel);
yakusPerWin[tileLabel] = yakuList;
}
// try red five variant
if (val === 5) {
const sim2r = new Hand();
for (const t of sim.getTiles()) sim2r.push(new Tile(t.getFamily(), t.getValue(), t.isRed()));
sim2r.push(new Tile(fam, val, true));
sim2r.sort();
if (sim2r.toGroup() !== undefined) {
const simGroups = sim2r.toGroup() as Array<Group> | undefined;
const combined = this.groups[player].concat(simGroups ? simGroups : []);
const yakuList = this.getYakusForHand(sim2r, combined);
const tileLabel = this.formatTile(new Tile(fam, val, true));
foundWinningTiles.push(tileLabel);
yakusPerWin[tileLabel] = yakuList;
}
}
}
}
for (let val = 1; val <= 4; val++) {
const sim2 = new Hand();
for (const t of sim.getTiles()) sim2.push(new Tile(t.getFamily(), t.getValue(), t.isRed()));
sim2.push(new Tile(4, val, false));
sim2.sort();
if (sim2.toGroup() !== undefined) {
const simGroups = sim2.toGroup() as Array<Group> | undefined;
const combined = this.groups[player].concat(simGroups ? simGroups : []);
const yakuList = this.getYakusForHand(sim2, combined);
const tileLabel = this.formatTile(new Tile(4, val, false));
foundWinningTiles.push(tileLabel);
yakusPerWin[tileLabel] = yakuList;
}
}
for (let val = 1; val <= 3; val++) {
const sim2 = new Hand();
for (const t of sim.getTiles()) sim2.push(new Tile(t.getFamily(), t.getValue(), t.isRed()));
sim2.push(new Tile(5, val, false));
sim2.sort();
if (sim2.toGroup() !== undefined) {
const simGroups = sim2.toGroup() as Array<Group> | undefined;
const combined = this.groups[player].concat(simGroups ? simGroups : []);
const yakuList = this.getYakusForHand(sim2, combined);
const tileLabel = this.formatTile(new Tile(5, val, false));
foundWinningTiles.push(tileLabel);
yakusPerWin[tileLabel] = yakuList;
}
}
} else {
// If yakus not enabled but sim is tenpai, we still consider it removable
if (!isTen) {
// no-op
}
}
info.winningTiles = foundWinningTiles;
info.yakusPerWin = yakusPerWin;
perRemoval.push(info);
if (!this.enableYakus && isTen) removableLabels.push(this.formatTile(tiles[i]));
if (this.enableYakus && foundWinningTiles.length > 0) removableLabels.push(this.formatTile(tiles[i]));
}
if (player === 0) {
if (removableLabels.length > 0) {
console.debug("canDeclareRiichi (14->13) removable tiles:", removableLabels);
} else {
console.debug("canDeclareRiichi (14->13) no removable tile makes tenpai", { player });
}
// Detailed per-removal dump for debugging
console.debug("canDeclareRiichi (14->13) details:", perRemoval);
}
return removableLabels.length > 0;
}
if (!this.enableYakus) {
const res = this.isTenpai(player);
if (player === 0) console.debug("canDeclareRiichi (yakus disabled) ->", { player, turn: this.turn, hasPicked: this.hasPicked, hasPlayed: this.hasPlayed, groups: this.groups[player]?.length, declaredRiichi: this.declaredRiichi[player], isTenpai: res });
return res;
}
const tenpai = this.isTenpai(player);
const tenpaiY = this.isTenpaiWithYaku(player);
if (player === 0) console.debug("canDeclareRiichi ->", { player, turn: this.turn, hasPicked: this.hasPicked, hasPlayed: this.hasPlayed, groups: this.groups[player]?.length, declaredRiichi: this.declaredRiichi[player], isTenpai: tenpai, isTenpaiWithYaku: tenpaiY });
return tenpai || tenpaiY;
}
// Helper: test tenpai for an arbitrary Hand instance (no caching)
private handIsTenpai(h: Hand): boolean {
// quick guard: hand should not already be winning
if (h.toGroup() !== undefined) return false;
// suits 1..3 values 1..9
for (let fam = 1; fam <= 3; fam++) {
for (let val = 1; val <= 9; val++) {
const sim = new Hand();
for (const t of h.getTiles()) sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed()));
sim.push(new Tile(fam, val, false));
sim.sort();
if (sim.toGroup() !== undefined) return true;
if (val === 5) {
const simR = new Hand();
for (const t of h.getTiles()) simR.push(new Tile(t.getFamily(), t.getValue(), t.isRed()));
simR.push(new Tile(fam, val, true));
simR.sort();
if (simR.toGroup() !== undefined) return true;
}
}
}
// winds family 4 values 1..4
for (let val = 1; val <= 4; val++) {
const sim = new Hand();
for (const t of h.getTiles()) sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed()));
sim.push(new Tile(4, val, false));
sim.sort();
if (sim.toGroup() !== undefined) return true;
}
// dragons family 5 values 1..3
for (let val = 1; val <= 3; val++) {
const sim = new Hand();
for (const t of h.getTiles()) sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed()));
sim.push(new Tile(5, val, false));
sim.sort();
if (sim.toGroup() !== undefined) return true;
}
return false;
}
// Helper: for an arbitrary Hand, check if any winning tile produces >=1 yaku
private handIsTenpaiWithYaku(h: Hand, groups: Array<Group>): boolean {
// If already winning and has yaku
if (h.toGroup() !== undefined) {
const simGroups = h.toGroup() as Array<Group> | undefined;
const combined = groups.concat(simGroups ? simGroups : []);
return this.handHasAnyYaku(h, combined);
}
// suits
for (let fam = 1; fam <= 3; fam++) {
for (let val = 1; val <= 9; val++) {
const sim = new Hand();
for (const t of h.getTiles()) sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed()));
sim.push(new Tile(fam, val, false));
sim.sort();
let simGroups = sim.toGroup() as Array<Group> | undefined;
if (simGroups !== undefined) {
const combined = groups.concat(simGroups);
if (this.handHasAnyYaku(sim, combined)) return true;
}
// try red five
if (val === 5) {
const simR = new Hand();
for (const t of h.getTiles()) simR.push(new Tile(t.getFamily(), t.getValue(), t.isRed()));
simR.push(new Tile(fam, val, true));
simR.sort();
simGroups = simR.toGroup() as Array<Group> | undefined;
if (simGroups !== undefined) {
const combined = groups.concat(simGroups);
if (this.handHasAnyYaku(simR, combined)) return true;
}
}
}
}
// winds
for (let val = 1; val <= 4; val++) {
const sim = new Hand();
for (const t of h.getTiles()) sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed()));
sim.push(new Tile(4, val, false));
sim.sort();
const simGroups = sim.toGroup() as Array<Group> | undefined;
if (simGroups !== undefined) {
const combined = groups.concat(simGroups);
if (this.handHasAnyYaku(sim, combined)) return true;
}
}
// dragons
for (let val = 1; val <= 3; val++) {
const sim = new Hand();
for (const t of h.getTiles()) sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed()));
sim.push(new Tile(5, val, false));
sim.sort();
const simGroups = sim.toGroup() as Array<Group> | undefined;
if (simGroups !== undefined) {
const combined = groups.concat(simGroups);
if (this.handHasAnyYaku(sim, combined)) return true;
}
}
return false;
}
/**
* Return true if the player's closed hand is waiting on at least one tile
* that would produce a winning hand containing at least one yaku.
*/
private isTenpaiWithYaku(player: number): boolean {
const h = this.hands[player];
// If hand already winning and has yaku, it's trivially true
if (h.toGroup() !== undefined && this.handHasAnyYaku(h, this.groups[player])) return true;
// Try all possible tile types
for (let fam = 1; fam <= 3; fam++) {
for (let val = 1; val <= 9; val++) {
const sim = new Hand();
for (const t of h.getTiles()) sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed()));
sim.push(new Tile(fam, val, false));
sim.sort();
const simGroups = sim.toGroup() as Array<Group> | undefined;
if (simGroups !== undefined) {
const combined = this.groups[player].concat(simGroups);
if (this.handHasAnyYaku(sim, combined)) return true;
}
}
}
for (let val = 1; val <= 4; val++) {
const sim = new Hand();
for (const t of h.getTiles()) sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed()));
sim.push(new Tile(4, val, false));
sim.sort();
const simGroups = sim.toGroup() as Array<Group> | undefined;
if (simGroups !== undefined) {
const combined = this.groups[player].concat(simGroups);
if (this.handHasAnyYaku(sim, combined)) return true;
}
}
for (let val = 1; val <= 3; val++) {
const sim = new Hand();
for (const t of h.getTiles()) sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed()));
sim.push(new Tile(5, val, false));
sim.sort();
const simGroups = sim.toGroup() as Array<Group> | undefined;
if (simGroups !== undefined) {
const combined = this.groups[player].concat(simGroups);
if (this.handHasAnyYaku(sim, combined)) return true;
}
}
return false;
} }
private declareRiichi(player: number): void { private declareRiichi(player: number): void {