Riichi/src/hand.ts
2025-04-12 11:50:00 +02:00

334 lines
8.4 KiB
TypeScript

import { Tile } from "./tile";
import { Group } from "./group";
// Constants to avoid magic numbers and improve readability
const TILE_TYPES = {
MANZU: { code: "m", family: 1 },
PINZU: { code: "p", family: 2 },
SOUZU: { code: "s", family: 3 },
WINDS: { code: "w", family: 4 },
DRAGONS: { code: "d", family: 5 }
};
export class Hand {
private tiles: Array<Tile>;
public isolate: boolean = false;
/**
* Create a hand from string representation
* @param stiles String representation of tiles (e.g., "m1p2s3w1d1")
*/
public constructor(stiles: string = "") {
this.tiles = [];
this.initializeFromString(stiles);
}
/**
* Parse string representation into tiles
*/
private initializeFromString(stiles: string): void {
for (let i = 0; i < stiles.length - 1; i++) {
const tileCode = stiles.substring(i, i + 2);
const type = tileCode[0];
const value = Number(tileCode[1]);
if (this.isValidTileCode(type, value)) {
this.addTileFromCode(type, value);
}
}
}
/**
* Check if tile code is valid
*/
private isValidTileCode(type: string, value: number): boolean {
return (
(type === TILE_TYPES.MANZU.code) ||
(type === TILE_TYPES.PINZU.code) ||
(type === TILE_TYPES.SOUZU.code) ||
(type === TILE_TYPES.WINDS.code) ||
(type === TILE_TYPES.DRAGONS.code)
);
}
/**
* Create a tile from type code and value
*/
private addTileFromCode(type: string, value: number): void {
const familyMap: { [key: string]: number } = {
[TILE_TYPES.MANZU.code]: TILE_TYPES.MANZU.family,
[TILE_TYPES.PINZU.code]: TILE_TYPES.PINZU.family,
[TILE_TYPES.SOUZU.code]: TILE_TYPES.SOUZU.family,
[TILE_TYPES.WINDS.code]: TILE_TYPES.WINDS.family,
[TILE_TYPES.DRAGONS.code]: TILE_TYPES.DRAGONS.family
};
const family = familyMap[type];
if (family !== undefined) {
this.tiles.push(new Tile(family, value, false));
}
}
/**
* Get all tiles in hand
*/
public getTiles(): Array<Tile> {
return this.tiles;
}
/**
* Get number of tiles in hand
*/
public length(): number {
return this.tiles.length;
}
/**
* Add a tile to hand
*/
public push(tile: Tile): void {
this.tiles.push(tile);
}
/**
* Remove and return the last tile
*/
public pop(): Tile | undefined {
return this.tiles.pop();
}
/**
* Find and remove a specific tile by family and value
*/
public find(family: number, value: number): Tile | undefined {
const index = this.findTileIndex(family, value);
if (index !== -1) {
// Swap with first tile and remove
[this.tiles[index], this.tiles[0]] = [this.tiles[0], this.tiles[index]];
const tile = this.tiles.shift();
this.sort();
return tile;
}
return undefined;
}
/**
* Find index of tile with specific family and value
*/
private findTileIndex(family: number, value: number): number {
return this.tiles.findIndex(
tile => tile.getFamily() === family && tile.getValue() === value
);
}
/**
* Remove tile at specific index
*/
public eject(idTile: number): Tile {
if (idTile < 0 || idTile >= this.tiles.length) {
throw new Error("Invalid tile index");
}
// Swap with first tile and remove
[this.tiles[0], this.tiles[idTile]] = [this.tiles[idTile], this.tiles[0]];
const tile = this.tiles.shift();
this.sort();
return tile as Tile;
}
/**
* Get tile at specific index without removing
*/
public get(idTile: number | undefined): Tile | undefined {
if (idTile === undefined || idTile < 0 || idTile >= this.tiles.length) {
return undefined;
}
return this.tiles[idTile];
}
/**
* Sort tiles in ascending order
*/
public sort(): void {
this.tiles.sort((a, b) => a.isLessThan(b) ? -1 : 1);
}
/**
* Count tiles with specific family and value
*/
public count(family: number, value: number): number {
return this.tiles.filter(
tile => tile.getFamily() === family && tile.getValue() === value
).length;
}
/**
* Try to form hand into groups (for winning detection)
*/
public toGroup(pair: boolean = false): Array<Group> | undefined {
if (this.tiles.length === 0) {
return [];
}
// Take last tile to try forming a group
const lastTile = this.tiles.pop() as Tile;
const family = lastTile.getFamily();
const value = lastTile.getValue();
// Try to form a pair
if (this.count(family, value) >= 1 && !pair) {
const result = this.tryFormPair(lastTile);
if (result) return result;
}
// Try to form a triplet (pon)
if (this.count(family, value) >= 2) {
const result = this.tryFormTriplet(lastTile, pair);
if (result) return result;
}
// Try to form a sequence (chii)
const hasMinusOne = this.count(family, value - 1) > 0;
const hasMinusTwo = this.count(family, value - 2) > 0;
if (hasMinusOne && hasMinusTwo) {
const result = this.tryFormSequence(lastTile, pair);
if (result) return result;
}
// If no valid group could be formed, put tile back and return undefined
this.tiles.push(lastTile);
this.sort();
return undefined;
}
/**
* Try to form a pair with the given tile
*/
private tryFormPair(tile: Tile): Array<Group> | undefined {
const pairTile = this.find(tile.getFamily(), tile.getValue()) as Tile;
const groups = this.toGroup(true);
// Put the tile back
this.tiles.push(pairTile);
this.sort();
if (groups !== undefined) {
this.tiles.push(tile);
this.sort();
groups.push(new Group([tile, pairTile], 0, 0));
return groups;
}
return undefined;
}
/**
* Try to form a triplet (pon) with the given tile
*/
private tryFormTriplet(tile: Tile, pair: boolean): Array<Group> | undefined {
const secondTile = this.find(tile.getFamily(), tile.getValue()) as Tile;
const thirdTile = this.find(tile.getFamily(), tile.getValue()) as Tile;
const groups = this.toGroup(pair);
// Put tiles back
this.tiles.push(secondTile);
this.tiles.push(thirdTile);
this.sort();
if (groups !== undefined) {
groups.push(new Group([tile, secondTile, thirdTile], 0, 0));
this.tiles.push(tile);
this.sort();
return groups;
}
return undefined;
}
/**
* Try to form a sequence (chii) with the given tile
*/
private tryFormSequence(tile: Tile, pair: boolean): Array<Group> | undefined {
const secondTile = this.find(tile.getFamily(), tile.getValue() - 1) as Tile;
const thirdTile = this.find(tile.getFamily(), tile.getValue() - 2) as Tile;
const groups = this.toGroup(pair);
// Put tiles back
this.tiles.push(secondTile);
this.tiles.push(thirdTile);
this.sort();
if (groups !== undefined) {
groups.push(new Group([thirdTile, secondTile, tile], 0, 0));
this.tiles.push(tile);
this.sort();
return groups;
}
return undefined;
}
/**
* Draw hand tiles on canvas
*/
public drawHand(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
offset: number,
size: number,
focusedTile: number | undefined = undefined,
hidden: boolean = false,
rotation: number = 0
): void {
const tileOffset = (75 + offset) * size;
const offsetX = Math.cos(rotation) * tileOffset;
const offsetY = Math.sin(rotation) * tileOffset;
for (let i = 0; i < this.tiles.length; i++) {
const isLastAndIsolated = (i === this.tiles.length - 1 && this.isolate) ? 10 : 0;
// Calculate position
let tileX = x + i * offsetX + isLastAndIsolated * size * Math.cos(rotation);
let tileY = y + i * offsetY + isLastAndIsolated * size * Math.sin(rotation);
// Add additional offset for focused tile
if (i === focusedTile) {
tileX += 25 * size * Math.sin(rotation);
tileY -= 25 * size * Math.cos(rotation);
}
// Draw tile
this.tiles[i].drawTile(
ctx,
tileX,
tileY,
size,
hidden,
rotation
);
}
}
/**
* Preload tile images
*/
public async preload(): Promise<void> {
await Promise.all(this.tiles.map(tile => tile.preloadImg()));
}
/**
* Clean up resources
*/
public cleanup(): void {
this.tiles.forEach(tile => tile.cleanup());
this.tiles = [];
}
}