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; 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 { 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 | 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 | 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 | 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 | 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 { await Promise.all(this.tiles.map(tile => tile.preloadImg())); } /** * Clean up resources */ public cleanup(): void { this.tiles.forEach(tile => tile.cleanup()); this.tiles = []; } }