293 lines
7.6 KiB
TypeScript
293 lines
7.6 KiB
TypeScript
import { Tile } from "./tile";
|
|
import { Hand } from "./hand";
|
|
|
|
type TileKey = `${number}-${number}`;
|
|
|
|
export class Deck {
|
|
private tiles: Tile[];
|
|
private tileIndexMap: Map<TileKey, number[]>; // Fast lookup for find() and count()
|
|
|
|
public constructor(allowRed: boolean = false) {
|
|
this.tiles = [];
|
|
this.tileIndexMap = new Map();
|
|
this.initTiles(allowRed);
|
|
}
|
|
|
|
/**
|
|
* Displays all tile families on the canvas
|
|
*/
|
|
public displayFamilies(
|
|
ctx: CanvasRenderingContext2D,
|
|
x: number,
|
|
y: number,
|
|
size: number,
|
|
xOffset: number = 0,
|
|
yOffset: number = 0
|
|
): void {
|
|
let posX = x;
|
|
let posY = y;
|
|
|
|
// Define tile layouts for each family
|
|
const familyLayouts = [
|
|
{ family: 1, start: 1, end: 9 }, // First suit
|
|
{ family: 2, start: 1, end: 9 }, // Second suit
|
|
{ family: 3, start: 1, end: 9 }, // Third suit
|
|
{ family: 4, start: 1, end: 4 }, // Winds
|
|
{ family: 5, start: 1, end: 3 } // Dragons
|
|
];
|
|
|
|
for (const layout of familyLayouts) {
|
|
for (let j = layout.start; j <= layout.end; j++) {
|
|
const tile = this.find(layout.family, j);
|
|
if (tile) {
|
|
tile.drawTile(ctx, posX, posY, size, false, 0, false);
|
|
posX += (75 + xOffset) * size;
|
|
}
|
|
}
|
|
posX = x;
|
|
posY += (100 + yOffset) * size;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the number of tiles in the deck
|
|
*/
|
|
public length(): number {
|
|
return this.tiles.length;
|
|
}
|
|
|
|
/**
|
|
* Removes and returns the last tile from the deck
|
|
* @throws Error if the deck is empty
|
|
*/
|
|
public pop(): Tile {
|
|
if (this.tiles.length === 0) {
|
|
throw new Error("Cannot pop from an empty deck");
|
|
}
|
|
|
|
const tile = this.tiles.pop()!;
|
|
// Update the index map
|
|
const key = this.getTileKey(tile.getFamily(), tile.getValue());
|
|
const indices = this.tileIndexMap.get(key);
|
|
|
|
if (indices && indices.length > 0) {
|
|
indices.pop(); // Remove the last index
|
|
if (indices.length === 0) {
|
|
this.tileIndexMap.delete(key);
|
|
} else {
|
|
this.tileIndexMap.set(key, indices);
|
|
}
|
|
}
|
|
|
|
return tile;
|
|
}
|
|
|
|
/**
|
|
* Adds a tile to the deck
|
|
*/
|
|
public push(tile: Tile): void {
|
|
const index = this.tiles.length;
|
|
this.tiles.push(tile);
|
|
|
|
// Update the index map
|
|
const key = this.getTileKey(tile.getFamily(), tile.getValue());
|
|
const indices = this.tileIndexMap.get(key) || [];
|
|
indices.push(index);
|
|
this.tileIndexMap.set(key, indices);
|
|
}
|
|
|
|
/**
|
|
* Finds and removes a specific tile from the deck
|
|
*/
|
|
public find(family: number, value: number): Tile | undefined {
|
|
const key = this.getTileKey(family, value);
|
|
const indices = this.tileIndexMap.get(key);
|
|
|
|
if (!indices || indices.length === 0) {
|
|
return undefined;
|
|
}
|
|
|
|
// Get the first occurrence
|
|
const index = indices[0];
|
|
if (index >= this.tiles.length) {
|
|
// Handle potential out-of-sync errors
|
|
this.rebuildIndexMap();
|
|
return this.find(family, value);
|
|
}
|
|
|
|
// Swap with the first element for efficient removal
|
|
[this.tiles[index], this.tiles[0]] = [this.tiles[0], this.tiles[index]];
|
|
|
|
// Update indices in the map
|
|
this.updateIndicesAfterSwap(0, index);
|
|
|
|
// Remove and return the tile
|
|
const tile = this.tiles.shift();
|
|
|
|
// Update all indices after shift
|
|
this.decrementIndicesAfterShift();
|
|
|
|
return tile;
|
|
}
|
|
|
|
/**
|
|
* Counts the number of tiles with specific family and value
|
|
*/
|
|
public count(family: number, value: number): number {
|
|
const key = this.getTileKey(family, value);
|
|
const indices = this.tileIndexMap.get(key);
|
|
return indices ? indices.length : 0;
|
|
}
|
|
|
|
/**
|
|
* Shuffles the deck using Fisher-Yates algorithm
|
|
*/
|
|
public shuffle(): void {
|
|
// Fisher-Yates shuffle algorithm
|
|
for (let i = this.tiles.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[this.tiles[i], this.tiles[j]] = [this.tiles[j], this.tiles[i]];
|
|
}
|
|
|
|
// Rebuild the index map after shuffling
|
|
this.rebuildIndexMap();
|
|
}
|
|
|
|
/**
|
|
* Creates a random hand from the deck
|
|
*/
|
|
public getRandomHand(): Hand {
|
|
const hand = new Hand();
|
|
this.shuffle();
|
|
|
|
// Handle case where deck doesn't have enough tiles
|
|
if (this.tiles.length < 13) {
|
|
throw new Error("Not enough tiles in deck to create a hand");
|
|
}
|
|
|
|
for (let i = 0; i < 13; i++) {
|
|
hand.push(this.pop());
|
|
}
|
|
|
|
return hand;
|
|
}
|
|
|
|
/**
|
|
* Cleans up resources used by tiles and empties the deck
|
|
*/
|
|
public cleanup(): void {
|
|
this.tiles.forEach(tile => tile.cleanup());
|
|
this.tiles = [];
|
|
this.tileIndexMap.clear();
|
|
}
|
|
|
|
/**
|
|
* Preloads all tile images
|
|
*/
|
|
public async preload(): Promise<void> {
|
|
const preloadPromises = this.tiles.map(tile => tile.preloadImg());
|
|
await Promise.all(preloadPromises);
|
|
}
|
|
|
|
/**
|
|
* Creates a unique key for each tile type
|
|
*/
|
|
private getTileKey(family: number, value: number): TileKey {
|
|
return `${family}-${value}`;
|
|
}
|
|
|
|
/**
|
|
* Updates the index map after swapping two tiles
|
|
*/
|
|
private updateIndicesAfterSwap(index1: number, index2: number): void {
|
|
if (index1 === index2) return;
|
|
|
|
const tile1 = this.tiles[index1];
|
|
const tile2 = this.tiles[index2];
|
|
|
|
const key1 = this.getTileKey(tile1.getFamily(), tile1.getValue());
|
|
const key2 = this.getTileKey(tile2.getFamily(), tile2.getValue());
|
|
|
|
const indices1 = this.tileIndexMap.get(key1) || [];
|
|
const indices2 = this.tileIndexMap.get(key2) || [];
|
|
|
|
// Update indices
|
|
const idx1 = indices1.indexOf(index2);
|
|
const idx2 = indices2.indexOf(index1);
|
|
|
|
if (idx1 !== -1) indices1[idx1] = index1;
|
|
if (idx2 !== -1) indices2[idx2] = index2;
|
|
|
|
this.tileIndexMap.set(key1, indices1);
|
|
this.tileIndexMap.set(key2, indices2);
|
|
}
|
|
|
|
/**
|
|
* Decrements all indices after a shift operation
|
|
*/
|
|
private decrementIndicesAfterShift(): void {
|
|
for (const [key, indices] of this.tileIndexMap.entries()) {
|
|
this.tileIndexMap.set(
|
|
key,
|
|
indices
|
|
.filter(idx => idx !== 0) // Remove the index 0 that was shifted
|
|
.map(idx => (idx > 0 ? idx - 1 : idx)) // Decrement all indices
|
|
);
|
|
|
|
// Clean up empty entries
|
|
if (this.tileIndexMap.get(key)?.length === 0) {
|
|
this.tileIndexMap.delete(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rebuilds the entire index map from scratch
|
|
*/
|
|
private rebuildIndexMap(): void {
|
|
this.tileIndexMap.clear();
|
|
|
|
for (let i = 0; i < this.tiles.length; i++) {
|
|
const tile = this.tiles[i];
|
|
const key = this.getTileKey(tile.getFamily(), tile.getValue());
|
|
const indices = this.tileIndexMap.get(key) || [];
|
|
indices.push(i);
|
|
this.tileIndexMap.set(key, indices);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initializes the deck with all tiles
|
|
*/
|
|
private initTiles(allowRed: boolean): void {
|
|
// Create suits (families 1-3)
|
|
for (let family = 1; family <= 3; family++) {
|
|
for (let value = 1; value <= 9; value++) {
|
|
// Each value appears 4 times in a suit
|
|
const isRedFive = value === 5 && allowRed;
|
|
|
|
// Add 3 regular tiles
|
|
for (let i = 0; i < 3; i++) {
|
|
this.push(new Tile(family, value, false));
|
|
}
|
|
|
|
// Add the 4th tile (potentially red five)
|
|
this.push(new Tile(family, value, isRedFive));
|
|
}
|
|
}
|
|
|
|
// Create winds (family 4)
|
|
for (let value = 1; value <= 4; value++) {
|
|
for (let i = 0; i < 4; i++) {
|
|
this.push(new Tile(4, value, false));
|
|
}
|
|
}
|
|
|
|
// Create dragons (family 5)
|
|
for (let value = 1; value <= 3; value++) {
|
|
for (let i = 0; i < 4; i++) {
|
|
this.push(new Tile(5, value, false));
|
|
}
|
|
}
|
|
}
|
|
}
|