optimized every code

This commit is contained in:
Didictateur 2025-04-12 11:50:00 +02:00
parent 15a6341879
commit e8f38c0cf9
25 changed files with 2657 additions and 1916 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,277 +1,311 @@
import { Tile } from "./tile"
import { Tile } from "./tile";
type button_t = (
arg0: CanvasRenderingContext2D,
arg1: number,
arg2: number
/**
* Type definition for button rendering functions
*/
type ButtonRenderer = (
ctx: CanvasRenderingContext2D,
x: number,
y: number
) => void;
/**
* Button configuration interface
*/
interface ButtonConfig {
text: string;
color: string;
action: number;
}
// Button constants
const BUTTON_RADIUS = 8;
const BUTTON_WIDTH = 110;
const BUTTON_HEIGHT = 50;
const BUTTON_SPACING = 120;
const BUTTON_AREA_Y_MIN = 838;
const BUTTON_AREA_Y_MAX = 888;
const BUTTON_MARGIN = 10;
const BASE_X_POSITION = 850;
// Button style configurations
const BUTTON_STYLES: Record<string, ButtonConfig> = {
pass: { text: "Ignorer", color: "#FF9030", action: 0 },
chii: { text: "Chii", color: "#FFCC33", action: 1 },
pon: { text: "Pon", color: "#FFCC33", action: 2 },
kan: { text: "Kan", color: "#FFCC33", action: 3 },
ron: { text: "Ron", color: "#FF3060", action: 4 },
tsumo: { text: "Tsumo", color: "#FF3060", action: 5 },
back: { text: "Retour", color: "#FF9030", action: 0 }
};
/**
* Determines which button was clicked based on coordinates
* @returns The action value of the clicked button or -1 if no button was clicked
*/
export function clickAction(
x: number,
y: number,
chii: boolean,
pon: boolean,
kan: boolean,
ron: boolean,
tsumo: boolean
x: number,
y: number,
chii: boolean,
pon: boolean,
kan: boolean,
ron: boolean,
tsumo: boolean
): number {
let buttons = [
[tsumo, 5],
[ron, 4],
[kan, 3],
[pon, 2],
[chii, 1]
]
if (buttons.some(c => c[0])) {
buttons.push([true, 0]);
} else {
return -1;
}
let xmin = 960 - buttons.filter(c => c[0]).length * 120;
let inside = 838 < y && y < 888;
let q = Math.floor((x - xmin) / 120);
let r = (x - xmin) - 120 * q;
if (
q >= 0 &&
q < buttons.filter(c => c[0]).length &&
r > 10 &&
inside
) {
return buttons.filter(c => c[0])[q][1] as number;
}
return -1;
const activeButtons = getActiveButtons(chii, pon, kan, ron, tsumo);
if (activeButtons.length === 0) {
return -1;
}
// Calculate starting X position based on number of buttons
const xmin = 960 - activeButtons.length * BUTTON_SPACING;
// Check if Y coordinate is within button area
const isYInButtonArea = BUTTON_AREA_Y_MIN < y && y < BUTTON_AREA_Y_MAX;
if (!isYInButtonArea) {
return -1;
}
// Calculate which button was clicked
const buttonIndex = Math.floor((x - xmin) / BUTTON_SPACING);
const xOffset = (x - xmin) - BUTTON_SPACING * buttonIndex;
if (
buttonIndex >= 0 &&
buttonIndex < activeButtons.length &&
xOffset > BUTTON_MARGIN
) {
return activeButtons[buttonIndex];
}
return -1;
}
/**
* Draw all active buttons on the canvas
*/
export function drawButtons(
ctx: CanvasRenderingContext2D,
chii: boolean,
pon: boolean,
kan: boolean,
ron: boolean,
tsumo: boolean
ctx: CanvasRenderingContext2D,
chii: boolean,
pon: boolean,
kan: boolean,
ron: boolean,
tsumo: boolean
): void {
let buttons = [
[chii, buttonChii],
[pon, buttonPon],
[kan, buttonKan],
[ron, buttonRon],
[tsumo, buttonTsumo]
]
if (buttons.some(c => c[0])) {
buttons.unshift([true, buttonPass]);
}
let dx = 0;
for (let i = 0; i < buttons.length; i++) {
if (buttons[i][0]) {
(buttons[i][1] as button_t)(
ctx,
850 - dx * 120,
835
);
dx++;
}
}
const buttonFunctions: [boolean, ButtonRenderer][] = [
[chii, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.chii)],
[pon, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.pon)],
[kan, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.kan)],
[ron, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.ron)],
[tsumo, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.tsumo)]
];
// Only show the pass button if at least one other button is active
const hasActiveButtons = buttonFunctions.some(([isActive]) => isActive);
if (hasActiveButtons) {
buttonFunctions.unshift([true, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.pass)]);
} else {
return; // No buttons to draw
}
// Draw active buttons
let positionOffset = 0;
for (const [isActive, renderFunc] of buttonFunctions) {
if (isActive) {
renderFunc(
ctx,
BASE_X_POSITION - positionOffset * BUTTON_SPACING,
835
);
positionOffset++;
}
}
}
/**
* Determines which Chi option was clicked
* @returns The value of the clicked Chi option or -1 if no option was clicked
*/
export function clickChii(
x: number,
y: number,
chiis: Array<Array<Tile>>
x: number,
y: number,
chiis: Array<Array<Tile>>
): number {
let xmin = 960 - (chiis.length + 1) * 120;
let inside = 838 < y && y < 888;
let q = Math.floor((x - xmin) / 120);
let r = (x - xmin) - 120 * q;
if (
q >= 0 &&
q < (chiis.length + 1) &&
r > 10 &&
inside
) {
return q === chiis.length ? 0 : chiis[q][0].getValue();
}
return -1;
if (chiis.length === 0) {
return -1;
}
// Calculate starting X position based on number of options
const xmin = 960 - (chiis.length + 1) * BUTTON_SPACING;
// Check if Y coordinate is within button area
const isYInButtonArea = BUTTON_AREA_Y_MIN < y && y < BUTTON_AREA_Y_MAX;
if (!isYInButtonArea) {
return -1;
}
// Calculate which option was clicked
const optionIndex = Math.floor((x - xmin) / BUTTON_SPACING);
const xOffset = (x - xmin) - BUTTON_SPACING * optionIndex;
if (
optionIndex >= 0 &&
optionIndex < (chiis.length + 1) &&
xOffset > BUTTON_MARGIN
) {
// Return 0 for "back" button or the value of the selected Chi option
return optionIndex === chiis.length ? 0 : chiis[optionIndex][0].getValue();
}
return -1;
}
/**
* Draw Chi options on the canvas
*/
export function drawChiis(
ctx: CanvasRenderingContext2D,
chiis: Array<Array<Tile>>
ctx: CanvasRenderingContext2D,
chiis: Array<Array<Tile>>
): void {
chiis.reverse();
const r = 8;
const w = 110;
const h = 50;
button(ctx, 850, 835, r, w, h, "#FF9030");
ctx.fillStyle = "black";
ctx.font = "30px garamond";
ctx.fillText("Retour", 850 + w * 0.1, 835 + h/2 * 1.3);
let dx = 1;
for (let i = 0; i < chiis.length; i++) {
drawOneChii(
ctx,
850 - dx * 120,
835,
chiis[i]
);
dx++;
}
// Create a copy to avoid modifying the original array
const chiiOptions = [...chiis].reverse();
// Draw "back" button
renderButton(ctx, BASE_X_POSITION, 835, BUTTON_STYLES.back);
// Draw Chi options
let positionOffset = 1;
for (const tiles of chiiOptions) {
drawOneChii(
ctx,
BASE_X_POSITION - positionOffset * BUTTON_SPACING,
835,
tiles
);
positionOffset++;
}
}
/**
* Draw a single Chi option
*/
function drawOneChii(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
tiles: Array<Tile>
ctx: CanvasRenderingContext2D,
x: number,
y: number,
tiles: Array<Tile>
): void {
const r = 8;
const w = 110;
const h = 50;
const dx = 32;
const x0 = x + 7;
const y0 = y + 5;
button(ctx, x, y, r, w, h, "#FFCC33");
tiles[0].drawTile(
ctx,
x0,
y0,
0.4,
false,
0,
false,
false
);
tiles[1].drawTile(
ctx,
x0 + dx,
y0,
0.4,
false,
0,
false,
false
);
tiles[2].drawTile(
ctx,
x0 + 2 * dx,
y0,
0.4,
false,
0,
false,
false
);
const tileOffset = 32;
const tileStartX = x + 7;
const tileStartY = y + 5;
// Draw the button background
drawButtonShape(ctx, x, y, BUTTON_RADIUS, BUTTON_WIDTH, BUTTON_HEIGHT, BUTTON_STYLES.chii.color);
// Draw the tiles
for (let i = 0; i < tiles.length; i++) {
tiles[i].drawTile(
ctx,
tileStartX + tileOffset * i,
tileStartY,
0.4,
false,
0,
false,
false
);
}
}
function button(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
r: number,
w: number,
h: number,
color: string
): void {
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.fill();
ctx.fillStyle = "#606060";
ctx.stroke();
/**
* Get the list of active button action values
*/
function getActiveButtons(
chii: boolean,
pon: boolean,
kan: boolean,
ron: boolean,
tsumo: boolean
): number[] {
const buttonConfigs: [boolean, number][] = [
[tsumo, BUTTON_STYLES.tsumo.action],
[ron, BUTTON_STYLES.ron.action],
[kan, BUTTON_STYLES.kan.action],
[pon, BUTTON_STYLES.pon.action],
[chii, BUTTON_STYLES.chii.action]
];
const activeButtons = buttonConfigs
.filter(([isActive]) => isActive)
.map(([, action]) => action);
// Add pass button if any other buttons are active
if (activeButtons.length > 0) {
activeButtons.push(BUTTON_STYLES.pass.action);
}
return activeButtons;
}
function buttonPass(
ctx: CanvasRenderingContext2D,
x: number,
y: number
/**
* Render a button with text
*/
function renderButton(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
config: ButtonConfig
): void {
const r = 8;
const w = 110;
const h = 50;
// button(ctx, x, y, r, w, h, "#FFAC4D");
button(ctx, x, y, r, w, h, "#FF9030");
ctx.fillStyle = "black";
ctx.font = "30px garamond";
ctx.fillText("Ignorer", x + w * 0.1, y + h/2 * 1.3);
drawButtonShape(ctx, x, y, BUTTON_RADIUS, BUTTON_WIDTH, BUTTON_HEIGHT, config.color);
// Add text to the button
ctx.fillStyle = "black";
ctx.font = "30px garamond";
// Center text based on its length
const textXPosition = x + BUTTON_WIDTH * (0.5 - config.text.length * 0.025);
const textYPosition = y + BUTTON_HEIGHT/2 * 1.3;
ctx.fillText(config.text, textXPosition, textYPosition);
}
function buttonPon(
ctx: CanvasRenderingContext2D,
x: number,
y: number
/**
* Draw a rounded rectangle button shape
*/
function drawButtonShape(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
radius: number,
width: number,
height: number,
color: string
): void {
const r = 8;
const w = 110;
const h = 50;
button(ctx, x, y, r, w, h, "#FFCC33");
ctx.fillStyle = "black";
ctx.font = "30px garamond";
ctx.fillText("Pon",x + w * 0.28, y + h/2 * 1.3);
}
ctx.fillStyle = color;
ctx.beginPath();
function buttonChii(
ctx: CanvasRenderingContext2D,
x: number,
y: number
): void {
const r = 8;
const w = 110;
const h = 50;
button(ctx, x, y, r, w, h, "#FFCC33");
ctx.fillStyle = "black";
ctx.font = "30px garamond";
ctx.fillText("Chii",x + w * 0.25, y + h/2 * 1.3);
}
// Top right corner
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
// Bottom right corner
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
// Bottom left corner
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
// Top left corner
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
function buttonKan(
ctx: CanvasRenderingContext2D,
x: number,
y: number
): void {
const r = 8;
const w = 110;
const h = 50;
button(ctx, x, y, r, w, h, "#FFCC33");
ctx.fillStyle = "black";
ctx.font = "30px garamond";
ctx.fillText("Kan",x + w * 0.28, y + h/2 * 1.3);
}
ctx.fill();
function buttonRon(
ctx: CanvasRenderingContext2D,
x: number,
y: number
): void {
const r = 8;
const w = 110;
const h = 50;
button(ctx, x, y, r, w, h, "#FF3060");
ctx.fillStyle = "black";
ctx.font = "30px garamond";
ctx.fillText("Ron",x + w * 0.28, y + h/2 * 1.3);
}
function buttonTsumo(
ctx: CanvasRenderingContext2D,
x: number,
y: number
): void {
const r = 8;
const w = 110;
const h = 50;
button(ctx, x, y, r, w, h, "#FF3060");
ctx.fillStyle = "black";
ctx.font = "30px garamond";
ctx.fillText("Tsumo",x + w * 0.13, y + h/2 * 1.3);
// Add border
ctx.fillStyle = "#606060";
ctx.stroke();
}

View file

@ -1,152 +1,293 @@
import { Tile } from "./tile"
import { Hand } from "./hand"
import { Tile } from "./tile";
import { Hand } from "./hand";
type TileKey = `${number}-${number}`;
export class Deck {
private tiles: Array<Tile>;
private tiles: Tile[];
private tileIndexMap: Map<TileKey, number[]>; // Fast lookup for find() and count()
public constructor(allowRed: boolean) {
this.tiles = [];
this.initTiles(allowRed);
}
public constructor(allowRed: boolean = false) {
this.tiles = [];
this.tileIndexMap = new Map();
this.initTiles(allowRed);
}
public displayFamilies(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
size: number,
xOffset: number = 0,
yOffset: number = 0
): void {
let posX = x;
let posY = y;
for (let i = 1; i < 6; i++) {
if (i < 4) { // famille
for (let j = 1; j < 10; j++) {
const tile = this.find(i, j) as NonNullable<Tile>;
tile.drawTile(ctx, posX, posY, size, false, 0, false);
posX += (75 + xOffset) * size;
}
posX = x;
posY += (100 + yOffset) * size;
} else if (i === 4) { //vent
for (let j = 1; j < 5; j++) {
const tile = this.find(i, j) as NonNullable<Tile>;
tile.drawTile(ctx, posX, posY, size);
posX += (75 + xOffset) * size;
}
posX = x;
posY += (100 + yOffset) * size;
} else if (i === 5) { //vent
for (let j = 1; j < 4; j++) {
const tile = this.find(i, j) as NonNullable<Tile>;
tile.drawTile(ctx, posX, posY, size);
posX += (75 + xOffset) * size;
}
}
}
}
/**
* 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
];
public length(): number {
return this.tiles.length;
}
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;
}
}
public pop(): Tile {
if (this.tiles.length === 0) {
}
return this.tiles.pop() as NonNullable<Tile>;
}
/**
* Returns the number of tiles in the deck
*/
public length(): number {
return this.tiles.length;
}
public push(tile: Tile) {
this.tiles.push(tile);
}
/**
* 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;
}
public find(family: number, value: number): Tile | undefined {
let n = undefined;
for (let i = 0; i < this.tiles.length; i++) {
if (
this.tiles[i].getFamily() === family &&
this.tiles[i].getValue() === value
) {
n = i;
}
}
if (n !== undefined) {
[this.tiles[n as NonNullable<number>], this.tiles[0]] = [this.tiles[0], this.tiles[n as NonNullable<number>]];
return this.tiles.shift();
} else {
return undefined;
}
}
/**
* 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);
}
public count(family: number, value: number): number {
let n = 0;
for (let i = 0; i < this.tiles.length; i++) {
if (
this.tiles[i].getFamily() === family &&
this.tiles[i].getValue() === value
) {
n++;
}
}
return n;
}
/**
* 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;
}
public shuffle(): undefined {
let newArray: Array<Tile> = [];
while (this.tiles.length > 0) {
let n = Math.floor(Math.random() * this.tiles.length);
[this.tiles[n], this.tiles[0]] = [this.tiles[0], this.tiles[n]];
newArray.push(this.tiles.shift() as NonNullable<Tile>);
}
this.tiles = newArray;
}
/**
* 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;
}
public getRandomHand(): Hand {
let hand = new Hand();
this.shuffle();
for (let i = 0; i < 13; i++) {
hand.push(this.pop());
}
return hand;
}
/**
* 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();
}
public cleanup(): void {
this.tiles.forEach(tile => tile.cleanup());
this.tiles = [];
}
/**
* 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;
}
public async preload(): Promise<void> {
for (let i = 0; i < this.tiles.length; i++) {
await this.tiles[i].preloadImg();
}
}
/**
* Cleans up resources used by tiles and empties the deck
*/
public cleanup(): void {
this.tiles.forEach(tile => tile.cleanup());
this.tiles = [];
this.tileIndexMap.clear();
}
private initTiles(allowRed: boolean): undefined {
for (let i = 1; i < 6; i++) {
if (i < 4) { // famille
for (let j = 1; j < 10; j++) {
this.tiles.push(new Tile(i, j, false));
this.tiles.push(new Tile(i, j, false));
this.tiles.push(new Tile(i, j, false));
if (j === 5 && allowRed) {
this.tiles.push(new Tile(i, j, true));
} else {
this.tiles.push(new Tile(i, j, false));
}
}
} else if (i === 4) { // vent
for (let j = 1; j < 5; j++) {
for (let k = 0; k < 4; k++) {
this.tiles.push(new Tile(i, j, false));
}
}
} else if (i === 5) { // dragon
for (let j = 1; j < 4; j++) {
for (let k = 0; k < 4; k++) {
this.tiles.push(new Tile(i, j, false));
}
}
}
}
}
/**
* 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));
}
}
}
}

View file

@ -1,144 +1,256 @@
import { Deck } from "../deck";
import { Hand } from "../hand"
import { Hand } from "../hand";
declare global {
interface Window {
cleanup: () => void;
}
}
export {}
const canvas = document.getElementById("myCanvas") as HTMLCanvasElement;
async function preloadDeck(deck: Deck) {
await deck.preload();
interface Window {
cleanup: () => void;
}
}
async function display () {
if (canvas) {
const ctx = canvas.getContext("2d") as NonNullable<CanvasRenderingContext2D>;
export {};
if (ctx) {
// double buffering
const offScreenCanvas = document.createElement('canvas') as HTMLCanvasElement;
offScreenCanvas.width = canvas.width;
offScreenCanvas.height = canvas.height;
const offScreenCtx = offScreenCanvas.getContext('2d') as NonNullable<CanvasRenderingContext2D>;
//animation parameter
let lastTime = 0;
const FPS = 30;
const interval = 1000 / FPS;
// tuiles
let x = 100;
let y = 150;
let os = 75;
let size = 0.75;
const deck = new Deck(false);
await preloadDeck(deck);
let hands: Array<Hand> = [];
for (let i = 0; i < 4; i++) {
const hand = deck.getRandomHand();
hand.sort();
hands.push(hand);
}
// interactive hand
const edeck = new Deck(false);
await edeck.preload();
const ehand = edeck.getRandomHand();
ehand.push(edeck.pop());
ehand.sort();
let selectedTile: number|undefined = undefined;
// function to draw
const drawCanvas = async () => {
// clean screeen
offScreenCtx.clearRect(0, 0, canvas.width, canvas.height);
// tapis
offScreenCtx.fillStyle = "#007730";
offScreenCtx.fillRect(50, 50, 1000, 1000);
// texte
offScreenCtx.fillStyle = "#DFDFFF";
offScreenCtx.font = "50px serif";
offScreenCtx.fillText("Exemples de main:", 65, 100);
// example hands
for (let i = 0; i < hands.length; i++) {
hands[i].drawHand(offScreenCtx, x, y + i * size * (100 + os), 5, size);
}
// dynamic hand
ehand.isolate = true;
ehand.drawHand(offScreenCtx, x, 800, 5, size, selectedTile);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(offScreenCanvas, 0, 0);
}
const animationLoop = (currentTime: number) => {
const deltaTime = currentTime - lastTime;
if (deltaTime >= interval) {
lastTime = currentTime;
drawCanvas();
}
requestAnimationFrame(animationLoop);
}
// mouse event
canvas.addEventListener(
"mousemove",
(event) => {
const rect = canvas.getBoundingClientRect();
const mouseX = event.clientX - rect.left - x;
const mouseY = event.clientY - rect.top;
let q = Math.floor(mouseX / (80 * size));
let r = mouseX - q * 80 * size;
if (r <= 75 && q >= 0 && q < 14 && mouseY >= 800 && mouseY <= 800 + 100*size) {
selectedTile = q;
} else {
selectedTile = undefined;
}
}
);
canvas.addEventListener(
"mousedown",
(event) => {
if (selectedTile !== undefined) {
edeck.push(ehand.eject(selectedTile));
edeck.shuffle();
ehand.sort();
ehand.push(edeck.pop());
}
}
);
requestAnimationFrame(animationLoop);
window.cleanup = () => {
deck.cleanup();
hands.forEach(hand => hand.cleanup());
hands = [];
edeck.cleanup();
ehand.cleanup();
ctx.clearRect(0, 0, canvas.width, canvas.height);
offScreenCtx.clearRect(0, 0, offScreenCanvas.width, offScreenCanvas.height);
selectedTile = undefined;
}
} else {
console.error("Impossible d'obtenir le contexte du canvas.");
}
} else {
console.error("Canvas introuvable dans le DOM.");
}
class RiichiDisplay {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
private offScreenCanvas: HTMLCanvasElement;
private offScreenCtx: CanvasRenderingContext2D;
private deck: Deck;
private hands: Hand[] = [];
private edeck: Deck;
private ehand: Hand;
private selectedTile: number | undefined = undefined;
private animationFrameId: number | null = null;
private isDirty: boolean = true;
// Constants
private readonly FPS: number = 30;
private readonly INTERVAL: number = 1000 / this.FPS;
private readonly X: number = 100;
private readonly Y: number = 150;
private readonly OS: number = 75;
private readonly SIZE: number = 0.75;
private readonly TILE_WIDTH: number = 80 * this.SIZE;
private readonly MAX_TILES: number = 14;
// Cache for mouse hit detection
private tileRects: Array<{x: number, y: number, width: number, height: number}> = [];
constructor() {
const canvas = document.getElementById("myCanvas") as HTMLCanvasElement;
if (!canvas) {
throw new Error("Canvas introuvable dans le DOM.");
}
this.canvas = canvas;
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error("Impossible d'obtenir le contexte du canvas.");
}
this.ctx = ctx;
// Create off-screen canvas for double buffering
this.offScreenCanvas = document.createElement('canvas');
this.offScreenCanvas.width = canvas.width;
this.offScreenCanvas.height = canvas.height;
const offCtx = this.offScreenCanvas.getContext('2d');
if (!offCtx) {
throw new Error("Impossible d'obtenir le contexte du canvas hors écran.");
}
this.offScreenCtx = offCtx;
// Initialize decks
this.deck = new Deck(false);
this.edeck = new Deck(false);
// Initialize with empty hand (will be populated after preload)
this.ehand = new Hand();
// Set up event listeners
this.setupEventListeners();
// Calculate tile hit areas once
this.calculateTileHitAreas();
}
private calculateTileHitAreas(): void {
this.tileRects = [];
for (let i = 0; i < this.MAX_TILES; i++) {
this.tileRects.push({
x: this.X + i * this.TILE_WIDTH,
y: 800,
width: 75,
height: 100 * this.SIZE
});
}
}
private setupEventListeners(): void {
this.canvas.addEventListener("mousemove", this.handleMouseMove.bind(this));
this.canvas.addEventListener("mousedown", this.handleMouseDown.bind(this));
}
private handleMouseMove(event: MouseEvent): void {
const rect = this.canvas.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
// Check if cursor is over any tile using pre-calculated hit areas
const oldSelectedTile = this.selectedTile;
this.selectedTile = undefined;
for (let i = 0; i < this.tileRects.length; i++) {
const tileRect = this.tileRects[i];
if (
mouseX >= tileRect.x &&
mouseX <= tileRect.x + tileRect.width &&
mouseY >= tileRect.y &&
mouseY <= tileRect.y + tileRect.height
) {
this.selectedTile = i;
break;
}
}
// Only mark as dirty if selection changed
if (oldSelectedTile !== this.selectedTile) {
this.isDirty = true;
}
}
private handleMouseDown(): void {
if (this.selectedTile !== undefined) {
this.edeck.push(this.ehand.eject(this.selectedTile));
this.edeck.shuffle();
this.ehand.sort();
this.ehand.push(this.edeck.pop());
this.isDirty = true;
}
}
public async initialize(): Promise<void> {
// Preload all assets in parallel
await Promise.all([
this.deck.preload(),
this.edeck.preload()
]);
// Generate sample hands
for (let i = 0; i < 4; i++) {
const hand = this.deck.getRandomHand();
hand.sort();
this.hands.push(hand);
}
// Initialize interactive hand
this.ehand = this.edeck.getRandomHand();
this.ehand.push(this.edeck.pop());
this.ehand.sort();
// Initial draw
this.drawCanvas();
// Start animation loop
this.startAnimationLoop();
}
private drawCanvas(): void {
// Only redraw if something changed (dirty flag)
if (!this.isDirty) return;
const ctx = this.offScreenCtx;
// Clear canvas
ctx.clearRect(0, 0, this.offScreenCanvas.width, this.offScreenCanvas.height);
// Draw background
ctx.fillStyle = "#007730";
ctx.fillRect(50, 50, 1000, 1000);
// Draw title
ctx.fillStyle = "#DFDFFF";
ctx.font = "50px serif";
ctx.fillText("Exemples de main:", 65, 100);
// Draw example hands
for (let i = 0; i < this.hands.length; i++) {
this.hands[i].drawHand(
ctx,
this.X,
this.Y + i * this.SIZE * (100 + this.OS),
5,
this.SIZE
);
}
// Draw interactive hand
this.ehand.isolate = true;
this.ehand.drawHand(
ctx,
this.X,
800,
5,
this.SIZE,
this.selectedTile
);
// Flip double buffer
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.drawImage(this.offScreenCanvas, 0, 0);
// Reset dirty flag
this.isDirty = false;
}
private startAnimationLoop(): void {
let lastTime = 0;
const animationLoop = (currentTime: number) => {
const deltaTime = currentTime - lastTime;
if (deltaTime >= this.INTERVAL) {
lastTime = currentTime - (deltaTime % this.INTERVAL);
this.drawCanvas();
}
this.animationFrameId = requestAnimationFrame(animationLoop);
};
this.animationFrameId = requestAnimationFrame(animationLoop);
}
public cleanup(): void {
// Cancel animation loop
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
// Clean up resources
this.deck.cleanup();
this.hands.forEach(hand => hand.cleanup());
this.hands = [];
this.edeck.cleanup();
this.ehand.cleanup();
// Clear canvases
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.offScreenCtx.clearRect(0, 0, this.offScreenCanvas.width, this.offScreenCanvas.height);
// Reset state
this.selectedTile = undefined;
this.isDirty = true;
}
}
display();
// Initialize and start
const riichiDisplay = new RiichiDisplay();
riichiDisplay.initialize().catch(error => {
console.error("Erreur lors de l'initialisation:", error);
});
// Expose cleanup function for window
window.cleanup = () => {
riichiDisplay.cleanup();
};

View file

@ -1,6 +1,6 @@
import { Deck } from "../deck";
import { Hand } from "../hand";
import { Group } from "../group"
import { Group } from "../group";
// Configuration globale
const CANVAS_ID = "myCanvas";
@ -9,185 +9,373 @@ const BG_RECT = { x: 50, y: 50, w: 1000, h: 1000 };
const FPS = 30;
const FRAME_INTERVAL = 1000 / FPS;
// variables globales
const DECKS: Array<Deck> = [];
const HANDS: Array<Hand> = [];
let selectedTile: number|undefined = undefined;
// Optimisation des références
let animationFrameId: number;
let lastFrameTime = 0;
const callbacks: Array<() => void> = [];
// Pré-calcul des dimensions
const canvas = document.getElementById(CANVAS_ID) as HTMLCanvasElement;
const ctx = canvas.getContext("2d") as NonNullable<CanvasRenderingContext2D>;
// Cache statique
const staticCanvas = document.createElement('canvas') as HTMLCanvasElement;
const staticCtx = staticCanvas.getContext("2d") as NonNullable<CanvasRenderingContext2D>;
staticCanvas.width = canvas.width;
staticCanvas.height = canvas.height;
// Pré-rendu du fond
function prerenderBackground() {
staticCtx.clearRect(0, 0, canvas.width, canvas.height);
staticCtx.fillStyle = BG_COLOR;
staticCtx.fillRect(BG_RECT.x, BG_RECT.y, BG_RECT.w, BG_RECT.h);
};
function drawFrame() {
if (!ctx) return;
// Effacement intelligent (uniquement la zone nécessaire)
prerenderBackground();
// Ici viendrait le dessin des éléments dynamiques
// Par exemple:
// drawDeck();
// drawHands();
let x = 300;
let y = 150;
let xos = 250;
let yos = 100;
let size = 0.75;
staticCtx.fillStyle = "#DFDFFF";
staticCtx.font = "50px serif";
staticCtx.fillText("Chii:", 75, y + 100 * size - 5);
HANDS[0].drawHand(staticCtx, x, y, 5, 0.75); // chii
HANDS[1].drawHand(staticCtx, x + (75+xos)*size, y, 5, 0.75);
staticCtx.fillText("Pon:", 75, y + (100+yos)*size + 100 * size - 5);
HANDS[2].drawHand(staticCtx, x, y + (100+yos)*size, 5, 0.75); // pon
HANDS[3].drawHand(staticCtx, x + (75+xos)*size, y + (100+yos)*size, 5, 0.75);
HANDS[4].drawHand(staticCtx, x + 2*(75+xos)*size, y + 2*(100+yos)*size, 5, 0.75);
staticCtx.fillText("Invalide:", 75, y + 2*(100+yos)*size + 100 * size - 5);
HANDS[5].drawHand(staticCtx, x, y + 2*(100+yos)*size, 5, 0.75); // wrong
HANDS[6].drawHand(staticCtx, x + (75+xos)*size, y + 2*(100+yos)*size, 5, 0.75);
HANDS[7].drawHand(staticCtx, x + 2*(75+xos)*size, y + 2*(100+yos)*size, 5, 0.75);
HANDS[8].isolate = true;
HANDS[8].drawHand(staticCtx, 100, 800, 5, size, selectedTile);
let groups = HANDS[8].toGroup();
if (groups !== undefined) {
staticCtx.fillStyle = "#FF0000";
staticCtx.font = "50px serif";
staticCtx.fillText("Tous les groupes sont formés !", 100, 750);
}
groups = [];
// Dessin du cache statique
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(staticCanvas, 0, 0);
}
function animationLoop(currentTime: number) {
animationFrameId = requestAnimationFrame(animationLoop);
const deltaTime = currentTime - lastFrameTime;
if (deltaTime < FRAME_INTERVAL) return;
lastFrameTime = currentTime - (deltaTime % FRAME_INTERVAL);
drawFrame();
}
function initEventListeners() {
const handlers = {
mousemove: (e: MouseEvent) => {
let size = 0.75;
let x = 100;
// Logique de gestion du mouvement de la souris
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left - x;
const mouseY = e.clientY - rect.top;
let q = Math.floor(mouseX / (80 * size));
let r = mouseX - q * 80 * size;
if (r <= 75 && q >= 0 && q < 14 && mouseY >= 800 && mouseY <= 800 + 100*size) {
selectedTile = q;
} else {
selectedTile = undefined;
}
},
mousedown: (e: MouseEvent) => {
// Logique de gestion du clic de souris
if (selectedTile !== undefined) {
DECKS[0].push(HANDS[8].eject(selectedTile));
DECKS[0].shuffle();
HANDS[8].sort();
HANDS[8].push(DECKS[0].pop());
}
}
};
callbacks.push(() => {
canvas.removeEventListener('mousemove', handlers.mousemove);
canvas.removeEventListener('mousedown', handlers.mousedown);
});
canvas.addEventListener('mousemove', handlers.mousemove);
canvas.addEventListener('mousedown', handlers.mousedown);
}
async function preloadDeck(deck: Deck) {
await deck.preload();
}
async function preloadHand(hand: Hand) {
await hand.preload();
}
export function cleanup() {
cancelAnimationFrame(animationFrameId);
callbacks.forEach(fn => fn());
}
export async function initDisplay() {
if (!ctx) {
console.error("Context canvas indisponible");
return;
/**
* Classe RiichiDisplay responsable de la gestion de l'affichage du jeu Riichi Mahjong
*/
class RiichiDisplay {
// Canvas principal et contexte
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
// Canvas pour le double-buffering
private staticCanvas: HTMLCanvasElement;
private staticCtx: CanvasRenderingContext2D;
// Ressources du jeu
private decks: Array<Deck> = [];
private hands: Array<Hand> = [];
// État de l'interface
private selectedTile: number | undefined = undefined;
private isDirty: boolean = true;
// Animation et événements
private animationFrameId: number | null = null;
private lastFrameTime: number = 0;
private cleanupCallbacks: Array<() => void> = [];
// Constantes pour le rendu
private readonly BASE_X: number = 300;
private readonly BASE_Y: number = 150;
private readonly X_OFFSET: number = 250;
private readonly Y_OFFSET: number = 100;
private readonly INTERACTIVE_X: number = 100;
private readonly INTERACTIVE_Y: number = 800;
private readonly SIZE: number = 0.75;
private readonly TILE_WIDTH: number = 80 * this.SIZE;
private readonly MAX_TILES: number = 14;
// Zones de détection pour l'interaction souris
private tileRects: Array<{ x: number, y: number, width: number, height: number }> = [];
constructor(canvasId: string = CANVAS_ID) {
// Initialisation du canvas
const canvas = document.getElementById(canvasId) as HTMLCanvasElement;
if (!canvas) {
throw new Error(`Canvas avec ID '${canvasId}' introuvable`);
}
// Préchargement des ressources si nécessaire
// const deck = new Deck();
// await preloadDeck(deck);
DECKS.push(
new Deck(false)
);
HANDS.push(
new Hand("s1s2s3"),
new Hand("m2m3m4"),
new Hand("p5p5p5"),
new Hand("w2w2w2"),
new Hand("d3d3d3"),
new Hand("s4p5m6"),
new Hand("m9s9p9"),
new Hand("d1d2d3"),
DECKS[0].getRandomHand()
);
await Promise.all(DECKS.map(d => preloadDeck(d)));
await Promise.all(HANDS.map(h => preloadHand(h)));
HANDS[8].push(DECKS[0].pop());
HANDS[8].sort();
this.canvas = canvas;
initEventListeners();
requestAnimationFrame(animationLoop);
// Récupération du contexte 2D
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error("Impossible d'obtenir le contexte 2D du canvas");
}
this.ctx = ctx;
// Initialisation du canvas pour le double-buffering
this.staticCanvas = document.createElement('canvas');
this.staticCanvas.width = canvas.width;
this.staticCanvas.height = canvas.height;
const staticCtx = this.staticCanvas.getContext("2d");
if (!staticCtx) {
throw new Error("Impossible d'obtenir le contexte du canvas statique");
}
this.staticCtx = staticCtx;
// Pré-calcul des zones de détection
this.calculateTileHitAreas();
}
/**
* Pré-calcule les zones de détection pour l'interaction souris
*/
private calculateTileHitAreas(): void {
this.tileRects = [];
for (let i = 0; i < this.MAX_TILES; i++) {
this.tileRects.push({
x: this.INTERACTIVE_X + i * this.TILE_WIDTH,
y: this.INTERACTIVE_Y,
width: 75,
height: 100 * this.SIZE
});
}
}
/**
* Pré-rendu du fond statique
*/
private prerenderBackground(): void {
this.staticCtx.clearRect(0, 0, this.staticCanvas.width, this.staticCanvas.height);
this.staticCtx.fillStyle = BG_COLOR;
this.staticCtx.fillRect(BG_RECT.x, BG_RECT.y, BG_RECT.w, BG_RECT.h);
}
/**
* Dessine une frame complète
*/
private drawFrame(): void {
// Vérifier si un redessinage est nécessaire
if (!this.isDirty) return;
// Pré-rendu du fond statique
this.prerenderBackground();
// Dessin des éléments statiques et dynamiques
this.drawHandsAndLabels();
// Affichage des informations sur les groupes
this.checkAndDisplayGroups();
// Copie du canvas statique vers le canvas principal
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.drawImage(this.staticCanvas, 0, 0);
// Réinitialisation du flag de modification
this.isDirty = false;
}
/**
* Dessine les mains et leurs étiquettes
*/
private drawHandsAndLabels(): void {
const ctx = this.staticCtx;
// Configurer le style de texte
ctx.fillStyle = "#DFDFFF";
ctx.font = "50px serif";
// Dessiner les mains "Chii"
ctx.fillText("Chii:", 75, this.BASE_Y + 100 * this.SIZE - 5);
this.hands[0].drawHand(ctx, this.BASE_X, this.BASE_Y, 5, this.SIZE);
this.hands[1].drawHand(ctx, this.BASE_X + (75 + this.X_OFFSET) * this.SIZE, this.BASE_Y, 5, this.SIZE);
// Dessiner les mains "Pon"
ctx.fillText("Pon:", 75, this.BASE_Y + (100 + this.Y_OFFSET) * this.SIZE + 100 * this.SIZE - 5);
this.hands[2].drawHand(ctx, this.BASE_X, this.BASE_Y + (100 + this.Y_OFFSET) * this.SIZE, 5, this.SIZE);
this.hands[3].drawHand(ctx, this.BASE_X + (75 + this.X_OFFSET) * this.SIZE, this.BASE_Y + (100 + this.Y_OFFSET) * this.SIZE, 5, this.SIZE);
// Dessiner les mains "Invalide"
ctx.fillText("Invalide:", 75, this.BASE_Y + 2 * (100 + this.Y_OFFSET) * this.SIZE + 100 * this.SIZE - 5);
this.hands[5].drawHand(ctx, this.BASE_X, this.BASE_Y + 2 * (100 + this.Y_OFFSET) * this.SIZE, 5, this.SIZE);
this.hands[6].drawHand(ctx, this.BASE_X + (75 + this.X_OFFSET) * this.SIZE, this.BASE_Y + 2 * (100 + this.Y_OFFSET) * this.SIZE, 5, this.SIZE);
this.hands[7].drawHand(ctx, this.BASE_X + 2 * (75 + this.X_OFFSET) * this.SIZE, this.BASE_Y + 2 * (100 + this.Y_OFFSET) * this.SIZE, 5, this.SIZE);
// Main supplémentaire (position spéciale)
this.hands[4].drawHand(ctx, this.BASE_X + 2 * (75 + this.X_OFFSET) * this.SIZE, this.BASE_Y + (100 + this.Y_OFFSET) * this.SIZE, 5, this.SIZE);
// Main interactive
this.hands[8].isolate = true;
this.hands[8].drawHand(ctx, this.INTERACTIVE_X, this.INTERACTIVE_Y, 5, this.SIZE, this.selectedTile);
}
/**
* Vérifie et affiche les informations sur les groupes formés
*/
private checkAndDisplayGroups(): void {
const groups = this.hands[8].toGroup();
if (groups !== undefined) {
this.staticCtx.fillStyle = "#FF0000";
this.staticCtx.font = "50px serif";
this.staticCtx.fillText("Tous les groupes sont formés !", 100, 750);
}
}
/**
* Démarre la boucle d'animation
*/
private startAnimationLoop(): void {
const animationLoop = (currentTime: number) => {
this.animationFrameId = requestAnimationFrame(animationLoop);
const deltaTime = currentTime - this.lastFrameTime;
if (deltaTime < FRAME_INTERVAL) return;
this.lastFrameTime = currentTime - (deltaTime % FRAME_INTERVAL);
this.drawFrame();
};
this.animationFrameId = requestAnimationFrame(animationLoop);
}
/**
* Initialise les écouteurs d'événements
*/
private initEventListeners(): void {
const mouseMoveHandler = this.handleMouseMove.bind(this);
const mouseDownHandler = this.handleMouseDown.bind(this);
this.canvas.addEventListener('mousemove', mouseMoveHandler);
this.canvas.addEventListener('mousedown', mouseDownHandler);
// Enregistrer les callbacks de nettoyage
this.cleanupCallbacks.push(() => {
this.canvas.removeEventListener('mousemove', mouseMoveHandler);
this.canvas.removeEventListener('mousedown', mouseDownHandler);
});
}
/**
* Gère le mouvement de la souris
*/
private handleMouseMove(e: MouseEvent): void {
const rect = this.canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
// Sauvegarde de l'état précédent pour détecter les changements
const previousSelectedTile = this.selectedTile;
this.selectedTile = undefined;
// Détection optimisée avec zones pré-calculées
for (let i = 0; i < this.tileRects.length; i++) {
const tileRect = this.tileRects[i];
if (
mouseX >= tileRect.x &&
mouseX <= tileRect.x + tileRect.width &&
mouseY >= tileRect.y &&
mouseY <= tileRect.y + tileRect.height
) {
this.selectedTile = i;
break;
}
}
// Marquer comme "dirty" uniquement si la sélection a changé
if (previousSelectedTile !== this.selectedTile) {
this.isDirty = true;
}
}
/**
* Gère le clic de souris
*/
private handleMouseDown(): void {
if (this.selectedTile !== undefined) {
// Exécuter l'action pour la tuile sélectionnée
this.decks[0].push(this.hands[8].eject(this.selectedTile));
this.decks[0].shuffle();
this.hands[8].sort();
this.hands[8].push(this.decks[0].pop());
// Marquer comme "dirty" pour forcer le redessinage
this.isDirty = true;
}
}
/**
* Précharge un deck
*/
private async preloadDeck(deck: Deck): Promise<void> {
await deck.preload();
}
/**
* Précharge une main
*/
private async preloadHand(hand: Hand): Promise<void> {
await hand.preload();
}
/**
* Initialise l'affichage et démarre l'application
*/
public async initialize(): Promise<void> {
try {
// Création du deck principal
this.decks.push(new Deck(false));
// Création des mains prédéfinies
this.hands.push(
new Hand("s1s2s3"), // Chii 1
new Hand("m2m3m4"), // Chii 2
new Hand("p5p5p5"), // Pon 1
new Hand("w2w2w2"), // Pon 2
new Hand("d3d3d3"), // Pon supplémentaire
new Hand("s4p5m6"), // Invalide 1
new Hand("m9s9p9"), // Invalide 2
new Hand("d1d2d3"), // Invalide 3
this.decks[0].getRandomHand() // Main interactive
);
// Préchargement parallèle des ressources
await Promise.all([
...this.decks.map(deck => this.preloadDeck(deck)),
...this.hands.map(hand => this.preloadHand(hand))
]);
// Configuration de la main interactive
this.hands[8].push(this.decks[0].pop());
this.hands[8].sort();
// Initialisation des écouteurs d'événements
this.initEventListeners();
// Premier rendu
this.isDirty = true;
this.drawFrame();
// Démarrage de la boucle d'animation
this.startAnimationLoop();
} catch (error) {
console.error("Erreur d'initialisation:", error);
}
}
/**
* Nettoie les ressources
*/
public cleanup(): void {
// Arrêter la boucle d'animation
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
// Exécuter tous les callbacks de nettoyage
this.cleanupCallbacks.forEach(callback => callback());
this.cleanupCallbacks = [];
// Nettoyer les ressources des decks et des mains
this.decks.forEach(deck => deck.cleanup());
this.hands.forEach(hand => hand.cleanup());
// Vider les collections
this.decks = [];
this.hands = [];
// Effacer les canvas
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.staticCtx.clearRect(0, 0, this.staticCanvas.width, this.staticCanvas.height);
}
}
// Instance globale pour le nettoyage
let displayInstance: RiichiDisplay | null = null;
/**
* Fonction de nettoyage exportée
*/
export function cleanup(): void {
if (displayInstance) {
displayInstance.cleanup();
displayInstance = null;
}
}
/**
* Fonction d'initialisation exportée
*/
export async function initDisplay(): Promise<void> {
try {
displayInstance = new RiichiDisplay(CANVAS_ID);
await displayInstance.initialize();
window.cleanup = cleanup;
} catch (error) {
console.error("Erreur lors de l'initialisation:", error);
}
}
// Déclaration globale pour TypeScript
declare global {
interface Window {
cleanup: () => void;
}
interface Window {
cleanup: () => void;
}
}
// Initialisation automatique si le script est chargé directement
if (typeof window !== 'undefined') {
initDisplay().catch(console.error);
initDisplay().catch(console.error);
}

View file

@ -1,122 +1,253 @@
import { Deck } from "../deck";
import { Hand } from "../hand";
import { Game } from "../game"
import { Game } from "../game";
// Configuration globale
const CANVAS_ID = "myCanvas";
const BG_RECT = { x: 0, y: 0, w: 1050, h: 1050 };
var MOUSE = { x: 0, y: 0 };
const FPS = 60;
const FRAME_INTERVAL = 1000 / FPS;
// variables globales
const DECKS: Array<Deck> = [];
const HANDS: Array<Hand> = [];
var GAME: Game|undefined;
// Optimisation des références
let animationFrameId: number;
let lastFrameTime = 0;
const callbacks: Array<() => void> = [];
// Pré-calcul des dimensions
const canvas = document.getElementById(CANVAS_ID) as HTMLCanvasElement;
const ctx = canvas.getContext("2d") as NonNullable<CanvasRenderingContext2D>;
canvas.width = BG_RECT.w;
canvas.height = BG_RECT.h;
// Cache statique
const staticCanvas = document.createElement('canvas') as HTMLCanvasElement;
const staticCtx = staticCanvas.getContext("2d") as NonNullable<CanvasRenderingContext2D>;
staticCanvas.width = canvas.width;
staticCanvas.height = canvas.height;
function drawFrame() {
if (!ctx) return;
GAME?.draw(MOUSE);
}
function animationLoop(currentTime: number) {
animationFrameId = requestAnimationFrame(animationLoop);
const deltaTime = currentTime - lastFrameTime;
if (deltaTime < FRAME_INTERVAL) return;
lastFrameTime = currentTime - (deltaTime % FRAME_INTERVAL);
drawFrame();
}
function initEventListeners() {
const handlers = {
mousedown: (e: MouseEvent) => {
GAME?.click(e);
},
mousemove: (e: MouseEvent) => {
MOUSE.x = e.x;
MOUSE.y = e.y;
class RiichiGameManager {
// Configuration globale
private readonly CANVAS_ID: string = "myCanvas";
private readonly BG_RECT = { x: 0, y: 0, w: 1050, h: 1050 };
private readonly FPS: number = 60;
private readonly FRAME_INTERVAL: number = 1000 / this.FPS;
// Singleton instance
private static instance: RiichiGameManager;
// Canvas et contextes
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
private staticCanvas: HTMLCanvasElement;
private staticCtx: CanvasRenderingContext2D;
// État du jeu
private mouse = { x: 0, y: 0 };
private game: Game | null = null;
private decks: Array<Deck> = [];
private hands: Array<Hand> = [];
// Animation et boucle de jeu
private animationFrameId: number | null = null;
private lastFrameTime: number = 0;
private isInitialized: boolean = false;
// Nettoyage
private cleanupCallbacks: Array<() => void> = [];
/**
* Constructeur privé pour le Singleton
*/
private constructor() {
// Initialisation du canvas principal
const canvas = document.getElementById(this.CANVAS_ID) as HTMLCanvasElement;
if (!canvas) {
throw new Error(`Canvas avec ID '${this.CANVAS_ID}' introuvable`);
}
this.canvas = canvas;
this.canvas.width = this.BG_RECT.w;
this.canvas.height = this.BG_RECT.h;
// Récupération du contexte 2D
const ctx = canvas.getContext("2d", { alpha: false });
if (!ctx) {
throw new Error("Impossible d'obtenir le contexte 2D du canvas");
}
this.ctx = ctx;
// Initialisation du canvas pour le double-buffering
this.staticCanvas = document.createElement('canvas');
this.staticCanvas.width = canvas.width;
this.staticCanvas.height = canvas.height;
const staticCtx = this.staticCanvas.getContext("2d", { alpha: false });
if (!staticCtx) {
throw new Error("Impossible d'obtenir le contexte du canvas statique");
}
this.staticCtx = staticCtx;
}
/**
* Accès à l'instance Singleton
*/
public static getInstance(): RiichiGameManager {
if (!RiichiGameManager.instance) {
RiichiGameManager.instance = new RiichiGameManager();
}
return RiichiGameManager.instance;
}
/**
* Dessine une frame du jeu
*/
private drawFrame(): void {
if (this.game) {
this.game.draw(this.mouse);
}
}
/**
* Démarre la boucle d'animation du jeu
*/
private startAnimationLoop(): void {
const animationLoop = (currentTime: number) => {
this.animationFrameId = requestAnimationFrame(animationLoop);
const deltaTime = currentTime - this.lastFrameTime;
if (deltaTime < this.FRAME_INTERVAL) return;
// Correction du time drift
this.lastFrameTime = currentTime - (deltaTime % this.FRAME_INTERVAL);
// Rendu d'une frame
this.drawFrame();
};
this.animationFrameId = requestAnimationFrame(animationLoop);
}
/**
* Initialise les écouteurs d'événements
*/
private initEventListeners(): void {
// Handler pour le déplacement de la souris
const mouseMoveHandler = (e: MouseEvent) => {
// Calcul des coordonnées relatives au canvas
const rect = this.canvas.getBoundingClientRect();
this.mouse.x = e.clientX - rect.left;
this.mouse.y = e.clientY - rect.top;
};
// Handler pour le clic de souris
const mouseDownHandler = (e: MouseEvent) => {
if (this.game) {
this.game.click(e);
}
};
callbacks.push(() => {
canvas.removeEventListener('mousemove', handlers.mousemove);
canvas.removeEventListener('mousedown', handlers.mousedown);
// Enregistrement des écouteurs
this.canvas.addEventListener('mousemove', mouseMoveHandler);
this.canvas.addEventListener('mousedown', mouseDownHandler);
// Enregistrement des callbacks de nettoyage
this.cleanupCallbacks.push(() => {
this.canvas.removeEventListener('mousemove', mouseMoveHandler);
this.canvas.removeEventListener('mousedown', mouseDownHandler);
});
canvas.addEventListener('mousemove', handlers.mousemove);
canvas.addEventListener('mousedown', handlers.mousedown);
}
async function preloadDeck(deck: Deck) {
}
/**
* Précharge un deck
*/
private async preloadDeck(deck: Deck): Promise<void> {
await deck.preload();
}
async function preloadHand(hand: Hand) {
await hand.preload();
}
export function cleanup() {
cancelAnimationFrame(animationFrameId);
callbacks.forEach(fn => fn());
}
export async function initDisplay() {
if (!ctx) {
console.error("Context canvas indisponible");
return;
}
/**
* Précharge une main
*/
private async preloadHand(hand: Hand): Promise<void> {
await hand.preload();
}
/**
* Initialise le jeu
*/
public async initialize(): Promise<void> {
if (this.isInitialized) return;
try {
console.log("Chargement en cours...");
// Création du jeu
this.game = new Game(
this.ctx,
this.canvas,
this.staticCtx,
this.staticCanvas
);
// Préchargement parallèle des ressources
await Promise.all([
...this.decks.map(deck => this.preloadDeck(deck)),
...this.hands.map(hand => this.preloadHand(hand)),
this.game.preload()
]);
console.log("Chargement terminé");
// Initialisation des écouteurs d'événements
this.initEventListeners();
// Démarrage de la boucle d'animation
this.startAnimationLoop();
// Marquer comme initialisé
this.isInitialized = true;
// Définir la fonction de nettoyage global
window.cleanup = this.cleanup.bind(this);
} catch (error) {
console.error("Erreur lors de l'initialisation:", error);
}
}
/**
* Nettoie les ressources
*/
public cleanup(): void {
// Arrêter la boucle d'animation
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
// Exécuter tous les callbacks de nettoyage
this.cleanupCallbacks.forEach(callback => callback());
this.cleanupCallbacks = [];
// Nettoyer les ressources du jeu
if (this.game) {
// Supposons que Game a une méthode cleanup
(this.game as any).cleanup?.();
this.game = null;
}
// Nettoyer les ressources des decks et des mains
this.decks.forEach(deck => deck.cleanup());
this.hands.forEach(hand => hand.cleanup());
// Vider les collections
this.decks = [];
this.hands = [];
// Réinitialiser l'état
this.isInitialized = false;
this.lastFrameTime = 0;
this.mouse = { x: 0, y: 0 };
// Effacer les canvas
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.staticCtx.clearRect(0, 0, this.staticCanvas.width, this.staticCanvas.height);
}
}
console.log("Load begining\n");
// Préchargement des ressources si nécessaire
// const deck = new Deck();
// await preloadDeck(deck);
DECKS.push(
);
HANDS.push(
);
GAME = new Game(
ctx,
canvas,
staticCtx,
staticCanvas
);
await Promise.all(DECKS.map(d => preloadDeck(d)));
await Promise.all(HANDS.map(h => preloadHand(h)));
await GAME?.preload();
// Fonction de nettoyage globale exportée
export function cleanup(): void {
RiichiGameManager.getInstance().cleanup();
}
console.log("Loaded completed\n");
initEventListeners();
requestAnimationFrame(animationLoop);
window.cleanup = cleanup;
// Fonction d'initialisation globale exportée
export async function initDisplay(): Promise<void> {
await RiichiGameManager.getInstance().initialize();
}
// Déclaration globale pour TypeScript
declare global {
interface Window {
cleanup: () => void;
}
interface Window {
cleanup: () => void;
}
}
// Initialisation automatique si le script est chargé directement
if (typeof window !== 'undefined') {
initDisplay().catch(console.error);
}

File diff suppressed because it is too large Load diff

View file

@ -1,154 +1,91 @@
import { Tile } from "./tile"
import { Tile } from "./tile";
export class Group {
private tiles: Array<Tile>;
private stolenFrom: number;
private belongsTo: number
constructor(
private tiles: Array<Tile>,
private stolenFrom: number,
private belongsTo: number
) {}
public constructor(
tiles: Array<Tile>,
stolenFrom: number,
belongsTo: number
) {
this.tiles = tiles;
this.stolenFrom = stolenFrom;
this.belongsTo = belongsTo;
}
public push(tile: Tile): void {
this.tiles.push(tile);
}
public push(tile: Tile): void {
this.tiles.push(tile);
}
public pop(): Tile | undefined {
return this.tiles.pop();
}
public pop(): Tile|undefined {
return this.tiles.pop();
}
public getTiles(): Array<Tile> {
return this.tiles;
}
public getTiles(): Array<Tile> {
return this.tiles;
}
public compare(g: Group): number {
// Compare les premiers tiles, puis les seconds si égalité
const firstComparison = this.tiles[0].compare(g.tiles[0]);
return firstComparison !== 0 ? firstComparison : this.tiles[1].compare(g.tiles[1]);
}
public compare(g: Group): number {
let c = this.tiles[0].compare(g.tiles[0]);
if (c !== 0) {
return c;
}
return this.tiles[1].compare(g.tiles[1]);
}
public drawGroup(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
os: number,
size: number,
rotation: number,
selectedTile?: Tile
): void {
// Sauvegarde et rotation du contexte
ctx.save();
ctx.translate(525, 525);
ctx.rotate(rotation);
ctx.translate(-525, -525);
public drawGroup(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
os: number,
size: number,
rotation: number,
selectedTile: Tile|undefined
): void {
ctx.save();
ctx.translate(525, 525);
ctx.rotate(rotation);
ctx.translate(-525, -525);
rotation = 0;
let v = 75 * size;
let w = 90 * size;
let osy = 25 * size / 2;
let p = (this.belongsTo - this.stolenFrom - 1 + 4) % 4;
// Calcul des paramètres de dessin
const v = 75 * size;
const w = 90 * size;
const osy = 25 * size / 2;
const p = (this.belongsTo - this.stolenFrom - 1 + 4) % 4;
// Détermination du tile sélectionné
const sf = selectedTile === undefined ? -1 : selectedTile.getFamily();
const sv = selectedTile === undefined ? 0 : selectedTile.getValue();
// Fonction helper pour éviter la répétition de code
const drawTile = (tile: Tile, tx: number, ty: number, angle: number) => {
tile.drawTile(
ctx,
tx,
ty,
size,
false,
angle,
tile.isEqual(sf, sv)
);
};
const HALF_PI = Math.PI / 2;
const sf = selectedTile === undefined ? -1 : selectedTile.getFamily();
const sv = selectedTile === undefined ? 0 : selectedTile.getValue();
if (p === 0) {
this.tiles[0].drawTile(
ctx,
x,
y + osy,
size,
false,
3.141592 / 2,
this.tiles[0].isEqual(sf, sv),
);
this.tiles[1].drawTile(
ctx,
x + w,
y,
size,
false,
0,
this.tiles[1].isEqual(sf, sv),
);
this.tiles[2].drawTile(
ctx,
x + w + v + os * size,
y,
size,
false,
0,
this.tiles[2].isEqual(sf, sv),
);
} else if (p === 1) {
this.tiles[0].drawTile(
ctx,
x,
y,
size,
false,
0,
this.tiles[0].isEqual(sf, sv),
);
this.tiles[1].drawTile(
ctx,
x + w,
y + osy,
size,
false,
0 - 3.141592 / 2,
this.tiles[1].isEqual(sf, sv),
);
this.tiles[2].drawTile(
ctx,
x + w + v + 3 *os * size,
y,
size,
false,
0,
this.tiles[2].isEqual(sf, sv),
);
} else if (p === 2) {
this.tiles[0].drawTile(
ctx,
x,
y,
size,
false,
0,
this.tiles[0].isEqual(sf, sv),
);
this.tiles[1].drawTile(
ctx,
x + v + os * size,
y,
size,
false,
0,
this.tiles[1].isEqual(sf, sv),
);
this.tiles[2].drawTile(
ctx,
x + w + v + os * size,
y + osy,
size,
false,
0 - 3.141592 / 2,
this.tiles[2].isEqual(sf, sv),
);
} else {
//TODO error
}
ctx.restore();
}
// Dessin selon la position
switch (p) {
case 0:
drawTile(this.tiles[0], x, y + osy, HALF_PI);
drawTile(this.tiles[1], x + w, y, 0);
drawTile(this.tiles[2], x + w + v + os * size, y, 0);
break;
case 1:
drawTile(this.tiles[0], x, y, 0);
drawTile(this.tiles[1], x + w, y + osy, -HALF_PI);
drawTile(this.tiles[2], x + w + v + 3 * os * size, y, 0);
break;
case 2:
drawTile(this.tiles[0], x, y, 0);
drawTile(this.tiles[1], x + v + os * size, y, 0);
drawTile(this.tiles[2], x + w + v + os * size, y + osy, -HALF_PI);
break;
default:
console.error(`Position non prise en charge: ${p}`);
}
ctx.restore();
}
}

View file

@ -1,200 +1,334 @@
import { Tile } from "./tile"
import { Group } from "./group"
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;
public constructor(stiles: string = "") {
this.tiles = [];
for (let i = 0; i < stiles.length - 1; i++) {
let ss = stiles.substring(i, i+2);
if (ss[0] === "m") {
this.tiles.push(new Tile(1, Number(ss[1]), false));
} else if (ss[0] === "p") {
this.tiles.push(new Tile(2, Number(ss[1]), false));
} else if (ss[0] === "s") {
this.tiles.push(new Tile(3, Number(ss[1]), false));
} else if (ss[0] === "w") {
this.tiles.push(new Tile(4, Number(ss[1]), false));
} else if (ss[0] === "d") {
this.tiles.push(new Tile(5, Number(ss[1]), false));
} else {}
}
}
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);
}
public getTiles(): Array<Tile> {
return this.tiles;
}
/**
* 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);
}
}
}
public length(): number {
return this.tiles.length;
}
/**
* 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)
);
}
public push(tile: Tile): void {
this.tiles.push(tile);
}
/**
* 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
};
public pop(): Tile|undefined {
return this.tiles.pop();
}
const family = familyMap[type];
if (family !== undefined) {
this.tiles.push(new Tile(family, value, false));
}
}
public find(family: number, value: number) :Tile|undefined {
let n = undefined;
for (let i = 0; i < this.tiles.length; i++) {
if (this.tiles[i].getFamily() === family && this.tiles[i].getValue() === value) {
n = i;
break;
}
}
if (n !== undefined) {
[this.tiles[n], this.tiles[0]] = [this.tiles[0], this.tiles[n]];
let t = this.tiles.shift();
this.sort();
return t;
} else {
return undefined;
}
}
/**
* Get all tiles in hand
*/
public getTiles(): Array<Tile> {
return this.tiles;
}
public eject(idTile: number): Tile {
[this.tiles[0], this.tiles[idTile]] = [this.tiles[idTile], this.tiles[0]];
let tile = this.tiles.shift();
this.sort();
return tile as NonNullable<Tile>;
}
/**
* Get number of tiles in hand
*/
public length(): number {
return this.tiles.length;
}
public get(idTile: number|undefined): Tile|undefined {
if (idTile !== undefined) {
return this.tiles[idTile];
} else {
return undefined;
}
}
/**
* Add a tile to hand
*/
public push(tile: Tile): void {
this.tiles.push(tile);
}
public sort(): undefined {
this.tiles.sort((a, b) => a.isLessThan(b) ? -1 : 1);
}
/**
* Remove and return the last tile
*/
public pop(): Tile | undefined {
return this.tiles.pop();
}
public count(family: number, value: number): number {
let c = 0;
this.tiles.forEach(
t => {
if (t.getFamily() === family && t.getValue() === value) {
c++;
}
}
);
return c;
}
/**
* 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;
}
public toGroup(pair: boolean = false): Array<Group>|undefined {
if (this.tiles.length > 0) {
let t1 = this.tiles.pop() as NonNullable<Tile>;
let c = this.count(t1.getFamily(), t1.getValue());
if (c >= 1 && !pair) { //can do a pair
let t2 = this.find(t1.getFamily(), t1.getValue()) as NonNullable<Tile>;
let groups = this.toGroup(true);
this.tiles.push(t2);
this.sort();
if (groups !== undefined) {
this.tiles.push(t1);
this.sort();
groups.push(new Group([t1, t2], 0, 0));
return groups;
}
}
if (c >= 2) { //can do a pon
let t2 = this.find(t1.getFamily(), t1.getValue()) as NonNullable<Tile>;
let t3 = this.find(t1.getFamily(), t1.getValue()) as NonNullable<Tile>;
let groups = this.toGroup(pair);
this.tiles.push(t2);
this.tiles.push(t3);
this.sort();
if (groups !== undefined) {
groups.push(new Group([t1, t2, t3], 0, 0));
this.tiles.push(t1);
this.sort();
return groups;
}
}
let c2 = this.count(t1.getFamily(), t1.getValue()-1);
let c3 = this.count(t1.getFamily(), t1.getValue()-2);
if (c2 * c3 > 0) { //can do a chii
let t2 = this.find(t1.getFamily(), t1.getValue()-1) as NonNullable<Tile>;
let t3 = this.find(t1.getFamily(), t1.getValue()-2) as NonNullable<Tile>;
let groups = this.toGroup(pair);
this.tiles.push(t2);
this.tiles.push(t3);
this.sort();
if (groups !== undefined) {
groups.push(new Group([t3, t2, t1], 0, 0));
this.tiles.push(t1);
this.sort();
return groups;
}
}
/**
* 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
);
}
this.tiles.push(t1);
this.tiles.sort();
return undefined;
/**
* 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;
}
} else {
return [];
}
}
public drawHand (
ctx: CanvasRenderingContext2D,
x: number,
y: number,
offset: number,
size: number,
focusedTiled: number|undefined = undefined,
hidden: boolean = false,
rotation: number = 0
): void {
let v = (75 + offset) * size;
let vx = Math.cos(rotation) * v;
let vy = Math.sin(rotation) * v;
for (let i = 0; i < this.tiles.length; i++) {
let e = (i === this.tiles.length - 1 && this.isolate) ? 10 : 0;
if (i === focusedTiled) {
this.tiles[i].drawTile(
ctx,
x +
i * vx +
25 * size * Math.sin(rotation) +
e * size * Math.cos(rotation),
y +
i * vy -
25 * size * Math.cos(rotation) +
e * size * Math.sin(rotation),
size,
hidden,
rotation
);
} else {
this.tiles[i].drawTile(
ctx,
x + i * vx + e * size * Math.cos(rotation),
y + i * vy + e * size * Math.sin(rotation),
size,
hidden,
rotation
);
}
}
}
/**
* 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];
}
public async preload(): Promise<void> {
await Promise.all(this.tiles.map(t => t.preloadImg()));
}
/**
* Sort tiles in ascending order
*/
public sort(): void {
this.tiles.sort((a, b) => a.isLessThan(b) ? -1 : 1);
}
public cleanup(): void {
this.tiles.forEach(tile => tile.cleanup());
this.tiles = [];
}
/**
* 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 = [];
}
}

View file

@ -1,181 +1,156 @@
export class Tile {
private family: number;
private value: number;
private red: boolean;
private imgSrc: string;
private imgFront: HTMLImageElement;
private imgBack: HTMLImageElement;
private imgGray: HTMLImageElement;
private img: HTMLImageElement;
private tilt: number;
public constructor(family: number, value: number , red: boolean) {
this.family = family;
this.value = value;
this.red = red;
this.imgSrc = "";
this.imgFront = new Image();
this.imgBack = new Image();
this.imgGray = new Image();
this.img = new Image();
this.tilt = 0;
this.setImgSrc();
}
private imgFront: HTMLImageElement = new Image();
private imgBack: HTMLImageElement = new Image();
private imgGray: HTMLImageElement = new Image();
private img: HTMLImageElement = new Image();
private imgSrc: string = "";
private tilt: number = 0;
constructor(
private family: number,
private value: number,
private red: boolean
) {
this.setImgSrc();
}
public getFamily(): number {
return this.family;
}
public getFamily(): number {
return this.family;
}
public getValue(): number {
return this.value;
}
public getValue(): number {
return this.value;
}
public isEqual(family: number, value: number): boolean {
return this.family === family && this.value === value;
}
public isEqual(family: number, value: number): boolean {
return this.family === family && this.value === value;
}
public isRed(): boolean {
return this.red;
}
public isRed(): boolean {
return this.red;
}
public compare(t: Tile): number {
if (this.family < t.family) {
return -1;
} else if (this.family > t.family) {
return 1;
}
if (this.value < t.value) {
return -1;
} else if (this.value > t.value) {
return 1;
}
return 0;
}
public compare(t: Tile): number {
// Compare d'abord par famille, puis par valeur
if (this.family !== t.family) {
return this.family < t.family ? -1 : 1;
}
if (this.value !== t.value) {
return this.value < t.value ? -1 : 1;
}
return 0;
}
public setTilt(): void {
this.tilt = (1 - 2 * Math.random()) * 0.04;
}
public isLessThan(t: Tile): boolean {
return this.family < t.family ||
(this.family === t.family && this.value <= t.value);
}
public drawTile(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
size: number,
hidden: boolean = false,
rotation: number = 0,
gray: boolean = false,
tilted: boolean = true
): void {
ctx.save();
ctx.translate(x + (75 * size) / 2, y + (100 * size) / 2);
if (tilted) {
ctx.rotate(rotation + this.tilt);
} else {
ctx.rotate(rotation);
}
public setTilt(): void {
this.tilt = (1 - 2 * Math.random()) * 0.04;
}
if (hidden) {
ctx.drawImage( // ombre
this.imgGray,
-(75 * size * 0.92) / 2,
-(100 * size * 0.91) / 2,
75 * size,
100 * size
);
ctx.drawImage( // le dos des tuiles
this.imgBack,
-(75 * size) / 2,
-(100 * size) / 2,
75 * size,
100 * size
);
} else {
ctx.drawImage( // ombre
this.imgGray,
-(75 * size * 0.92) / 2,
-(100 * size * 0.91) / 2,
75 * size,
100 * size
);
ctx.drawImage( // tuile à vide
this.imgFront,
-(75 * size) / 2,
-(100 * size) / 2,
75 * size, 100 * size
);
ctx.drawImage( // le dessin sur la tuile
this.img,
-((75 - 7) * size) / 2,
-((100 - 10) * size) / 2,
75 * size * 0.9,
100 * size * 0.9
);
if (gray) { // grisé
ctx.drawImage(
this.imgGray,
-(75 * size) / 2,
-(100 * size) / 2,
75 * size,
100 * size
);
}
}
public drawTile(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
size: number,
hidden: boolean = false,
rotation: number = 0,
gray: boolean = false,
tilted: boolean = true
): void {
const tileWidth = 75 * size;
const tileHeight = 100 * size;
const halfWidth = tileWidth / 2;
const halfHeight = tileHeight / 2;
const shadowScale = 0.92;
// Sauvegarde du contexte et positionnement
ctx.save();
ctx.translate(x + halfWidth, y + halfHeight);
ctx.rotate(rotation + (tilted ? this.tilt : 0));
// Position de l'ombre (légèrement décalée)
const shadowX = -(tileWidth * shadowScale) / 2;
const shadowY = -(tileHeight * shadowScale) / 2;
// Dessin de l'ombre (commun aux deux cas)
ctx.drawImage(this.imgGray, shadowX, shadowY, tileWidth, tileHeight);
if (hidden) {
// Dessin du dos de la tuile
ctx.drawImage(this.imgBack, -halfWidth, -halfHeight, tileWidth, tileHeight);
} else {
// Dessin de la tuile face visible
ctx.drawImage(this.imgFront, -halfWidth, -halfHeight, tileWidth, tileHeight);
// Dessin du motif sur la tuile (légèrement plus petit)
const patternScale = 0.9;
const patternWidth = tileWidth * patternScale;
const patternHeight = tileHeight * patternScale;
const patternX = -((75 - 7) * size) / 2;
const patternY = -((100 - 10) * size) / 2;
ctx.drawImage(this.img, patternX, patternY, patternWidth, patternHeight);
// Appliquer un filtre gris si demandé
if (gray) {
ctx.drawImage(this.imgGray, -halfWidth, -halfHeight, tileWidth, tileHeight);
}
}
ctx.restore();
}
ctx.restore();
}
public cleanup(): void {
// Supprimer tous les gestionnaires d'événements
const images = [this.imgFront, this.imgBack, this.imgGray, this.img];
images.forEach(img => {
img.onload = null;
img.onerror = null;
});
}
public isLessThan(t: Tile): boolean {
if (this.family < t.family) {
return true;
} else if (this.family === t.family && this.value <= t.value) {
return true;
} else {
return false;
}
}
public async preloadImg(): Promise<void> {
const imagesToLoad = [
{ img: this.imgFront, src: "img/Regular/Front.svg" },
{ img: this.imgBack, src: "img/Regular/Back.svg" },
{ img: this.imgGray, src: "img/Regular/Gray.svg" },
{ img: this.img, src: this.imgSrc }
];
await Promise.all(
imagesToLoad.map(({ img, src }) => this.loadImg(img, src))
);
}
public cleanup(): void {
this.imgFront.onload = null;
this.imgFront.onerror = null;
this.imgBack.onload = null;
this.imgBack.onerror = null;
this.imgGray.onload = null;
this.imgGray.onerror = null;
this.img.onload = null;
this.img.onerror = null;
}
private loadImg(img: HTMLImageElement, src: string): Promise<void> {
return new Promise((resolve, reject) => {
img.onload = () => resolve();
img.onerror = () => reject();
img.src = src;
});
}
private setImgSrc(): void {
this.imgSrc = "img/Regular/"
if (this.family <= 3) {
this.imgSrc += ["", "Man", "Pin", "Sou"][this.family];
this.imgSrc += String(this.value);
if (this.red) {
this.imgSrc += "-Dora";
}
this.imgSrc += ".svg";
} else if (this.family === 4) {
this.imgSrc += ["", "Ton", "Nan", "Shaa", "Pei"][this.value] + ".svg";
} else if (this.family === 5) {
this.imgSrc += ["", "Chun", "Hatsu", "Haku"][this.value] + ".svg";
}
}
public async preloadImg(): Promise<void> {
await Promise.all([
this.loadImg(this.imgFront, "img/Regular/Front.svg"),
// this.loadImg(this.imgFront, "/~img/Export/Regular/Front.png"),
this.loadImg(this.imgBack, "img/Regular/Back.svg"),
this.loadImg(this.imgGray, "img/Regular/Gray.svg"),
this.loadImg(this.img, this.imgSrc)
]);
}
private loadImg(img: HTMLImageElement, src: string): Promise<void> {
return new Promise((resolve, reject) => {
img.onload = () => resolve();
img.onerror = () => reject();
img.src = src;
});
}
private setImgSrc(): void {
this.imgSrc = "img/Regular/";
if (this.family <= 3) {
const families = ["", "Man", "Pin", "Sou"];
this.imgSrc += families[this.family] + String(this.value);
if (this.red) {
this.imgSrc += "-Dora";
}
} else if (this.family === 4) {
const winds = ["", "Ton", "Nan", "Shaa", "Pei"];
this.imgSrc += winds[this.value];
} else if (this.family === 5) {
const dragons = ["", "Chun", "Hatsu", "Haku"];
this.imgSrc += dragons[this.value];
}
this.imgSrc += ".svg";
}
}

View file

@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "es2015",
"module": "esnext",
"strict": true,
}