optimized every code
This commit is contained in:
parent
15a6341879
commit
e8f38c0cf9
25 changed files with 2657 additions and 1916 deletions
12
build/dp0.js
12
build/dp0.js
File diff suppressed because one or more lines are too long
10
build/dp1.js
10
build/dp1.js
File diff suppressed because one or more lines are too long
10
build/dp2.js
10
build/dp2.js
File diff suppressed because one or more lines are too long
10
build/dp3.js
10
build/dp3.js
File diff suppressed because one or more lines are too long
16
build/dp4.js
16
build/dp4.js
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
420
src/button.ts
420
src/button.ts
|
|
@ -1,11 +1,48 @@
|
|||
import { Tile } from "./tile"
|
||||
import { Tile } from "./tile";
|
||||
|
||||
type button_t = (
|
||||
arg0: CanvasRenderingContext2D,
|
||||
arg1: number,
|
||||
arg2: number
|
||||
/**
|
||||
* Type definition for button rendering functions
|
||||
*/
|
||||
type ButtonRenderer = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Button configuration interface
|
||||
*/
|
||||
interface ButtonConfig {
|
||||
text: string;
|
||||
color: string;
|
||||
action: number;
|
||||
}
|
||||
|
||||
// Button constants
|
||||
const BUTTON_RADIUS = 8;
|
||||
const BUTTON_WIDTH = 110;
|
||||
const BUTTON_HEIGHT = 50;
|
||||
const BUTTON_SPACING = 120;
|
||||
const BUTTON_AREA_Y_MIN = 838;
|
||||
const BUTTON_AREA_Y_MAX = 888;
|
||||
const BUTTON_MARGIN = 10;
|
||||
const BASE_X_POSITION = 850;
|
||||
|
||||
// Button style configurations
|
||||
const BUTTON_STYLES: Record<string, ButtonConfig> = {
|
||||
pass: { text: "Ignorer", color: "#FF9030", action: 0 },
|
||||
chii: { text: "Chii", color: "#FFCC33", action: 1 },
|
||||
pon: { text: "Pon", color: "#FFCC33", action: 2 },
|
||||
kan: { text: "Kan", color: "#FFCC33", action: 3 },
|
||||
ron: { text: "Ron", color: "#FF3060", action: 4 },
|
||||
tsumo: { text: "Tsumo", color: "#FF3060", action: 5 },
|
||||
back: { text: "Retour", color: "#FF9030", action: 0 }
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines which button was clicked based on coordinates
|
||||
* @returns The action value of the clicked button or -1 if no button was clicked
|
||||
*/
|
||||
export function clickAction(
|
||||
x: number,
|
||||
y: number,
|
||||
|
|
@ -15,33 +52,39 @@ export function clickAction(
|
|||
ron: boolean,
|
||||
tsumo: boolean
|
||||
): number {
|
||||
let buttons = [
|
||||
[tsumo, 5],
|
||||
[ron, 4],
|
||||
[kan, 3],
|
||||
[pon, 2],
|
||||
[chii, 1]
|
||||
]
|
||||
if (buttons.some(c => c[0])) {
|
||||
buttons.push([true, 0]);
|
||||
} else {
|
||||
const activeButtons = getActiveButtons(chii, pon, kan, ron, tsumo);
|
||||
|
||||
if (activeButtons.length === 0) {
|
||||
return -1;
|
||||
}
|
||||
let xmin = 960 - buttons.filter(c => c[0]).length * 120;
|
||||
let inside = 838 < y && y < 888;
|
||||
let q = Math.floor((x - xmin) / 120);
|
||||
let r = (x - xmin) - 120 * q;
|
||||
if (
|
||||
q >= 0 &&
|
||||
q < buttons.filter(c => c[0]).length &&
|
||||
r > 10 &&
|
||||
inside
|
||||
) {
|
||||
return buttons.filter(c => c[0])[q][1] as number;
|
||||
|
||||
// Calculate starting X position based on number of buttons
|
||||
const xmin = 960 - activeButtons.length * BUTTON_SPACING;
|
||||
|
||||
// Check if Y coordinate is within button area
|
||||
const isYInButtonArea = BUTTON_AREA_Y_MIN < y && y < BUTTON_AREA_Y_MAX;
|
||||
if (!isYInButtonArea) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Calculate which button was clicked
|
||||
const buttonIndex = Math.floor((x - xmin) / BUTTON_SPACING);
|
||||
const xOffset = (x - xmin) - BUTTON_SPACING * buttonIndex;
|
||||
|
||||
if (
|
||||
buttonIndex >= 0 &&
|
||||
buttonIndex < activeButtons.length &&
|
||||
xOffset > BUTTON_MARGIN
|
||||
) {
|
||||
return activeButtons[buttonIndex];
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw all active buttons on the canvas
|
||||
*/
|
||||
export function drawButtons(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
chii: boolean,
|
||||
|
|
@ -50,228 +93,219 @@ export function drawButtons(
|
|||
ron: boolean,
|
||||
tsumo: boolean
|
||||
): void {
|
||||
let buttons = [
|
||||
[chii, buttonChii],
|
||||
[pon, buttonPon],
|
||||
[kan, buttonKan],
|
||||
[ron, buttonRon],
|
||||
[tsumo, buttonTsumo]
|
||||
]
|
||||
if (buttons.some(c => c[0])) {
|
||||
buttons.unshift([true, buttonPass]);
|
||||
const buttonFunctions: [boolean, ButtonRenderer][] = [
|
||||
[chii, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.chii)],
|
||||
[pon, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.pon)],
|
||||
[kan, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.kan)],
|
||||
[ron, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.ron)],
|
||||
[tsumo, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.tsumo)]
|
||||
];
|
||||
|
||||
// Only show the pass button if at least one other button is active
|
||||
const hasActiveButtons = buttonFunctions.some(([isActive]) => isActive);
|
||||
|
||||
if (hasActiveButtons) {
|
||||
buttonFunctions.unshift([true, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.pass)]);
|
||||
} else {
|
||||
return; // No buttons to draw
|
||||
}
|
||||
let dx = 0;
|
||||
for (let i = 0; i < buttons.length; i++) {
|
||||
if (buttons[i][0]) {
|
||||
(buttons[i][1] as button_t)(
|
||||
|
||||
// Draw active buttons
|
||||
let positionOffset = 0;
|
||||
for (const [isActive, renderFunc] of buttonFunctions) {
|
||||
if (isActive) {
|
||||
renderFunc(
|
||||
ctx,
|
||||
850 - dx * 120,
|
||||
BASE_X_POSITION - positionOffset * BUTTON_SPACING,
|
||||
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(
|
||||
x: number,
|
||||
y: number,
|
||||
chiis: Array<Array<Tile>>
|
||||
): number {
|
||||
let xmin = 960 - (chiis.length + 1) * 120;
|
||||
let inside = 838 < y && y < 888;
|
||||
let q = Math.floor((x - xmin) / 120);
|
||||
let r = (x - xmin) - 120 * q;
|
||||
if (
|
||||
q >= 0 &&
|
||||
q < (chiis.length + 1) &&
|
||||
r > 10 &&
|
||||
inside
|
||||
) {
|
||||
return q === chiis.length ? 0 : chiis[q][0].getValue();
|
||||
if (chiis.length === 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Calculate starting X position based on number of options
|
||||
const xmin = 960 - (chiis.length + 1) * BUTTON_SPACING;
|
||||
|
||||
// Check if Y coordinate is within button area
|
||||
const isYInButtonArea = BUTTON_AREA_Y_MIN < y && y < BUTTON_AREA_Y_MAX;
|
||||
if (!isYInButtonArea) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Calculate which option was clicked
|
||||
const optionIndex = Math.floor((x - xmin) / BUTTON_SPACING);
|
||||
const xOffset = (x - xmin) - BUTTON_SPACING * optionIndex;
|
||||
|
||||
if (
|
||||
optionIndex >= 0 &&
|
||||
optionIndex < (chiis.length + 1) &&
|
||||
xOffset > BUTTON_MARGIN
|
||||
) {
|
||||
// Return 0 for "back" button or the value of the selected Chi option
|
||||
return optionIndex === chiis.length ? 0 : chiis[optionIndex][0].getValue();
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw Chi options on the canvas
|
||||
*/
|
||||
export function drawChiis(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
chiis: Array<Array<Tile>>
|
||||
): void {
|
||||
chiis.reverse();
|
||||
const r = 8;
|
||||
const w = 110;
|
||||
const h = 50;
|
||||
button(ctx, 850, 835, r, w, h, "#FF9030");
|
||||
ctx.fillStyle = "black";
|
||||
ctx.font = "30px garamond";
|
||||
ctx.fillText("Retour", 850 + w * 0.1, 835 + h/2 * 1.3);
|
||||
// Create a copy to avoid modifying the original array
|
||||
const chiiOptions = [...chiis].reverse();
|
||||
|
||||
let dx = 1;
|
||||
for (let i = 0; i < chiis.length; i++) {
|
||||
// Draw "back" button
|
||||
renderButton(ctx, BASE_X_POSITION, 835, BUTTON_STYLES.back);
|
||||
|
||||
// Draw Chi options
|
||||
let positionOffset = 1;
|
||||
for (const tiles of chiiOptions) {
|
||||
drawOneChii(
|
||||
ctx,
|
||||
850 - dx * 120,
|
||||
BASE_X_POSITION - positionOffset * BUTTON_SPACING,
|
||||
835,
|
||||
chiis[i]
|
||||
tiles
|
||||
);
|
||||
dx++;
|
||||
positionOffset++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a single Chi option
|
||||
*/
|
||||
function drawOneChii(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
tiles: Array<Tile>
|
||||
): void {
|
||||
const r = 8;
|
||||
const w = 110;
|
||||
const h = 50;
|
||||
const dx = 32;
|
||||
const x0 = x + 7;
|
||||
const y0 = y + 5;
|
||||
button(ctx, x, y, r, w, h, "#FFCC33");
|
||||
tiles[0].drawTile(
|
||||
const tileOffset = 32;
|
||||
const tileStartX = x + 7;
|
||||
const tileStartY = y + 5;
|
||||
|
||||
// Draw the button background
|
||||
drawButtonShape(ctx, x, y, BUTTON_RADIUS, BUTTON_WIDTH, BUTTON_HEIGHT, BUTTON_STYLES.chii.color);
|
||||
|
||||
// Draw the tiles
|
||||
for (let i = 0; i < tiles.length; i++) {
|
||||
tiles[i].drawTile(
|
||||
ctx,
|
||||
x0,
|
||||
y0,
|
||||
0.4,
|
||||
false,
|
||||
0,
|
||||
false,
|
||||
false
|
||||
);
|
||||
tiles[1].drawTile(
|
||||
ctx,
|
||||
x0 + dx,
|
||||
y0,
|
||||
0.4,
|
||||
false,
|
||||
0,
|
||||
false,
|
||||
false
|
||||
);
|
||||
tiles[2].drawTile(
|
||||
ctx,
|
||||
x0 + 2 * dx,
|
||||
y0,
|
||||
tileStartX + tileOffset * i,
|
||||
tileStartY,
|
||||
0.4,
|
||||
false,
|
||||
0,
|
||||
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,
|
||||
x: number,
|
||||
y: number,
|
||||
r: number,
|
||||
w: number,
|
||||
h: number,
|
||||
config: ButtonConfig
|
||||
): void {
|
||||
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
|
||||
): void {
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
|
||||
ctx.moveTo(x + r, y);
|
||||
ctx.lineTo(x + w - r, y);
|
||||
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
|
||||
ctx.lineTo(x + w, y + h - r);
|
||||
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
|
||||
ctx.lineTo(x + r, y + h);
|
||||
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
|
||||
ctx.lineTo(x, y + r);
|
||||
ctx.quadraticCurveTo(x, y, x + r, y);
|
||||
// Top right corner
|
||||
ctx.moveTo(x + radius, y);
|
||||
ctx.lineTo(x + width - radius, y);
|
||||
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
||||
|
||||
// Bottom right corner
|
||||
ctx.lineTo(x + width, y + height - radius);
|
||||
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
||||
|
||||
// Bottom left corner
|
||||
ctx.lineTo(x + radius, y + height);
|
||||
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
||||
|
||||
// Top left corner
|
||||
ctx.lineTo(x, y + radius);
|
||||
ctx.quadraticCurveTo(x, y, x + radius, y);
|
||||
|
||||
ctx.fill();
|
||||
|
||||
// Add border
|
||||
ctx.fillStyle = "#606060";
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
301
src/deck.ts
301
src/deck.ts
|
|
@ -1,14 +1,21 @@
|
|||
import { Tile } from "./tile"
|
||||
import { Hand } from "./hand"
|
||||
import { Tile } from "./tile";
|
||||
import { Hand } from "./hand";
|
||||
|
||||
type TileKey = `${number}-${number}`;
|
||||
|
||||
export class Deck {
|
||||
private tiles: Array<Tile>;
|
||||
private tiles: Tile[];
|
||||
private tileIndexMap: Map<TileKey, number[]>; // Fast lookup for find() and count()
|
||||
|
||||
public constructor(allowRed: boolean) {
|
||||
public constructor(allowRed: boolean = false) {
|
||||
this.tiles = [];
|
||||
this.tileIndexMap = new Map();
|
||||
this.initTiles(allowRed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays all tile families on the canvas
|
||||
*/
|
||||
public displayFamilies(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
|
|
@ -19,133 +26,267 @@ export class Deck {
|
|||
): void {
|
||||
let posX = x;
|
||||
let posY = y;
|
||||
for (let i = 1; i < 6; i++) {
|
||||
if (i < 4) { // famille
|
||||
for (let j = 1; j < 10; j++) {
|
||||
const tile = this.find(i, j) as NonNullable<Tile>;
|
||||
|
||||
// Define tile layouts for each family
|
||||
const familyLayouts = [
|
||||
{ family: 1, start: 1, end: 9 }, // First suit
|
||||
{ family: 2, start: 1, end: 9 }, // Second suit
|
||||
{ family: 3, start: 1, end: 9 }, // Third suit
|
||||
{ family: 4, start: 1, end: 4 }, // Winds
|
||||
{ family: 5, start: 1, end: 3 } // Dragons
|
||||
];
|
||||
|
||||
for (const layout of familyLayouts) {
|
||||
for (let j = layout.start; j <= layout.end; j++) {
|
||||
const tile = this.find(layout.family, j);
|
||||
if (tile) {
|
||||
tile.drawTile(ctx, posX, posY, size, false, 0, false);
|
||||
posX += (75 + xOffset) * size;
|
||||
}
|
||||
posX = x;
|
||||
posY += (100 + yOffset) * size;
|
||||
} else if (i === 4) { //vent
|
||||
for (let j = 1; j < 5; j++) {
|
||||
const tile = this.find(i, j) as NonNullable<Tile>;
|
||||
tile.drawTile(ctx, posX, posY, size);
|
||||
posX += (75 + xOffset) * size;
|
||||
}
|
||||
posX = x;
|
||||
posY += (100 + yOffset) * size;
|
||||
} else if (i === 5) { //vent
|
||||
for (let j = 1; j < 4; j++) {
|
||||
const tile = this.find(i, j) as NonNullable<Tile>;
|
||||
tile.drawTile(ctx, posX, posY, size);
|
||||
posX += (75 + xOffset) * size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of tiles in the deck
|
||||
*/
|
||||
public length(): number {
|
||||
return this.tiles.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes and returns the last tile from the deck
|
||||
* @throws Error if the deck is empty
|
||||
*/
|
||||
public pop(): Tile {
|
||||
if (this.tiles.length === 0) {
|
||||
}
|
||||
return this.tiles.pop() as NonNullable<Tile>;
|
||||
throw new Error("Cannot pop from an empty deck");
|
||||
}
|
||||
|
||||
public push(tile: Tile) {
|
||||
this.tiles.push(tile);
|
||||
}
|
||||
const tile = this.tiles.pop()!;
|
||||
// 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 {
|
||||
let n = undefined;
|
||||
for (let i = 0; i < this.tiles.length; i++) {
|
||||
if (
|
||||
this.tiles[i].getFamily() === family &&
|
||||
this.tiles[i].getValue() === value
|
||||
) {
|
||||
n = i;
|
||||
}
|
||||
}
|
||||
if (n !== undefined) {
|
||||
[this.tiles[n as NonNullable<number>], this.tiles[0]] = [this.tiles[0], this.tiles[n as NonNullable<number>]];
|
||||
return this.tiles.shift();
|
||||
if (indices && indices.length > 0) {
|
||||
indices.pop(); // Remove the last index
|
||||
if (indices.length === 0) {
|
||||
this.tileIndexMap.delete(key);
|
||||
} else {
|
||||
this.tileIndexMap.set(key, indices);
|
||||
}
|
||||
}
|
||||
|
||||
return tile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a tile to the deck
|
||||
*/
|
||||
public push(tile: Tile): void {
|
||||
const index = this.tiles.length;
|
||||
this.tiles.push(tile);
|
||||
|
||||
// Update the index map
|
||||
const key = this.getTileKey(tile.getFamily(), tile.getValue());
|
||||
const indices = this.tileIndexMap.get(key) || [];
|
||||
indices.push(index);
|
||||
this.tileIndexMap.set(key, indices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and removes a specific tile from the deck
|
||||
*/
|
||||
public find(family: number, value: number): Tile | undefined {
|
||||
const key = this.getTileKey(family, value);
|
||||
const indices = this.tileIndexMap.get(key);
|
||||
|
||||
if (!indices || indices.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Get the first occurrence
|
||||
const index = indices[0];
|
||||
if (index >= this.tiles.length) {
|
||||
// Handle potential out-of-sync errors
|
||||
this.rebuildIndexMap();
|
||||
return this.find(family, value);
|
||||
}
|
||||
|
||||
// Swap with the first element for efficient removal
|
||||
[this.tiles[index], this.tiles[0]] = [this.tiles[0], this.tiles[index]];
|
||||
|
||||
// Update indices in the map
|
||||
this.updateIndicesAfterSwap(0, index);
|
||||
|
||||
// Remove and return the tile
|
||||
const tile = this.tiles.shift();
|
||||
|
||||
// Update all indices after shift
|
||||
this.decrementIndicesAfterShift();
|
||||
|
||||
return tile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of tiles with specific family and value
|
||||
*/
|
||||
public count(family: number, value: number): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i < this.tiles.length; i++) {
|
||||
if (
|
||||
this.tiles[i].getFamily() === family &&
|
||||
this.tiles[i].getValue() === value
|
||||
) {
|
||||
n++;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
const key = this.getTileKey(family, value);
|
||||
const indices = this.tileIndexMap.get(key);
|
||||
return indices ? indices.length : 0;
|
||||
}
|
||||
|
||||
public shuffle(): undefined {
|
||||
let newArray: Array<Tile> = [];
|
||||
while (this.tiles.length > 0) {
|
||||
let n = Math.floor(Math.random() * this.tiles.length);
|
||||
[this.tiles[n], this.tiles[0]] = [this.tiles[0], this.tiles[n]];
|
||||
newArray.push(this.tiles.shift() as NonNullable<Tile>);
|
||||
}
|
||||
this.tiles = newArray;
|
||||
/**
|
||||
* Shuffles the deck using Fisher-Yates algorithm
|
||||
*/
|
||||
public shuffle(): void {
|
||||
// Fisher-Yates shuffle algorithm
|
||||
for (let i = this.tiles.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[this.tiles[i], this.tiles[j]] = [this.tiles[j], this.tiles[i]];
|
||||
}
|
||||
|
||||
// Rebuild the index map after shuffling
|
||||
this.rebuildIndexMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a random hand from the deck
|
||||
*/
|
||||
public getRandomHand(): Hand {
|
||||
let hand = new Hand();
|
||||
const hand = new Hand();
|
||||
this.shuffle();
|
||||
|
||||
// Handle case where deck doesn't have enough tiles
|
||||
if (this.tiles.length < 13) {
|
||||
throw new Error("Not enough tiles in deck to create a hand");
|
||||
}
|
||||
|
||||
for (let i = 0; i < 13; i++) {
|
||||
hand.push(this.pop());
|
||||
}
|
||||
|
||||
return hand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up resources used by tiles and empties the deck
|
||||
*/
|
||||
public cleanup(): void {
|
||||
this.tiles.forEach(tile => tile.cleanup());
|
||||
this.tiles = [];
|
||||
this.tileIndexMap.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Preloads all tile images
|
||||
*/
|
||||
public async preload(): Promise<void> {
|
||||
for (let i = 0; i < this.tiles.length; i++) {
|
||||
await this.tiles[i].preloadImg();
|
||||
const preloadPromises = this.tiles.map(tile => tile.preloadImg());
|
||||
await Promise.all(preloadPromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a unique key for each tile type
|
||||
*/
|
||||
private getTileKey(family: number, value: number): TileKey {
|
||||
return `${family}-${value}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the index map after swapping two tiles
|
||||
*/
|
||||
private updateIndicesAfterSwap(index1: number, index2: number): void {
|
||||
if (index1 === index2) return;
|
||||
|
||||
const tile1 = this.tiles[index1];
|
||||
const tile2 = this.tiles[index2];
|
||||
|
||||
const key1 = this.getTileKey(tile1.getFamily(), tile1.getValue());
|
||||
const key2 = this.getTileKey(tile2.getFamily(), tile2.getValue());
|
||||
|
||||
const indices1 = this.tileIndexMap.get(key1) || [];
|
||||
const indices2 = this.tileIndexMap.get(key2) || [];
|
||||
|
||||
// Update indices
|
||||
const idx1 = indices1.indexOf(index2);
|
||||
const idx2 = indices2.indexOf(index1);
|
||||
|
||||
if (idx1 !== -1) indices1[idx1] = index1;
|
||||
if (idx2 !== -1) indices2[idx2] = index2;
|
||||
|
||||
this.tileIndexMap.set(key1, indices1);
|
||||
this.tileIndexMap.set(key2, indices2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrements all indices after a shift operation
|
||||
*/
|
||||
private decrementIndicesAfterShift(): void {
|
||||
for (const [key, indices] of this.tileIndexMap.entries()) {
|
||||
this.tileIndexMap.set(
|
||||
key,
|
||||
indices
|
||||
.filter(idx => idx !== 0) // Remove the index 0 that was shifted
|
||||
.map(idx => (idx > 0 ? idx - 1 : idx)) // Decrement all indices
|
||||
);
|
||||
|
||||
// Clean up empty entries
|
||||
if (this.tileIndexMap.get(key)?.length === 0) {
|
||||
this.tileIndexMap.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private initTiles(allowRed: boolean): undefined {
|
||||
for (let i = 1; i < 6; i++) {
|
||||
if (i < 4) { // famille
|
||||
for (let j = 1; j < 10; j++) {
|
||||
this.tiles.push(new Tile(i, j, false));
|
||||
this.tiles.push(new Tile(i, j, false));
|
||||
this.tiles.push(new Tile(i, j, false));
|
||||
if (j === 5 && allowRed) {
|
||||
this.tiles.push(new Tile(i, j, true));
|
||||
} else {
|
||||
this.tiles.push(new Tile(i, j, false));
|
||||
/**
|
||||
* Rebuilds the entire index map from scratch
|
||||
*/
|
||||
private rebuildIndexMap(): void {
|
||||
this.tileIndexMap.clear();
|
||||
|
||||
for (let i = 0; i < this.tiles.length; i++) {
|
||||
const tile = this.tiles[i];
|
||||
const key = this.getTileKey(tile.getFamily(), tile.getValue());
|
||||
const indices = this.tileIndexMap.get(key) || [];
|
||||
indices.push(i);
|
||||
this.tileIndexMap.set(key, indices);
|
||||
}
|
||||
}
|
||||
} else if (i === 4) { // vent
|
||||
for (let j = 1; j < 5; j++) {
|
||||
for (let k = 0; k < 4; k++) {
|
||||
this.tiles.push(new Tile(i, j, false));
|
||||
|
||||
/**
|
||||
* Initializes the deck with all tiles
|
||||
*/
|
||||
private initTiles(allowRed: boolean): void {
|
||||
// Create suits (families 1-3)
|
||||
for (let family = 1; family <= 3; family++) {
|
||||
for (let value = 1; value <= 9; value++) {
|
||||
// Each value appears 4 times in a suit
|
||||
const isRedFive = value === 5 && allowRed;
|
||||
|
||||
// Add 3 regular tiles
|
||||
for (let i = 0; i < 3; i++) {
|
||||
this.push(new Tile(family, value, false));
|
||||
}
|
||||
|
||||
// Add the 4th tile (potentially red five)
|
||||
this.push(new Tile(family, value, isRedFive));
|
||||
}
|
||||
}
|
||||
} else if (i === 5) { // dragon
|
||||
for (let j = 1; j < 4; j++) {
|
||||
for (let k = 0; k < 4; k++) {
|
||||
this.tiles.push(new Tile(i, j, false));
|
||||
|
||||
// Create winds (family 4)
|
||||
for (let value = 1; value <= 4; value++) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.push(new Tile(4, value, false));
|
||||
}
|
||||
}
|
||||
|
||||
// Create dragons (family 5)
|
||||
for (let value = 1; value <= 3; value++) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.push(new Tile(5, value, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,144 +1,256 @@
|
|||
import { Deck } from "../deck";
|
||||
import { Hand } from "../hand"
|
||||
import { Hand } from "../hand";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
cleanup: () => void;
|
||||
}
|
||||
}
|
||||
export {}
|
||||
|
||||
const canvas = document.getElementById("myCanvas") as HTMLCanvasElement;
|
||||
export {};
|
||||
|
||||
async function preloadDeck(deck: Deck) {
|
||||
await deck.preload();
|
||||
}
|
||||
class RiichiDisplay {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
private offScreenCanvas: HTMLCanvasElement;
|
||||
private offScreenCtx: CanvasRenderingContext2D;
|
||||
|
||||
async function display () {
|
||||
if (canvas) {
|
||||
const ctx = canvas.getContext("2d") as NonNullable<CanvasRenderingContext2D>;
|
||||
private deck: Deck;
|
||||
private hands: Hand[] = [];
|
||||
private edeck: Deck;
|
||||
private ehand: Hand;
|
||||
|
||||
if (ctx) {
|
||||
// double buffering
|
||||
const offScreenCanvas = document.createElement('canvas') as HTMLCanvasElement;
|
||||
offScreenCanvas.width = canvas.width;
|
||||
offScreenCanvas.height = canvas.height;
|
||||
const offScreenCtx = offScreenCanvas.getContext('2d') as NonNullable<CanvasRenderingContext2D>;
|
||||
private selectedTile: number | undefined = undefined;
|
||||
private animationFrameId: number | null = null;
|
||||
private isDirty: boolean = true;
|
||||
|
||||
//animation parameter
|
||||
let lastTime = 0;
|
||||
const FPS = 30;
|
||||
const interval = 1000 / FPS;
|
||||
// Constants
|
||||
private readonly FPS: number = 30;
|
||||
private readonly INTERVAL: number = 1000 / this.FPS;
|
||||
private readonly X: number = 100;
|
||||
private readonly Y: number = 150;
|
||||
private readonly OS: number = 75;
|
||||
private readonly SIZE: number = 0.75;
|
||||
private readonly TILE_WIDTH: number = 80 * this.SIZE;
|
||||
private readonly MAX_TILES: number = 14;
|
||||
|
||||
// tuiles
|
||||
let x = 100;
|
||||
let y = 150;
|
||||
let os = 75;
|
||||
let size = 0.75;
|
||||
const deck = new Deck(false);
|
||||
await preloadDeck(deck);
|
||||
// Cache for mouse hit detection
|
||||
private tileRects: Array<{x: number, y: number, width: number, height: number}> = [];
|
||||
|
||||
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++) {
|
||||
const hand = deck.getRandomHand();
|
||||
const hand = this.deck.getRandomHand();
|
||||
hand.sort();
|
||||
hands.push(hand);
|
||||
this.hands.push(hand);
|
||||
}
|
||||
|
||||
// interactive hand
|
||||
const edeck = new Deck(false);
|
||||
await edeck.preload();
|
||||
const ehand = edeck.getRandomHand();
|
||||
ehand.push(edeck.pop());
|
||||
ehand.sort();
|
||||
// Initialize interactive hand
|
||||
this.ehand = this.edeck.getRandomHand();
|
||||
this.ehand.push(this.edeck.pop());
|
||||
this.ehand.sort();
|
||||
|
||||
let selectedTile: number|undefined = undefined;
|
||||
// Initial draw
|
||||
this.drawCanvas();
|
||||
|
||||
// function to draw
|
||||
const drawCanvas = async () => {
|
||||
// clean screeen
|
||||
offScreenCtx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// tapis
|
||||
offScreenCtx.fillStyle = "#007730";
|
||||
offScreenCtx.fillRect(50, 50, 1000, 1000);
|
||||
|
||||
// texte
|
||||
offScreenCtx.fillStyle = "#DFDFFF";
|
||||
offScreenCtx.font = "50px serif";
|
||||
offScreenCtx.fillText("Exemples de main:", 65, 100);
|
||||
|
||||
// example hands
|
||||
for (let i = 0; i < hands.length; i++) {
|
||||
hands[i].drawHand(offScreenCtx, x, y + i * size * (100 + os), 5, size);
|
||||
// Start animation loop
|
||||
this.startAnimationLoop();
|
||||
}
|
||||
|
||||
// dynamic hand
|
||||
ehand.isolate = true;
|
||||
ehand.drawHand(offScreenCtx, x, 800, 5, size, selectedTile);
|
||||
private drawCanvas(): void {
|
||||
// Only redraw if something changed (dirty flag)
|
||||
if (!this.isDirty) return;
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(offScreenCanvas, 0, 0);
|
||||
const ctx = this.offScreenCtx;
|
||||
|
||||
// Clear canvas
|
||||
ctx.clearRect(0, 0, this.offScreenCanvas.width, this.offScreenCanvas.height);
|
||||
|
||||
// Draw background
|
||||
ctx.fillStyle = "#007730";
|
||||
ctx.fillRect(50, 50, 1000, 1000);
|
||||
|
||||
// Draw title
|
||||
ctx.fillStyle = "#DFDFFF";
|
||||
ctx.font = "50px serif";
|
||||
ctx.fillText("Exemples de main:", 65, 100);
|
||||
|
||||
// Draw example hands
|
||||
for (let i = 0; i < this.hands.length; i++) {
|
||||
this.hands[i].drawHand(
|
||||
ctx,
|
||||
this.X,
|
||||
this.Y + i * this.SIZE * (100 + this.OS),
|
||||
5,
|
||||
this.SIZE
|
||||
);
|
||||
}
|
||||
|
||||
// Draw interactive hand
|
||||
this.ehand.isolate = true;
|
||||
this.ehand.drawHand(
|
||||
ctx,
|
||||
this.X,
|
||||
800,
|
||||
5,
|
||||
this.SIZE,
|
||||
this.selectedTile
|
||||
);
|
||||
|
||||
// Flip double buffer
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.ctx.drawImage(this.offScreenCanvas, 0, 0);
|
||||
|
||||
// Reset dirty flag
|
||||
this.isDirty = false;
|
||||
}
|
||||
|
||||
private startAnimationLoop(): void {
|
||||
let lastTime = 0;
|
||||
|
||||
const animationLoop = (currentTime: number) => {
|
||||
const deltaTime = currentTime - lastTime;
|
||||
|
||||
if (deltaTime >= interval) {
|
||||
lastTime = currentTime;
|
||||
drawCanvas();
|
||||
}
|
||||
requestAnimationFrame(animationLoop);
|
||||
if (deltaTime >= this.INTERVAL) {
|
||||
lastTime = currentTime - (deltaTime % this.INTERVAL);
|
||||
this.drawCanvas();
|
||||
}
|
||||
|
||||
// mouse event
|
||||
canvas.addEventListener(
|
||||
"mousemove",
|
||||
(event) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mouseX = event.clientX - rect.left - x;
|
||||
const mouseY = event.clientY - rect.top;
|
||||
this.animationFrameId = requestAnimationFrame(animationLoop);
|
||||
};
|
||||
|
||||
let q = Math.floor(mouseX / (80 * size));
|
||||
let r = mouseX - q * 80 * size;
|
||||
if (r <= 75 && q >= 0 && q < 14 && mouseY >= 800 && mouseY <= 800 + 100*size) {
|
||||
selectedTile = q;
|
||||
} else {
|
||||
selectedTile = undefined;
|
||||
}
|
||||
}
|
||||
);
|
||||
canvas.addEventListener(
|
||||
"mousedown",
|
||||
(event) => {
|
||||
if (selectedTile !== undefined) {
|
||||
edeck.push(ehand.eject(selectedTile));
|
||||
edeck.shuffle();
|
||||
ehand.sort();
|
||||
ehand.push(edeck.pop());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
requestAnimationFrame(animationLoop);
|
||||
|
||||
window.cleanup = () => {
|
||||
deck.cleanup();
|
||||
hands.forEach(hand => hand.cleanup());
|
||||
hands = [];
|
||||
edeck.cleanup();
|
||||
ehand.cleanup();
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
offScreenCtx.clearRect(0, 0, offScreenCanvas.width, offScreenCanvas.height);
|
||||
selectedTile = undefined;
|
||||
this.animationFrameId = requestAnimationFrame(animationLoop);
|
||||
}
|
||||
|
||||
} else {
|
||||
console.error("Impossible d'obtenir le contexte du canvas.");
|
||||
public cleanup(): void {
|
||||
// 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();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Deck } from "../deck";
|
||||
import { Hand } from "../hand";
|
||||
import { Group } from "../group"
|
||||
import { Group } from "../group";
|
||||
|
||||
// Configuration globale
|
||||
const CANVAS_ID = "myCanvas";
|
||||
|
|
@ -9,174 +9,363 @@ const BG_RECT = { x: 50, y: 50, w: 1000, h: 1000 };
|
|||
const FPS = 30;
|
||||
const FRAME_INTERVAL = 1000 / FPS;
|
||||
|
||||
// variables globales
|
||||
const DECKS: Array<Deck> = [];
|
||||
const HANDS: Array<Hand> = [];
|
||||
let selectedTile: number|undefined = undefined;
|
||||
/**
|
||||
* Classe RiichiDisplay responsable de la gestion de l'affichage du jeu Riichi Mahjong
|
||||
*/
|
||||
class RiichiDisplay {
|
||||
// Canvas principal et contexte
|
||||
private canvas: HTMLCanvasElement;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
|
||||
// Optimisation des références
|
||||
let animationFrameId: number;
|
||||
let lastFrameTime = 0;
|
||||
const callbacks: Array<() => void> = [];
|
||||
// Canvas pour le double-buffering
|
||||
private staticCanvas: HTMLCanvasElement;
|
||||
private staticCtx: CanvasRenderingContext2D;
|
||||
|
||||
// Pré-calcul des dimensions
|
||||
const canvas = document.getElementById(CANVAS_ID) as HTMLCanvasElement;
|
||||
const ctx = canvas.getContext("2d") as NonNullable<CanvasRenderingContext2D>;
|
||||
// Ressources du jeu
|
||||
private decks: Array<Deck> = [];
|
||||
private hands: Array<Hand> = [];
|
||||
|
||||
// Cache statique
|
||||
const staticCanvas = document.createElement('canvas') as HTMLCanvasElement;
|
||||
const staticCtx = staticCanvas.getContext("2d") as NonNullable<CanvasRenderingContext2D>;
|
||||
staticCanvas.width = canvas.width;
|
||||
staticCanvas.height = canvas.height;
|
||||
// État de l'interface
|
||||
private selectedTile: number | undefined = undefined;
|
||||
private isDirty: boolean = true;
|
||||
|
||||
// Pré-rendu du fond
|
||||
function prerenderBackground() {
|
||||
staticCtx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
staticCtx.fillStyle = BG_COLOR;
|
||||
staticCtx.fillRect(BG_RECT.x, BG_RECT.y, BG_RECT.w, BG_RECT.h);
|
||||
};
|
||||
// Animation et événements
|
||||
private animationFrameId: number | null = null;
|
||||
private lastFrameTime: number = 0;
|
||||
private cleanupCallbacks: Array<() => void> = [];
|
||||
|
||||
function drawFrame() {
|
||||
if (!ctx) return;
|
||||
// Constantes pour le rendu
|
||||
private readonly BASE_X: number = 300;
|
||||
private readonly BASE_Y: number = 150;
|
||||
private readonly X_OFFSET: number = 250;
|
||||
private readonly Y_OFFSET: number = 100;
|
||||
private readonly INTERACTIVE_X: number = 100;
|
||||
private readonly INTERACTIVE_Y: number = 800;
|
||||
private readonly SIZE: number = 0.75;
|
||||
private readonly TILE_WIDTH: number = 80 * this.SIZE;
|
||||
private readonly MAX_TILES: number = 14;
|
||||
|
||||
// Effacement intelligent (uniquement la zone nécessaire)
|
||||
prerenderBackground();
|
||||
// Zones de détection pour l'interaction souris
|
||||
private tileRects: Array<{ x: number, y: number, width: number, height: number }> = [];
|
||||
|
||||
// Ici viendrait le dessin des éléments dynamiques
|
||||
// Par exemple:
|
||||
// drawDeck();
|
||||
// drawHands();
|
||||
let x = 300;
|
||||
let y = 150;
|
||||
let xos = 250;
|
||||
let yos = 100;
|
||||
let size = 0.75;
|
||||
|
||||
staticCtx.fillStyle = "#DFDFFF";
|
||||
staticCtx.font = "50px serif";
|
||||
|
||||
staticCtx.fillText("Chii:", 75, y + 100 * size - 5);
|
||||
HANDS[0].drawHand(staticCtx, x, y, 5, 0.75); // chii
|
||||
HANDS[1].drawHand(staticCtx, x + (75+xos)*size, y, 5, 0.75);
|
||||
|
||||
staticCtx.fillText("Pon:", 75, y + (100+yos)*size + 100 * size - 5);
|
||||
HANDS[2].drawHand(staticCtx, x, y + (100+yos)*size, 5, 0.75); // pon
|
||||
HANDS[3].drawHand(staticCtx, x + (75+xos)*size, y + (100+yos)*size, 5, 0.75);
|
||||
HANDS[4].drawHand(staticCtx, x + 2*(75+xos)*size, y + 2*(100+yos)*size, 5, 0.75);
|
||||
|
||||
staticCtx.fillText("Invalide:", 75, y + 2*(100+yos)*size + 100 * size - 5);
|
||||
HANDS[5].drawHand(staticCtx, x, y + 2*(100+yos)*size, 5, 0.75); // wrong
|
||||
HANDS[6].drawHand(staticCtx, x + (75+xos)*size, y + 2*(100+yos)*size, 5, 0.75);
|
||||
HANDS[7].drawHand(staticCtx, x + 2*(75+xos)*size, y + 2*(100+yos)*size, 5, 0.75);
|
||||
|
||||
HANDS[8].isolate = true;
|
||||
HANDS[8].drawHand(staticCtx, 100, 800, 5, size, selectedTile);
|
||||
|
||||
let groups = HANDS[8].toGroup();
|
||||
if (groups !== undefined) {
|
||||
staticCtx.fillStyle = "#FF0000";
|
||||
staticCtx.font = "50px serif";
|
||||
staticCtx.fillText("Tous les groupes sont formés !", 100, 750);
|
||||
constructor(canvasId: string = CANVAS_ID) {
|
||||
// Initialisation du canvas
|
||||
const canvas = document.getElementById(canvasId) as HTMLCanvasElement;
|
||||
if (!canvas) {
|
||||
throw new Error(`Canvas avec ID '${canvasId}' introuvable`);
|
||||
}
|
||||
groups = [];
|
||||
this.canvas = canvas;
|
||||
|
||||
// Dessin du cache statique
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(staticCanvas, 0, 0);
|
||||
}
|
||||
// Récupération du contexte 2D
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) {
|
||||
throw new Error("Impossible d'obtenir le contexte 2D du canvas");
|
||||
}
|
||||
this.ctx = ctx;
|
||||
|
||||
function animationLoop(currentTime: number) {
|
||||
animationFrameId = requestAnimationFrame(animationLoop);
|
||||
// Initialisation du canvas pour le double-buffering
|
||||
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;
|
||||
|
||||
lastFrameTime = currentTime - (deltaTime % FRAME_INTERVAL);
|
||||
drawFrame();
|
||||
}
|
||||
|
||||
function initEventListeners() {
|
||||
const handlers = {
|
||||
mousemove: (e: MouseEvent) => {
|
||||
let size = 0.75;
|
||||
let x = 100;
|
||||
// Logique de gestion du mouvement de la souris
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mouseX = e.clientX - rect.left - x;
|
||||
const mouseY = e.clientY - rect.top;
|
||||
|
||||
let q = Math.floor(mouseX / (80 * size));
|
||||
let r = mouseX - q * 80 * size;
|
||||
if (r <= 75 && q >= 0 && q < 14 && mouseY >= 800 && mouseY <= 800 + 100*size) {
|
||||
selectedTile = q;
|
||||
} else {
|
||||
selectedTile = undefined;
|
||||
}
|
||||
},
|
||||
mousedown: (e: MouseEvent) => {
|
||||
// Logique de gestion du clic de souris
|
||||
if (selectedTile !== undefined) {
|
||||
DECKS[0].push(HANDS[8].eject(selectedTile));
|
||||
DECKS[0].shuffle();
|
||||
HANDS[8].sort();
|
||||
HANDS[8].push(DECKS[0].pop());
|
||||
}
|
||||
}
|
||||
this.lastFrameTime = currentTime - (deltaTime % FRAME_INTERVAL);
|
||||
this.drawFrame();
|
||||
};
|
||||
|
||||
callbacks.push(() => {
|
||||
canvas.removeEventListener('mousemove', handlers.mousemove);
|
||||
canvas.removeEventListener('mousedown', handlers.mousedown);
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousemove', handlers.mousemove);
|
||||
canvas.addEventListener('mousedown', handlers.mousedown);
|
||||
}
|
||||
|
||||
async function preloadDeck(deck: Deck) {
|
||||
await deck.preload();
|
||||
}
|
||||
async function preloadHand(hand: Hand) {
|
||||
await hand.preload();
|
||||
}
|
||||
|
||||
export function cleanup() {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
callbacks.forEach(fn => fn());
|
||||
}
|
||||
|
||||
export async function initDisplay() {
|
||||
if (!ctx) {
|
||||
console.error("Context canvas indisponible");
|
||||
return;
|
||||
this.animationFrameId = requestAnimationFrame(animationLoop);
|
||||
}
|
||||
|
||||
// Préchargement des ressources si nécessaire
|
||||
// const deck = new Deck();
|
||||
// await preloadDeck(deck);
|
||||
DECKS.push(
|
||||
new Deck(false)
|
||||
);
|
||||
HANDS.push(
|
||||
new Hand("s1s2s3"),
|
||||
new Hand("m2m3m4"),
|
||||
new Hand("p5p5p5"),
|
||||
new Hand("w2w2w2"),
|
||||
new Hand("d3d3d3"),
|
||||
new Hand("s4p5m6"),
|
||||
new Hand("m9s9p9"),
|
||||
new Hand("d1d2d3"),
|
||||
DECKS[0].getRandomHand()
|
||||
);
|
||||
await Promise.all(DECKS.map(d => preloadDeck(d)));
|
||||
await Promise.all(HANDS.map(h => preloadHand(h)));
|
||||
/**
|
||||
* Initialise les écouteurs d'événements
|
||||
*/
|
||||
private initEventListeners(): void {
|
||||
const mouseMoveHandler = this.handleMouseMove.bind(this);
|
||||
const mouseDownHandler = this.handleMouseDown.bind(this);
|
||||
|
||||
HANDS[8].push(DECKS[0].pop());
|
||||
HANDS[8].sort();
|
||||
this.canvas.addEventListener('mousemove', mouseMoveHandler);
|
||||
this.canvas.addEventListener('mousedown', mouseDownHandler);
|
||||
|
||||
initEventListeners();
|
||||
requestAnimationFrame(animationLoop);
|
||||
// Enregistrer les callbacks de nettoyage
|
||||
this.cleanupCallbacks.push(() => {
|
||||
this.canvas.removeEventListener('mousemove', mouseMoveHandler);
|
||||
this.canvas.removeEventListener('mousedown', mouseDownHandler);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère le mouvement de la souris
|
||||
*/
|
||||
private handleMouseMove(e: MouseEvent): void {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const mouseX = e.clientX - rect.left;
|
||||
const mouseY = e.clientY - rect.top;
|
||||
|
||||
// Sauvegarde de l'état précédent pour détecter les changements
|
||||
const previousSelectedTile = this.selectedTile;
|
||||
this.selectedTile = undefined;
|
||||
|
||||
// Détection optimisée avec zones pré-calculées
|
||||
for (let i = 0; i < this.tileRects.length; i++) {
|
||||
const tileRect = this.tileRects[i];
|
||||
if (
|
||||
mouseX >= tileRect.x &&
|
||||
mouseX <= tileRect.x + tileRect.width &&
|
||||
mouseY >= tileRect.y &&
|
||||
mouseY <= tileRect.y + tileRect.height
|
||||
) {
|
||||
this.selectedTile = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Marquer comme "dirty" uniquement si la sélection a changé
|
||||
if (previousSelectedTile !== this.selectedTile) {
|
||||
this.isDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère le clic de souris
|
||||
*/
|
||||
private handleMouseDown(): void {
|
||||
if (this.selectedTile !== undefined) {
|
||||
// Exécuter l'action pour la tuile sélectionnée
|
||||
this.decks[0].push(this.hands[8].eject(this.selectedTile));
|
||||
this.decks[0].shuffle();
|
||||
this.hands[8].sort();
|
||||
this.hands[8].push(this.decks[0].pop());
|
||||
|
||||
// Marquer comme "dirty" pour forcer le redessinage
|
||||
this.isDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Précharge un deck
|
||||
*/
|
||||
private async preloadDeck(deck: Deck): Promise<void> {
|
||||
await deck.preload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Précharge une main
|
||||
*/
|
||||
private async preloadHand(hand: Hand): Promise<void> {
|
||||
await hand.preload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise l'affichage et démarre l'application
|
||||
*/
|
||||
public async initialize(): Promise<void> {
|
||||
try {
|
||||
// Création du deck principal
|
||||
this.decks.push(new Deck(false));
|
||||
|
||||
// Création des mains prédéfinies
|
||||
this.hands.push(
|
||||
new Hand("s1s2s3"), // Chii 1
|
||||
new Hand("m2m3m4"), // Chii 2
|
||||
new Hand("p5p5p5"), // Pon 1
|
||||
new Hand("w2w2w2"), // Pon 2
|
||||
new Hand("d3d3d3"), // Pon supplémentaire
|
||||
new Hand("s4p5m6"), // Invalide 1
|
||||
new Hand("m9s9p9"), // Invalide 2
|
||||
new Hand("d1d2d3"), // Invalide 3
|
||||
this.decks[0].getRandomHand() // Main interactive
|
||||
);
|
||||
|
||||
// Préchargement parallèle des ressources
|
||||
await Promise.all([
|
||||
...this.decks.map(deck => this.preloadDeck(deck)),
|
||||
...this.hands.map(hand => this.preloadHand(hand))
|
||||
]);
|
||||
|
||||
// Configuration de la main interactive
|
||||
this.hands[8].push(this.decks[0].pop());
|
||||
this.hands[8].sort();
|
||||
|
||||
// Initialisation des écouteurs d'événements
|
||||
this.initEventListeners();
|
||||
|
||||
// Premier rendu
|
||||
this.isDirty = true;
|
||||
this.drawFrame();
|
||||
|
||||
// Démarrage de la boucle d'animation
|
||||
this.startAnimationLoop();
|
||||
|
||||
} catch (error) {
|
||||
console.error("Erreur d'initialisation:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Nettoie les ressources
|
||||
*/
|
||||
public cleanup(): void {
|
||||
// Arrêter la boucle d'animation
|
||||
if (this.animationFrameId !== null) {
|
||||
cancelAnimationFrame(this.animationFrameId);
|
||||
this.animationFrameId = null;
|
||||
}
|
||||
|
||||
// Exécuter tous les callbacks de nettoyage
|
||||
this.cleanupCallbacks.forEach(callback => callback());
|
||||
this.cleanupCallbacks = [];
|
||||
|
||||
// Nettoyer les ressources des decks et des mains
|
||||
this.decks.forEach(deck => deck.cleanup());
|
||||
this.hands.forEach(hand => hand.cleanup());
|
||||
|
||||
// Vider les collections
|
||||
this.decks = [];
|
||||
this.hands = [];
|
||||
|
||||
// Effacer les canvas
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.staticCtx.clearRect(0, 0, this.staticCanvas.width, this.staticCanvas.height);
|
||||
}
|
||||
}
|
||||
|
||||
// Instance globale pour le nettoyage
|
||||
let displayInstance: RiichiDisplay | null = null;
|
||||
|
||||
/**
|
||||
* Fonction de nettoyage exportée
|
||||
*/
|
||||
export function cleanup(): void {
|
||||
if (displayInstance) {
|
||||
displayInstance.cleanup();
|
||||
displayInstance = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fonction d'initialisation exportée
|
||||
*/
|
||||
export async function initDisplay(): Promise<void> {
|
||||
try {
|
||||
displayInstance = new RiichiDisplay(CANVAS_ID);
|
||||
await displayInstance.initialize();
|
||||
window.cleanup = cleanup;
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de l'initialisation:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Déclaration globale pour TypeScript
|
||||
|
|
@ -190,4 +379,3 @@ declare global {
|
|||
if (typeof window !== 'undefined') {
|
||||
initDisplay().catch(console.error);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,111 +1,243 @@
|
|||
import { Deck } from "../deck";
|
||||
import { Hand } from "../hand";
|
||||
import { Game } from "../game"
|
||||
import { Game } from "../game";
|
||||
|
||||
// Configuration globale
|
||||
const CANVAS_ID = "myCanvas";
|
||||
const BG_RECT = { x: 0, y: 0, w: 1050, h: 1050 };
|
||||
var MOUSE = { x: 0, y: 0 };
|
||||
const FPS = 60;
|
||||
const FRAME_INTERVAL = 1000 / FPS;
|
||||
class RiichiGameManager {
|
||||
// Configuration globale
|
||||
private readonly CANVAS_ID: string = "myCanvas";
|
||||
private readonly BG_RECT = { x: 0, y: 0, w: 1050, h: 1050 };
|
||||
private readonly FPS: number = 60;
|
||||
private readonly FRAME_INTERVAL: number = 1000 / this.FPS;
|
||||
|
||||
// variables globales
|
||||
const DECKS: Array<Deck> = [];
|
||||
const HANDS: Array<Hand> = [];
|
||||
var GAME: Game|undefined;
|
||||
// Singleton instance
|
||||
private static instance: RiichiGameManager;
|
||||
|
||||
// Optimisation des références
|
||||
let animationFrameId: number;
|
||||
let lastFrameTime = 0;
|
||||
const callbacks: Array<() => void> = [];
|
||||
// Canvas et contextes
|
||||
private canvas: HTMLCanvasElement;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
private staticCanvas: HTMLCanvasElement;
|
||||
private staticCtx: CanvasRenderingContext2D;
|
||||
|
||||
// Pré-calcul des dimensions
|
||||
const canvas = document.getElementById(CANVAS_ID) as HTMLCanvasElement;
|
||||
const ctx = canvas.getContext("2d") as NonNullable<CanvasRenderingContext2D>;
|
||||
canvas.width = BG_RECT.w;
|
||||
canvas.height = BG_RECT.h;
|
||||
// État du jeu
|
||||
private mouse = { x: 0, y: 0 };
|
||||
private game: Game | null = null;
|
||||
private decks: Array<Deck> = [];
|
||||
private hands: Array<Hand> = [];
|
||||
|
||||
// Cache statique
|
||||
const staticCanvas = document.createElement('canvas') as HTMLCanvasElement;
|
||||
const staticCtx = staticCanvas.getContext("2d") as NonNullable<CanvasRenderingContext2D>;
|
||||
staticCanvas.width = canvas.width;
|
||||
staticCanvas.height = canvas.height;
|
||||
// Animation et boucle de jeu
|
||||
private animationFrameId: number | null = null;
|
||||
private lastFrameTime: number = 0;
|
||||
private isInitialized: boolean = false;
|
||||
|
||||
function drawFrame() {
|
||||
if (!ctx) return;
|
||||
GAME?.draw(MOUSE);
|
||||
}
|
||||
// Nettoyage
|
||||
private cleanupCallbacks: Array<() => void> = [];
|
||||
|
||||
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;
|
||||
if (deltaTime < FRAME_INTERVAL) return;
|
||||
// Récupération du contexte 2D
|
||||
const ctx = canvas.getContext("2d", { alpha: false });
|
||||
if (!ctx) {
|
||||
throw new Error("Impossible d'obtenir le contexte 2D du canvas");
|
||||
}
|
||||
this.ctx = ctx;
|
||||
|
||||
lastFrameTime = currentTime - (deltaTime % FRAME_INTERVAL);
|
||||
drawFrame();
|
||||
}
|
||||
// Initialisation du canvas pour le double-buffering
|
||||
this.staticCanvas = document.createElement('canvas');
|
||||
this.staticCanvas.width = canvas.width;
|
||||
this.staticCanvas.height = canvas.height;
|
||||
|
||||
function initEventListeners() {
|
||||
const handlers = {
|
||||
mousedown: (e: MouseEvent) => {
|
||||
GAME?.click(e);
|
||||
},
|
||||
mousemove: (e: MouseEvent) => {
|
||||
MOUSE.x = e.x;
|
||||
MOUSE.y = e.y;
|
||||
const staticCtx = this.staticCanvas.getContext("2d", { alpha: false });
|
||||
if (!staticCtx) {
|
||||
throw new Error("Impossible d'obtenir le contexte du canvas statique");
|
||||
}
|
||||
this.staticCtx = staticCtx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accès à l'instance Singleton
|
||||
*/
|
||||
public static getInstance(): RiichiGameManager {
|
||||
if (!RiichiGameManager.instance) {
|
||||
RiichiGameManager.instance = new RiichiGameManager();
|
||||
}
|
||||
return RiichiGameManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dessine une frame du jeu
|
||||
*/
|
||||
private drawFrame(): void {
|
||||
if (this.game) {
|
||||
this.game.draw(this.mouse);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Démarre la boucle d'animation du jeu
|
||||
*/
|
||||
private startAnimationLoop(): void {
|
||||
const animationLoop = (currentTime: number) => {
|
||||
this.animationFrameId = requestAnimationFrame(animationLoop);
|
||||
|
||||
const deltaTime = currentTime - this.lastFrameTime;
|
||||
if (deltaTime < this.FRAME_INTERVAL) return;
|
||||
|
||||
// Correction du time drift
|
||||
this.lastFrameTime = currentTime - (deltaTime % this.FRAME_INTERVAL);
|
||||
|
||||
// Rendu d'une frame
|
||||
this.drawFrame();
|
||||
};
|
||||
|
||||
this.animationFrameId = requestAnimationFrame(animationLoop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise les écouteurs d'événements
|
||||
*/
|
||||
private initEventListeners(): void {
|
||||
// Handler pour le déplacement de la souris
|
||||
const mouseMoveHandler = (e: MouseEvent) => {
|
||||
// Calcul des coordonnées relatives au canvas
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
this.mouse.x = e.clientX - rect.left;
|
||||
this.mouse.y = e.clientY - rect.top;
|
||||
};
|
||||
|
||||
// Handler pour le clic de souris
|
||||
const mouseDownHandler = (e: MouseEvent) => {
|
||||
if (this.game) {
|
||||
this.game.click(e);
|
||||
}
|
||||
};
|
||||
|
||||
callbacks.push(() => {
|
||||
canvas.removeEventListener('mousemove', handlers.mousemove);
|
||||
canvas.removeEventListener('mousedown', handlers.mousedown);
|
||||
// Enregistrement des écouteurs
|
||||
this.canvas.addEventListener('mousemove', mouseMoveHandler);
|
||||
this.canvas.addEventListener('mousedown', mouseDownHandler);
|
||||
|
||||
// Enregistrement des callbacks de nettoyage
|
||||
this.cleanupCallbacks.push(() => {
|
||||
this.canvas.removeEventListener('mousemove', mouseMoveHandler);
|
||||
this.canvas.removeEventListener('mousedown', mouseDownHandler);
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousemove', handlers.mousemove);
|
||||
canvas.addEventListener('mousedown', handlers.mousedown);
|
||||
}
|
||||
|
||||
async function preloadDeck(deck: Deck) {
|
||||
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
|
||||
// const deck = new Deck();
|
||||
// await preloadDeck(deck);
|
||||
DECKS.push(
|
||||
);
|
||||
HANDS.push(
|
||||
);
|
||||
GAME = new Game(
|
||||
ctx,
|
||||
canvas,
|
||||
staticCtx,
|
||||
staticCanvas
|
||||
);
|
||||
await Promise.all(DECKS.map(d => preloadDeck(d)));
|
||||
await Promise.all(HANDS.map(h => preloadHand(h)));
|
||||
await GAME?.preload();
|
||||
/**
|
||||
* Précharge un deck
|
||||
*/
|
||||
private async preloadDeck(deck: Deck): Promise<void> {
|
||||
await deck.preload();
|
||||
}
|
||||
|
||||
console.log("Loaded completed\n");
|
||||
initEventListeners();
|
||||
requestAnimationFrame(animationLoop);
|
||||
window.cleanup = cleanup;
|
||||
/**
|
||||
* Précharge une main
|
||||
*/
|
||||
private async preloadHand(hand: Hand): Promise<void> {
|
||||
await hand.preload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise le jeu
|
||||
*/
|
||||
public async initialize(): Promise<void> {
|
||||
if (this.isInitialized) return;
|
||||
|
||||
try {
|
||||
console.log("Chargement en cours...");
|
||||
|
||||
// Création du jeu
|
||||
this.game = new Game(
|
||||
this.ctx,
|
||||
this.canvas,
|
||||
this.staticCtx,
|
||||
this.staticCanvas
|
||||
);
|
||||
|
||||
// Préchargement parallèle des ressources
|
||||
await Promise.all([
|
||||
...this.decks.map(deck => this.preloadDeck(deck)),
|
||||
...this.hands.map(hand => this.preloadHand(hand)),
|
||||
this.game.preload()
|
||||
]);
|
||||
|
||||
console.log("Chargement terminé");
|
||||
|
||||
// Initialisation des écouteurs d'événements
|
||||
this.initEventListeners();
|
||||
|
||||
// Démarrage de la boucle d'animation
|
||||
this.startAnimationLoop();
|
||||
|
||||
// Marquer comme initialisé
|
||||
this.isInitialized = true;
|
||||
|
||||
// Définir la fonction de nettoyage global
|
||||
window.cleanup = this.cleanup.bind(this);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de l'initialisation:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Nettoie les ressources
|
||||
*/
|
||||
public cleanup(): void {
|
||||
// Arrêter la boucle d'animation
|
||||
if (this.animationFrameId !== null) {
|
||||
cancelAnimationFrame(this.animationFrameId);
|
||||
this.animationFrameId = null;
|
||||
}
|
||||
|
||||
// Exécuter tous les callbacks de nettoyage
|
||||
this.cleanupCallbacks.forEach(callback => callback());
|
||||
this.cleanupCallbacks = [];
|
||||
|
||||
// Nettoyer les ressources du jeu
|
||||
if (this.game) {
|
||||
// Supposons que Game a une méthode cleanup
|
||||
(this.game as any).cleanup?.();
|
||||
this.game = null;
|
||||
}
|
||||
|
||||
// Nettoyer les ressources des decks et des mains
|
||||
this.decks.forEach(deck => deck.cleanup());
|
||||
this.hands.forEach(hand => hand.cleanup());
|
||||
|
||||
// Vider les collections
|
||||
this.decks = [];
|
||||
this.hands = [];
|
||||
|
||||
// Réinitialiser l'état
|
||||
this.isInitialized = false;
|
||||
this.lastFrameTime = 0;
|
||||
this.mouse = { x: 0, y: 0 };
|
||||
|
||||
// Effacer les canvas
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.staticCtx.clearRect(0, 0, this.staticCanvas.width, this.staticCanvas.height);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
@ -119,4 +251,3 @@ declare global {
|
|||
if (typeof window !== 'undefined') {
|
||||
initDisplay().catch(console.error);
|
||||
}
|
||||
|
||||
|
|
|
|||
609
src/game.ts
609
src/game.ts
|
|
@ -5,23 +5,40 @@ import { Group } from "./group";
|
|||
import { drawButtons, clickAction, drawChiis, clickChii } from "./button";
|
||||
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 {
|
||||
private deck: Deck;
|
||||
private deadWall: Array<Tile> = [];
|
||||
private hands: Array<Hand> = [];
|
||||
private discards: Array<Array<Tile>>;
|
||||
private lastDiscard: number|undefined;
|
||||
private groups: Array<Array<Group>>;
|
||||
private discards: Array<Array<Tile>> = [];
|
||||
private lastDiscard: number | undefined;
|
||||
private groups: Array<Array<Group>> = [];
|
||||
|
||||
// game values
|
||||
// Game state
|
||||
private level: number;
|
||||
private turn = 0;
|
||||
private minWaitingTime = 500;
|
||||
private maxWaitingTime = 2000;
|
||||
private waitingTime = 700;
|
||||
private selectedTile: number|undefined = undefined;
|
||||
private waitingTime = GAME_CONSTANTS.WAITING_TIME.DEFAULT;
|
||||
private selectedTile: number | undefined = undefined;
|
||||
private canCall: boolean = false;
|
||||
private hasPicked: boolean = false;
|
||||
private hasPlayed: boolean = false;
|
||||
|
|
@ -30,19 +47,17 @@ export class Game {
|
|||
private end: boolean = false;
|
||||
private result: number = -1;
|
||||
|
||||
// display parameter
|
||||
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
|
||||
// Canvas elements
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
private cv: HTMLCanvasElement;
|
||||
private staticCtx: CanvasRenderingContext2D;
|
||||
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,
|
||||
cv: HTMLCanvasElement,
|
||||
staticCtx: CanvasRenderingContext2D,
|
||||
|
|
@ -55,25 +70,37 @@ export class Game {
|
|||
this.staticCtx = staticCtx;
|
||||
this.staticCv = staticCv;
|
||||
this.level = level;
|
||||
|
||||
// Initialize game elements
|
||||
this.deck = new Deck(red);
|
||||
this.initializeGame();
|
||||
}
|
||||
|
||||
private initializeGame(): void {
|
||||
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());
|
||||
}
|
||||
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.pick(0);
|
||||
}
|
||||
|
||||
public draw(mp: mousePos) {
|
||||
// background
|
||||
public draw(mp: MousePos): void {
|
||||
// Clear static canvas once per frame
|
||||
this.staticCtx.clearRect(0, 0, this.cv.width, this.cv.height);
|
||||
|
||||
// Draw background
|
||||
this.staticCtx.fillStyle = this.BG_RECT.color;
|
||||
this.staticCtx.fillRect(
|
||||
this.BG_RECT.x,
|
||||
|
|
@ -83,8 +110,9 @@ export class Game {
|
|||
);
|
||||
|
||||
this.getSelected(mp);
|
||||
|
||||
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.drawImage(this.staticCv, 0, 0);
|
||||
}
|
||||
|
|
@ -101,31 +129,23 @@ export class Game {
|
|||
return this.end;
|
||||
}
|
||||
|
||||
public click( // player is playing
|
||||
mp: mousePos,
|
||||
): void {
|
||||
public click(mp: MousePos): void {
|
||||
const rect = this.cv.getBoundingClientRect();
|
||||
|
||||
if (this.hasWin(0)) {
|
||||
this.end = true;
|
||||
this.result = 1;
|
||||
} else if (this.chooseChii) { // is choosing
|
||||
let allChii = this.getChii(0);
|
||||
let c = clickChii(
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.chooseChii) {
|
||||
this.handleChiiSelection(mp, rect);
|
||||
return;
|
||||
}
|
||||
|
||||
const action = clickAction(
|
||||
mp.x - rect.x,
|
||||
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.canDoAPon(),
|
||||
false && this.level > 1,
|
||||
|
|
@ -133,115 +153,160 @@ export class Game {
|
|||
false && this.level > 0
|
||||
);
|
||||
|
||||
if (this.canCall && action !== -1) { // can call
|
||||
if (action === 0) { // pass
|
||||
this.canCall = false;
|
||||
if (this.turn === 3) {
|
||||
this.turn = 0;
|
||||
this.pick(0);
|
||||
} else {
|
||||
this.turn++;
|
||||
if (this.canCall && action !== -1) {
|
||||
this.handleCallAction(action);
|
||||
} else if (this.turn === 0 && this.selectedTile !== undefined) {
|
||||
this.handlePlayerDiscard();
|
||||
}
|
||||
this.updateWaitingTime();
|
||||
this.hasPicked = false;
|
||||
this.hasPlayed = false;
|
||||
} else if (action === 1) { // chii
|
||||
let chiis = this.canDoAChii();
|
||||
if (chiis.length === 1) { // only one possible
|
||||
}
|
||||
|
||||
private handleChiiSelection(mp: MousePos, rect: DOMRect): void {
|
||||
const allChii = this.getChii(0);
|
||||
const selection = clickChii(
|
||||
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);
|
||||
} else {
|
||||
this.chooseChii = true;
|
||||
this.drawGame();
|
||||
}
|
||||
} else if (action == 2) { // pon
|
||||
} else if (action === 2) { // Pon
|
||||
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()) {
|
||||
let chiis = this.canDoAChii(1);
|
||||
const chiis = this.canDoAChii(1);
|
||||
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);
|
||||
} else {
|
||||
this.advanceTurn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private advanceTurn(): void {
|
||||
this.updateWaitingTime();
|
||||
this.turn = (this.turn + 1) % 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.turn = (this.turn + 1) % GAME_CONSTANTS.PLAYERS;
|
||||
this.hasPicked = false;
|
||||
this.hasPlayed = false;
|
||||
}
|
||||
|
||||
private getSelected(
|
||||
mp: mousePos,
|
||||
): void {
|
||||
private getSelected(mp: MousePos): void {
|
||||
const rect = this.cv.getBoundingClientRect();
|
||||
let x = 2.5 * 75 * 0.75;
|
||||
let y = 1050 - 250 * 0.6;
|
||||
const x = 2.5 * 75 * 0.75;
|
||||
const y = 1050 - 250 * 0.6;
|
||||
const sizeHand = GAME_CONSTANTS.DISPLAY.HAND_SIZE;
|
||||
|
||||
const mouseX = mp.x - rect.left - x;
|
||||
const mouseY = mp.y - rect.top;
|
||||
const s = 83.9;
|
||||
const mouseX = mp.x - x;
|
||||
const mouseY = mp.y;
|
||||
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 (
|
||||
r <= (s - 3) * this.sizeHand &&
|
||||
q >= 0 &&
|
||||
q < this.hands[0].length() &&
|
||||
relativeX <= (tileWidth - 3) * sizeHand &&
|
||||
tileIndex >= 0 &&
|
||||
tileIndex < this.hands[0].length() &&
|
||||
mouseY >= y &&
|
||||
mouseY <= y + 100 * this.sizeHand
|
||||
mouseY <= y + 100 * sizeHand
|
||||
) {
|
||||
this.selectedTile = q;
|
||||
this.selectedTile = tileIndex;
|
||||
} else {
|
||||
this.selectedTile = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private updateWaitingTime(): void {
|
||||
this.waitingTime =
|
||||
Math.floor(
|
||||
this.waitingTime = Math.floor(
|
||||
Math.random() *
|
||||
(this.maxWaitingTime - this.minWaitingTime) +
|
||||
this.minWaitingTime
|
||||
(GAME_CONSTANTS.WAITING_TIME.MAX - GAME_CONSTANTS.WAITING_TIME.MIN) +
|
||||
GAME_CONSTANTS.WAITING_TIME.MIN
|
||||
);
|
||||
}
|
||||
|
||||
private play(): void {
|
||||
if (
|
||||
this.turn !== 0 && !this.end
|
||||
) { // bot playing
|
||||
if (!this.hasPicked) { // begin of his turn
|
||||
if (this.turn !== 0 && !this.end) {
|
||||
if (!this.hasPicked) {
|
||||
// Begin of turn
|
||||
this.lastPlayed = Date.now();
|
||||
this.pick(this.turn);
|
||||
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)) {
|
||||
this.end = true;
|
||||
this.result = 2;
|
||||
} else if (Date.now() - this.lastPlayed > this.waitingTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Date.now() - this.lastPlayed > this.waitingTime) {
|
||||
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.hasPlayed = true;
|
||||
|
||||
if (this.deck.length() <= 0) {
|
||||
this.result = 0;
|
||||
this.end = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.end) {
|
||||
this.checkPon();
|
||||
if (this.turn !== 3 && this.canDoAChii(this.turn + 1).length > 0) {
|
||||
let chiis = this.canDoAChii(this.turn + 1);
|
||||
let i = Math.floor(Math.random() * chiis.length);
|
||||
this.chii(chiis[i], this.turn + 1);
|
||||
}
|
||||
this.checkForChii();
|
||||
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) {
|
||||
this.turn = 0;
|
||||
this.pick(0);
|
||||
|
|
@ -252,12 +317,11 @@ export class Game {
|
|||
} else {
|
||||
this.turn++;
|
||||
}
|
||||
|
||||
this.updateWaitingTime();
|
||||
this.hasPicked = false;
|
||||
this.hasPlayed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private pick(player: number): void {
|
||||
this.hands[player].push(this.deck.pop());
|
||||
|
|
@ -265,70 +329,84 @@ export class Game {
|
|||
}
|
||||
|
||||
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();
|
||||
|
||||
tile.setTilt();
|
||||
this.discards[player].push(tile);
|
||||
|
||||
this.hands[player].isolate = false;
|
||||
this.hands[player].sort();
|
||||
|
||||
this.lastDiscard = player;
|
||||
this.lastPlayed = Date.now();
|
||||
}
|
||||
|
||||
private canDoAChii(p: number = 0): Array<number> {
|
||||
let chii = [] as Array<number>;
|
||||
const chii: number[] = [];
|
||||
|
||||
// Check if chii is possible
|
||||
if (
|
||||
this.lastDiscard !== undefined &&
|
||||
(this.lastDiscard + 1) % 4 === p &&
|
||||
(this.turn + 1) % 4 === p &&
|
||||
this.discards[this.lastDiscard][this.discards[this.lastDiscard].length-1].getFamily() < 4
|
||||
this.lastDiscard === undefined ||
|
||||
(this.lastDiscard + 1) % 4 !== p ||
|
||||
(this.turn + 1) % 4 !== p ||
|
||||
this.discards[this.lastDiscard][this.discards[this.lastDiscard].length - 1].getFamily() >= 4
|
||||
) {
|
||||
let t = this.discards[this.lastDiscard][this.discards[this.lastDiscard].length-1];
|
||||
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);
|
||||
return chii;
|
||||
}
|
||||
if (
|
||||
h.count(t.getFamily(), t.getValue()-1) > 0 &&
|
||||
h.count(t.getFamily(), t.getValue()+1) > 0
|
||||
) {
|
||||
chii.push(t.getValue()-1);
|
||||
|
||||
const t = this.discards[this.lastDiscard][this.discards[this.lastDiscard].length - 1];
|
||||
const h = this.hands[p];
|
||||
const family = t.getFamily();
|
||||
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 &&
|
||||
h.count(t.getFamily(), t.getValue()+2) > 0
|
||||
) {
|
||||
chii.push(t.getValue());
|
||||
|
||||
if (h.count(family, value - 1) > 0 && h.count(family, value + 1) > 0) {
|
||||
chii.push(value - 1);
|
||||
}
|
||||
|
||||
if (h.count(family, value + 1) > 0 && h.count(family, value + 2) > 0) {
|
||||
chii.push(value);
|
||||
}
|
||||
|
||||
return chii;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const tt: Tile[] = [];
|
||||
let tn = 0;
|
||||
let v = minValue;
|
||||
let tt: Array<Tile> = [];
|
||||
|
||||
// Find the right tiles for the chii
|
||||
while (tn < 2) {
|
||||
if (v === t.getValue()) {
|
||||
v++;
|
||||
} 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++;
|
||||
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)) {
|
||||
this.end = true;
|
||||
this.result = p === 0 ? 1 : 2;
|
||||
}
|
||||
|
||||
this.updateWaitingTime();
|
||||
this.turn = p;
|
||||
this.hasPicked = true;
|
||||
|
|
@ -336,34 +414,37 @@ export class Game {
|
|||
}
|
||||
|
||||
private getChii(p: number): Array<Array<Tile>> {
|
||||
let chiis = [] as Array<Array<Tile>>;
|
||||
let numChii = this.canDoAChii();
|
||||
|
||||
let d = this.discards[p === 0 ? 3 : p - 1];
|
||||
let t = d[d.length - 1]
|
||||
const chiis: Array<Array<Tile>> = [];
|
||||
const numChii = this.canDoAChii();
|
||||
const discardPlayer = p === 0 ? 3 : p - 1;
|
||||
const d = this.discards[discardPlayer];
|
||||
const t = d[d.length - 1];
|
||||
|
||||
for (let i = 0; i < numChii.length; i++) {
|
||||
let v = numChii[i];
|
||||
let chii = [];
|
||||
const v = numChii[i];
|
||||
const chii: Tile[] = [];
|
||||
|
||||
for (let dv = 0; dv < 3; dv++) {
|
||||
if (v + dv === t.getValue()) {
|
||||
chii.push(t);
|
||||
} 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);
|
||||
this.hands[p].push(tt);
|
||||
this.hands[p].sort();
|
||||
}
|
||||
}
|
||||
|
||||
chiis.push(chii);
|
||||
}
|
||||
|
||||
return chiis;
|
||||
}
|
||||
|
||||
private checkPon(): boolean {
|
||||
for (var p = 1; p < 4; p++) {
|
||||
for (let p = 1; p < GAME_CONSTANTS.PLAYERS; p++) {
|
||||
if (this.canDoAPon(p)) {
|
||||
this.pon(this.lastDiscard as NonNullable<number>, p);
|
||||
this.pon(this.lastDiscard as number, p);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -372,30 +453,34 @@ export class Game {
|
|||
|
||||
private canDoAPon(player: number = 0): boolean {
|
||||
if (
|
||||
this.lastDiscard !== undefined && // il y a une défausse
|
||||
this.lastDiscard !== player && // pas sa propre défausse
|
||||
this.turn !== player && // pas son propre tour
|
||||
!(this.hasPicked && !this.hasPlayed) // pas un joueur en train de jouer
|
||||
this.lastDiscard === undefined ||
|
||||
this.lastDiscard === player ||
|
||||
this.turn === player ||
|
||||
(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;
|
||||
}
|
||||
|
||||
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 {
|
||||
let t = this.discards[p].pop() as NonNullable<Tile>;
|
||||
const t = this.discards[p].pop() as Tile;
|
||||
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>;
|
||||
[t, t2, t3].forEach(t => t.setTilt());
|
||||
|
||||
const t2 = this.hands[thief].find(t.getFamily(), t.getValue()) as Tile;
|
||||
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));
|
||||
|
||||
if (this.hasWin(thief)) {
|
||||
this.end = true;
|
||||
this.result = thief === 0 ? 1 : 2;
|
||||
}
|
||||
|
||||
this.updateWaitingTime();
|
||||
this.turn = thief;
|
||||
this.hasPicked = true;
|
||||
|
|
@ -403,35 +488,29 @@ export class Game {
|
|||
}
|
||||
|
||||
private hasWin(p: number): boolean {
|
||||
return this.hands[p].toGroup() !== undefined
|
||||
return this.hands[p].toGroup() !== undefined;
|
||||
}
|
||||
|
||||
private drawGame(): void {
|
||||
// update game
|
||||
// Update game state
|
||||
this.play();
|
||||
|
||||
// draw winds, discard, riichi etc...
|
||||
// Draw game elements
|
||||
drawState(this.staticCtx, this.turn);
|
||||
this.drawDiscardSize();
|
||||
|
||||
// draw result
|
||||
this.drawResult();
|
||||
|
||||
// hands
|
||||
this.drawHands();
|
||||
this.drawGroups(GAME_CONSTANTS.DISPLAY.GROUP_SIZE);
|
||||
|
||||
// groups
|
||||
this.drawGroups(0.6);
|
||||
|
||||
// discards
|
||||
for (let i = 0; i < 4; i++) {
|
||||
// Draw discards for all players
|
||||
for (let i = 0; i < GAME_CONSTANTS.PLAYERS; i++) {
|
||||
this.drawDiscard(
|
||||
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) {
|
||||
drawChiis(this.staticCtx, this.getChii(0));
|
||||
} else {
|
||||
|
|
@ -446,166 +525,177 @@ export class Game {
|
|||
}
|
||||
}
|
||||
|
||||
private drawHands() {
|
||||
const pi = 3.141592;
|
||||
private drawHands(): void {
|
||||
const showHands = false;
|
||||
const { HAND_SIZE, HIDDEN_HAND_SIZE } = GAME_CONSTANTS.DISPLAY;
|
||||
|
||||
// Draw player's hand
|
||||
this.hands[0].drawHand(
|
||||
this.staticCtx,
|
||||
2.5 * 75 * 0.75,
|
||||
1000 - 150 * this.sizeHand,
|
||||
5 * this.sizeHand,
|
||||
1000 - 150 * HAND_SIZE,
|
||||
5 * HAND_SIZE,
|
||||
0.75,
|
||||
this.selectedTile,
|
||||
false,
|
||||
0
|
||||
);
|
||||
|
||||
// Draw opponents' hands
|
||||
this.hands[1].drawHand(
|
||||
this.staticCtx,
|
||||
1000 - 150 * this.sizeHiddenHand,
|
||||
1000 - 75 * 5 * this.sizeHiddenHand,
|
||||
5 * this.sizeHiddenHand,
|
||||
this.sizeHiddenHand,
|
||||
1000 - 150 * HIDDEN_HAND_SIZE,
|
||||
1000 - 75 * 5 * HIDDEN_HAND_SIZE,
|
||||
5 * HIDDEN_HAND_SIZE,
|
||||
HIDDEN_HAND_SIZE,
|
||||
undefined,
|
||||
!showHands,
|
||||
- pi / 2
|
||||
-GAME_CONSTANTS.PI / 2
|
||||
);
|
||||
|
||||
this.hands[2].drawHand(
|
||||
this.staticCtx,
|
||||
1000 - 75 * 5 * this.sizeHiddenHand,
|
||||
150 * this.sizeHiddenHand,
|
||||
5 * this.sizeHiddenHand,
|
||||
this.sizeHiddenHand,
|
||||
1000 - 75 * 5 * HIDDEN_HAND_SIZE,
|
||||
150 * HIDDEN_HAND_SIZE,
|
||||
5 * HIDDEN_HAND_SIZE,
|
||||
HIDDEN_HAND_SIZE,
|
||||
undefined,
|
||||
!showHands,
|
||||
- pi
|
||||
-GAME_CONSTANTS.PI
|
||||
);
|
||||
|
||||
this.hands[3].drawHand(
|
||||
this.staticCtx,
|
||||
150 * this.sizeHiddenHand,
|
||||
75 * 5 * this.sizeHiddenHand,
|
||||
5 * this.sizeHiddenHand,
|
||||
this.sizeHiddenHand,
|
||||
150 * HIDDEN_HAND_SIZE,
|
||||
75 * 5 * HIDDEN_HAND_SIZE,
|
||||
5 * HIDDEN_HAND_SIZE,
|
||||
HIDDEN_HAND_SIZE,
|
||||
undefined,
|
||||
!showHands,
|
||||
pi / 2
|
||||
GAME_CONSTANTS.PI / 2
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
private drawGroups(
|
||||
size: number,
|
||||
): void {
|
||||
let os = 25;
|
||||
const pi = 3.141592;
|
||||
for ( let p = 0; p < 4; p++) {
|
||||
let rotation = [0, -pi / 2, -pi, pi / 2][p];
|
||||
if (this.groups[p].length > 0) {
|
||||
for (let i = this.groups[p].length-1; i >= 0; i--) {
|
||||
this.groups[p][i].drawGroup(
|
||||
private drawGroups(size: number): void {
|
||||
const offset = 25;
|
||||
|
||||
for (let p = 0; p < GAME_CONSTANTS.PLAYERS; p++) {
|
||||
const rotation = this.rotations[p];
|
||||
const groups = this.groups[p];
|
||||
|
||||
if (groups.length > 0) {
|
||||
for (let i = groups.length - 1; i >= 0; i--) {
|
||||
groups[i].drawGroup(
|
||||
this.staticCtx,
|
||||
1050 - 240 - (260 + os) * size * i,
|
||||
1050 - 240 - (260 + offset) * size * i,
|
||||
1050 - 62,
|
||||
5,
|
||||
0.6,
|
||||
rotation,
|
||||
this.hands[0].get(this.selectedTile)
|
||||
this.selectedTile !== undefined ? this.hands[0].get(this.selectedTile) : undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private drawDiscard(
|
||||
p: number,
|
||||
highlitedTile: Tile|undefined
|
||||
): void {
|
||||
const pi = 3.141592;
|
||||
private drawDiscard(p: number, highlightedTile: Tile | undefined): void {
|
||||
const sizeDiscard = GAME_CONSTANTS.DISPLAY.DISCARD_SIZE;
|
||||
|
||||
this.staticCtx.save();
|
||||
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;
|
||||
let y = this.sizeDiscard * (475 / 2 + 5);
|
||||
const x = -sizeDiscard * 475 / 2;
|
||||
const y = sizeDiscard * (475 / 2 + 5);
|
||||
|
||||
// Draw all discarded tiles
|
||||
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) {
|
||||
tile.drawTile(
|
||||
this.staticCtx,
|
||||
x + (i % 6) * 80 * this.sizeDiscard,
|
||||
y + Math.floor(i / 6) * 105 * this.sizeDiscard,
|
||||
this.sizeDiscard,
|
||||
false,
|
||||
0,
|
||||
highlitedTile?.isEqual(tile.getFamily(), tile.getValue())
|
||||
);
|
||||
tx = x + (i % 6) * 80 * sizeDiscard;
|
||||
ty = y + Math.floor(i / 6) * 105 * sizeDiscard;
|
||||
} else {
|
||||
tx = x + (i - 12) * 80 * sizeDiscard;
|
||||
ty = y + 2 * 105 * sizeDiscard;
|
||||
}
|
||||
|
||||
tile.drawTile(
|
||||
this.staticCtx,
|
||||
x + (i - 12) * 80 * this.sizeDiscard,
|
||||
y + 2 * 105 * this.sizeDiscard,
|
||||
this.sizeDiscard,
|
||||
tx,
|
||||
ty,
|
||||
sizeDiscard,
|
||||
false,
|
||||
0,
|
||||
highlitedTile?.isEqual(tile.getFamily(), tile.getValue())
|
||||
highlightedTile?.isEqual(tile.getFamily(), tile.getValue())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw indicator for last discard
|
||||
if (this.lastDiscard === p) {
|
||||
const j = this.discards[p].length - 1;
|
||||
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.drawLastDiscardIndicator(p);
|
||||
}
|
||||
|
||||
this.staticCtx.restore();
|
||||
}
|
||||
|
||||
private drawDiscardSize() {
|
||||
this.staticCtx.fillStyle = "#f070f0";
|
||||
private drawLastDiscardIndicator(p: number): void {
|
||||
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 l = this.deck.length();
|
||||
if (l < 10) {
|
||||
this.staticCtx.fillText(l.toString(), 517, 537);
|
||||
} else {
|
||||
this.staticCtx.fillText(l.toString(), 507, 537);
|
||||
let tx = j < 12
|
||||
? x + (j % 6) * 80 * sizeDiscard
|
||||
: x + (j - 12) * 80 * sizeDiscard;
|
||||
|
||||
let ty = j < 12
|
||||
? 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 {
|
||||
if (this.result !== -1) {
|
||||
if (this.result === -1) return;
|
||||
|
||||
// Draw result background
|
||||
this.staticCtx.fillStyle = "#e0e0f0";
|
||||
this.staticCtx.fillRect(450, 430, 150, 190);
|
||||
this.staticCtx.fillRect(430, 450, 190, 150);
|
||||
|
||||
// Draw result text
|
||||
this.staticCtx.fillStyle = "#ff0000";
|
||||
this.staticCtx.font = "45px garamond";
|
||||
|
||||
}
|
||||
if (this.result === 0) { // Égalité
|
||||
if (this.result === 0) {
|
||||
this.staticCtx.fillText("Égalité", 450, 535);
|
||||
} else if (this.result === 1) { // victoire
|
||||
} else if (this.result === 1) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -614,5 +704,4 @@ export class Game {
|
|||
await this.deck.preload();
|
||||
await Promise.all(this.hands.map(h => h.preload()));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
153
src/group.ts
153
src/group.ts
|
|
@ -1,25 +1,17 @@
|
|||
import { Tile } from "./tile"
|
||||
import { Tile } from "./tile";
|
||||
|
||||
export class Group {
|
||||
private tiles: Array<Tile>;
|
||||
private stolenFrom: number;
|
||||
constructor(
|
||||
private tiles: Array<Tile>,
|
||||
private stolenFrom: number,
|
||||
private belongsTo: number
|
||||
|
||||
public constructor(
|
||||
tiles: Array<Tile>,
|
||||
stolenFrom: number,
|
||||
belongsTo: number
|
||||
) {
|
||||
this.tiles = tiles;
|
||||
this.stolenFrom = stolenFrom;
|
||||
this.belongsTo = belongsTo;
|
||||
}
|
||||
) {}
|
||||
|
||||
public push(tile: Tile): void {
|
||||
this.tiles.push(tile);
|
||||
}
|
||||
|
||||
public pop(): Tile|undefined {
|
||||
public pop(): Tile | undefined {
|
||||
return this.tiles.pop();
|
||||
}
|
||||
|
||||
|
|
@ -28,11 +20,9 @@ export class Group {
|
|||
}
|
||||
|
||||
public compare(g: Group): number {
|
||||
let c = this.tiles[0].compare(g.tiles[0]);
|
||||
if (c !== 0) {
|
||||
return c;
|
||||
}
|
||||
return this.tiles[1].compare(g.tiles[1]);
|
||||
// Compare les premiers tiles, puis les seconds si égalité
|
||||
const firstComparison = this.tiles[0].compare(g.tiles[0]);
|
||||
return firstComparison !== 0 ? firstComparison : this.tiles[1].compare(g.tiles[1]);
|
||||
}
|
||||
|
||||
public drawGroup(
|
||||
|
|
@ -42,111 +32,58 @@ export class Group {
|
|||
os: number,
|
||||
size: number,
|
||||
rotation: number,
|
||||
selectedTile: Tile|undefined
|
||||
selectedTile?: Tile
|
||||
): void {
|
||||
// Sauvegarde et rotation du contexte
|
||||
ctx.save();
|
||||
ctx.translate(525, 525);
|
||||
ctx.rotate(rotation);
|
||||
ctx.translate(-525, -525);
|
||||
|
||||
rotation = 0;
|
||||
let v = 75 * size;
|
||||
let w = 90 * size;
|
||||
let osy = 25 * size / 2;
|
||||
let p = (this.belongsTo - this.stolenFrom - 1 + 4) % 4;
|
||||
// Calcul des paramètres de dessin
|
||||
const v = 75 * size;
|
||||
const w = 90 * size;
|
||||
const osy = 25 * size / 2;
|
||||
const p = (this.belongsTo - this.stolenFrom - 1 + 4) % 4;
|
||||
|
||||
// Détermination du tile sélectionné
|
||||
const sf = selectedTile === undefined ? -1 : selectedTile.getFamily();
|
||||
const sv = selectedTile === undefined ? 0 : selectedTile.getValue();
|
||||
|
||||
if (p === 0) {
|
||||
this.tiles[0].drawTile(
|
||||
// Fonction helper pour éviter la répétition de code
|
||||
const drawTile = (tile: Tile, tx: number, ty: number, angle: number) => {
|
||||
tile.drawTile(
|
||||
ctx,
|
||||
x,
|
||||
y + osy,
|
||||
tx,
|
||||
ty,
|
||||
size,
|
||||
false,
|
||||
3.141592 / 2,
|
||||
this.tiles[0].isEqual(sf, sv),
|
||||
);
|
||||
this.tiles[1].drawTile(
|
||||
ctx,
|
||||
x + w,
|
||||
y,
|
||||
size,
|
||||
false,
|
||||
0,
|
||||
this.tiles[1].isEqual(sf, sv),
|
||||
);
|
||||
this.tiles[2].drawTile(
|
||||
ctx,
|
||||
x + w + v + os * size,
|
||||
y,
|
||||
size,
|
||||
false,
|
||||
0,
|
||||
this.tiles[2].isEqual(sf, sv),
|
||||
angle,
|
||||
tile.isEqual(sf, sv)
|
||||
);
|
||||
};
|
||||
|
||||
} else if (p === 1) {
|
||||
this.tiles[0].drawTile(
|
||||
ctx,
|
||||
x,
|
||||
y,
|
||||
size,
|
||||
false,
|
||||
0,
|
||||
this.tiles[0].isEqual(sf, sv),
|
||||
);
|
||||
this.tiles[1].drawTile(
|
||||
ctx,
|
||||
x + w,
|
||||
y + osy,
|
||||
size,
|
||||
false,
|
||||
0 - 3.141592 / 2,
|
||||
this.tiles[1].isEqual(sf, sv),
|
||||
);
|
||||
this.tiles[2].drawTile(
|
||||
ctx,
|
||||
x + w + v + 3 *os * size,
|
||||
y,
|
||||
size,
|
||||
false,
|
||||
0,
|
||||
this.tiles[2].isEqual(sf, sv),
|
||||
);
|
||||
const HALF_PI = Math.PI / 2;
|
||||
|
||||
} else if (p === 2) {
|
||||
this.tiles[0].drawTile(
|
||||
ctx,
|
||||
x,
|
||||
y,
|
||||
size,
|
||||
false,
|
||||
0,
|
||||
this.tiles[0].isEqual(sf, sv),
|
||||
);
|
||||
this.tiles[1].drawTile(
|
||||
ctx,
|
||||
x + v + os * size,
|
||||
y,
|
||||
size,
|
||||
false,
|
||||
0,
|
||||
this.tiles[1].isEqual(sf, sv),
|
||||
);
|
||||
this.tiles[2].drawTile(
|
||||
ctx,
|
||||
x + w + v + os * size,
|
||||
y + osy,
|
||||
size,
|
||||
false,
|
||||
0 - 3.141592 / 2,
|
||||
this.tiles[2].isEqual(sf, sv),
|
||||
);
|
||||
|
||||
} else {
|
||||
//TODO error
|
||||
// Dessin selon la position
|
||||
switch (p) {
|
||||
case 0:
|
||||
drawTile(this.tiles[0], x, y + osy, HALF_PI);
|
||||
drawTile(this.tiles[1], x + w, y, 0);
|
||||
drawTile(this.tiles[2], x + w + v + os * size, y, 0);
|
||||
break;
|
||||
case 1:
|
||||
drawTile(this.tiles[0], x, y, 0);
|
||||
drawTile(this.tiles[1], x + w, y + osy, -HALF_PI);
|
||||
drawTile(this.tiles[2], x + w + v + 3 * os * size, y, 0);
|
||||
break;
|
||||
case 2:
|
||||
drawTile(this.tiles[0], x, y, 0);
|
||||
drawTile(this.tiles[1], x + v + os * size, y, 0);
|
||||
drawTile(this.tiles[2], x + w + v + os * size, y + osy, -HALF_PI);
|
||||
break;
|
||||
default:
|
||||
console.error(`Position non prise en charge: ${p}`);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
|
|
|
|||
400
src/hand.ts
400
src/hand.ts
|
|
@ -1,198 +1,332 @@
|
|||
import { Tile } from "./tile"
|
||||
import { Group } from "./group"
|
||||
import { Tile } from "./tile";
|
||||
import { Group } from "./group";
|
||||
|
||||
// Constants to avoid magic numbers and improve readability
|
||||
const TILE_TYPES = {
|
||||
MANZU: { code: "m", family: 1 },
|
||||
PINZU: { code: "p", family: 2 },
|
||||
SOUZU: { code: "s", family: 3 },
|
||||
WINDS: { code: "w", family: 4 },
|
||||
DRAGONS: { code: "d", family: 5 }
|
||||
};
|
||||
|
||||
export class Hand {
|
||||
private tiles: Array<Tile>;
|
||||
public isolate: boolean = false;
|
||||
|
||||
/**
|
||||
* Create a hand from string representation
|
||||
* @param stiles String representation of tiles (e.g., "m1p2s3w1d1")
|
||||
*/
|
||||
public constructor(stiles: string = "") {
|
||||
this.tiles = [];
|
||||
this.initializeFromString(stiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse string representation into tiles
|
||||
*/
|
||||
private initializeFromString(stiles: string): void {
|
||||
for (let i = 0; i < stiles.length - 1; i++) {
|
||||
let ss = stiles.substring(i, i+2);
|
||||
if (ss[0] === "m") {
|
||||
this.tiles.push(new Tile(1, Number(ss[1]), false));
|
||||
} else if (ss[0] === "p") {
|
||||
this.tiles.push(new Tile(2, Number(ss[1]), false));
|
||||
} else if (ss[0] === "s") {
|
||||
this.tiles.push(new Tile(3, Number(ss[1]), false));
|
||||
} else if (ss[0] === "w") {
|
||||
this.tiles.push(new Tile(4, Number(ss[1]), false));
|
||||
} else if (ss[0] === "d") {
|
||||
this.tiles.push(new Tile(5, Number(ss[1]), false));
|
||||
} else {}
|
||||
const tileCode = stiles.substring(i, i + 2);
|
||||
const type = tileCode[0];
|
||||
const value = Number(tileCode[1]);
|
||||
|
||||
if (this.isValidTileCode(type, value)) {
|
||||
this.addTileFromCode(type, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if tile code is valid
|
||||
*/
|
||||
private isValidTileCode(type: string, value: number): boolean {
|
||||
return (
|
||||
(type === TILE_TYPES.MANZU.code) ||
|
||||
(type === TILE_TYPES.PINZU.code) ||
|
||||
(type === TILE_TYPES.SOUZU.code) ||
|
||||
(type === TILE_TYPES.WINDS.code) ||
|
||||
(type === TILE_TYPES.DRAGONS.code)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tile from type code and value
|
||||
*/
|
||||
private addTileFromCode(type: string, value: number): void {
|
||||
const familyMap: { [key: string]: number } = {
|
||||
[TILE_TYPES.MANZU.code]: TILE_TYPES.MANZU.family,
|
||||
[TILE_TYPES.PINZU.code]: TILE_TYPES.PINZU.family,
|
||||
[TILE_TYPES.SOUZU.code]: TILE_TYPES.SOUZU.family,
|
||||
[TILE_TYPES.WINDS.code]: TILE_TYPES.WINDS.family,
|
||||
[TILE_TYPES.DRAGONS.code]: TILE_TYPES.DRAGONS.family
|
||||
};
|
||||
|
||||
const family = familyMap[type];
|
||||
if (family !== undefined) {
|
||||
this.tiles.push(new Tile(family, value, false));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tiles in hand
|
||||
*/
|
||||
public getTiles(): Array<Tile> {
|
||||
return this.tiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get number of tiles in hand
|
||||
*/
|
||||
public length(): number {
|
||||
return this.tiles.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a tile to hand
|
||||
*/
|
||||
public push(tile: Tile): void {
|
||||
this.tiles.push(tile);
|
||||
}
|
||||
|
||||
public pop(): Tile|undefined {
|
||||
/**
|
||||
* Remove and return the last tile
|
||||
*/
|
||||
public pop(): Tile | undefined {
|
||||
return this.tiles.pop();
|
||||
}
|
||||
|
||||
public find(family: number, value: number) :Tile|undefined {
|
||||
let n = undefined;
|
||||
for (let i = 0; i < this.tiles.length; i++) {
|
||||
if (this.tiles[i].getFamily() === family && this.tiles[i].getValue() === value) {
|
||||
n = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (n !== undefined) {
|
||||
[this.tiles[n], this.tiles[0]] = [this.tiles[0], this.tiles[n]];
|
||||
let t = this.tiles.shift();
|
||||
/**
|
||||
* Find and remove a specific tile by family and value
|
||||
*/
|
||||
public find(family: number, value: number): Tile | undefined {
|
||||
const index = this.findTileIndex(family, value);
|
||||
|
||||
if (index !== -1) {
|
||||
// Swap with first tile and remove
|
||||
[this.tiles[index], this.tiles[0]] = [this.tiles[0], this.tiles[index]];
|
||||
const tile = this.tiles.shift();
|
||||
this.sort();
|
||||
return t;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
return tile;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find index of tile with specific family and value
|
||||
*/
|
||||
private findTileIndex(family: number, value: number): number {
|
||||
return this.tiles.findIndex(
|
||||
tile => tile.getFamily() === family && tile.getValue() === value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove tile at specific index
|
||||
*/
|
||||
public eject(idTile: number): Tile {
|
||||
[this.tiles[0], this.tiles[idTile]] = [this.tiles[idTile], this.tiles[0]];
|
||||
let tile = this.tiles.shift();
|
||||
this.sort();
|
||||
return tile as NonNullable<Tile>;
|
||||
if (idTile < 0 || idTile >= this.tiles.length) {
|
||||
throw new Error("Invalid tile index");
|
||||
}
|
||||
|
||||
public get(idTile: number|undefined): Tile|undefined {
|
||||
if (idTile !== undefined) {
|
||||
return this.tiles[idTile];
|
||||
} else {
|
||||
// Swap with first tile and remove
|
||||
[this.tiles[0], this.tiles[idTile]] = [this.tiles[idTile], this.tiles[0]];
|
||||
const tile = this.tiles.shift();
|
||||
this.sort();
|
||||
|
||||
return tile as Tile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tile at specific index without removing
|
||||
*/
|
||||
public get(idTile: number | undefined): Tile | undefined {
|
||||
if (idTile === undefined || idTile < 0 || idTile >= this.tiles.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.tiles[idTile];
|
||||
}
|
||||
|
||||
public sort(): undefined {
|
||||
/**
|
||||
* Sort tiles in ascending order
|
||||
*/
|
||||
public sort(): void {
|
||||
this.tiles.sort((a, b) => a.isLessThan(b) ? -1 : 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count tiles with specific family and value
|
||||
*/
|
||||
public count(family: number, value: number): number {
|
||||
let c = 0;
|
||||
this.tiles.forEach(
|
||||
t => {
|
||||
if (t.getFamily() === family && t.getValue() === value) {
|
||||
c++;
|
||||
}
|
||||
}
|
||||
);
|
||||
return c;
|
||||
return this.tiles.filter(
|
||||
tile => tile.getFamily() === family && tile.getValue() === value
|
||||
).length;
|
||||
}
|
||||
|
||||
public toGroup(pair: boolean = false): Array<Group>|undefined {
|
||||
if (this.tiles.length > 0) {
|
||||
let t1 = this.tiles.pop() as NonNullable<Tile>;
|
||||
|
||||
let c = this.count(t1.getFamily(), t1.getValue());
|
||||
if (c >= 1 && !pair) { //can do a pair
|
||||
let t2 = this.find(t1.getFamily(), t1.getValue()) as NonNullable<Tile>;
|
||||
let groups = this.toGroup(true);
|
||||
this.tiles.push(t2);
|
||||
this.sort();
|
||||
if (groups !== undefined) {
|
||||
this.tiles.push(t1);
|
||||
this.sort();
|
||||
groups.push(new Group([t1, t2], 0, 0));
|
||||
return groups;
|
||||
}
|
||||
}
|
||||
if (c >= 2) { //can do a pon
|
||||
let t2 = this.find(t1.getFamily(), t1.getValue()) as NonNullable<Tile>;
|
||||
let t3 = this.find(t1.getFamily(), t1.getValue()) as NonNullable<Tile>;
|
||||
let groups = this.toGroup(pair);
|
||||
this.tiles.push(t2);
|
||||
this.tiles.push(t3);
|
||||
this.sort();
|
||||
if (groups !== undefined) {
|
||||
groups.push(new Group([t1, t2, t3], 0, 0));
|
||||
this.tiles.push(t1);
|
||||
this.sort();
|
||||
return groups;
|
||||
}
|
||||
}
|
||||
|
||||
let c2 = this.count(t1.getFamily(), t1.getValue()-1);
|
||||
let c3 = this.count(t1.getFamily(), t1.getValue()-2);
|
||||
if (c2 * c3 > 0) { //can do a chii
|
||||
let t2 = this.find(t1.getFamily(), t1.getValue()-1) as NonNullable<Tile>;
|
||||
let t3 = this.find(t1.getFamily(), t1.getValue()-2) as NonNullable<Tile>;
|
||||
let groups = this.toGroup(pair);
|
||||
this.tiles.push(t2);
|
||||
this.tiles.push(t3);
|
||||
this.sort();
|
||||
if (groups !== undefined) {
|
||||
groups.push(new Group([t3, t2, t1], 0, 0));
|
||||
this.tiles.push(t1);
|
||||
this.sort();
|
||||
return groups;
|
||||
}
|
||||
}
|
||||
|
||||
this.tiles.push(t1);
|
||||
this.tiles.sort();
|
||||
return undefined;
|
||||
|
||||
} else {
|
||||
/**
|
||||
* Try to form hand into groups (for winning detection)
|
||||
*/
|
||||
public toGroup(pair: boolean = false): Array<Group> | undefined {
|
||||
if (this.tiles.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Take last tile to try forming a group
|
||||
const lastTile = this.tiles.pop() as Tile;
|
||||
const family = lastTile.getFamily();
|
||||
const value = lastTile.getValue();
|
||||
|
||||
// Try to form a pair
|
||||
if (this.count(family, value) >= 1 && !pair) {
|
||||
const result = this.tryFormPair(lastTile);
|
||||
if (result) return result;
|
||||
}
|
||||
|
||||
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,
|
||||
x: number,
|
||||
y: number,
|
||||
offset: number,
|
||||
size: number,
|
||||
focusedTiled: number|undefined = undefined,
|
||||
focusedTile: number | undefined = undefined,
|
||||
hidden: boolean = false,
|
||||
rotation: number = 0
|
||||
): void {
|
||||
let v = (75 + offset) * size;
|
||||
let vx = Math.cos(rotation) * v;
|
||||
let vy = Math.sin(rotation) * v;
|
||||
const tileOffset = (75 + offset) * size;
|
||||
const offsetX = Math.cos(rotation) * tileOffset;
|
||||
const offsetY = Math.sin(rotation) * tileOffset;
|
||||
|
||||
for (let i = 0; i < this.tiles.length; i++) {
|
||||
let e = (i === this.tiles.length - 1 && this.isolate) ? 10 : 0;
|
||||
if (i === focusedTiled) {
|
||||
this.tiles[i].drawTile(
|
||||
ctx,
|
||||
x +
|
||||
i * vx +
|
||||
25 * size * Math.sin(rotation) +
|
||||
e * size * Math.cos(rotation),
|
||||
y +
|
||||
i * vy -
|
||||
25 * size * Math.cos(rotation) +
|
||||
e * size * Math.sin(rotation),
|
||||
size,
|
||||
hidden,
|
||||
rotation
|
||||
);
|
||||
} else {
|
||||
this.tiles[i].drawTile(
|
||||
ctx,
|
||||
x + i * vx + e * size * Math.cos(rotation),
|
||||
y + i * vy + e * size * Math.sin(rotation),
|
||||
size,
|
||||
hidden,
|
||||
rotation
|
||||
);
|
||||
const isLastAndIsolated = (i === this.tiles.length - 1 && this.isolate) ? 10 : 0;
|
||||
|
||||
// Calculate position
|
||||
let tileX = x + i * offsetX + isLastAndIsolated * size * Math.cos(rotation);
|
||||
let tileY = y + i * offsetY + isLastAndIsolated * size * Math.sin(rotation);
|
||||
|
||||
// Add additional offset for focused tile
|
||||
if (i === focusedTile) {
|
||||
tileX += 25 * size * Math.sin(rotation);
|
||||
tileY -= 25 * size * Math.cos(rotation);
|
||||
}
|
||||
|
||||
// Draw tile
|
||||
this.tiles[i].drawTile(
|
||||
ctx,
|
||||
tileX,
|
||||
tileY,
|
||||
size,
|
||||
hidden,
|
||||
rotation
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload tile images
|
||||
*/
|
||||
public async preload(): Promise<void> {
|
||||
await Promise.all(this.tiles.map(t => t.preloadImg()));
|
||||
await Promise.all(this.tiles.map(tile => tile.preloadImg()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources
|
||||
*/
|
||||
public cleanup(): void {
|
||||
this.tiles.forEach(tile => tile.cleanup());
|
||||
this.tiles = [];
|
||||
|
|
|
|||
207
src/tile.ts
207
src/tile.ts
|
|
@ -1,24 +1,16 @@
|
|||
export class Tile {
|
||||
private family: number;
|
||||
private value: number;
|
||||
private red: boolean;
|
||||
private imgSrc: string;
|
||||
private imgFront: HTMLImageElement;
|
||||
private imgBack: HTMLImageElement;
|
||||
private imgGray: HTMLImageElement;
|
||||
private img: HTMLImageElement;
|
||||
private tilt: number;
|
||||
private imgFront: HTMLImageElement = new Image();
|
||||
private imgBack: HTMLImageElement = new Image();
|
||||
private imgGray: HTMLImageElement = new Image();
|
||||
private img: HTMLImageElement = new Image();
|
||||
private imgSrc: string = "";
|
||||
private tilt: number = 0;
|
||||
|
||||
public constructor(family: number, value: number , red: boolean) {
|
||||
this.family = family;
|
||||
this.value = value;
|
||||
this.red = red;
|
||||
this.imgSrc = "";
|
||||
this.imgFront = new Image();
|
||||
this.imgBack = new Image();
|
||||
this.imgGray = new Image();
|
||||
this.img = new Image();
|
||||
this.tilt = 0;
|
||||
constructor(
|
||||
private family: number,
|
||||
private value: number,
|
||||
private red: boolean
|
||||
) {
|
||||
this.setImgSrc();
|
||||
}
|
||||
|
||||
|
|
@ -39,19 +31,21 @@ export class Tile {
|
|||
}
|
||||
|
||||
public compare(t: Tile): number {
|
||||
if (this.family < t.family) {
|
||||
return -1;
|
||||
} else if (this.family > t.family) {
|
||||
return 1;
|
||||
// Compare d'abord par famille, puis par valeur
|
||||
if (this.family !== t.family) {
|
||||
return this.family < t.family ? -1 : 1;
|
||||
}
|
||||
if (this.value < t.value) {
|
||||
return -1;
|
||||
} else if (this.value > t.value) {
|
||||
return 1;
|
||||
if (this.value !== t.value) {
|
||||
return this.value < t.value ? -1 : 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public isLessThan(t: Tile): boolean {
|
||||
return this.family < t.family ||
|
||||
(this.family === t.family && this.value <= t.value);
|
||||
}
|
||||
|
||||
public setTilt(): void {
|
||||
this.tilt = (1 - 2 * Math.random()) * 0.04;
|
||||
}
|
||||
|
|
@ -66,109 +60,69 @@ export class Tile {
|
|||
gray: boolean = false,
|
||||
tilted: boolean = true
|
||||
): void {
|
||||
const tileWidth = 75 * size;
|
||||
const tileHeight = 100 * size;
|
||||
const halfWidth = tileWidth / 2;
|
||||
const halfHeight = tileHeight / 2;
|
||||
const shadowScale = 0.92;
|
||||
|
||||
// Sauvegarde du contexte et positionnement
|
||||
ctx.save();
|
||||
ctx.translate(x + (75 * size) / 2, y + (100 * size) / 2);
|
||||
if (tilted) {
|
||||
ctx.rotate(rotation + this.tilt);
|
||||
} else {
|
||||
ctx.rotate(rotation);
|
||||
}
|
||||
ctx.translate(x + halfWidth, y + halfHeight);
|
||||
ctx.rotate(rotation + (tilted ? this.tilt : 0));
|
||||
|
||||
// Position de l'ombre (légèrement décalée)
|
||||
const shadowX = -(tileWidth * shadowScale) / 2;
|
||||
const shadowY = -(tileHeight * shadowScale) / 2;
|
||||
|
||||
// Dessin de l'ombre (commun aux deux cas)
|
||||
ctx.drawImage(this.imgGray, shadowX, shadowY, tileWidth, tileHeight);
|
||||
|
||||
if (hidden) {
|
||||
ctx.drawImage( // ombre
|
||||
this.imgGray,
|
||||
-(75 * size * 0.92) / 2,
|
||||
-(100 * size * 0.91) / 2,
|
||||
75 * size,
|
||||
100 * size
|
||||
);
|
||||
ctx.drawImage( // le dos des tuiles
|
||||
this.imgBack,
|
||||
-(75 * size) / 2,
|
||||
-(100 * size) / 2,
|
||||
75 * size,
|
||||
100 * size
|
||||
);
|
||||
// Dessin du dos de la tuile
|
||||
ctx.drawImage(this.imgBack, -halfWidth, -halfHeight, tileWidth, tileHeight);
|
||||
} else {
|
||||
ctx.drawImage( // ombre
|
||||
this.imgGray,
|
||||
-(75 * size * 0.92) / 2,
|
||||
-(100 * size * 0.91) / 2,
|
||||
75 * size,
|
||||
100 * size
|
||||
);
|
||||
ctx.drawImage( // tuile à vide
|
||||
this.imgFront,
|
||||
-(75 * size) / 2,
|
||||
-(100 * size) / 2,
|
||||
75 * size, 100 * size
|
||||
);
|
||||
ctx.drawImage( // le dessin sur la tuile
|
||||
this.img,
|
||||
-((75 - 7) * size) / 2,
|
||||
-((100 - 10) * size) / 2,
|
||||
75 * size * 0.9,
|
||||
100 * size * 0.9
|
||||
);
|
||||
if (gray) { // grisé
|
||||
ctx.drawImage(
|
||||
this.imgGray,
|
||||
-(75 * size) / 2,
|
||||
-(100 * size) / 2,
|
||||
75 * size,
|
||||
100 * size
|
||||
);
|
||||
// Dessin de la tuile face visible
|
||||
ctx.drawImage(this.imgFront, -halfWidth, -halfHeight, tileWidth, tileHeight);
|
||||
|
||||
// Dessin du motif sur la tuile (légèrement plus petit)
|
||||
const patternScale = 0.9;
|
||||
const patternWidth = tileWidth * patternScale;
|
||||
const patternHeight = tileHeight * patternScale;
|
||||
const patternX = -((75 - 7) * size) / 2;
|
||||
const patternY = -((100 - 10) * size) / 2;
|
||||
|
||||
ctx.drawImage(this.img, patternX, patternY, patternWidth, patternHeight);
|
||||
|
||||
// Appliquer un filtre gris si demandé
|
||||
if (gray) {
|
||||
ctx.drawImage(this.imgGray, -halfWidth, -halfHeight, tileWidth, tileHeight);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
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 {
|
||||
this.imgFront.onload = null;
|
||||
this.imgFront.onerror = null;
|
||||
this.imgBack.onload = null;
|
||||
this.imgBack.onerror = null;
|
||||
this.imgGray.onload = null;
|
||||
this.imgGray.onerror = null;
|
||||
this.img.onload = null;
|
||||
this.img.onerror = null;
|
||||
}
|
||||
|
||||
private 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";
|
||||
}
|
||||
// Supprimer tous les gestionnaires d'événements
|
||||
const images = [this.imgFront, this.imgBack, this.imgGray, this.img];
|
||||
images.forEach(img => {
|
||||
img.onload = null;
|
||||
img.onerror = null;
|
||||
});
|
||||
}
|
||||
|
||||
public async preloadImg(): Promise<void> {
|
||||
await Promise.all([
|
||||
this.loadImg(this.imgFront, "img/Regular/Front.svg"),
|
||||
// this.loadImg(this.imgFront, "/~img/Export/Regular/Front.png"),
|
||||
this.loadImg(this.imgBack, "img/Regular/Back.svg"),
|
||||
this.loadImg(this.imgGray, "img/Regular/Gray.svg"),
|
||||
this.loadImg(this.img, this.imgSrc)
|
||||
]);
|
||||
const imagesToLoad = [
|
||||
{ img: this.imgFront, src: "img/Regular/Front.svg" },
|
||||
{ img: this.imgBack, src: "img/Regular/Back.svg" },
|
||||
{ img: this.imgGray, src: "img/Regular/Gray.svg" },
|
||||
{ img: this.img, src: this.imgSrc }
|
||||
];
|
||||
|
||||
await Promise.all(
|
||||
imagesToLoad.map(({ img, src }) => this.loadImg(img, src))
|
||||
);
|
||||
}
|
||||
|
||||
private loadImg(img: HTMLImageElement, src: string): Promise<void> {
|
||||
|
|
@ -178,4 +132,25 @@ export class Tile {
|
|||
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";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"target": "es2015",
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue