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,11 +1,48 @@
import { Tile } from "./tile" import { Tile } from "./tile";
type button_t = ( /**
arg0: CanvasRenderingContext2D, * Type definition for button rendering functions
arg1: number, */
arg2: number type ButtonRenderer = (
ctx: CanvasRenderingContext2D,
x: number,
y: number
) => void; ) => 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( export function clickAction(
x: number, x: number,
y: number, y: number,
@ -15,33 +52,39 @@ export function clickAction(
ron: boolean, ron: boolean,
tsumo: boolean tsumo: boolean
): number { ): number {
let buttons = [ const activeButtons = getActiveButtons(chii, pon, kan, ron, tsumo);
[tsumo, 5],
[ron, 4], if (activeButtons.length === 0) {
[kan, 3],
[pon, 2],
[chii, 1]
]
if (buttons.some(c => c[0])) {
buttons.push([true, 0]);
} else {
return -1; return -1;
} }
let xmin = 960 - buttons.filter(c => c[0]).length * 120;
let inside = 838 < y && y < 888; // Calculate starting X position based on number of buttons
let q = Math.floor((x - xmin) / 120); const xmin = 960 - activeButtons.length * BUTTON_SPACING;
let r = (x - xmin) - 120 * q;
if ( // Check if Y coordinate is within button area
q >= 0 && const isYInButtonArea = BUTTON_AREA_Y_MIN < y && y < BUTTON_AREA_Y_MAX;
q < buttons.filter(c => c[0]).length && if (!isYInButtonArea) {
r > 10 && return -1;
inside
) {
return buttons.filter(c => c[0])[q][1] as number;
} }
// 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; return -1;
} }
/**
* Draw all active buttons on the canvas
*/
export function drawButtons( export function drawButtons(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
chii: boolean, chii: boolean,
@ -50,228 +93,219 @@ export function drawButtons(
ron: boolean, ron: boolean,
tsumo: boolean tsumo: boolean
): void { ): void {
let buttons = [ const buttonFunctions: [boolean, ButtonRenderer][] = [
[chii, buttonChii], [chii, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.chii)],
[pon, buttonPon], [pon, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.pon)],
[kan, buttonKan], [kan, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.kan)],
[ron, buttonRon], [ron, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.ron)],
[tsumo, buttonTsumo] [tsumo, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.tsumo)]
] ];
if (buttons.some(c => c[0])) {
buttons.unshift([true, buttonPass]); // 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
} }
let dx = 0;
for (let i = 0; i < buttons.length; i++) { // Draw active buttons
if (buttons[i][0]) { let positionOffset = 0;
(buttons[i][1] as button_t)( for (const [isActive, renderFunc] of buttonFunctions) {
if (isActive) {
renderFunc(
ctx, ctx,
850 - dx * 120, BASE_X_POSITION - positionOffset * BUTTON_SPACING,
835 835
); );
dx++; 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( export function clickChii(
x: number, x: number,
y: number, y: number,
chiis: Array<Array<Tile>> chiis: Array<Array<Tile>>
): number { ): number {
let xmin = 960 - (chiis.length + 1) * 120; if (chiis.length === 0) {
let inside = 838 < y && y < 888; return -1;
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();
} }
// 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; return -1;
} }
/**
* Draw Chi options on the canvas
*/
export function drawChiis( export function drawChiis(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
chiis: Array<Array<Tile>> chiis: Array<Array<Tile>>
): void { ): void {
chiis.reverse(); // Create a copy to avoid modifying the original array
const r = 8; const chiiOptions = [...chiis].reverse();
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; // Draw "back" button
for (let i = 0; i < chiis.length; i++) { renderButton(ctx, BASE_X_POSITION, 835, BUTTON_STYLES.back);
// Draw Chi options
let positionOffset = 1;
for (const tiles of chiiOptions) {
drawOneChii( drawOneChii(
ctx, ctx,
850 - dx * 120, BASE_X_POSITION - positionOffset * BUTTON_SPACING,
835, 835,
chiis[i] tiles
); );
dx++; positionOffset++;
} }
} }
/**
* Draw a single Chi option
*/
function drawOneChii( function drawOneChii(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
x: number, x: number,
y: number, y: number,
tiles: Array<Tile> tiles: Array<Tile>
): void { ): void {
const r = 8; const tileOffset = 32;
const w = 110; const tileStartX = x + 7;
const h = 50; const tileStartY = y + 5;
const dx = 32;
const x0 = x + 7; // Draw the button background
const y0 = y + 5; drawButtonShape(ctx, x, y, BUTTON_RADIUS, BUTTON_WIDTH, BUTTON_HEIGHT, BUTTON_STYLES.chii.color);
button(ctx, x, y, r, w, h, "#FFCC33");
tiles[0].drawTile( // Draw the tiles
for (let i = 0; i < tiles.length; i++) {
tiles[i].drawTile(
ctx, ctx,
x0, tileStartX + tileOffset * i,
y0, tileStartY,
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, 0.4,
false, false,
0, 0,
false, false,
false false
); );
}
} }
function button( /**
* 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;
}
/**
* Render a button with text
*/
function renderButton(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
x: number, x: number,
y: number, y: number,
r: number, config: ButtonConfig
w: number, ): void {
h: number, 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);
}
/**
* Draw a rounded rectangle button shape
*/
function drawButtonShape(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
radius: number,
width: number,
height: number,
color: string color: string
): void { ): void {
ctx.fillStyle = color; ctx.fillStyle = color;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(x + r, y); // Top right corner
ctx.lineTo(x + w - r, y); ctx.moveTo(x + radius, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r); ctx.lineTo(x + width - radius, y);
ctx.lineTo(x + w, y + h - r); ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h); // Bottom right corner
ctx.quadraticCurveTo(x, y + h, x, y + h - r); ctx.lineTo(x + width, y + height - radius);
ctx.lineTo(x, y + r); ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.quadraticCurveTo(x, y, x + r, y);
// 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);
ctx.fill(); ctx.fill();
// Add border
ctx.fillStyle = "#606060"; ctx.fillStyle = "#606060";
ctx.stroke(); ctx.stroke();
} }
function buttonPass(
ctx: CanvasRenderingContext2D,
x: number,
y: number
): 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);
}
function buttonPon(
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("Pon",x + w * 0.28, y + h/2 * 1.3);
}
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);
}
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);
}
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);
}

View file

@ -1,14 +1,21 @@
import { Tile } from "./tile" import { Tile } from "./tile";
import { Hand } from "./hand" import { Hand } from "./hand";
type TileKey = `${number}-${number}`;
export class Deck { 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) { public constructor(allowRed: boolean = false) {
this.tiles = []; this.tiles = [];
this.tileIndexMap = new Map();
this.initTiles(allowRed); this.initTiles(allowRed);
} }
/**
* Displays all tile families on the canvas
*/
public displayFamilies( public displayFamilies(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
x: number, x: number,
@ -19,133 +26,267 @@ export class Deck {
): void { ): void {
let posX = x; let posX = x;
let posY = y; let posY = y;
for (let i = 1; i < 6; i++) {
if (i < 4) { // famille // Define tile layouts for each family
for (let j = 1; j < 10; j++) { const familyLayouts = [
const tile = this.find(i, j) as NonNullable<Tile>; { 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); tile.drawTile(ctx, posX, posY, size, false, 0, false);
posX += (75 + xOffset) * size; 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; posX = x;
posY += (100 + yOffset) * size; 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;
}
}
} }
} }
/**
* Returns the number of tiles in the deck
*/
public length(): number { public length(): number {
return this.tiles.length; return this.tiles.length;
} }
/**
* Removes and returns the last tile from the deck
* @throws Error if the deck is empty
*/
public pop(): Tile { public pop(): Tile {
if (this.tiles.length === 0) { if (this.tiles.length === 0) {
} throw new Error("Cannot pop from an empty deck");
return this.tiles.pop() as NonNullable<Tile>;
} }
public push(tile: Tile) { const tile = this.tiles.pop()!;
this.tiles.push(tile); // Update the index map
} const key = this.getTileKey(tile.getFamily(), tile.getValue());
const indices = this.tileIndexMap.get(key);
public find(family: number, value: number): Tile | undefined { if (indices && indices.length > 0) {
let n = undefined; indices.pop(); // Remove the last index
for (let i = 0; i < this.tiles.length; i++) { if (indices.length === 0) {
if ( this.tileIndexMap.delete(key);
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 { } 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; 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 { public count(family: number, value: number): number {
let n = 0; const key = this.getTileKey(family, value);
for (let i = 0; i < this.tiles.length; i++) { const indices = this.tileIndexMap.get(key);
if ( return indices ? indices.length : 0;
this.tiles[i].getFamily() === family &&
this.tiles[i].getValue() === value
) {
n++;
}
}
return n;
} }
public shuffle(): undefined { /**
let newArray: Array<Tile> = []; * Shuffles the deck using Fisher-Yates algorithm
while (this.tiles.length > 0) { */
let n = Math.floor(Math.random() * this.tiles.length); public shuffle(): void {
[this.tiles[n], this.tiles[0]] = [this.tiles[0], this.tiles[n]]; // Fisher-Yates shuffle algorithm
newArray.push(this.tiles.shift() as NonNullable<Tile>); for (let i = this.tiles.length - 1; i > 0; i--) {
} const j = Math.floor(Math.random() * (i + 1));
this.tiles = newArray; [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 { public getRandomHand(): Hand {
let hand = new Hand(); const hand = new Hand();
this.shuffle(); 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++) { for (let i = 0; i < 13; i++) {
hand.push(this.pop()); hand.push(this.pop());
} }
return hand; return hand;
} }
/**
* Cleans up resources used by tiles and empties the deck
*/
public cleanup(): void { public cleanup(): void {
this.tiles.forEach(tile => tile.cleanup()); this.tiles.forEach(tile => tile.cleanup());
this.tiles = []; this.tiles = [];
this.tileIndexMap.clear();
} }
/**
* Preloads all tile images
*/
public async preload(): Promise<void> { public async preload(): Promise<void> {
for (let i = 0; i < this.tiles.length; i++) { const preloadPromises = this.tiles.map(tile => tile.preloadImg());
await this.tiles[i].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);
}
} }
} }
private initTiles(allowRed: boolean): undefined { /**
for (let i = 1; i < 6; i++) { * Rebuilds the entire index map from scratch
if (i < 4) { // famille */
for (let j = 1; j < 10; j++) { private rebuildIndexMap(): void {
this.tiles.push(new Tile(i, j, false)); this.tileIndexMap.clear();
this.tiles.push(new Tile(i, j, false));
this.tiles.push(new Tile(i, j, false)); for (let i = 0; i < this.tiles.length; i++) {
if (j === 5 && allowRed) { const tile = this.tiles[i];
this.tiles.push(new Tile(i, j, true)); const key = this.getTileKey(tile.getFamily(), tile.getValue());
} else { const indices = this.tileIndexMap.get(key) || [];
this.tiles.push(new Tile(i, j, false)); indices.push(i);
this.tileIndexMap.set(key, indices);
} }
} }
} else if (i === 4) { // vent
for (let j = 1; j < 5; j++) { /**
for (let k = 0; k < 4; k++) { * Initializes the deck with all tiles
this.tiles.push(new Tile(i, j, false)); */
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));
} }
} }
} else if (i === 5) { // dragon
for (let j = 1; j < 4; j++) { // Create winds (family 4)
for (let k = 0; k < 4; k++) { for (let value = 1; value <= 4; value++) {
this.tiles.push(new Tile(i, j, false)); 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 { Deck } from "../deck";
import { Hand } from "../hand" import { Hand } from "../hand";
declare global { declare global {
interface Window { interface Window {
cleanup: () => void; cleanup: () => void;
} }
} }
export {}
const canvas = document.getElementById("myCanvas") as HTMLCanvasElement; export {};
async function preloadDeck(deck: Deck) { class RiichiDisplay {
await deck.preload(); private canvas: HTMLCanvasElement;
} private ctx: CanvasRenderingContext2D;
private offScreenCanvas: HTMLCanvasElement;
private offScreenCtx: CanvasRenderingContext2D;
async function display () { private deck: Deck;
if (canvas) { private hands: Hand[] = [];
const ctx = canvas.getContext("2d") as NonNullable<CanvasRenderingContext2D>; private edeck: Deck;
private ehand: Hand;
if (ctx) { private selectedTile: number | undefined = undefined;
// double buffering private animationFrameId: number | null = null;
const offScreenCanvas = document.createElement('canvas') as HTMLCanvasElement; private isDirty: boolean = true;
offScreenCanvas.width = canvas.width;
offScreenCanvas.height = canvas.height;
const offScreenCtx = offScreenCanvas.getContext('2d') as NonNullable<CanvasRenderingContext2D>;
//animation parameter // Constants
let lastTime = 0; private readonly FPS: number = 30;
const FPS = 30; private readonly INTERVAL: number = 1000 / this.FPS;
const interval = 1000 / 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;
// tuiles // Cache for mouse hit detection
let x = 100; private tileRects: Array<{x: number, y: number, width: number, height: number}> = [];
let y = 150;
let os = 75;
let size = 0.75;
const deck = new Deck(false);
await preloadDeck(deck);
let hands: Array<Hand> = []; 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++) { for (let i = 0; i < 4; i++) {
const hand = deck.getRandomHand(); const hand = this.deck.getRandomHand();
hand.sort(); hand.sort();
hands.push(hand); this.hands.push(hand);
} }
// interactive hand // Initialize interactive hand
const edeck = new Deck(false); this.ehand = this.edeck.getRandomHand();
await edeck.preload(); this.ehand.push(this.edeck.pop());
const ehand = edeck.getRandomHand(); this.ehand.sort();
ehand.push(edeck.pop());
ehand.sort();
let selectedTile: number|undefined = undefined; // Initial draw
this.drawCanvas();
// function to draw // Start animation loop
const drawCanvas = async () => { this.startAnimationLoop();
// 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 private drawCanvas(): void {
ehand.isolate = true; // Only redraw if something changed (dirty flag)
ehand.drawHand(offScreenCtx, x, 800, 5, size, selectedTile); if (!this.isDirty) return;
ctx.clearRect(0, 0, canvas.width, canvas.height); const ctx = this.offScreenCtx;
ctx.drawImage(offScreenCanvas, 0, 0);
// 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 animationLoop = (currentTime: number) => {
const deltaTime = currentTime - lastTime; const deltaTime = currentTime - lastTime;
if (deltaTime >= interval) { if (deltaTime >= this.INTERVAL) {
lastTime = currentTime; lastTime = currentTime - (deltaTime % this.INTERVAL);
drawCanvas(); this.drawCanvas();
}
requestAnimationFrame(animationLoop);
} }
// mouse event this.animationFrameId = requestAnimationFrame(animationLoop);
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)); this.animationFrameId = requestAnimationFrame(animationLoop);
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 { public cleanup(): void {
console.error("Impossible d'obtenir le contexte du canvas."); // Cancel animation loop
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
} }
} else {
console.error("Canvas introuvable dans le DOM."); // 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 { Deck } from "../deck";
import { Hand } from "../hand"; import { Hand } from "../hand";
import { Group } from "../group" import { Group } from "../group";
// Configuration globale // Configuration globale
const CANVAS_ID = "myCanvas"; const CANVAS_ID = "myCanvas";
@ -9,174 +9,363 @@ const BG_RECT = { x: 50, y: 50, w: 1000, h: 1000 };
const FPS = 30; const FPS = 30;
const FRAME_INTERVAL = 1000 / FPS; const FRAME_INTERVAL = 1000 / FPS;
// variables globales /**
const DECKS: Array<Deck> = []; * Classe RiichiDisplay responsable de la gestion de l'affichage du jeu Riichi Mahjong
const HANDS: Array<Hand> = []; */
let selectedTile: number|undefined = undefined; class RiichiDisplay {
// Canvas principal et contexte
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
// Optimisation des références // Canvas pour le double-buffering
let animationFrameId: number; private staticCanvas: HTMLCanvasElement;
let lastFrameTime = 0; private staticCtx: CanvasRenderingContext2D;
const callbacks: Array<() => void> = [];
// Pré-calcul des dimensions // Ressources du jeu
const canvas = document.getElementById(CANVAS_ID) as HTMLCanvasElement; private decks: Array<Deck> = [];
const ctx = canvas.getContext("2d") as NonNullable<CanvasRenderingContext2D>; private hands: Array<Hand> = [];
// Cache statique // État de l'interface
const staticCanvas = document.createElement('canvas') as HTMLCanvasElement; private selectedTile: number | undefined = undefined;
const staticCtx = staticCanvas.getContext("2d") as NonNullable<CanvasRenderingContext2D>; private isDirty: boolean = true;
staticCanvas.width = canvas.width;
staticCanvas.height = canvas.height;
// Pré-rendu du fond // Animation et événements
function prerenderBackground() { private animationFrameId: number | null = null;
staticCtx.clearRect(0, 0, canvas.width, canvas.height); private lastFrameTime: number = 0;
staticCtx.fillStyle = BG_COLOR; private cleanupCallbacks: Array<() => void> = [];
staticCtx.fillRect(BG_RECT.x, BG_RECT.y, BG_RECT.w, BG_RECT.h);
};
function drawFrame() { // Constantes pour le rendu
if (!ctx) return; 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;
// Effacement intelligent (uniquement la zone nécessaire) // Zones de détection pour l'interaction souris
prerenderBackground(); private tileRects: Array<{ x: number, y: number, width: number, height: number }> = [];
// Ici viendrait le dessin des éléments dynamiques constructor(canvasId: string = CANVAS_ID) {
// Par exemple: // Initialisation du canvas
// drawDeck(); const canvas = document.getElementById(canvasId) as HTMLCanvasElement;
// drawHands(); if (!canvas) {
let x = 300; throw new Error(`Canvas avec ID '${canvasId}' introuvable`);
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 = []; this.canvas = canvas;
// Dessin du cache statique // Récupération du contexte 2D
ctx.clearRect(0, 0, canvas.width, canvas.height); const ctx = canvas.getContext("2d");
ctx.drawImage(staticCanvas, 0, 0); if (!ctx) {
} throw new Error("Impossible d'obtenir le contexte 2D du canvas");
}
this.ctx = ctx;
function animationLoop(currentTime: number) { // Initialisation du canvas pour le double-buffering
animationFrameId = requestAnimationFrame(animationLoop); this.staticCanvas = document.createElement('canvas');
this.staticCanvas.width = canvas.width;
this.staticCanvas.height = canvas.height;
const deltaTime = currentTime - lastFrameTime; 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; if (deltaTime < FRAME_INTERVAL) return;
lastFrameTime = currentTime - (deltaTime % FRAME_INTERVAL); this.lastFrameTime = currentTime - (deltaTime % FRAME_INTERVAL);
drawFrame(); this.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(() => { this.animationFrameId = requestAnimationFrame(animationLoop);
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;
} }
// Préchargement des ressources si nécessaire /**
// const deck = new Deck(); * Initialise les écouteurs d'événements
// await preloadDeck(deck); */
DECKS.push( private initEventListeners(): void {
new Deck(false) const mouseMoveHandler = this.handleMouseMove.bind(this);
); const mouseDownHandler = this.handleMouseDown.bind(this);
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()); this.canvas.addEventListener('mousemove', mouseMoveHandler);
HANDS[8].sort(); this.canvas.addEventListener('mousedown', mouseDownHandler);
initEventListeners(); // Enregistrer les callbacks de nettoyage
requestAnimationFrame(animationLoop); 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; window.cleanup = cleanup;
} catch (error) {
console.error("Erreur lors de l'initialisation:", error);
}
} }
// Déclaration globale pour TypeScript // Déclaration globale pour TypeScript
@ -190,4 +379,3 @@ declare global {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
initDisplay().catch(console.error); initDisplay().catch(console.error);
} }

View file

@ -1,111 +1,243 @@
import { Deck } from "../deck"; import { Deck } from "../deck";
import { Hand } from "../hand"; import { Hand } from "../hand";
import { Game } from "../game" import { Game } from "../game";
// Configuration globale class RiichiGameManager {
const CANVAS_ID = "myCanvas"; // Configuration globale
const BG_RECT = { x: 0, y: 0, w: 1050, h: 1050 }; private readonly CANVAS_ID: string = "myCanvas";
var MOUSE = { x: 0, y: 0 }; private readonly BG_RECT = { x: 0, y: 0, w: 1050, h: 1050 };
const FPS = 60; private readonly FPS: number = 60;
const FRAME_INTERVAL = 1000 / FPS; private readonly FRAME_INTERVAL: number = 1000 / this.FPS;
// variables globales // Singleton instance
const DECKS: Array<Deck> = []; private static instance: RiichiGameManager;
const HANDS: Array<Hand> = [];
var GAME: Game|undefined;
// Optimisation des références // Canvas et contextes
let animationFrameId: number; private canvas: HTMLCanvasElement;
let lastFrameTime = 0; private ctx: CanvasRenderingContext2D;
const callbacks: Array<() => void> = []; private staticCanvas: HTMLCanvasElement;
private staticCtx: CanvasRenderingContext2D;
// Pré-calcul des dimensions // État du jeu
const canvas = document.getElementById(CANVAS_ID) as HTMLCanvasElement; private mouse = { x: 0, y: 0 };
const ctx = canvas.getContext("2d") as NonNullable<CanvasRenderingContext2D>; private game: Game | null = null;
canvas.width = BG_RECT.w; private decks: Array<Deck> = [];
canvas.height = BG_RECT.h; private hands: Array<Hand> = [];
// Cache statique // Animation et boucle de jeu
const staticCanvas = document.createElement('canvas') as HTMLCanvasElement; private animationFrameId: number | null = null;
const staticCtx = staticCanvas.getContext("2d") as NonNullable<CanvasRenderingContext2D>; private lastFrameTime: number = 0;
staticCanvas.width = canvas.width; private isInitialized: boolean = false;
staticCanvas.height = canvas.height;
function drawFrame() { // Nettoyage
if (!ctx) return; private cleanupCallbacks: Array<() => void> = [];
GAME?.draw(MOUSE);
}
function animationLoop(currentTime: number) { /**
animationFrameId = requestAnimationFrame(animationLoop); * 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;
const deltaTime = currentTime - lastFrameTime; // Récupération du contexte 2D
if (deltaTime < FRAME_INTERVAL) return; const ctx = canvas.getContext("2d", { alpha: false });
if (!ctx) {
throw new Error("Impossible d'obtenir le contexte 2D du canvas");
}
this.ctx = ctx;
lastFrameTime = currentTime - (deltaTime % FRAME_INTERVAL); // Initialisation du canvas pour le double-buffering
drawFrame(); this.staticCanvas = document.createElement('canvas');
} this.staticCanvas.width = canvas.width;
this.staticCanvas.height = canvas.height;
function initEventListeners() { const staticCtx = this.staticCanvas.getContext("2d", { alpha: false });
const handlers = { if (!staticCtx) {
mousedown: (e: MouseEvent) => { throw new Error("Impossible d'obtenir le contexte du canvas statique");
GAME?.click(e); }
}, this.staticCtx = staticCtx;
mousemove: (e: MouseEvent) => { }
MOUSE.x = e.x;
MOUSE.y = e.y; /**
* 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(() => { // Enregistrement des écouteurs
canvas.removeEventListener('mousemove', handlers.mousemove); this.canvas.addEventListener('mousemove', mouseMoveHandler);
canvas.removeEventListener('mousedown', handlers.mousedown); 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) {
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;
} }
console.log("Load begining\n"); /**
// Préchargement des ressources si nécessaire * Précharge un deck
// const deck = new Deck(); */
// await preloadDeck(deck); private async preloadDeck(deck: Deck): Promise<void> {
DECKS.push( await deck.preload();
); }
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();
console.log("Loaded completed\n"); /**
initEventListeners(); * Précharge une main
requestAnimationFrame(animationLoop); */
window.cleanup = cleanup; 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);
}
}
// Fonction de nettoyage globale exportée
export function cleanup(): void {
RiichiGameManager.getInstance().cleanup();
}
// Fonction d'initialisation globale exportée
export async function initDisplay(): Promise<void> {
await RiichiGameManager.getInstance().initialize();
} }
// Déclaration globale pour TypeScript // Déclaration globale pour TypeScript
@ -119,4 +251,3 @@ declare global {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
initDisplay().catch(console.error); initDisplay().catch(console.error);
} }

View file

@ -5,23 +5,40 @@ import { Group } from "./group";
import { drawButtons, clickAction, drawChiis, clickChii } from "./button"; import { drawButtons, clickAction, drawChiis, clickChii } from "./button";
import { drawState } from "./state"; import { drawState } from "./state";
export type mousePos = { x: number, y: number}; export type MousePos = { x: number, y: number };
// Game constants to avoid magic numbers and improve readability
const GAME_CONSTANTS = {
PLAYERS: 4,
BACKGROUND: { color: "#007730", x: 0, y: 0, w: 1050, h: 1050 },
DEAD_WALL_SIZE: 14,
WAITING_TIME: {
MIN: 500,
MAX: 2000,
DEFAULT: 700
},
DISPLAY: {
HAND_SIZE: 0.7,
HIDDEN_HAND_SIZE: 0.6,
DISCARD_SIZE: 0.6,
GROUP_SIZE: 0.6
},
PI: Math.PI
};
export class Game { export class Game {
private deck: Deck; private deck: Deck;
private deadWall: Array<Tile> = []; private deadWall: Array<Tile> = [];
private hands: Array<Hand> = []; private hands: Array<Hand> = [];
private discards: Array<Array<Tile>>; private discards: Array<Array<Tile>> = [];
private lastDiscard: number|undefined; private lastDiscard: number | undefined;
private groups: Array<Array<Group>>; private groups: Array<Array<Group>> = [];
// game values // Game state
private level: number; private level: number;
private turn = 0; private turn = 0;
private minWaitingTime = 500; private waitingTime = GAME_CONSTANTS.WAITING_TIME.DEFAULT;
private maxWaitingTime = 2000; private selectedTile: number | undefined = undefined;
private waitingTime = 700;
private selectedTile: number|undefined = undefined;
private canCall: boolean = false; private canCall: boolean = false;
private hasPicked: boolean = false; private hasPicked: boolean = false;
private hasPlayed: boolean = false; private hasPlayed: boolean = false;
@ -30,19 +47,17 @@ export class Game {
private end: boolean = false; private end: boolean = false;
private result: number = -1; private result: number = -1;
// display parameter // Canvas elements
private BG_RECT = {color: "#007730", x: 0, y: 0, w: 1050, h: 1050};
private sizeHand = 0.7;
private sizeHiddenHand = 0.6;
private sizeDiscard = 0.6;
// canvas
private ctx: CanvasRenderingContext2D; private ctx: CanvasRenderingContext2D;
private cv: HTMLCanvasElement; private cv: HTMLCanvasElement;
private staticCtx: CanvasRenderingContext2D; private staticCtx: CanvasRenderingContext2D;
private staticCv: HTMLCanvasElement; private staticCv: HTMLCanvasElement;
public constructor( // Cached values to avoid recalculations
private readonly BG_RECT = GAME_CONSTANTS.BACKGROUND;
private readonly rotations = [0, -GAME_CONSTANTS.PI / 2, -GAME_CONSTANTS.PI, GAME_CONSTANTS.PI / 2];
constructor(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
cv: HTMLCanvasElement, cv: HTMLCanvasElement,
staticCtx: CanvasRenderingContext2D, staticCtx: CanvasRenderingContext2D,
@ -55,25 +70,37 @@ export class Game {
this.staticCtx = staticCtx; this.staticCtx = staticCtx;
this.staticCv = staticCv; this.staticCv = staticCv;
this.level = level; this.level = level;
// Initialize game elements
this.deck = new Deck(red); this.deck = new Deck(red);
this.initializeGame();
}
private initializeGame(): void {
this.deck.shuffle(); this.deck.shuffle();
for (let i = 0; i < 14; i++) {
// Set up dead wall
for (let i = 0; i < GAME_CONSTANTS.DEAD_WALL_SIZE; i++) {
this.deadWall.push(this.deck.pop()); this.deadWall.push(this.deck.pop());
} }
for (let i = 0; i < 4; i++) {
this.hands.push(this.deck.getRandomHand());
}
this.discards = [[], [], [], []];
this.lastDiscard = undefined;
this.groups = [[], [], [], []];
// Create player hands
for (let i = 0; i < GAME_CONSTANTS.PLAYERS; i++) {
this.hands.push(this.deck.getRandomHand());
this.discards.push([]);
this.groups.push([]);
}
this.lastDiscard = undefined;
this.hands[0].sort(); this.hands[0].sort();
this.pick(0); this.pick(0);
} }
public draw(mp: mousePos) { public draw(mp: MousePos): void {
// background // Clear static canvas once per frame
this.staticCtx.clearRect(0, 0, this.cv.width, this.cv.height); this.staticCtx.clearRect(0, 0, this.cv.width, this.cv.height);
// Draw background
this.staticCtx.fillStyle = this.BG_RECT.color; this.staticCtx.fillStyle = this.BG_RECT.color;
this.staticCtx.fillRect( this.staticCtx.fillRect(
this.BG_RECT.x, this.BG_RECT.x,
@ -83,8 +110,9 @@ export class Game {
); );
this.getSelected(mp); this.getSelected(mp);
this.drawGame(); this.drawGame();
// Use a single drawImage operation instead of clearing and redrawing
this.ctx.clearRect(0, 0, this.cv.width, this.cv.height); this.ctx.clearRect(0, 0, this.cv.width, this.cv.height);
this.ctx.drawImage(this.staticCv, 0, 0); this.ctx.drawImage(this.staticCv, 0, 0);
} }
@ -101,31 +129,23 @@ export class Game {
return this.end; return this.end;
} }
public click( // player is playing public click(mp: MousePos): void {
mp: mousePos,
): void {
const rect = this.cv.getBoundingClientRect(); const rect = this.cv.getBoundingClientRect();
if (this.hasWin(0)) { if (this.hasWin(0)) {
this.end = true; this.end = true;
this.result = 1; this.result = 1;
} else if (this.chooseChii) { // is choosing return;
let allChii = this.getChii(0); }
let c = clickChii(
if (this.chooseChii) {
this.handleChiiSelection(mp, rect);
return;
}
const action = clickAction(
mp.x - rect.x, mp.x - rect.x,
mp.y - rect.y, mp.y - rect.y,
allChii
);
if (c === 0) {
this.chooseChii = false;
} else {
this.chooseChii = false;
this.chii(c, 0);
}
} else {
let action = clickAction(
mp.x - rect.left,
mp.y - rect.top,
this.canDoAChii().length > 0, this.canDoAChii().length > 0,
this.canDoAPon(), this.canDoAPon(),
false && this.level > 1, false && this.level > 1,
@ -133,115 +153,160 @@ export class Game {
false && this.level > 0 false && this.level > 0
); );
if (this.canCall && action !== -1) { // can call if (this.canCall && action !== -1) {
if (action === 0) { // pass this.handleCallAction(action);
this.canCall = false; } else if (this.turn === 0 && this.selectedTile !== undefined) {
if (this.turn === 3) { this.handlePlayerDiscard();
this.turn = 0;
this.pick(0);
} else {
this.turn++;
} }
this.updateWaitingTime(); }
this.hasPicked = false;
this.hasPlayed = false; private handleChiiSelection(mp: MousePos, rect: DOMRect): void {
} else if (action === 1) { // chii const allChii = this.getChii(0);
let chiis = this.canDoAChii(); const selection = clickChii(
if (chiis.length === 1) { // only one possible mp.x - rect.x,
mp.y - rect.y,
allChii
);
if (selection === 0) {
this.chooseChii = false;
} else {
this.chooseChii = false;
this.chii(selection, 0);
}
}
private handleCallAction(action: number): void {
if (action === 0) { // Pass
this.canCall = false;
this.advanceTurn();
} else if (action === 1) { // Chii
const chiis = this.canDoAChii();
if (chiis.length === 1) {
this.chii(chiis[0], 0); this.chii(chiis[0], 0);
} else { } else {
this.chooseChii = true; this.chooseChii = true;
this.drawGame(); this.drawGame();
} }
} else if (action == 2) { // pon } else if (action === 2) { // Pon
this.pon(this.turn); this.pon(this.turn);
} }
}
private handlePlayerDiscard(): void {
this.discard(0, this.selectedTile as number);
} else { // nothing unusual
if (this.turn === 0 && this.selectedTile !== undefined) {
this.discard(0, this.selectedTile as NonNullable<number>);
if (!this.checkPon()) { if (!this.checkPon()) {
let chiis = this.canDoAChii(1); const chiis = this.canDoAChii(1);
if (chiis.length > 0) { if (chiis.length > 0) {
let i = Math.floor(Math.random() * chiis.length); const i = Math.floor(Math.random() * chiis.length);
this.chii(chiis[i], 1); this.chii(chiis[i], 1);
} else { } else {
this.advanceTurn();
}
}
}
private advanceTurn(): void {
this.updateWaitingTime(); this.updateWaitingTime();
this.turn = (this.turn + 1) % 4; this.turn = (this.turn + 1) % GAME_CONSTANTS.PLAYERS;
} this.hasPicked = false;
} this.hasPlayed = false;
}
}
}
} }
private getSelected( private getSelected(mp: MousePos): void {
mp: mousePos,
): void {
const rect = this.cv.getBoundingClientRect(); const rect = this.cv.getBoundingClientRect();
let x = 2.5 * 75 * 0.75; const x = 2.5 * 75 * 0.75;
let y = 1050 - 250 * 0.6; const y = 1050 - 250 * 0.6;
const sizeHand = GAME_CONSTANTS.DISPLAY.HAND_SIZE;
const mouseX = mp.x - rect.left - x; const mouseX = mp.x - x;
const mouseY = mp.y - rect.top; const mouseY = mp.y;
const s = 83.9; const tileWidth = 83.9;
const tileIndex = Math.floor(mouseX / (tileWidth * sizeHand));
const relativeX = mouseX - tileIndex * tileWidth * sizeHand;
let q = Math.floor(mouseX / (s * this.sizeHand));
let r = mouseX - q * s * this.sizeHand;
if ( if (
r <= (s - 3) * this.sizeHand && relativeX <= (tileWidth - 3) * sizeHand &&
q >= 0 && tileIndex >= 0 &&
q < this.hands[0].length() && tileIndex < this.hands[0].length() &&
mouseY >= y && mouseY >= y &&
mouseY <= y + 100 * this.sizeHand mouseY <= y + 100 * sizeHand
) { ) {
this.selectedTile = q; this.selectedTile = tileIndex;
} else { } else {
this.selectedTile = undefined; this.selectedTile = undefined;
} }
}; }
private updateWaitingTime(): void { private updateWaitingTime(): void {
this.waitingTime = this.waitingTime = Math.floor(
Math.floor(
Math.random() * Math.random() *
(this.maxWaitingTime - this.minWaitingTime) + (GAME_CONSTANTS.WAITING_TIME.MAX - GAME_CONSTANTS.WAITING_TIME.MIN) +
this.minWaitingTime GAME_CONSTANTS.WAITING_TIME.MIN
); );
} }
private play(): void { private play(): void {
if ( if (this.turn !== 0 && !this.end) {
this.turn !== 0 && !this.end if (!this.hasPicked) {
) { // bot playing // Begin of turn
if (!this.hasPicked) { // begin of his turn
this.lastPlayed = Date.now(); this.lastPlayed = Date.now();
this.pick(this.turn); this.pick(this.turn);
this.hasPicked = true; this.hasPicked = true;
} else if (!this.hasPlayed) { // middle of his turn } else if (!this.hasPlayed) {
// Middle of turn
this.handleBotTurn();
} else if (!this.canCall) {
// End of turn
this.advanceBotTurn();
}
}
}
private handleBotTurn(): void {
if (this.hasWin(this.turn)) { if (this.hasWin(this.turn)) {
this.end = true; this.end = true;
this.result = 2; this.result = 2;
} else if (Date.now() - this.lastPlayed > this.waitingTime) { return;
}
if (Date.now() - this.lastPlayed > this.waitingTime) {
this.lastPlayed = Date.now(); this.lastPlayed = Date.now();
let n = Math.floor(this.hands[this.turn].length() * Math.random());
// Choose random tile to discard
const n = Math.floor(this.hands[this.turn].length() * Math.random());
this.discard(this.turn, n); this.discard(this.turn, n);
this.hasPlayed = true; this.hasPlayed = true;
if (this.deck.length() <= 0) { if (this.deck.length() <= 0) {
this.result = 0; this.result = 0;
this.end = true; this.end = true;
return;
} }
if (!this.end) { if (!this.end) {
this.checkPon(); this.checkPon();
if (this.turn !== 3 && this.canDoAChii(this.turn + 1).length > 0) { this.checkForChii();
let chiis = this.canDoAChii(this.turn + 1);
let i = Math.floor(Math.random() * chiis.length);
this.chii(chiis[i], this.turn + 1);
}
this.canCall = this.canDoAChii().length > 0 || this.canDoAPon(); this.canCall = this.canDoAChii().length > 0 || this.canDoAPon();
} }
} }
} else if (!this.canCall) { // end of his turn }
private checkForChii(): void {
if (this.turn === 3) return;
const nextPlayer = this.turn + 1;
const chiis = this.canDoAChii(nextPlayer);
if (chiis.length > 0) {
const i = Math.floor(Math.random() * chiis.length);
this.chii(chiis[i], nextPlayer);
}
}
private advanceBotTurn(): void {
if (this.turn === 3) { if (this.turn === 3) {
this.turn = 0; this.turn = 0;
this.pick(0); this.pick(0);
@ -252,12 +317,11 @@ export class Game {
} else { } else {
this.turn++; this.turn++;
} }
this.updateWaitingTime(); this.updateWaitingTime();
this.hasPicked = false; this.hasPicked = false;
this.hasPlayed = false; this.hasPlayed = false;
} }
}
}
private pick(player: number): void { private pick(player: number): void {
this.hands[player].push(this.deck.pop()); this.hands[player].push(this.deck.pop());
@ -265,70 +329,84 @@ export class Game {
} }
private discard(player: number, n: number): void { private discard(player: number, n: number): void {
let tile = this.hands[player].eject(n); const tile = this.hands[player].eject(n);
this.hands[player].sort(); this.hands[player].sort();
tile.setTilt(); tile.setTilt();
this.discards[player].push(tile); this.discards[player].push(tile);
this.hands[player].isolate = false; this.hands[player].isolate = false;
this.hands[player].sort(); this.hands[player].sort();
this.lastDiscard = player; this.lastDiscard = player;
this.lastPlayed = Date.now(); this.lastPlayed = Date.now();
} }
private canDoAChii(p: number = 0): Array<number> { private canDoAChii(p: number = 0): Array<number> {
let chii = [] as Array<number>; const chii: number[] = [];
// Check if chii is possible
if ( if (
this.lastDiscard !== undefined && this.lastDiscard === undefined ||
(this.lastDiscard + 1) % 4 === p && (this.lastDiscard + 1) % 4 !== p ||
(this.turn + 1) % 4 === p && (this.turn + 1) % 4 !== p ||
this.discards[this.lastDiscard][this.discards[this.lastDiscard].length-1].getFamily() < 4 this.discards[this.lastDiscard][this.discards[this.lastDiscard].length - 1].getFamily() >= 4
) { ) {
let t = this.discards[this.lastDiscard][this.discards[this.lastDiscard].length-1]; return chii;
let h = this.hands[p];
if (
h.count(t.getFamily(), t.getValue()-2) > 0 &&
h.count(t.getFamily(), t.getValue()-1) > 0
) {
chii.push(t.getValue()-2);
} }
if (
h.count(t.getFamily(), t.getValue()-1) > 0 && const t = this.discards[this.lastDiscard][this.discards[this.lastDiscard].length - 1];
h.count(t.getFamily(), t.getValue()+1) > 0 const h = this.hands[p];
) { const family = t.getFamily();
chii.push(t.getValue()-1); const value = t.getValue();
// Check for three possible chii patterns
if (h.count(family, value - 2) > 0 && h.count(family, value - 1) > 0) {
chii.push(value - 2);
} }
if (
h.count(t.getFamily(), t.getValue()+1) > 0 && if (h.count(family, value - 1) > 0 && h.count(family, value + 1) > 0) {
h.count(t.getFamily(), t.getValue()+2) > 0 chii.push(value - 1);
) {
chii.push(t.getValue());
} }
if (h.count(family, value + 1) > 0 && h.count(family, value + 2) > 0) {
chii.push(value);
} }
return chii; return chii;
} }
private chii(minValue: number, p: number): void { private chii(minValue: number, p: number): void {
let t = this.discards[p === 0 ? 3 : p - 1].pop() as NonNullable<Tile>; const discardPlayer = p === 0 ? 3 : p - 1;
const t = this.discards[discardPlayer].pop() as Tile;
this.lastDiscard = undefined; this.lastDiscard = undefined;
const tt: Tile[] = [];
let tn = 0; let tn = 0;
let v = minValue; let v = minValue;
let tt: Array<Tile> = [];
// Find the right tiles for the chii
while (tn < 2) { while (tn < 2) {
if (v === t.getValue()) { if (v === t.getValue()) {
v++; v++;
} else { } else {
tt[tn] = this.hands[p].find(t.getFamily(), v) as NonNullable<Tile>; tt[tn] = this.hands[p].find(t.getFamily(), v) as Tile;
tn++; tn++;
v++; v++;
} }
} }
[t, tt[0], tt[1]].forEach(t => t.setTilt());
this.groups[p].push(new Group([t, tt[0], tt[1]], p === 0 ? 3 : p - 1, p)); // Set tilt for all tiles in the group
[t, tt[0], tt[1]].forEach(tile => tile.setTilt());
// Create new group
this.groups[p].push(new Group([t, tt[0], tt[1]], discardPlayer, p));
if (this.hasWin(p)) { if (this.hasWin(p)) {
this.end = true; this.end = true;
this.result = p === 0 ? 1 : 2; this.result = p === 0 ? 1 : 2;
} }
this.updateWaitingTime(); this.updateWaitingTime();
this.turn = p; this.turn = p;
this.hasPicked = true; this.hasPicked = true;
@ -336,34 +414,37 @@ export class Game {
} }
private getChii(p: number): Array<Array<Tile>> { private getChii(p: number): Array<Array<Tile>> {
let chiis = [] as Array<Array<Tile>>; const chiis: Array<Array<Tile>> = [];
let numChii = this.canDoAChii(); const numChii = this.canDoAChii();
const discardPlayer = p === 0 ? 3 : p - 1;
let d = this.discards[p === 0 ? 3 : p - 1]; const d = this.discards[discardPlayer];
let t = d[d.length - 1] const t = d[d.length - 1];
for (let i = 0; i < numChii.length; i++) { for (let i = 0; i < numChii.length; i++) {
let v = numChii[i]; const v = numChii[i];
let chii = []; const chii: Tile[] = [];
for (let dv = 0; dv < 3; dv++) { for (let dv = 0; dv < 3; dv++) {
if (v + dv === t.getValue()) { if (v + dv === t.getValue()) {
chii.push(t); chii.push(t);
} else { } else {
let tt = this.hands[p].find(t.getFamily(), v + dv) as NonNullable<Tile>; const tt = this.hands[p].find(t.getFamily(), v + dv) as Tile;
chii.push(tt); chii.push(tt);
this.hands[p].push(tt); this.hands[p].push(tt);
this.hands[p].sort(); this.hands[p].sort();
} }
} }
chiis.push(chii); chiis.push(chii);
} }
return chiis; return chiis;
} }
private checkPon(): boolean { private checkPon(): boolean {
for (var p = 1; p < 4; p++) { for (let p = 1; p < GAME_CONSTANTS.PLAYERS; p++) {
if (this.canDoAPon(p)) { if (this.canDoAPon(p)) {
this.pon(this.lastDiscard as NonNullable<number>, p); this.pon(this.lastDiscard as number, p);
return true; return true;
} }
} }
@ -372,30 +453,34 @@ export class Game {
private canDoAPon(player: number = 0): boolean { private canDoAPon(player: number = 0): boolean {
if ( if (
this.lastDiscard !== undefined && // il y a une défausse this.lastDiscard === undefined ||
this.lastDiscard !== player && // pas sa propre défausse this.lastDiscard === player ||
this.turn !== player && // pas son propre tour this.turn === player ||
!(this.hasPicked && !this.hasPlayed) // pas un joueur en train de jouer (this.hasPicked && !this.hasPlayed)
) { ) {
let t = this.discards[this.lastDiscard][this.discards[this.lastDiscard].length-1];
return this.hands[player].count(t.getFamily(), t.getValue()) >= 2;
} else {
return false; return false;
} }
const t = this.discards[this.lastDiscard][this.discards[this.lastDiscard].length - 1];
return this.hands[player].count(t.getFamily(), t.getValue()) >= 2;
} }
private pon(p: number, thief: number = 0): void { private pon(p: number, thief: number = 0): void {
let t = this.discards[p].pop() as NonNullable<Tile>; const t = this.discards[p].pop() as Tile;
this.lastDiscard = undefined; this.lastDiscard = undefined;
let t2 = this.hands[thief].find(t.getFamily(), t.getValue()) as NonNullable<Tile>;
let t3 = this.hands[thief].find(t.getFamily(), t.getValue()) as NonNullable<Tile>; const t2 = this.hands[thief].find(t.getFamily(), t.getValue()) as Tile;
[t, t2, t3].forEach(t => t.setTilt()); const t3 = this.hands[thief].find(t.getFamily(), t.getValue()) as Tile;
[t, t2, t3].forEach(tile => tile.setTilt());
this.groups[thief].push(new Group([t, t2, t3], p, thief)); this.groups[thief].push(new Group([t, t2, t3], p, thief));
if (this.hasWin(thief)) { if (this.hasWin(thief)) {
this.end = true; this.end = true;
this.result = thief === 0 ? 1 : 2; this.result = thief === 0 ? 1 : 2;
} }
this.updateWaitingTime(); this.updateWaitingTime();
this.turn = thief; this.turn = thief;
this.hasPicked = true; this.hasPicked = true;
@ -403,35 +488,29 @@ export class Game {
} }
private hasWin(p: number): boolean { private hasWin(p: number): boolean {
return this.hands[p].toGroup() !== undefined return this.hands[p].toGroup() !== undefined;
} }
private drawGame(): void { private drawGame(): void {
// update game // Update game state
this.play(); this.play();
// draw winds, discard, riichi etc... // Draw game elements
drawState(this.staticCtx, this.turn); drawState(this.staticCtx, this.turn);
this.drawDiscardSize(); this.drawDiscardSize();
// draw result
this.drawResult(); this.drawResult();
// hands
this.drawHands(); this.drawHands();
this.drawGroups(GAME_CONSTANTS.DISPLAY.GROUP_SIZE);
// groups // Draw discards for all players
this.drawGroups(0.6); for (let i = 0; i < GAME_CONSTANTS.PLAYERS; i++) {
// discards
for (let i = 0; i < 4; i++) {
this.drawDiscard( this.drawDiscard(
i, i,
this.hands[0].get(this.selectedTile) as NonNullable<Tile> this.selectedTile !== undefined ? this.hands[0].get(this.selectedTile) : undefined
); );
} }
// called // Draw UI elements
if (this.chooseChii) { if (this.chooseChii) {
drawChiis(this.staticCtx, this.getChii(0)); drawChiis(this.staticCtx, this.getChii(0));
} else { } else {
@ -446,166 +525,177 @@ export class Game {
} }
} }
private drawHands() { private drawHands(): void {
const pi = 3.141592;
const showHands = false; const showHands = false;
const { HAND_SIZE, HIDDEN_HAND_SIZE } = GAME_CONSTANTS.DISPLAY;
// Draw player's hand
this.hands[0].drawHand( this.hands[0].drawHand(
this.staticCtx, this.staticCtx,
2.5 * 75 * 0.75, 2.5 * 75 * 0.75,
1000 - 150 * this.sizeHand, 1000 - 150 * HAND_SIZE,
5 * this.sizeHand, 5 * HAND_SIZE,
0.75, 0.75,
this.selectedTile, this.selectedTile,
false, false,
0 0
); );
// Draw opponents' hands
this.hands[1].drawHand( this.hands[1].drawHand(
this.staticCtx, this.staticCtx,
1000 - 150 * this.sizeHiddenHand, 1000 - 150 * HIDDEN_HAND_SIZE,
1000 - 75 * 5 * this.sizeHiddenHand, 1000 - 75 * 5 * HIDDEN_HAND_SIZE,
5 * this.sizeHiddenHand, 5 * HIDDEN_HAND_SIZE,
this.sizeHiddenHand, HIDDEN_HAND_SIZE,
undefined, undefined,
!showHands, !showHands,
- pi / 2 -GAME_CONSTANTS.PI / 2
); );
this.hands[2].drawHand( this.hands[2].drawHand(
this.staticCtx, this.staticCtx,
1000 - 75 * 5 * this.sizeHiddenHand, 1000 - 75 * 5 * HIDDEN_HAND_SIZE,
150 * this.sizeHiddenHand, 150 * HIDDEN_HAND_SIZE,
5 * this.sizeHiddenHand, 5 * HIDDEN_HAND_SIZE,
this.sizeHiddenHand, HIDDEN_HAND_SIZE,
undefined, undefined,
!showHands, !showHands,
- pi -GAME_CONSTANTS.PI
); );
this.hands[3].drawHand( this.hands[3].drawHand(
this.staticCtx, this.staticCtx,
150 * this.sizeHiddenHand, 150 * HIDDEN_HAND_SIZE,
75 * 5 * this.sizeHiddenHand, 75 * 5 * HIDDEN_HAND_SIZE,
5 * this.sizeHiddenHand, 5 * HIDDEN_HAND_SIZE,
this.sizeHiddenHand, HIDDEN_HAND_SIZE,
undefined, undefined,
!showHands, !showHands,
pi / 2 GAME_CONSTANTS.PI / 2
); );
} }
private drawGroups( private drawGroups(size: number): void {
size: number, const offset = 25;
): void {
let os = 25; for (let p = 0; p < GAME_CONSTANTS.PLAYERS; p++) {
const pi = 3.141592; const rotation = this.rotations[p];
for ( let p = 0; p < 4; p++) { const groups = this.groups[p];
let rotation = [0, -pi / 2, -pi, pi / 2][p];
if (this.groups[p].length > 0) { if (groups.length > 0) {
for (let i = this.groups[p].length-1; i >= 0; i--) { for (let i = groups.length - 1; i >= 0; i--) {
this.groups[p][i].drawGroup( groups[i].drawGroup(
this.staticCtx, this.staticCtx,
1050 - 240 - (260 + os) * size * i, 1050 - 240 - (260 + offset) * size * i,
1050 - 62, 1050 - 62,
5, 5,
0.6, 0.6,
rotation, rotation,
this.hands[0].get(this.selectedTile) this.selectedTile !== undefined ? this.hands[0].get(this.selectedTile) : undefined
); );
} }
} }
} }
} }
private drawDiscard( private drawDiscard(p: number, highlightedTile: Tile | undefined): void {
p: number, const sizeDiscard = GAME_CONSTANTS.DISPLAY.DISCARD_SIZE;
highlitedTile: Tile|undefined
): void {
const pi = 3.141592;
this.staticCtx.save(); this.staticCtx.save();
this.staticCtx.translate(525, 525); this.staticCtx.translate(525, 525);
this.staticCtx.rotate([0, -pi/2, -pi, pi/2][p]) this.staticCtx.rotate(this.rotations[p]);
let x = - this.sizeDiscard * 475 / 2; const x = -sizeDiscard * 475 / 2;
let y = this.sizeDiscard * (475 / 2 + 5); const y = sizeDiscard * (475 / 2 + 5);
// Draw all discarded tiles
for (let i = 0; i < this.discards[p].length; i++) { for (let i = 0; i < this.discards[p].length; i++) {
let tile = this.discards[p][i]; const tile = this.discards[p][i];
let tx, ty;
if (i < 12) { if (i < 12) {
tile.drawTile( tx = x + (i % 6) * 80 * sizeDiscard;
this.staticCtx, ty = y + Math.floor(i / 6) * 105 * sizeDiscard;
x + (i % 6) * 80 * this.sizeDiscard,
y + Math.floor(i / 6) * 105 * this.sizeDiscard,
this.sizeDiscard,
false,
0,
highlitedTile?.isEqual(tile.getFamily(), tile.getValue())
);
} else { } else {
tx = x + (i - 12) * 80 * sizeDiscard;
ty = y + 2 * 105 * sizeDiscard;
}
tile.drawTile( tile.drawTile(
this.staticCtx, this.staticCtx,
x + (i - 12) * 80 * this.sizeDiscard, tx,
y + 2 * 105 * this.sizeDiscard, ty,
this.sizeDiscard, sizeDiscard,
false, false,
0, 0,
highlitedTile?.isEqual(tile.getFamily(), tile.getValue()) highlightedTile?.isEqual(tile.getFamily(), tile.getValue())
); );
} }
}
// Draw indicator for last discard
if (this.lastDiscard === p) { if (this.lastDiscard === p) {
const j = this.discards[p].length - 1; this.drawLastDiscardIndicator(p);
let tx =
j < 12 ?
x + (j % 6) * 80 * this.sizeDiscard :
x + (j - 12) * 80 * this.sizeDiscard;
let ty =
j < 12 ?
y + Math.floor(j / 6) * 105 * this.sizeDiscard :
y + 2 * 105 * this.sizeDiscard;
tx += 75 / 2 * this.sizeDiscard;
ty += 115 * this.sizeDiscard;
const ts = 10;
this.staticCtx.fillStyle = "#ff0000";
this.staticCtx.beginPath();
this.staticCtx.moveTo(tx, ty);
this.staticCtx.lineTo(tx + ts/2, ty + 0.866 * ts);
this.staticCtx.lineTo(tx - ts/2, ty + 0.866 * ts);
this.staticCtx.lineTo(tx, ty);
this.staticCtx.fill();
this.staticCtx.stroke();
} }
this.staticCtx.restore(); this.staticCtx.restore();
} }
private drawDiscardSize() { private drawLastDiscardIndicator(p: number): void {
this.staticCtx.fillStyle = "#f070f0"; const j = this.discards[p].length - 1;
const sizeDiscard = GAME_CONSTANTS.DISPLAY.DISCARD_SIZE;
const x = -sizeDiscard * 475 / 2;
const y = sizeDiscard * (475 / 2 + 5);
this.staticCtx.font = "40px garamond"; let tx = j < 12
let l = this.deck.length(); ? x + (j % 6) * 80 * sizeDiscard
if (l < 10) { : x + (j - 12) * 80 * sizeDiscard;
this.staticCtx.fillText(l.toString(), 517, 537);
} else { let ty = j < 12
this.staticCtx.fillText(l.toString(), 507, 537); ? y + Math.floor(j / 6) * 105 * sizeDiscard
: y + 2 * 105 * sizeDiscard;
tx += 75 / 2 * sizeDiscard;
ty += 115 * sizeDiscard;
const triangleSize = 10;
this.staticCtx.fillStyle = "#ff0000";
this.staticCtx.beginPath();
this.staticCtx.moveTo(tx, ty);
this.staticCtx.lineTo(tx + triangleSize / 2, ty + 0.866 * triangleSize);
this.staticCtx.lineTo(tx - triangleSize / 2, ty + 0.866 * triangleSize);
this.staticCtx.lineTo(tx, ty);
this.staticCtx.fill();
this.staticCtx.stroke();
} }
private drawDiscardSize(): void {
this.staticCtx.fillStyle = "#f070f0";
this.staticCtx.font = "40px garamond";
const remainingTiles = this.deck.length();
const x = remainingTiles < 10 ? 517 : 507;
this.staticCtx.fillText(remainingTiles.toString(), x, 537);
} }
private drawResult(): void { private drawResult(): void {
if (this.result !== -1) { if (this.result === -1) return;
// Draw result background
this.staticCtx.fillStyle = "#e0e0f0"; this.staticCtx.fillStyle = "#e0e0f0";
this.staticCtx.fillRect(450, 430, 150, 190); this.staticCtx.fillRect(450, 430, 150, 190);
this.staticCtx.fillRect(430, 450, 190, 150); this.staticCtx.fillRect(430, 450, 190, 150);
// Draw result text
this.staticCtx.fillStyle = "#ff0000"; this.staticCtx.fillStyle = "#ff0000";
this.staticCtx.font = "45px garamond"; this.staticCtx.font = "45px garamond";
} if (this.result === 0) {
if (this.result === 0) { // Égalité
this.staticCtx.fillText("Égalité", 450, 535); this.staticCtx.fillText("Égalité", 450, 535);
} else if (this.result === 1) { // victoire } else if (this.result === 1) {
this.staticCtx.fillText("Victoire !", 440, 535); this.staticCtx.fillText("Victoire !", 440, 535);
} else if (this.result === 2) { // Défaite } else if (this.result === 2) {
this.staticCtx.fillText("Défaite...", 440, 535); this.staticCtx.fillText("Défaite...", 440, 535);
} }
} }
@ -614,5 +704,4 @@ export class Game {
await this.deck.preload(); await this.deck.preload();
await Promise.all(this.hands.map(h => h.preload())); await Promise.all(this.hands.map(h => h.preload()));
} }
} }

View file

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

View file

@ -1,198 +1,332 @@
import { Tile } from "./tile" import { Tile } from "./tile";
import { Group } from "./group" 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 { export class Hand {
private tiles: Array<Tile>; private tiles: Array<Tile>;
public isolate: boolean = false; public isolate: boolean = false;
/**
* Create a hand from string representation
* @param stiles String representation of tiles (e.g., "m1p2s3w1d1")
*/
public constructor(stiles: string = "") { public constructor(stiles: string = "") {
this.tiles = []; this.tiles = [];
this.initializeFromString(stiles);
}
/**
* Parse string representation into tiles
*/
private initializeFromString(stiles: string): void {
for (let i = 0; i < stiles.length - 1; i++) { for (let i = 0; i < stiles.length - 1; i++) {
let ss = stiles.substring(i, i+2); const tileCode = stiles.substring(i, i + 2);
if (ss[0] === "m") { const type = tileCode[0];
this.tiles.push(new Tile(1, Number(ss[1]), false)); const value = Number(tileCode[1]);
} else if (ss[0] === "p") {
this.tiles.push(new Tile(2, Number(ss[1]), false)); if (this.isValidTileCode(type, value)) {
} else if (ss[0] === "s") { this.addTileFromCode(type, value);
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 {}
} }
} }
/**
* Check if tile code is valid
*/
private isValidTileCode(type: string, value: number): boolean {
return (
(type === TILE_TYPES.MANZU.code) ||
(type === TILE_TYPES.PINZU.code) ||
(type === TILE_TYPES.SOUZU.code) ||
(type === TILE_TYPES.WINDS.code) ||
(type === TILE_TYPES.DRAGONS.code)
);
}
/**
* Create a tile from type code and value
*/
private addTileFromCode(type: string, value: number): void {
const familyMap: { [key: string]: number } = {
[TILE_TYPES.MANZU.code]: TILE_TYPES.MANZU.family,
[TILE_TYPES.PINZU.code]: TILE_TYPES.PINZU.family,
[TILE_TYPES.SOUZU.code]: TILE_TYPES.SOUZU.family,
[TILE_TYPES.WINDS.code]: TILE_TYPES.WINDS.family,
[TILE_TYPES.DRAGONS.code]: TILE_TYPES.DRAGONS.family
};
const family = familyMap[type];
if (family !== undefined) {
this.tiles.push(new Tile(family, value, false));
}
}
/**
* Get all tiles in hand
*/
public getTiles(): Array<Tile> { public getTiles(): Array<Tile> {
return this.tiles; return this.tiles;
} }
/**
* Get number of tiles in hand
*/
public length(): number { public length(): number {
return this.tiles.length; return this.tiles.length;
} }
/**
* Add a tile to hand
*/
public push(tile: Tile): void { public push(tile: Tile): void {
this.tiles.push(tile); this.tiles.push(tile);
} }
public pop(): Tile|undefined { /**
* Remove and return the last tile
*/
public pop(): Tile | undefined {
return this.tiles.pop(); return this.tiles.pop();
} }
public find(family: number, value: number) :Tile|undefined { /**
let n = undefined; * Find and remove a specific tile by family and value
for (let i = 0; i < this.tiles.length; i++) { */
if (this.tiles[i].getFamily() === family && this.tiles[i].getValue() === value) { public find(family: number, value: number): Tile | undefined {
n = i; const index = this.findTileIndex(family, value);
break;
} if (index !== -1) {
} // Swap with first tile and remove
if (n !== undefined) { [this.tiles[index], this.tiles[0]] = [this.tiles[0], this.tiles[index]];
[this.tiles[n], this.tiles[0]] = [this.tiles[0], this.tiles[n]]; const tile = this.tiles.shift();
let t = this.tiles.shift();
this.sort(); this.sort();
return t; return tile;
} else {
return undefined;
}
} }
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 { public eject(idTile: number): Tile {
[this.tiles[0], this.tiles[idTile]] = [this.tiles[idTile], this.tiles[0]]; if (idTile < 0 || idTile >= this.tiles.length) {
let tile = this.tiles.shift(); throw new Error("Invalid tile index");
this.sort();
return tile as NonNullable<Tile>;
} }
public get(idTile: number|undefined): Tile|undefined { // Swap with first tile and remove
if (idTile !== undefined) { [this.tiles[0], this.tiles[idTile]] = [this.tiles[idTile], this.tiles[0]];
return this.tiles[idTile]; const tile = this.tiles.shift();
} else { 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 undefined;
} }
return this.tiles[idTile];
} }
public sort(): undefined { /**
* Sort tiles in ascending order
*/
public sort(): void {
this.tiles.sort((a, b) => a.isLessThan(b) ? -1 : 1); this.tiles.sort((a, b) => a.isLessThan(b) ? -1 : 1);
} }
/**
* Count tiles with specific family and value
*/
public count(family: number, value: number): number { public count(family: number, value: number): number {
let c = 0; return this.tiles.filter(
this.tiles.forEach( tile => tile.getFamily() === family && tile.getValue() === value
t => { ).length;
if (t.getFamily() === family && t.getValue() === value) {
c++;
}
}
);
return c;
} }
public toGroup(pair: boolean = false): Array<Group>|undefined { /**
if (this.tiles.length > 0) { * Try to form hand into groups (for winning detection)
let t1 = this.tiles.pop() as NonNullable<Tile>; */
public toGroup(pair: boolean = false): Array<Group> | undefined {
let c = this.count(t1.getFamily(), t1.getValue()); if (this.tiles.length === 0) {
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;
}
}
this.tiles.push(t1);
this.tiles.sort();
return undefined;
} else {
return []; 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;
} }
public drawHand ( // 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, ctx: CanvasRenderingContext2D,
x: number, x: number,
y: number, y: number,
offset: number, offset: number,
size: number, size: number,
focusedTiled: number|undefined = undefined, focusedTile: number | undefined = undefined,
hidden: boolean = false, hidden: boolean = false,
rotation: number = 0 rotation: number = 0
): void { ): void {
let v = (75 + offset) * size; const tileOffset = (75 + offset) * size;
let vx = Math.cos(rotation) * v; const offsetX = Math.cos(rotation) * tileOffset;
let vy = Math.sin(rotation) * v; const offsetY = Math.sin(rotation) * tileOffset;
for (let i = 0; i < this.tiles.length; i++) { for (let i = 0; i < this.tiles.length; i++) {
let e = (i === this.tiles.length - 1 && this.isolate) ? 10 : 0; const isLastAndIsolated = (i === this.tiles.length - 1 && this.isolate) ? 10 : 0;
if (i === focusedTiled) {
this.tiles[i].drawTile( // Calculate position
ctx, let tileX = x + i * offsetX + isLastAndIsolated * size * Math.cos(rotation);
x + let tileY = y + i * offsetY + isLastAndIsolated * size * Math.sin(rotation);
i * vx +
25 * size * Math.sin(rotation) + // Add additional offset for focused tile
e * size * Math.cos(rotation), if (i === focusedTile) {
y + tileX += 25 * size * Math.sin(rotation);
i * vy - tileY -= 25 * size * Math.cos(rotation);
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
);
} }
// Draw tile
this.tiles[i].drawTile(
ctx,
tileX,
tileY,
size,
hidden,
rotation
);
} }
} }
/**
* Preload tile images
*/
public async preload(): Promise<void> { public async preload(): Promise<void> {
await Promise.all(this.tiles.map(t => t.preloadImg())); await Promise.all(this.tiles.map(tile => tile.preloadImg()));
} }
/**
* Clean up resources
*/
public cleanup(): void { public cleanup(): void {
this.tiles.forEach(tile => tile.cleanup()); this.tiles.forEach(tile => tile.cleanup());
this.tiles = []; this.tiles = [];

View file

@ -1,24 +1,16 @@
export class Tile { export class Tile {
private family: number; private imgFront: HTMLImageElement = new Image();
private value: number; private imgBack: HTMLImageElement = new Image();
private red: boolean; private imgGray: HTMLImageElement = new Image();
private imgSrc: string; private img: HTMLImageElement = new Image();
private imgFront: HTMLImageElement; private imgSrc: string = "";
private imgBack: HTMLImageElement; private tilt: number = 0;
private imgGray: HTMLImageElement;
private img: HTMLImageElement;
private tilt: number;
public constructor(family: number, value: number , red: boolean) { constructor(
this.family = family; private family: number,
this.value = value; private value: number,
this.red = red; private red: boolean
this.imgSrc = ""; ) {
this.imgFront = new Image();
this.imgBack = new Image();
this.imgGray = new Image();
this.img = new Image();
this.tilt = 0;
this.setImgSrc(); this.setImgSrc();
} }
@ -39,19 +31,21 @@ export class Tile {
} }
public compare(t: Tile): number { public compare(t: Tile): number {
if (this.family < t.family) { // Compare d'abord par famille, puis par valeur
return -1; if (this.family !== t.family) {
} else if (this.family > t.family) { return this.family < t.family ? -1 : 1;
return 1;
} }
if (this.value < t.value) { if (this.value !== t.value) {
return -1; return this.value < t.value ? -1 : 1;
} else if (this.value > t.value) {
return 1;
} }
return 0; return 0;
} }
public isLessThan(t: Tile): boolean {
return this.family < t.family ||
(this.family === t.family && this.value <= t.value);
}
public setTilt(): void { public setTilt(): void {
this.tilt = (1 - 2 * Math.random()) * 0.04; this.tilt = (1 - 2 * Math.random()) * 0.04;
} }
@ -66,109 +60,69 @@ export class Tile {
gray: boolean = false, gray: boolean = false,
tilted: boolean = true tilted: boolean = true
): void { ): 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.save();
ctx.translate(x + (75 * size) / 2, y + (100 * size) / 2); ctx.translate(x + halfWidth, y + halfHeight);
if (tilted) { ctx.rotate(rotation + (tilted ? this.tilt : 0));
ctx.rotate(rotation + this.tilt);
} else { // Position de l'ombre (légèrement décalée)
ctx.rotate(rotation); 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) { if (hidden) {
ctx.drawImage( // ombre // Dessin du dos de la tuile
this.imgGray, ctx.drawImage(this.imgBack, -halfWidth, -halfHeight, tileWidth, tileHeight);
-(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 { } else {
ctx.drawImage( // ombre // Dessin de la tuile face visible
this.imgGray, ctx.drawImage(this.imgFront, -halfWidth, -halfHeight, tileWidth, tileHeight);
-(75 * size * 0.92) / 2,
-(100 * size * 0.91) / 2, // Dessin du motif sur la tuile (légèrement plus petit)
75 * size, const patternScale = 0.9;
100 * size const patternWidth = tileWidth * patternScale;
); const patternHeight = tileHeight * patternScale;
ctx.drawImage( // tuile à vide const patternX = -((75 - 7) * size) / 2;
this.imgFront, const patternY = -((100 - 10) * size) / 2;
-(75 * size) / 2,
-(100 * size) / 2, ctx.drawImage(this.img, patternX, patternY, patternWidth, patternHeight);
75 * size, 100 * size
); // Appliquer un filtre gris si demandé
ctx.drawImage( // le dessin sur la tuile if (gray) {
this.img, ctx.drawImage(this.imgGray, -halfWidth, -halfHeight, tileWidth, tileHeight);
-((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
);
} }
} }
ctx.restore(); ctx.restore();
} }
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 cleanup(): void { public cleanup(): void {
this.imgFront.onload = null; // Supprimer tous les gestionnaires d'événements
this.imgFront.onerror = null; const images = [this.imgFront, this.imgBack, this.imgGray, this.img];
this.imgBack.onload = null; images.forEach(img => {
this.imgBack.onerror = null; img.onload = null;
this.imgGray.onload = null; img.onerror = null;
this.imgGray.onerror = null; });
this.img.onload = null;
this.img.onerror = null;
}
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> { public async preloadImg(): Promise<void> {
await Promise.all([ const imagesToLoad = [
this.loadImg(this.imgFront, "img/Regular/Front.svg"), { img: this.imgFront, src: "img/Regular/Front.svg" },
// this.loadImg(this.imgFront, "/~img/Export/Regular/Front.png"), { img: this.imgBack, src: "img/Regular/Back.svg" },
this.loadImg(this.imgBack, "img/Regular/Back.svg"), { img: this.imgGray, src: "img/Regular/Gray.svg" },
this.loadImg(this.imgGray, "img/Regular/Gray.svg"), { img: this.img, src: this.imgSrc }
this.loadImg(this.img, this.imgSrc) ];
]);
await Promise.all(
imagesToLoad.map(({ img, src }) => this.loadImg(img, src))
);
} }
private loadImg(img: HTMLImageElement, src: string): Promise<void> { private loadImg(img: HTMLImageElement, src: string): Promise<void> {
@ -178,4 +132,25 @@ export class Tile {
img.src = src; 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": { "compilerOptions": {
"target": "es5", "target": "es2015",
"module": "esnext", "module": "esnext",
"strict": true, "strict": true,
} }