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
512
src/button.ts
512
src/button.ts
|
|
@ -1,277 +1,311 @@
|
||||||
import { Tile } from "./tile"
|
import { Tile } from "./tile";
|
||||||
|
|
||||||
type button_t = (
|
/**
|
||||||
arg0: CanvasRenderingContext2D,
|
* Type definition for button rendering functions
|
||||||
arg1: number,
|
*/
|
||||||
arg2: number
|
type ButtonRenderer = (
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
x: number,
|
||||||
|
y: number
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button configuration interface
|
||||||
|
*/
|
||||||
|
interface ButtonConfig {
|
||||||
|
text: string;
|
||||||
|
color: string;
|
||||||
|
action: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button constants
|
||||||
|
const BUTTON_RADIUS = 8;
|
||||||
|
const BUTTON_WIDTH = 110;
|
||||||
|
const BUTTON_HEIGHT = 50;
|
||||||
|
const BUTTON_SPACING = 120;
|
||||||
|
const BUTTON_AREA_Y_MIN = 838;
|
||||||
|
const BUTTON_AREA_Y_MAX = 888;
|
||||||
|
const BUTTON_MARGIN = 10;
|
||||||
|
const BASE_X_POSITION = 850;
|
||||||
|
|
||||||
|
// Button style configurations
|
||||||
|
const BUTTON_STYLES: Record<string, ButtonConfig> = {
|
||||||
|
pass: { text: "Ignorer", color: "#FF9030", action: 0 },
|
||||||
|
chii: { text: "Chii", color: "#FFCC33", action: 1 },
|
||||||
|
pon: { text: "Pon", color: "#FFCC33", action: 2 },
|
||||||
|
kan: { text: "Kan", color: "#FFCC33", action: 3 },
|
||||||
|
ron: { text: "Ron", color: "#FF3060", action: 4 },
|
||||||
|
tsumo: { text: "Tsumo", color: "#FF3060", action: 5 },
|
||||||
|
back: { text: "Retour", color: "#FF9030", action: 0 }
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines which button was clicked based on coordinates
|
||||||
|
* @returns The action value of the clicked button or -1 if no button was clicked
|
||||||
|
*/
|
||||||
export function clickAction(
|
export function clickAction(
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
chii: boolean,
|
chii: boolean,
|
||||||
pon: boolean,
|
pon: boolean,
|
||||||
kan: boolean,
|
kan: boolean,
|
||||||
ron: boolean,
|
ron: boolean,
|
||||||
tsumo: boolean
|
tsumo: boolean
|
||||||
): number {
|
): number {
|
||||||
let buttons = [
|
const activeButtons = getActiveButtons(chii, pon, kan, ron, tsumo);
|
||||||
[tsumo, 5],
|
|
||||||
[ron, 4],
|
if (activeButtons.length === 0) {
|
||||||
[kan, 3],
|
return -1;
|
||||||
[pon, 2],
|
}
|
||||||
[chii, 1]
|
|
||||||
]
|
// Calculate starting X position based on number of buttons
|
||||||
if (buttons.some(c => c[0])) {
|
const xmin = 960 - activeButtons.length * BUTTON_SPACING;
|
||||||
buttons.push([true, 0]);
|
|
||||||
} else {
|
// Check if Y coordinate is within button area
|
||||||
return -1;
|
const isYInButtonArea = BUTTON_AREA_Y_MIN < y && y < BUTTON_AREA_Y_MAX;
|
||||||
}
|
if (!isYInButtonArea) {
|
||||||
let xmin = 960 - buttons.filter(c => c[0]).length * 120;
|
return -1;
|
||||||
let inside = 838 < y && y < 888;
|
}
|
||||||
let q = Math.floor((x - xmin) / 120);
|
|
||||||
let r = (x - xmin) - 120 * q;
|
// Calculate which button was clicked
|
||||||
if (
|
const buttonIndex = Math.floor((x - xmin) / BUTTON_SPACING);
|
||||||
q >= 0 &&
|
const xOffset = (x - xmin) - BUTTON_SPACING * buttonIndex;
|
||||||
q < buttons.filter(c => c[0]).length &&
|
|
||||||
r > 10 &&
|
if (
|
||||||
inside
|
buttonIndex >= 0 &&
|
||||||
) {
|
buttonIndex < activeButtons.length &&
|
||||||
return buttons.filter(c => c[0])[q][1] as number;
|
xOffset > BUTTON_MARGIN
|
||||||
}
|
) {
|
||||||
return -1;
|
return activeButtons[buttonIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw all active buttons on the canvas
|
||||||
|
*/
|
||||||
export function drawButtons(
|
export function drawButtons(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
chii: boolean,
|
chii: boolean,
|
||||||
pon: boolean,
|
pon: boolean,
|
||||||
kan: boolean,
|
kan: boolean,
|
||||||
ron: boolean,
|
ron: boolean,
|
||||||
tsumo: boolean
|
tsumo: boolean
|
||||||
): void {
|
): void {
|
||||||
let buttons = [
|
const buttonFunctions: [boolean, ButtonRenderer][] = [
|
||||||
[chii, buttonChii],
|
[chii, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.chii)],
|
||||||
[pon, buttonPon],
|
[pon, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.pon)],
|
||||||
[kan, buttonKan],
|
[kan, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.kan)],
|
||||||
[ron, buttonRon],
|
[ron, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.ron)],
|
||||||
[tsumo, buttonTsumo]
|
[tsumo, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.tsumo)]
|
||||||
]
|
];
|
||||||
if (buttons.some(c => c[0])) {
|
|
||||||
buttons.unshift([true, buttonPass]);
|
// Only show the pass button if at least one other button is active
|
||||||
}
|
const hasActiveButtons = buttonFunctions.some(([isActive]) => isActive);
|
||||||
let dx = 0;
|
|
||||||
for (let i = 0; i < buttons.length; i++) {
|
if (hasActiveButtons) {
|
||||||
if (buttons[i][0]) {
|
buttonFunctions.unshift([true, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.pass)]);
|
||||||
(buttons[i][1] as button_t)(
|
} else {
|
||||||
ctx,
|
return; // No buttons to draw
|
||||||
850 - dx * 120,
|
}
|
||||||
835
|
|
||||||
);
|
// Draw active buttons
|
||||||
dx++;
|
let positionOffset = 0;
|
||||||
}
|
for (const [isActive, renderFunc] of buttonFunctions) {
|
||||||
}
|
if (isActive) {
|
||||||
|
renderFunc(
|
||||||
|
ctx,
|
||||||
|
BASE_X_POSITION - positionOffset * BUTTON_SPACING,
|
||||||
|
835
|
||||||
|
);
|
||||||
|
positionOffset++;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines which Chi option was clicked
|
||||||
|
* @returns The value of the clicked Chi option or -1 if no option was clicked
|
||||||
|
*/
|
||||||
export function clickChii(
|
export function clickChii(
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
chiis: Array<Array<Tile>>
|
chiis: Array<Array<Tile>>
|
||||||
): number {
|
): number {
|
||||||
let xmin = 960 - (chiis.length + 1) * 120;
|
if (chiis.length === 0) {
|
||||||
let inside = 838 < y && y < 888;
|
return -1;
|
||||||
let q = Math.floor((x - xmin) / 120);
|
}
|
||||||
let r = (x - xmin) - 120 * q;
|
|
||||||
if (
|
// Calculate starting X position based on number of options
|
||||||
q >= 0 &&
|
const xmin = 960 - (chiis.length + 1) * BUTTON_SPACING;
|
||||||
q < (chiis.length + 1) &&
|
|
||||||
r > 10 &&
|
// Check if Y coordinate is within button area
|
||||||
inside
|
const isYInButtonArea = BUTTON_AREA_Y_MIN < y && y < BUTTON_AREA_Y_MAX;
|
||||||
) {
|
if (!isYInButtonArea) {
|
||||||
return q === chiis.length ? 0 : chiis[q][0].getValue();
|
return -1;
|
||||||
}
|
}
|
||||||
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(
|
export function drawChiis(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
chiis: Array<Array<Tile>>
|
chiis: Array<Array<Tile>>
|
||||||
): void {
|
): void {
|
||||||
chiis.reverse();
|
// Create a copy to avoid modifying the original array
|
||||||
const r = 8;
|
const chiiOptions = [...chiis].reverse();
|
||||||
const w = 110;
|
|
||||||
const h = 50;
|
|
||||||
button(ctx, 850, 835, r, w, h, "#FF9030");
|
|
||||||
ctx.fillStyle = "black";
|
|
||||||
ctx.font = "30px garamond";
|
|
||||||
ctx.fillText("Retour", 850 + w * 0.1, 835 + h/2 * 1.3);
|
|
||||||
|
|
||||||
let dx = 1;
|
// Draw "back" button
|
||||||
for (let i = 0; i < chiis.length; i++) {
|
renderButton(ctx, BASE_X_POSITION, 835, BUTTON_STYLES.back);
|
||||||
drawOneChii(
|
|
||||||
ctx,
|
// Draw Chi options
|
||||||
850 - dx * 120,
|
let positionOffset = 1;
|
||||||
835,
|
for (const tiles of chiiOptions) {
|
||||||
chiis[i]
|
drawOneChii(
|
||||||
);
|
ctx,
|
||||||
dx++;
|
BASE_X_POSITION - positionOffset * BUTTON_SPACING,
|
||||||
}
|
835,
|
||||||
|
tiles
|
||||||
|
);
|
||||||
|
positionOffset++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a single Chi option
|
||||||
|
*/
|
||||||
function drawOneChii(
|
function drawOneChii(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
tiles: Array<Tile>
|
tiles: Array<Tile>
|
||||||
): void {
|
): void {
|
||||||
const r = 8;
|
const tileOffset = 32;
|
||||||
const w = 110;
|
const tileStartX = x + 7;
|
||||||
const h = 50;
|
const tileStartY = y + 5;
|
||||||
const dx = 32;
|
|
||||||
const x0 = x + 7;
|
// Draw the button background
|
||||||
const y0 = y + 5;
|
drawButtonShape(ctx, x, y, BUTTON_RADIUS, BUTTON_WIDTH, BUTTON_HEIGHT, BUTTON_STYLES.chii.color);
|
||||||
button(ctx, x, y, r, w, h, "#FFCC33");
|
|
||||||
tiles[0].drawTile(
|
// Draw the tiles
|
||||||
ctx,
|
for (let i = 0; i < tiles.length; i++) {
|
||||||
x0,
|
tiles[i].drawTile(
|
||||||
y0,
|
ctx,
|
||||||
0.4,
|
tileStartX + tileOffset * i,
|
||||||
false,
|
tileStartY,
|
||||||
0,
|
0.4,
|
||||||
false,
|
false,
|
||||||
false
|
0,
|
||||||
);
|
false,
|
||||||
tiles[1].drawTile(
|
false
|
||||||
ctx,
|
);
|
||||||
x0 + dx,
|
}
|
||||||
y0,
|
|
||||||
0.4,
|
|
||||||
false,
|
|
||||||
0,
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
tiles[2].drawTile(
|
|
||||||
ctx,
|
|
||||||
x0 + 2 * dx,
|
|
||||||
y0,
|
|
||||||
0.4,
|
|
||||||
false,
|
|
||||||
0,
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function button(
|
/**
|
||||||
ctx: CanvasRenderingContext2D,
|
* Get the list of active button action values
|
||||||
x: number,
|
*/
|
||||||
y: number,
|
function getActiveButtons(
|
||||||
r: number,
|
chii: boolean,
|
||||||
w: number,
|
pon: boolean,
|
||||||
h: number,
|
kan: boolean,
|
||||||
color: string
|
ron: boolean,
|
||||||
): void {
|
tsumo: boolean
|
||||||
ctx.fillStyle = color;
|
): number[] {
|
||||||
ctx.beginPath();
|
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]
|
||||||
|
];
|
||||||
|
|
||||||
ctx.moveTo(x + r, y);
|
const activeButtons = buttonConfigs
|
||||||
ctx.lineTo(x + w - r, y);
|
.filter(([isActive]) => isActive)
|
||||||
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
|
.map(([, action]) => action);
|
||||||
ctx.lineTo(x + w, y + h - r);
|
|
||||||
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
|
|
||||||
ctx.lineTo(x + r, y + h);
|
|
||||||
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
|
|
||||||
ctx.lineTo(x, y + r);
|
|
||||||
ctx.quadraticCurveTo(x, y, x + r, y);
|
|
||||||
|
|
||||||
ctx.fill();
|
// Add pass button if any other buttons are active
|
||||||
|
if (activeButtons.length > 0) {
|
||||||
|
activeButtons.push(BUTTON_STYLES.pass.action);
|
||||||
|
}
|
||||||
|
|
||||||
ctx.fillStyle = "#606060";
|
return activeButtons;
|
||||||
ctx.stroke();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buttonPass(
|
/**
|
||||||
ctx: CanvasRenderingContext2D,
|
* Render a button with text
|
||||||
x: number,
|
*/
|
||||||
y: number
|
function renderButton(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
config: ButtonConfig
|
||||||
): void {
|
): void {
|
||||||
const r = 8;
|
drawButtonShape(ctx, x, y, BUTTON_RADIUS, BUTTON_WIDTH, BUTTON_HEIGHT, config.color);
|
||||||
const w = 110;
|
|
||||||
const h = 50;
|
// Add text to the button
|
||||||
// button(ctx, x, y, r, w, h, "#FFAC4D");
|
ctx.fillStyle = "black";
|
||||||
button(ctx, x, y, r, w, h, "#FF9030");
|
ctx.font = "30px garamond";
|
||||||
ctx.fillStyle = "black";
|
|
||||||
ctx.font = "30px garamond";
|
// Center text based on its length
|
||||||
ctx.fillText("Ignorer", x + w * 0.1, y + h/2 * 1.3);
|
const textXPosition = x + BUTTON_WIDTH * (0.5 - config.text.length * 0.025);
|
||||||
|
const textYPosition = y + BUTTON_HEIGHT/2 * 1.3;
|
||||||
|
|
||||||
|
ctx.fillText(config.text, textXPosition, textYPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buttonPon(
|
/**
|
||||||
ctx: CanvasRenderingContext2D,
|
* Draw a rounded rectangle button shape
|
||||||
x: number,
|
*/
|
||||||
y: number
|
function drawButtonShape(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
radius: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
color: string
|
||||||
): void {
|
): void {
|
||||||
const r = 8;
|
ctx.fillStyle = color;
|
||||||
const w = 110;
|
ctx.beginPath();
|
||||||
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(
|
// Top right corner
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx.moveTo(x + radius, y);
|
||||||
x: number,
|
ctx.lineTo(x + width - radius, y);
|
||||||
y: number
|
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
||||||
): 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(
|
// Bottom right corner
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx.lineTo(x + width, y + height - radius);
|
||||||
x: number,
|
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
||||||
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(
|
// Bottom left corner
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx.lineTo(x + radius, y + height);
|
||||||
x: number,
|
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
||||||
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(
|
// Top left corner
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx.lineTo(x, y + radius);
|
||||||
x: number,
|
ctx.quadraticCurveTo(x, y, x + radius, y);
|
||||||
y: number
|
|
||||||
): void {
|
ctx.fill();
|
||||||
const r = 8;
|
|
||||||
const w = 110;
|
// Add border
|
||||||
const h = 50;
|
ctx.fillStyle = "#606060";
|
||||||
button(ctx, x, y, r, w, h, "#FF3060");
|
ctx.stroke();
|
||||||
ctx.fillStyle = "black";
|
|
||||||
ctx.font = "30px garamond";
|
|
||||||
ctx.fillText("Tsumo",x + w * 0.13, y + h/2 * 1.3);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
415
src/deck.ts
415
src/deck.ts
|
|
@ -1,152 +1,293 @@
|
||||||
import { Tile } from "./tile"
|
import { Tile } from "./tile";
|
||||||
import { Hand } from "./hand"
|
import { Hand } from "./hand";
|
||||||
|
|
||||||
|
type TileKey = `${number}-${number}`;
|
||||||
|
|
||||||
export class Deck {
|
export class Deck {
|
||||||
private tiles: Array<Tile>;
|
private tiles: Tile[];
|
||||||
|
private tileIndexMap: Map<TileKey, number[]>; // Fast lookup for find() and count()
|
||||||
|
|
||||||
public constructor(allowRed: boolean) {
|
public constructor(allowRed: boolean = false) {
|
||||||
this.tiles = [];
|
this.tiles = [];
|
||||||
this.initTiles(allowRed);
|
this.tileIndexMap = new Map();
|
||||||
}
|
this.initTiles(allowRed);
|
||||||
|
}
|
||||||
|
|
||||||
public displayFamilies(
|
/**
|
||||||
ctx: CanvasRenderingContext2D,
|
* Displays all tile families on the canvas
|
||||||
x: number,
|
*/
|
||||||
y: number,
|
public displayFamilies(
|
||||||
size: number,
|
ctx: CanvasRenderingContext2D,
|
||||||
xOffset: number = 0,
|
x: number,
|
||||||
yOffset: number = 0
|
y: number,
|
||||||
): void {
|
size: number,
|
||||||
let posX = x;
|
xOffset: number = 0,
|
||||||
let posY = y;
|
yOffset: number = 0
|
||||||
for (let i = 1; i < 6; i++) {
|
): void {
|
||||||
if (i < 4) { // famille
|
let posX = x;
|
||||||
for (let j = 1; j < 10; j++) {
|
let posY = y;
|
||||||
const tile = this.find(i, j) as NonNullable<Tile>;
|
|
||||||
tile.drawTile(ctx, posX, posY, size, false, 0, false);
|
|
||||||
posX += (75 + xOffset) * size;
|
|
||||||
}
|
|
||||||
posX = x;
|
|
||||||
posY += (100 + yOffset) * size;
|
|
||||||
} else if (i === 4) { //vent
|
|
||||||
for (let j = 1; j < 5; j++) {
|
|
||||||
const tile = this.find(i, j) as NonNullable<Tile>;
|
|
||||||
tile.drawTile(ctx, posX, posY, size);
|
|
||||||
posX += (75 + xOffset) * size;
|
|
||||||
}
|
|
||||||
posX = x;
|
|
||||||
posY += (100 + yOffset) * size;
|
|
||||||
} else if (i === 5) { //vent
|
|
||||||
for (let j = 1; j < 4; j++) {
|
|
||||||
const tile = this.find(i, j) as NonNullable<Tile>;
|
|
||||||
tile.drawTile(ctx, posX, posY, size);
|
|
||||||
posX += (75 + xOffset) * size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public length(): number {
|
// Define tile layouts for each family
|
||||||
return this.tiles.length;
|
const familyLayouts = [
|
||||||
}
|
{ family: 1, start: 1, end: 9 }, // First suit
|
||||||
|
{ family: 2, start: 1, end: 9 }, // Second suit
|
||||||
|
{ family: 3, start: 1, end: 9 }, // Third suit
|
||||||
|
{ family: 4, start: 1, end: 4 }, // Winds
|
||||||
|
{ family: 5, start: 1, end: 3 } // Dragons
|
||||||
|
];
|
||||||
|
|
||||||
public pop(): Tile {
|
for (const layout of familyLayouts) {
|
||||||
if (this.tiles.length === 0) {
|
for (let j = layout.start; j <= layout.end; j++) {
|
||||||
}
|
const tile = this.find(layout.family, j);
|
||||||
return this.tiles.pop() as NonNullable<Tile>;
|
if (tile) {
|
||||||
}
|
tile.drawTile(ctx, posX, posY, size, false, 0, false);
|
||||||
|
posX += (75 + xOffset) * size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
posX = x;
|
||||||
|
posY += (100 + yOffset) * size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public push(tile: Tile) {
|
/**
|
||||||
this.tiles.push(tile);
|
* Returns the number of tiles in the deck
|
||||||
}
|
*/
|
||||||
|
public length(): number {
|
||||||
|
return this.tiles.length;
|
||||||
|
}
|
||||||
|
|
||||||
public find(family: number, value: number): Tile | undefined {
|
/**
|
||||||
let n = undefined;
|
* Removes and returns the last tile from the deck
|
||||||
for (let i = 0; i < this.tiles.length; i++) {
|
* @throws Error if the deck is empty
|
||||||
if (
|
*/
|
||||||
this.tiles[i].getFamily() === family &&
|
public pop(): Tile {
|
||||||
this.tiles[i].getValue() === value
|
if (this.tiles.length === 0) {
|
||||||
) {
|
throw new Error("Cannot pop from an empty deck");
|
||||||
n = i;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
if (n !== undefined) {
|
|
||||||
[this.tiles[n as NonNullable<number>], this.tiles[0]] = [this.tiles[0], this.tiles[n as NonNullable<number>]];
|
|
||||||
return this.tiles.shift();
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public count(family: number, value: number): number {
|
const tile = this.tiles.pop()!;
|
||||||
let n = 0;
|
// Update the index map
|
||||||
for (let i = 0; i < this.tiles.length; i++) {
|
const key = this.getTileKey(tile.getFamily(), tile.getValue());
|
||||||
if (
|
const indices = this.tileIndexMap.get(key);
|
||||||
this.tiles[i].getFamily() === family &&
|
|
||||||
this.tiles[i].getValue() === value
|
|
||||||
) {
|
|
||||||
n++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
public shuffle(): undefined {
|
if (indices && indices.length > 0) {
|
||||||
let newArray: Array<Tile> = [];
|
indices.pop(); // Remove the last index
|
||||||
while (this.tiles.length > 0) {
|
if (indices.length === 0) {
|
||||||
let n = Math.floor(Math.random() * this.tiles.length);
|
this.tileIndexMap.delete(key);
|
||||||
[this.tiles[n], this.tiles[0]] = [this.tiles[0], this.tiles[n]];
|
} else {
|
||||||
newArray.push(this.tiles.shift() as NonNullable<Tile>);
|
this.tileIndexMap.set(key, indices);
|
||||||
}
|
}
|
||||||
this.tiles = newArray;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public getRandomHand(): Hand {
|
return tile;
|
||||||
let hand = new Hand();
|
}
|
||||||
this.shuffle();
|
|
||||||
for (let i = 0; i < 13; i++) {
|
|
||||||
hand.push(this.pop());
|
|
||||||
}
|
|
||||||
return hand;
|
|
||||||
}
|
|
||||||
|
|
||||||
public cleanup(): void {
|
/**
|
||||||
this.tiles.forEach(tile => tile.cleanup());
|
* Adds a tile to the deck
|
||||||
this.tiles = [];
|
*/
|
||||||
}
|
public push(tile: Tile): void {
|
||||||
|
const index = this.tiles.length;
|
||||||
|
this.tiles.push(tile);
|
||||||
|
|
||||||
public async preload(): Promise<void> {
|
// Update the index map
|
||||||
for (let i = 0; i < this.tiles.length; i++) {
|
const key = this.getTileKey(tile.getFamily(), tile.getValue());
|
||||||
await this.tiles[i].preloadImg();
|
const indices = this.tileIndexMap.get(key) || [];
|
||||||
}
|
indices.push(index);
|
||||||
}
|
this.tileIndexMap.set(key, indices);
|
||||||
|
}
|
||||||
|
|
||||||
private initTiles(allowRed: boolean): undefined {
|
/**
|
||||||
for (let i = 1; i < 6; i++) {
|
* Finds and removes a specific tile from the deck
|
||||||
if (i < 4) { // famille
|
*/
|
||||||
for (let j = 1; j < 10; j++) {
|
public find(family: number, value: number): Tile | undefined {
|
||||||
this.tiles.push(new Tile(i, j, false));
|
const key = this.getTileKey(family, value);
|
||||||
this.tiles.push(new Tile(i, j, false));
|
const indices = this.tileIndexMap.get(key);
|
||||||
this.tiles.push(new Tile(i, j, false));
|
|
||||||
if (j === 5 && allowRed) {
|
if (!indices || indices.length === 0) {
|
||||||
this.tiles.push(new Tile(i, j, true));
|
return undefined;
|
||||||
} else {
|
}
|
||||||
this.tiles.push(new Tile(i, j, false));
|
|
||||||
}
|
// Get the first occurrence
|
||||||
}
|
const index = indices[0];
|
||||||
} else if (i === 4) { // vent
|
if (index >= this.tiles.length) {
|
||||||
for (let j = 1; j < 5; j++) {
|
// Handle potential out-of-sync errors
|
||||||
for (let k = 0; k < 4; k++) {
|
this.rebuildIndexMap();
|
||||||
this.tiles.push(new Tile(i, j, false));
|
return this.find(family, value);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else if (i === 5) { // dragon
|
// Swap with the first element for efficient removal
|
||||||
for (let j = 1; j < 4; j++) {
|
[this.tiles[index], this.tiles[0]] = [this.tiles[0], this.tiles[index]];
|
||||||
for (let k = 0; k < 4; k++) {
|
|
||||||
this.tiles.push(new Tile(i, j, false));
|
// Update indices in the map
|
||||||
}
|
this.updateIndicesAfterSwap(0, index);
|
||||||
}
|
|
||||||
}
|
// Remove and return the tile
|
||||||
}
|
const tile = this.tiles.shift();
|
||||||
}
|
|
||||||
|
// Update all indices after shift
|
||||||
|
this.decrementIndicesAfterShift();
|
||||||
|
|
||||||
|
return tile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts the number of tiles with specific family and value
|
||||||
|
*/
|
||||||
|
public count(family: number, value: number): number {
|
||||||
|
const key = this.getTileKey(family, value);
|
||||||
|
const indices = this.tileIndexMap.get(key);
|
||||||
|
return indices ? indices.length : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shuffles the deck using Fisher-Yates algorithm
|
||||||
|
*/
|
||||||
|
public shuffle(): void {
|
||||||
|
// Fisher-Yates shuffle algorithm
|
||||||
|
for (let i = this.tiles.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[this.tiles[i], this.tiles[j]] = [this.tiles[j], this.tiles[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild the index map after shuffling
|
||||||
|
this.rebuildIndexMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a random hand from the deck
|
||||||
|
*/
|
||||||
|
public getRandomHand(): Hand {
|
||||||
|
const hand = new Hand();
|
||||||
|
this.shuffle();
|
||||||
|
|
||||||
|
// Handle case where deck doesn't have enough tiles
|
||||||
|
if (this.tiles.length < 13) {
|
||||||
|
throw new Error("Not enough tiles in deck to create a hand");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < 13; i++) {
|
||||||
|
hand.push(this.pop());
|
||||||
|
}
|
||||||
|
|
||||||
|
return hand;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up resources used by tiles and empties the deck
|
||||||
|
*/
|
||||||
|
public cleanup(): void {
|
||||||
|
this.tiles.forEach(tile => tile.cleanup());
|
||||||
|
this.tiles = [];
|
||||||
|
this.tileIndexMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preloads all tile images
|
||||||
|
*/
|
||||||
|
public async preload(): Promise<void> {
|
||||||
|
const preloadPromises = this.tiles.map(tile => tile.preloadImg());
|
||||||
|
await Promise.all(preloadPromises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a unique key for each tile type
|
||||||
|
*/
|
||||||
|
private getTileKey(family: number, value: number): TileKey {
|
||||||
|
return `${family}-${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the index map after swapping two tiles
|
||||||
|
*/
|
||||||
|
private updateIndicesAfterSwap(index1: number, index2: number): void {
|
||||||
|
if (index1 === index2) return;
|
||||||
|
|
||||||
|
const tile1 = this.tiles[index1];
|
||||||
|
const tile2 = this.tiles[index2];
|
||||||
|
|
||||||
|
const key1 = this.getTileKey(tile1.getFamily(), tile1.getValue());
|
||||||
|
const key2 = this.getTileKey(tile2.getFamily(), tile2.getValue());
|
||||||
|
|
||||||
|
const indices1 = this.tileIndexMap.get(key1) || [];
|
||||||
|
const indices2 = this.tileIndexMap.get(key2) || [];
|
||||||
|
|
||||||
|
// Update indices
|
||||||
|
const idx1 = indices1.indexOf(index2);
|
||||||
|
const idx2 = indices2.indexOf(index1);
|
||||||
|
|
||||||
|
if (idx1 !== -1) indices1[idx1] = index1;
|
||||||
|
if (idx2 !== -1) indices2[idx2] = index2;
|
||||||
|
|
||||||
|
this.tileIndexMap.set(key1, indices1);
|
||||||
|
this.tileIndexMap.set(key2, indices2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrements all indices after a shift operation
|
||||||
|
*/
|
||||||
|
private decrementIndicesAfterShift(): void {
|
||||||
|
for (const [key, indices] of this.tileIndexMap.entries()) {
|
||||||
|
this.tileIndexMap.set(
|
||||||
|
key,
|
||||||
|
indices
|
||||||
|
.filter(idx => idx !== 0) // Remove the index 0 that was shifted
|
||||||
|
.map(idx => (idx > 0 ? idx - 1 : idx)) // Decrement all indices
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clean up empty entries
|
||||||
|
if (this.tileIndexMap.get(key)?.length === 0) {
|
||||||
|
this.tileIndexMap.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuilds the entire index map from scratch
|
||||||
|
*/
|
||||||
|
private rebuildIndexMap(): void {
|
||||||
|
this.tileIndexMap.clear();
|
||||||
|
|
||||||
|
for (let i = 0; i < this.tiles.length; i++) {
|
||||||
|
const tile = this.tiles[i];
|
||||||
|
const key = this.getTileKey(tile.getFamily(), tile.getValue());
|
||||||
|
const indices = this.tileIndexMap.get(key) || [];
|
||||||
|
indices.push(i);
|
||||||
|
this.tileIndexMap.set(key, indices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the deck with all tiles
|
||||||
|
*/
|
||||||
|
private initTiles(allowRed: boolean): void {
|
||||||
|
// Create suits (families 1-3)
|
||||||
|
for (let family = 1; family <= 3; family++) {
|
||||||
|
for (let value = 1; value <= 9; value++) {
|
||||||
|
// Each value appears 4 times in a suit
|
||||||
|
const isRedFive = value === 5 && allowRed;
|
||||||
|
|
||||||
|
// Add 3 regular tiles
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
this.push(new Tile(family, value, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the 4th tile (potentially red five)
|
||||||
|
this.push(new Tile(family, value, isRedFive));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create winds (family 4)
|
||||||
|
for (let value = 1; value <= 4; value++) {
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
this.push(new Tile(4, value, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create dragons (family 5)
|
||||||
|
for (let value = 1; value <= 3; value++) {
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
this.push(new Tile(5, value, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,144 +1,256 @@
|
||||||
import { Deck } from "../deck";
|
import { Deck } from "../deck";
|
||||||
import { Hand } from "../hand"
|
import { Hand } from "../hand";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
cleanup: () => void;
|
cleanup: () => void;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
export {}
|
|
||||||
|
|
||||||
const canvas = document.getElementById("myCanvas") as HTMLCanvasElement;
|
|
||||||
|
|
||||||
async function preloadDeck(deck: Deck) {
|
|
||||||
await deck.preload();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function display () {
|
export {};
|
||||||
if (canvas) {
|
|
||||||
const ctx = canvas.getContext("2d") as NonNullable<CanvasRenderingContext2D>;
|
|
||||||
|
|
||||||
if (ctx) {
|
class RiichiDisplay {
|
||||||
// double buffering
|
private canvas: HTMLCanvasElement;
|
||||||
const offScreenCanvas = document.createElement('canvas') as HTMLCanvasElement;
|
private ctx: CanvasRenderingContext2D;
|
||||||
offScreenCanvas.width = canvas.width;
|
private offScreenCanvas: HTMLCanvasElement;
|
||||||
offScreenCanvas.height = canvas.height;
|
private offScreenCtx: CanvasRenderingContext2D;
|
||||||
const offScreenCtx = offScreenCanvas.getContext('2d') as NonNullable<CanvasRenderingContext2D>;
|
|
||||||
|
|
||||||
//animation parameter
|
private deck: Deck;
|
||||||
let lastTime = 0;
|
private hands: Hand[] = [];
|
||||||
const FPS = 30;
|
private edeck: Deck;
|
||||||
const interval = 1000 / FPS;
|
private ehand: Hand;
|
||||||
|
|
||||||
// tuiles
|
private selectedTile: number | undefined = undefined;
|
||||||
let x = 100;
|
private animationFrameId: number | null = null;
|
||||||
let y = 150;
|
private isDirty: boolean = true;
|
||||||
let os = 75;
|
|
||||||
let size = 0.75;
|
|
||||||
const deck = new Deck(false);
|
|
||||||
await preloadDeck(deck);
|
|
||||||
|
|
||||||
let hands: Array<Hand> = [];
|
// Constants
|
||||||
for (let i = 0; i < 4; i++) {
|
private readonly FPS: number = 30;
|
||||||
const hand = deck.getRandomHand();
|
private readonly INTERVAL: number = 1000 / this.FPS;
|
||||||
hand.sort();
|
private readonly X: number = 100;
|
||||||
hands.push(hand);
|
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;
|
||||||
|
|
||||||
// interactive hand
|
// Cache for mouse hit detection
|
||||||
const edeck = new Deck(false);
|
private tileRects: Array<{x: number, y: number, width: number, height: number}> = [];
|
||||||
await edeck.preload();
|
|
||||||
const ehand = edeck.getRandomHand();
|
|
||||||
ehand.push(edeck.pop());
|
|
||||||
ehand.sort();
|
|
||||||
|
|
||||||
let selectedTile: number|undefined = undefined;
|
constructor() {
|
||||||
|
const canvas = document.getElementById("myCanvas") as HTMLCanvasElement;
|
||||||
|
if (!canvas) {
|
||||||
|
throw new Error("Canvas introuvable dans le DOM.");
|
||||||
|
}
|
||||||
|
|
||||||
// function to draw
|
this.canvas = canvas;
|
||||||
const drawCanvas = async () => {
|
const ctx = canvas.getContext("2d");
|
||||||
// clean screeen
|
if (!ctx) {
|
||||||
offScreenCtx.clearRect(0, 0, canvas.width, canvas.height);
|
throw new Error("Impossible d'obtenir le contexte du canvas.");
|
||||||
|
}
|
||||||
|
this.ctx = ctx;
|
||||||
|
|
||||||
// tapis
|
// Create off-screen canvas for double buffering
|
||||||
offScreenCtx.fillStyle = "#007730";
|
this.offScreenCanvas = document.createElement('canvas');
|
||||||
offScreenCtx.fillRect(50, 50, 1000, 1000);
|
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;
|
||||||
|
|
||||||
// texte
|
// Initialize decks
|
||||||
offScreenCtx.fillStyle = "#DFDFFF";
|
this.deck = new Deck(false);
|
||||||
offScreenCtx.font = "50px serif";
|
this.edeck = new Deck(false);
|
||||||
offScreenCtx.fillText("Exemples de main:", 65, 100);
|
|
||||||
|
|
||||||
// example hands
|
// Initialize with empty hand (will be populated after preload)
|
||||||
for (let i = 0; i < hands.length; i++) {
|
this.ehand = new Hand();
|
||||||
hands[i].drawHand(offScreenCtx, x, y + i * size * (100 + os), 5, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
// dynamic hand
|
// Set up event listeners
|
||||||
ehand.isolate = true;
|
this.setupEventListeners();
|
||||||
ehand.drawHand(offScreenCtx, x, 800, 5, size, selectedTile);
|
|
||||||
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
// Calculate tile hit areas once
|
||||||
ctx.drawImage(offScreenCanvas, 0, 0);
|
this.calculateTileHitAreas();
|
||||||
}
|
}
|
||||||
|
|
||||||
const animationLoop = (currentTime: number) => {
|
private calculateTileHitAreas(): void {
|
||||||
const deltaTime = currentTime - lastTime;
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (deltaTime >= interval) {
|
private setupEventListeners(): void {
|
||||||
lastTime = currentTime;
|
this.canvas.addEventListener("mousemove", this.handleMouseMove.bind(this));
|
||||||
drawCanvas();
|
this.canvas.addEventListener("mousedown", this.handleMouseDown.bind(this));
|
||||||
}
|
}
|
||||||
requestAnimationFrame(animationLoop);
|
|
||||||
}
|
|
||||||
|
|
||||||
// mouse event
|
private handleMouseMove(event: MouseEvent): void {
|
||||||
canvas.addEventListener(
|
const rect = this.canvas.getBoundingClientRect();
|
||||||
"mousemove",
|
const mouseX = event.clientX - rect.left;
|
||||||
(event) => {
|
const mouseY = event.clientY - rect.top;
|
||||||
const rect = canvas.getBoundingClientRect();
|
|
||||||
const mouseX = event.clientX - rect.left - x;
|
|
||||||
const mouseY = event.clientY - rect.top;
|
|
||||||
|
|
||||||
let q = Math.floor(mouseX / (80 * size));
|
// Check if cursor is over any tile using pre-calculated hit areas
|
||||||
let r = mouseX - q * 80 * size;
|
const oldSelectedTile = this.selectedTile;
|
||||||
if (r <= 75 && q >= 0 && q < 14 && mouseY >= 800 && mouseY <= 800 + 100*size) {
|
this.selectedTile = undefined;
|
||||||
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);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.cleanup = () => {
|
// Only mark as dirty if selection changed
|
||||||
deck.cleanup();
|
if (oldSelectedTile !== this.selectedTile) {
|
||||||
hands.forEach(hand => hand.cleanup());
|
this.isDirty = true;
|
||||||
hands = [];
|
}
|
||||||
edeck.cleanup();
|
}
|
||||||
ehand.cleanup();
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
offScreenCtx.clearRect(0, 0, offScreenCanvas.width, offScreenCanvas.height);
|
|
||||||
selectedTile = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
private handleMouseDown(): void {
|
||||||
console.error("Impossible d'obtenir le contexte du canvas.");
|
if (this.selectedTile !== undefined) {
|
||||||
}
|
this.edeck.push(this.ehand.eject(this.selectedTile));
|
||||||
} else {
|
this.edeck.shuffle();
|
||||||
console.error("Canvas introuvable dans le DOM.");
|
this.ehand.sort();
|
||||||
}
|
this.ehand.push(this.edeck.pop());
|
||||||
|
this.isDirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async initialize(): Promise<void> {
|
||||||
|
// Preload all assets in parallel
|
||||||
|
await Promise.all([
|
||||||
|
this.deck.preload(),
|
||||||
|
this.edeck.preload()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Generate sample hands
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
const hand = this.deck.getRandomHand();
|
||||||
|
hand.sort();
|
||||||
|
this.hands.push(hand);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize interactive hand
|
||||||
|
this.ehand = this.edeck.getRandomHand();
|
||||||
|
this.ehand.push(this.edeck.pop());
|
||||||
|
this.ehand.sort();
|
||||||
|
|
||||||
|
// Initial draw
|
||||||
|
this.drawCanvas();
|
||||||
|
|
||||||
|
// Start animation loop
|
||||||
|
this.startAnimationLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawCanvas(): void {
|
||||||
|
// Only redraw if something changed (dirty flag)
|
||||||
|
if (!this.isDirty) return;
|
||||||
|
|
||||||
|
const ctx = this.offScreenCtx;
|
||||||
|
|
||||||
|
// Clear canvas
|
||||||
|
ctx.clearRect(0, 0, this.offScreenCanvas.width, this.offScreenCanvas.height);
|
||||||
|
|
||||||
|
// Draw background
|
||||||
|
ctx.fillStyle = "#007730";
|
||||||
|
ctx.fillRect(50, 50, 1000, 1000);
|
||||||
|
|
||||||
|
// Draw title
|
||||||
|
ctx.fillStyle = "#DFDFFF";
|
||||||
|
ctx.font = "50px serif";
|
||||||
|
ctx.fillText("Exemples de main:", 65, 100);
|
||||||
|
|
||||||
|
// Draw example hands
|
||||||
|
for (let i = 0; i < this.hands.length; i++) {
|
||||||
|
this.hands[i].drawHand(
|
||||||
|
ctx,
|
||||||
|
this.X,
|
||||||
|
this.Y + i * this.SIZE * (100 + this.OS),
|
||||||
|
5,
|
||||||
|
this.SIZE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw interactive hand
|
||||||
|
this.ehand.isolate = true;
|
||||||
|
this.ehand.drawHand(
|
||||||
|
ctx,
|
||||||
|
this.X,
|
||||||
|
800,
|
||||||
|
5,
|
||||||
|
this.SIZE,
|
||||||
|
this.selectedTile
|
||||||
|
);
|
||||||
|
|
||||||
|
// Flip double buffer
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
this.ctx.drawImage(this.offScreenCanvas, 0, 0);
|
||||||
|
|
||||||
|
// Reset dirty flag
|
||||||
|
this.isDirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private startAnimationLoop(): void {
|
||||||
|
let lastTime = 0;
|
||||||
|
|
||||||
|
const animationLoop = (currentTime: number) => {
|
||||||
|
const deltaTime = currentTime - lastTime;
|
||||||
|
|
||||||
|
if (deltaTime >= this.INTERVAL) {
|
||||||
|
lastTime = currentTime - (deltaTime % this.INTERVAL);
|
||||||
|
this.drawCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.animationFrameId = requestAnimationFrame(animationLoop);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.animationFrameId = requestAnimationFrame(animationLoop);
|
||||||
|
}
|
||||||
|
|
||||||
|
public cleanup(): void {
|
||||||
|
// Cancel animation loop
|
||||||
|
if (this.animationFrameId !== null) {
|
||||||
|
cancelAnimationFrame(this.animationFrameId);
|
||||||
|
this.animationFrameId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up resources
|
||||||
|
this.deck.cleanup();
|
||||||
|
this.hands.forEach(hand => hand.cleanup());
|
||||||
|
this.hands = [];
|
||||||
|
this.edeck.cleanup();
|
||||||
|
this.ehand.cleanup();
|
||||||
|
|
||||||
|
// Clear canvases
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
this.offScreenCtx.clearRect(0, 0, this.offScreenCanvas.width, this.offScreenCanvas.height);
|
||||||
|
|
||||||
|
// Reset state
|
||||||
|
this.selectedTile = undefined;
|
||||||
|
this.isDirty = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
display();
|
// Initialize and start
|
||||||
|
const riichiDisplay = new RiichiDisplay();
|
||||||
|
riichiDisplay.initialize().catch(error => {
|
||||||
|
console.error("Erreur lors de l'initialisation:", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expose cleanup function for window
|
||||||
|
window.cleanup = () => {
|
||||||
|
riichiDisplay.cleanup();
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Deck } from "../deck";
|
import { Deck } from "../deck";
|
||||||
import { Hand } from "../hand";
|
import { Hand } from "../hand";
|
||||||
import { Group } from "../group"
|
import { Group } from "../group";
|
||||||
|
|
||||||
// Configuration globale
|
// Configuration globale
|
||||||
const CANVAS_ID = "myCanvas";
|
const CANVAS_ID = "myCanvas";
|
||||||
|
|
@ -9,185 +9,373 @@ const BG_RECT = { x: 50, y: 50, w: 1000, h: 1000 };
|
||||||
const FPS = 30;
|
const FPS = 30;
|
||||||
const FRAME_INTERVAL = 1000 / FPS;
|
const FRAME_INTERVAL = 1000 / FPS;
|
||||||
|
|
||||||
// variables globales
|
/**
|
||||||
const DECKS: Array<Deck> = [];
|
* Classe RiichiDisplay responsable de la gestion de l'affichage du jeu Riichi Mahjong
|
||||||
const HANDS: Array<Hand> = [];
|
*/
|
||||||
let selectedTile: number|undefined = undefined;
|
class RiichiDisplay {
|
||||||
|
// Canvas principal et contexte
|
||||||
|
private canvas: HTMLCanvasElement;
|
||||||
|
private ctx: CanvasRenderingContext2D;
|
||||||
|
|
||||||
// Optimisation des références
|
// Canvas pour le double-buffering
|
||||||
let animationFrameId: number;
|
private staticCanvas: HTMLCanvasElement;
|
||||||
let lastFrameTime = 0;
|
private staticCtx: CanvasRenderingContext2D;
|
||||||
const callbacks: Array<() => void> = [];
|
|
||||||
|
|
||||||
// Pré-calcul des dimensions
|
// Ressources du jeu
|
||||||
const canvas = document.getElementById(CANVAS_ID) as HTMLCanvasElement;
|
private decks: Array<Deck> = [];
|
||||||
const ctx = canvas.getContext("2d") as NonNullable<CanvasRenderingContext2D>;
|
private hands: Array<Hand> = [];
|
||||||
|
|
||||||
// Cache statique
|
// État de l'interface
|
||||||
const staticCanvas = document.createElement('canvas') as HTMLCanvasElement;
|
private selectedTile: number | undefined = undefined;
|
||||||
const staticCtx = staticCanvas.getContext("2d") as NonNullable<CanvasRenderingContext2D>;
|
private isDirty: boolean = true;
|
||||||
staticCanvas.width = canvas.width;
|
|
||||||
staticCanvas.height = canvas.height;
|
|
||||||
|
|
||||||
// Pré-rendu du fond
|
// Animation et événements
|
||||||
function prerenderBackground() {
|
private animationFrameId: number | null = null;
|
||||||
staticCtx.clearRect(0, 0, canvas.width, canvas.height);
|
private lastFrameTime: number = 0;
|
||||||
staticCtx.fillStyle = BG_COLOR;
|
private cleanupCallbacks: Array<() => void> = [];
|
||||||
staticCtx.fillRect(BG_RECT.x, BG_RECT.y, BG_RECT.w, BG_RECT.h);
|
|
||||||
};
|
|
||||||
|
|
||||||
function drawFrame() {
|
// Constantes pour le rendu
|
||||||
if (!ctx) return;
|
private readonly BASE_X: number = 300;
|
||||||
|
private readonly BASE_Y: number = 150;
|
||||||
|
private readonly X_OFFSET: number = 250;
|
||||||
|
private readonly Y_OFFSET: number = 100;
|
||||||
|
private readonly INTERACTIVE_X: number = 100;
|
||||||
|
private readonly INTERACTIVE_Y: number = 800;
|
||||||
|
private readonly SIZE: number = 0.75;
|
||||||
|
private readonly TILE_WIDTH: number = 80 * this.SIZE;
|
||||||
|
private readonly MAX_TILES: number = 14;
|
||||||
|
|
||||||
// Effacement intelligent (uniquement la zone nécessaire)
|
// Zones de détection pour l'interaction souris
|
||||||
prerenderBackground();
|
private tileRects: Array<{ x: number, y: number, width: number, height: number }> = [];
|
||||||
|
|
||||||
// Ici viendrait le dessin des éléments dynamiques
|
constructor(canvasId: string = CANVAS_ID) {
|
||||||
// Par exemple:
|
// Initialisation du canvas
|
||||||
// drawDeck();
|
const canvas = document.getElementById(canvasId) as HTMLCanvasElement;
|
||||||
// drawHands();
|
if (!canvas) {
|
||||||
let x = 300;
|
throw new Error(`Canvas avec ID '${canvasId}' introuvable`);
|
||||||
let y = 150;
|
}
|
||||||
let xos = 250;
|
this.canvas = canvas;
|
||||||
let yos = 100;
|
|
||||||
let size = 0.75;
|
|
||||||
|
|
||||||
staticCtx.fillStyle = "#DFDFFF";
|
// Récupération du contexte 2D
|
||||||
staticCtx.font = "50px serif";
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error("Impossible d'obtenir le contexte 2D du canvas");
|
||||||
|
}
|
||||||
|
this.ctx = ctx;
|
||||||
|
|
||||||
staticCtx.fillText("Chii:", 75, y + 100 * size - 5);
|
// Initialisation du canvas pour le double-buffering
|
||||||
HANDS[0].drawHand(staticCtx, x, y, 5, 0.75); // chii
|
this.staticCanvas = document.createElement('canvas');
|
||||||
HANDS[1].drawHand(staticCtx, x + (75+xos)*size, y, 5, 0.75);
|
this.staticCanvas.width = canvas.width;
|
||||||
|
this.staticCanvas.height = canvas.height;
|
||||||
|
|
||||||
staticCtx.fillText("Pon:", 75, y + (100+yos)*size + 100 * size - 5);
|
const staticCtx = this.staticCanvas.getContext("2d");
|
||||||
HANDS[2].drawHand(staticCtx, x, y + (100+yos)*size, 5, 0.75); // pon
|
if (!staticCtx) {
|
||||||
HANDS[3].drawHand(staticCtx, x + (75+xos)*size, y + (100+yos)*size, 5, 0.75);
|
throw new Error("Impossible d'obtenir le contexte du canvas statique");
|
||||||
HANDS[4].drawHand(staticCtx, x + 2*(75+xos)*size, y + 2*(100+yos)*size, 5, 0.75);
|
}
|
||||||
|
this.staticCtx = staticCtx;
|
||||||
|
|
||||||
staticCtx.fillText("Invalide:", 75, y + 2*(100+yos)*size + 100 * size - 5);
|
// Pré-calcul des zones de détection
|
||||||
HANDS[5].drawHand(staticCtx, x, y + 2*(100+yos)*size, 5, 0.75); // wrong
|
this.calculateTileHitAreas();
|
||||||
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);
|
* 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let groups = HANDS[8].toGroup();
|
/**
|
||||||
if (groups !== undefined) {
|
* Pré-rendu du fond statique
|
||||||
staticCtx.fillStyle = "#FF0000";
|
*/
|
||||||
staticCtx.font = "50px serif";
|
private prerenderBackground(): void {
|
||||||
staticCtx.fillText("Tous les groupes sont formés !", 100, 750);
|
this.staticCtx.clearRect(0, 0, this.staticCanvas.width, this.staticCanvas.height);
|
||||||
}
|
this.staticCtx.fillStyle = BG_COLOR;
|
||||||
groups = [];
|
this.staticCtx.fillRect(BG_RECT.x, BG_RECT.y, BG_RECT.w, BG_RECT.h);
|
||||||
|
}
|
||||||
|
|
||||||
// Dessin du cache statique
|
/**
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
* Dessine une frame complète
|
||||||
ctx.drawImage(staticCanvas, 0, 0);
|
*/
|
||||||
}
|
private drawFrame(): void {
|
||||||
|
// Vérifier si un redessinage est nécessaire
|
||||||
|
if (!this.isDirty) return;
|
||||||
|
|
||||||
function animationLoop(currentTime: number) {
|
// Pré-rendu du fond statique
|
||||||
animationFrameId = requestAnimationFrame(animationLoop);
|
this.prerenderBackground();
|
||||||
|
|
||||||
const deltaTime = currentTime - lastFrameTime;
|
// Dessin des éléments statiques et dynamiques
|
||||||
if (deltaTime < FRAME_INTERVAL) return;
|
this.drawHandsAndLabels();
|
||||||
|
|
||||||
lastFrameTime = currentTime - (deltaTime % FRAME_INTERVAL);
|
// Affichage des informations sur les groupes
|
||||||
drawFrame();
|
this.checkAndDisplayGroups();
|
||||||
}
|
|
||||||
|
|
||||||
function initEventListeners() {
|
// Copie du canvas statique vers le canvas principal
|
||||||
const handlers = {
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
mousemove: (e: MouseEvent) => {
|
this.ctx.drawImage(this.staticCanvas, 0, 0);
|
||||||
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));
|
// Réinitialisation du flag de modification
|
||||||
let r = mouseX - q * 80 * size;
|
this.isDirty = false;
|
||||||
if (r <= 75 && q >= 0 && q < 14 && mouseY >= 800 && mouseY <= 800 + 100*size) {
|
}
|
||||||
selectedTile = q;
|
|
||||||
} else {
|
/**
|
||||||
selectedTile = undefined;
|
* Dessine les mains et leurs étiquettes
|
||||||
}
|
*/
|
||||||
},
|
private drawHandsAndLabels(): void {
|
||||||
mousedown: (e: MouseEvent) => {
|
const ctx = this.staticCtx;
|
||||||
// Logique de gestion du clic de souris
|
|
||||||
if (selectedTile !== undefined) {
|
// Configurer le style de texte
|
||||||
DECKS[0].push(HANDS[8].eject(selectedTile));
|
ctx.fillStyle = "#DFDFFF";
|
||||||
DECKS[0].shuffle();
|
ctx.font = "50px serif";
|
||||||
HANDS[8].sort();
|
|
||||||
HANDS[8].push(DECKS[0].pop());
|
// Dessiner les mains "Chii"
|
||||||
}
|
ctx.fillText("Chii:", 75, this.BASE_Y + 100 * this.SIZE - 5);
|
||||||
}
|
this.hands[0].drawHand(ctx, this.BASE_X, this.BASE_Y, 5, this.SIZE);
|
||||||
|
this.hands[1].drawHand(ctx, this.BASE_X + (75 + this.X_OFFSET) * this.SIZE, this.BASE_Y, 5, this.SIZE);
|
||||||
|
|
||||||
|
// Dessiner les mains "Pon"
|
||||||
|
ctx.fillText("Pon:", 75, this.BASE_Y + (100 + this.Y_OFFSET) * this.SIZE + 100 * this.SIZE - 5);
|
||||||
|
this.hands[2].drawHand(ctx, this.BASE_X, this.BASE_Y + (100 + this.Y_OFFSET) * this.SIZE, 5, this.SIZE);
|
||||||
|
this.hands[3].drawHand(ctx, this.BASE_X + (75 + this.X_OFFSET) * this.SIZE, this.BASE_Y + (100 + this.Y_OFFSET) * this.SIZE, 5, this.SIZE);
|
||||||
|
|
||||||
|
// Dessiner les mains "Invalide"
|
||||||
|
ctx.fillText("Invalide:", 75, this.BASE_Y + 2 * (100 + this.Y_OFFSET) * this.SIZE + 100 * this.SIZE - 5);
|
||||||
|
this.hands[5].drawHand(ctx, this.BASE_X, this.BASE_Y + 2 * (100 + this.Y_OFFSET) * this.SIZE, 5, this.SIZE);
|
||||||
|
this.hands[6].drawHand(ctx, this.BASE_X + (75 + this.X_OFFSET) * this.SIZE, this.BASE_Y + 2 * (100 + this.Y_OFFSET) * this.SIZE, 5, this.SIZE);
|
||||||
|
this.hands[7].drawHand(ctx, this.BASE_X + 2 * (75 + this.X_OFFSET) * this.SIZE, this.BASE_Y + 2 * (100 + this.Y_OFFSET) * this.SIZE, 5, this.SIZE);
|
||||||
|
|
||||||
|
// Main supplémentaire (position spéciale)
|
||||||
|
this.hands[4].drawHand(ctx, this.BASE_X + 2 * (75 + this.X_OFFSET) * this.SIZE, this.BASE_Y + (100 + this.Y_OFFSET) * this.SIZE, 5, this.SIZE);
|
||||||
|
|
||||||
|
// Main interactive
|
||||||
|
this.hands[8].isolate = true;
|
||||||
|
this.hands[8].drawHand(ctx, this.INTERACTIVE_X, this.INTERACTIVE_Y, 5, this.SIZE, this.selectedTile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie et affiche les informations sur les groupes formés
|
||||||
|
*/
|
||||||
|
private checkAndDisplayGroups(): void {
|
||||||
|
const groups = this.hands[8].toGroup();
|
||||||
|
if (groups !== undefined) {
|
||||||
|
this.staticCtx.fillStyle = "#FF0000";
|
||||||
|
this.staticCtx.font = "50px serif";
|
||||||
|
this.staticCtx.fillText("Tous les groupes sont formés !", 100, 750);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Démarre la boucle d'animation
|
||||||
|
*/
|
||||||
|
private startAnimationLoop(): void {
|
||||||
|
const animationLoop = (currentTime: number) => {
|
||||||
|
this.animationFrameId = requestAnimationFrame(animationLoop);
|
||||||
|
|
||||||
|
const deltaTime = currentTime - this.lastFrameTime;
|
||||||
|
if (deltaTime < FRAME_INTERVAL) return;
|
||||||
|
|
||||||
|
this.lastFrameTime = currentTime - (deltaTime % FRAME_INTERVAL);
|
||||||
|
this.drawFrame();
|
||||||
};
|
};
|
||||||
|
|
||||||
callbacks.push(() => {
|
this.animationFrameId = requestAnimationFrame(animationLoop);
|
||||||
canvas.removeEventListener('mousemove', handlers.mousemove);
|
}
|
||||||
canvas.removeEventListener('mousedown', handlers.mousedown);
|
|
||||||
|
/**
|
||||||
|
* Initialise les écouteurs d'événements
|
||||||
|
*/
|
||||||
|
private initEventListeners(): void {
|
||||||
|
const mouseMoveHandler = this.handleMouseMove.bind(this);
|
||||||
|
const mouseDownHandler = this.handleMouseDown.bind(this);
|
||||||
|
|
||||||
|
this.canvas.addEventListener('mousemove', mouseMoveHandler);
|
||||||
|
this.canvas.addEventListener('mousedown', mouseDownHandler);
|
||||||
|
|
||||||
|
// Enregistrer les callbacks de nettoyage
|
||||||
|
this.cleanupCallbacks.push(() => {
|
||||||
|
this.canvas.removeEventListener('mousemove', mouseMoveHandler);
|
||||||
|
this.canvas.removeEventListener('mousedown', mouseDownHandler);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
canvas.addEventListener('mousemove', handlers.mousemove);
|
/**
|
||||||
canvas.addEventListener('mousedown', handlers.mousedown);
|
* 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;
|
||||||
|
|
||||||
async function preloadDeck(deck: Deck) {
|
// Sauvegarde de l'état précédent pour détecter les changements
|
||||||
await deck.preload();
|
const previousSelectedTile = this.selectedTile;
|
||||||
}
|
this.selectedTile = undefined;
|
||||||
async function preloadHand(hand: Hand) {
|
|
||||||
await hand.preload();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cleanup() {
|
// Détection optimisée avec zones pré-calculées
|
||||||
cancelAnimationFrame(animationFrameId);
|
for (let i = 0; i < this.tileRects.length; i++) {
|
||||||
callbacks.forEach(fn => fn());
|
const tileRect = this.tileRects[i];
|
||||||
}
|
if (
|
||||||
|
mouseX >= tileRect.x &&
|
||||||
export async function initDisplay() {
|
mouseX <= tileRect.x + tileRect.width &&
|
||||||
if (!ctx) {
|
mouseY >= tileRect.y &&
|
||||||
console.error("Context canvas indisponible");
|
mouseY <= tileRect.y + tileRect.height
|
||||||
return;
|
) {
|
||||||
|
this.selectedTile = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Préchargement des ressources si nécessaire
|
// Marquer comme "dirty" uniquement si la sélection a changé
|
||||||
// const deck = new Deck();
|
if (previousSelectedTile !== this.selectedTile) {
|
||||||
// await preloadDeck(deck);
|
this.isDirty = true;
|
||||||
DECKS.push(
|
}
|
||||||
new Deck(false)
|
}
|
||||||
);
|
|
||||||
HANDS.push(
|
|
||||||
new Hand("s1s2s3"),
|
|
||||||
new Hand("m2m3m4"),
|
|
||||||
new Hand("p5p5p5"),
|
|
||||||
new Hand("w2w2w2"),
|
|
||||||
new Hand("d3d3d3"),
|
|
||||||
new Hand("s4p5m6"),
|
|
||||||
new Hand("m9s9p9"),
|
|
||||||
new Hand("d1d2d3"),
|
|
||||||
DECKS[0].getRandomHand()
|
|
||||||
);
|
|
||||||
await Promise.all(DECKS.map(d => preloadDeck(d)));
|
|
||||||
await Promise.all(HANDS.map(h => preloadHand(h)));
|
|
||||||
|
|
||||||
HANDS[8].push(DECKS[0].pop());
|
/**
|
||||||
HANDS[8].sort();
|
* 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());
|
||||||
|
|
||||||
initEventListeners();
|
// Marquer comme "dirty" pour forcer le redessinage
|
||||||
requestAnimationFrame(animationLoop);
|
this.isDirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Précharge un deck
|
||||||
|
*/
|
||||||
|
private async preloadDeck(deck: Deck): Promise<void> {
|
||||||
|
await deck.preload();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Précharge une main
|
||||||
|
*/
|
||||||
|
private async preloadHand(hand: Hand): Promise<void> {
|
||||||
|
await hand.preload();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise l'affichage et démarre l'application
|
||||||
|
*/
|
||||||
|
public async initialize(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Création du deck principal
|
||||||
|
this.decks.push(new Deck(false));
|
||||||
|
|
||||||
|
// Création des mains prédéfinies
|
||||||
|
this.hands.push(
|
||||||
|
new Hand("s1s2s3"), // Chii 1
|
||||||
|
new Hand("m2m3m4"), // Chii 2
|
||||||
|
new Hand("p5p5p5"), // Pon 1
|
||||||
|
new Hand("w2w2w2"), // Pon 2
|
||||||
|
new Hand("d3d3d3"), // Pon supplémentaire
|
||||||
|
new Hand("s4p5m6"), // Invalide 1
|
||||||
|
new Hand("m9s9p9"), // Invalide 2
|
||||||
|
new Hand("d1d2d3"), // Invalide 3
|
||||||
|
this.decks[0].getRandomHand() // Main interactive
|
||||||
|
);
|
||||||
|
|
||||||
|
// Préchargement parallèle des ressources
|
||||||
|
await Promise.all([
|
||||||
|
...this.decks.map(deck => this.preloadDeck(deck)),
|
||||||
|
...this.hands.map(hand => this.preloadHand(hand))
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Configuration de la main interactive
|
||||||
|
this.hands[8].push(this.decks[0].pop());
|
||||||
|
this.hands[8].sort();
|
||||||
|
|
||||||
|
// Initialisation des écouteurs d'événements
|
||||||
|
this.initEventListeners();
|
||||||
|
|
||||||
|
// Premier rendu
|
||||||
|
this.isDirty = true;
|
||||||
|
this.drawFrame();
|
||||||
|
|
||||||
|
// Démarrage de la boucle d'animation
|
||||||
|
this.startAnimationLoop();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur d'initialisation:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoie les ressources
|
||||||
|
*/
|
||||||
|
public cleanup(): void {
|
||||||
|
// Arrêter la boucle d'animation
|
||||||
|
if (this.animationFrameId !== null) {
|
||||||
|
cancelAnimationFrame(this.animationFrameId);
|
||||||
|
this.animationFrameId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exécuter tous les callbacks de nettoyage
|
||||||
|
this.cleanupCallbacks.forEach(callback => callback());
|
||||||
|
this.cleanupCallbacks = [];
|
||||||
|
|
||||||
|
// Nettoyer les ressources des decks et des mains
|
||||||
|
this.decks.forEach(deck => deck.cleanup());
|
||||||
|
this.hands.forEach(hand => hand.cleanup());
|
||||||
|
|
||||||
|
// Vider les collections
|
||||||
|
this.decks = [];
|
||||||
|
this.hands = [];
|
||||||
|
|
||||||
|
// Effacer les canvas
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
this.staticCtx.clearRect(0, 0, this.staticCanvas.width, this.staticCanvas.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instance globale pour le nettoyage
|
||||||
|
let displayInstance: RiichiDisplay | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fonction de nettoyage exportée
|
||||||
|
*/
|
||||||
|
export function cleanup(): void {
|
||||||
|
if (displayInstance) {
|
||||||
|
displayInstance.cleanup();
|
||||||
|
displayInstance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fonction d'initialisation exportée
|
||||||
|
*/
|
||||||
|
export async function initDisplay(): Promise<void> {
|
||||||
|
try {
|
||||||
|
displayInstance = new RiichiDisplay(CANVAS_ID);
|
||||||
|
await displayInstance.initialize();
|
||||||
window.cleanup = cleanup;
|
window.cleanup = cleanup;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de l'initialisation:", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Déclaration globale pour TypeScript
|
// Déclaration globale pour TypeScript
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
cleanup: () => void;
|
cleanup: () => void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialisation automatique si le script est chargé directement
|
// Initialisation automatique si le script est chargé directement
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
initDisplay().catch(console.error);
|
initDisplay().catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,122 +1,253 @@
|
||||||
import { Deck } from "../deck";
|
import { Deck } from "../deck";
|
||||||
import { Hand } from "../hand";
|
import { Hand } from "../hand";
|
||||||
import { Game } from "../game"
|
import { Game } from "../game";
|
||||||
|
|
||||||
// Configuration globale
|
class RiichiGameManager {
|
||||||
const CANVAS_ID = "myCanvas";
|
// Configuration globale
|
||||||
const BG_RECT = { x: 0, y: 0, w: 1050, h: 1050 };
|
private readonly CANVAS_ID: string = "myCanvas";
|
||||||
var MOUSE = { x: 0, y: 0 };
|
private readonly BG_RECT = { x: 0, y: 0, w: 1050, h: 1050 };
|
||||||
const FPS = 60;
|
private readonly FPS: number = 60;
|
||||||
const FRAME_INTERVAL = 1000 / FPS;
|
private readonly FRAME_INTERVAL: number = 1000 / this.FPS;
|
||||||
|
|
||||||
// variables globales
|
// Singleton instance
|
||||||
const DECKS: Array<Deck> = [];
|
private static instance: RiichiGameManager;
|
||||||
const HANDS: Array<Hand> = [];
|
|
||||||
var GAME: Game|undefined;
|
|
||||||
|
|
||||||
// Optimisation des références
|
// Canvas et contextes
|
||||||
let animationFrameId: number;
|
private canvas: HTMLCanvasElement;
|
||||||
let lastFrameTime = 0;
|
private ctx: CanvasRenderingContext2D;
|
||||||
const callbacks: Array<() => void> = [];
|
private staticCanvas: HTMLCanvasElement;
|
||||||
|
private staticCtx: CanvasRenderingContext2D;
|
||||||
|
|
||||||
// Pré-calcul des dimensions
|
// État du jeu
|
||||||
const canvas = document.getElementById(CANVAS_ID) as HTMLCanvasElement;
|
private mouse = { x: 0, y: 0 };
|
||||||
const ctx = canvas.getContext("2d") as NonNullable<CanvasRenderingContext2D>;
|
private game: Game | null = null;
|
||||||
canvas.width = BG_RECT.w;
|
private decks: Array<Deck> = [];
|
||||||
canvas.height = BG_RECT.h;
|
private hands: Array<Hand> = [];
|
||||||
|
|
||||||
// Cache statique
|
// Animation et boucle de jeu
|
||||||
const staticCanvas = document.createElement('canvas') as HTMLCanvasElement;
|
private animationFrameId: number | null = null;
|
||||||
const staticCtx = staticCanvas.getContext("2d") as NonNullable<CanvasRenderingContext2D>;
|
private lastFrameTime: number = 0;
|
||||||
staticCanvas.width = canvas.width;
|
private isInitialized: boolean = false;
|
||||||
staticCanvas.height = canvas.height;
|
|
||||||
|
|
||||||
function drawFrame() {
|
// Nettoyage
|
||||||
if (!ctx) return;
|
private cleanupCallbacks: Array<() => void> = [];
|
||||||
GAME?.draw(MOUSE);
|
|
||||||
}
|
|
||||||
|
|
||||||
function animationLoop(currentTime: number) {
|
/**
|
||||||
animationFrameId = requestAnimationFrame(animationLoop);
|
* Constructeur privé pour le Singleton
|
||||||
|
*/
|
||||||
|
private constructor() {
|
||||||
|
// Initialisation du canvas principal
|
||||||
|
const canvas = document.getElementById(this.CANVAS_ID) as HTMLCanvasElement;
|
||||||
|
if (!canvas) {
|
||||||
|
throw new Error(`Canvas avec ID '${this.CANVAS_ID}' introuvable`);
|
||||||
|
}
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.canvas.width = this.BG_RECT.w;
|
||||||
|
this.canvas.height = this.BG_RECT.h;
|
||||||
|
|
||||||
const deltaTime = currentTime - lastFrameTime;
|
// Récupération du contexte 2D
|
||||||
if (deltaTime < FRAME_INTERVAL) return;
|
const ctx = canvas.getContext("2d", { alpha: false });
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error("Impossible d'obtenir le contexte 2D du canvas");
|
||||||
|
}
|
||||||
|
this.ctx = ctx;
|
||||||
|
|
||||||
lastFrameTime = currentTime - (deltaTime % FRAME_INTERVAL);
|
// Initialisation du canvas pour le double-buffering
|
||||||
drawFrame();
|
this.staticCanvas = document.createElement('canvas');
|
||||||
}
|
this.staticCanvas.width = canvas.width;
|
||||||
|
this.staticCanvas.height = canvas.height;
|
||||||
|
|
||||||
function initEventListeners() {
|
const staticCtx = this.staticCanvas.getContext("2d", { alpha: false });
|
||||||
const handlers = {
|
if (!staticCtx) {
|
||||||
mousedown: (e: MouseEvent) => {
|
throw new Error("Impossible d'obtenir le contexte du canvas statique");
|
||||||
GAME?.click(e);
|
}
|
||||||
},
|
this.staticCtx = staticCtx;
|
||||||
mousemove: (e: MouseEvent) => {
|
}
|
||||||
MOUSE.x = e.x;
|
|
||||||
MOUSE.y = e.y;
|
/**
|
||||||
|
* Accès à l'instance Singleton
|
||||||
|
*/
|
||||||
|
public static getInstance(): RiichiGameManager {
|
||||||
|
if (!RiichiGameManager.instance) {
|
||||||
|
RiichiGameManager.instance = new RiichiGameManager();
|
||||||
|
}
|
||||||
|
return RiichiGameManager.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dessine une frame du jeu
|
||||||
|
*/
|
||||||
|
private drawFrame(): void {
|
||||||
|
if (this.game) {
|
||||||
|
this.game.draw(this.mouse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Démarre la boucle d'animation du jeu
|
||||||
|
*/
|
||||||
|
private startAnimationLoop(): void {
|
||||||
|
const animationLoop = (currentTime: number) => {
|
||||||
|
this.animationFrameId = requestAnimationFrame(animationLoop);
|
||||||
|
|
||||||
|
const deltaTime = currentTime - this.lastFrameTime;
|
||||||
|
if (deltaTime < this.FRAME_INTERVAL) return;
|
||||||
|
|
||||||
|
// Correction du time drift
|
||||||
|
this.lastFrameTime = currentTime - (deltaTime % this.FRAME_INTERVAL);
|
||||||
|
|
||||||
|
// Rendu d'une frame
|
||||||
|
this.drawFrame();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.animationFrameId = requestAnimationFrame(animationLoop);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise les écouteurs d'événements
|
||||||
|
*/
|
||||||
|
private initEventListeners(): void {
|
||||||
|
// Handler pour le déplacement de la souris
|
||||||
|
const mouseMoveHandler = (e: MouseEvent) => {
|
||||||
|
// Calcul des coordonnées relatives au canvas
|
||||||
|
const rect = this.canvas.getBoundingClientRect();
|
||||||
|
this.mouse.x = e.clientX - rect.left;
|
||||||
|
this.mouse.y = e.clientY - rect.top;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handler pour le clic de souris
|
||||||
|
const mouseDownHandler = (e: MouseEvent) => {
|
||||||
|
if (this.game) {
|
||||||
|
this.game.click(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
callbacks.push(() => {
|
// Enregistrement des écouteurs
|
||||||
canvas.removeEventListener('mousemove', handlers.mousemove);
|
this.canvas.addEventListener('mousemove', mouseMoveHandler);
|
||||||
canvas.removeEventListener('mousedown', handlers.mousedown);
|
this.canvas.addEventListener('mousedown', mouseDownHandler);
|
||||||
|
|
||||||
|
// Enregistrement des callbacks de nettoyage
|
||||||
|
this.cleanupCallbacks.push(() => {
|
||||||
|
this.canvas.removeEventListener('mousemove', mouseMoveHandler);
|
||||||
|
this.canvas.removeEventListener('mousedown', mouseDownHandler);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
canvas.addEventListener('mousemove', handlers.mousemove);
|
/**
|
||||||
canvas.addEventListener('mousedown', handlers.mousedown);
|
* Précharge un deck
|
||||||
}
|
*/
|
||||||
|
private async preloadDeck(deck: Deck): Promise<void> {
|
||||||
async function preloadDeck(deck: Deck) {
|
|
||||||
await deck.preload();
|
await deck.preload();
|
||||||
}
|
}
|
||||||
async function preloadHand(hand: Hand) {
|
|
||||||
await hand.preload();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cleanup() {
|
/**
|
||||||
cancelAnimationFrame(animationFrameId);
|
* Précharge une main
|
||||||
callbacks.forEach(fn => fn());
|
*/
|
||||||
}
|
private async preloadHand(hand: Hand): Promise<void> {
|
||||||
|
await hand.preload();
|
||||||
|
}
|
||||||
|
|
||||||
export async function initDisplay() {
|
/**
|
||||||
if (!ctx) {
|
* Initialise le jeu
|
||||||
console.error("Context canvas indisponible");
|
*/
|
||||||
return;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Load begining\n");
|
// Exécuter tous les callbacks de nettoyage
|
||||||
// Préchargement des ressources si nécessaire
|
this.cleanupCallbacks.forEach(callback => callback());
|
||||||
// const deck = new Deck();
|
this.cleanupCallbacks = [];
|
||||||
// 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();
|
|
||||||
|
|
||||||
console.log("Loaded completed\n");
|
// Nettoyer les ressources du jeu
|
||||||
initEventListeners();
|
if (this.game) {
|
||||||
requestAnimationFrame(animationLoop);
|
// Supposons que Game a une méthode cleanup
|
||||||
window.cleanup = cleanup;
|
(this.game as any).cleanup?.();
|
||||||
|
this.game = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nettoyer les ressources des decks et des mains
|
||||||
|
this.decks.forEach(deck => deck.cleanup());
|
||||||
|
this.hands.forEach(hand => hand.cleanup());
|
||||||
|
|
||||||
|
// Vider les collections
|
||||||
|
this.decks = [];
|
||||||
|
this.hands = [];
|
||||||
|
|
||||||
|
// Réinitialiser l'état
|
||||||
|
this.isInitialized = false;
|
||||||
|
this.lastFrameTime = 0;
|
||||||
|
this.mouse = { x: 0, y: 0 };
|
||||||
|
|
||||||
|
// Effacer les canvas
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
this.staticCtx.clearRect(0, 0, this.staticCanvas.width, this.staticCanvas.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction de nettoyage globale exportée
|
||||||
|
export function cleanup(): void {
|
||||||
|
RiichiGameManager.getInstance().cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction d'initialisation globale exportée
|
||||||
|
export async function initDisplay(): Promise<void> {
|
||||||
|
await RiichiGameManager.getInstance().initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Déclaration globale pour TypeScript
|
// Déclaration globale pour TypeScript
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
cleanup: () => void;
|
cleanup: () => void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialisation automatique si le script est chargé directement
|
// Initialisation automatique si le script est chargé directement
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
initDisplay().catch(console.error);
|
initDisplay().catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
1193
src/game.ts
1193
src/game.ts
File diff suppressed because it is too large
Load diff
217
src/group.ts
217
src/group.ts
|
|
@ -1,154 +1,91 @@
|
||||||
import { Tile } from "./tile"
|
import { Tile } from "./tile";
|
||||||
|
|
||||||
export class Group {
|
export class Group {
|
||||||
private tiles: Array<Tile>;
|
constructor(
|
||||||
private stolenFrom: number;
|
private tiles: Array<Tile>,
|
||||||
private belongsTo: number
|
private stolenFrom: number,
|
||||||
|
private belongsTo: number
|
||||||
|
) {}
|
||||||
|
|
||||||
public constructor(
|
public push(tile: Tile): void {
|
||||||
tiles: Array<Tile>,
|
this.tiles.push(tile);
|
||||||
stolenFrom: number,
|
}
|
||||||
belongsTo: number
|
|
||||||
) {
|
|
||||||
this.tiles = tiles;
|
|
||||||
this.stolenFrom = stolenFrom;
|
|
||||||
this.belongsTo = belongsTo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public push(tile: Tile): void {
|
public pop(): Tile | undefined {
|
||||||
this.tiles.push(tile);
|
return this.tiles.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
public pop(): Tile|undefined {
|
public getTiles(): Array<Tile> {
|
||||||
return this.tiles.pop();
|
return this.tiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTiles(): Array<Tile> {
|
public compare(g: Group): number {
|
||||||
return this.tiles;
|
// Compare les premiers tiles, puis les seconds si égalité
|
||||||
}
|
const firstComparison = this.tiles[0].compare(g.tiles[0]);
|
||||||
|
return firstComparison !== 0 ? firstComparison : this.tiles[1].compare(g.tiles[1]);
|
||||||
|
}
|
||||||
|
|
||||||
public compare(g: Group): number {
|
public drawGroup(
|
||||||
let c = this.tiles[0].compare(g.tiles[0]);
|
ctx: CanvasRenderingContext2D,
|
||||||
if (c !== 0) {
|
x: number,
|
||||||
return c;
|
y: number,
|
||||||
}
|
os: number,
|
||||||
return this.tiles[1].compare(g.tiles[1]);
|
size: number,
|
||||||
}
|
rotation: number,
|
||||||
|
selectedTile?: Tile
|
||||||
|
): void {
|
||||||
|
// Sauvegarde et rotation du contexte
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(525, 525);
|
||||||
|
ctx.rotate(rotation);
|
||||||
|
ctx.translate(-525, -525);
|
||||||
|
|
||||||
public drawGroup(
|
// Calcul des paramètres de dessin
|
||||||
ctx: CanvasRenderingContext2D,
|
const v = 75 * size;
|
||||||
x: number,
|
const w = 90 * size;
|
||||||
y: number,
|
const osy = 25 * size / 2;
|
||||||
os: number,
|
const p = (this.belongsTo - this.stolenFrom - 1 + 4) % 4;
|
||||||
size: number,
|
|
||||||
rotation: number,
|
|
||||||
selectedTile: Tile|undefined
|
|
||||||
): void {
|
|
||||||
ctx.save();
|
|
||||||
ctx.translate(525, 525);
|
|
||||||
ctx.rotate(rotation);
|
|
||||||
ctx.translate(-525, -525);
|
|
||||||
|
|
||||||
rotation = 0;
|
// Détermination du tile sélectionné
|
||||||
let v = 75 * size;
|
const sf = selectedTile === undefined ? -1 : selectedTile.getFamily();
|
||||||
let w = 90 * size;
|
const sv = selectedTile === undefined ? 0 : selectedTile.getValue();
|
||||||
let osy = 25 * size / 2;
|
|
||||||
let p = (this.belongsTo - this.stolenFrom - 1 + 4) % 4;
|
|
||||||
|
|
||||||
const sf = selectedTile === undefined ? -1 : selectedTile.getFamily();
|
// Fonction helper pour éviter la répétition de code
|
||||||
const sv = selectedTile === undefined ? 0 : selectedTile.getValue();
|
const drawTile = (tile: Tile, tx: number, ty: number, angle: number) => {
|
||||||
|
tile.drawTile(
|
||||||
|
ctx,
|
||||||
|
tx,
|
||||||
|
ty,
|
||||||
|
size,
|
||||||
|
false,
|
||||||
|
angle,
|
||||||
|
tile.isEqual(sf, sv)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
if (p === 0) {
|
const HALF_PI = Math.PI / 2;
|
||||||
this.tiles[0].drawTile(
|
|
||||||
ctx,
|
|
||||||
x,
|
|
||||||
y + osy,
|
|
||||||
size,
|
|
||||||
false,
|
|
||||||
3.141592 / 2,
|
|
||||||
this.tiles[0].isEqual(sf, sv),
|
|
||||||
);
|
|
||||||
this.tiles[1].drawTile(
|
|
||||||
ctx,
|
|
||||||
x + w,
|
|
||||||
y,
|
|
||||||
size,
|
|
||||||
false,
|
|
||||||
0,
|
|
||||||
this.tiles[1].isEqual(sf, sv),
|
|
||||||
);
|
|
||||||
this.tiles[2].drawTile(
|
|
||||||
ctx,
|
|
||||||
x + w + v + os * size,
|
|
||||||
y,
|
|
||||||
size,
|
|
||||||
false,
|
|
||||||
0,
|
|
||||||
this.tiles[2].isEqual(sf, sv),
|
|
||||||
);
|
|
||||||
|
|
||||||
} else if (p === 1) {
|
// Dessin selon la position
|
||||||
this.tiles[0].drawTile(
|
switch (p) {
|
||||||
ctx,
|
case 0:
|
||||||
x,
|
drawTile(this.tiles[0], x, y + osy, HALF_PI);
|
||||||
y,
|
drawTile(this.tiles[1], x + w, y, 0);
|
||||||
size,
|
drawTile(this.tiles[2], x + w + v + os * size, y, 0);
|
||||||
false,
|
break;
|
||||||
0,
|
case 1:
|
||||||
this.tiles[0].isEqual(sf, sv),
|
drawTile(this.tiles[0], x, y, 0);
|
||||||
);
|
drawTile(this.tiles[1], x + w, y + osy, -HALF_PI);
|
||||||
this.tiles[1].drawTile(
|
drawTile(this.tiles[2], x + w + v + 3 * os * size, y, 0);
|
||||||
ctx,
|
break;
|
||||||
x + w,
|
case 2:
|
||||||
y + osy,
|
drawTile(this.tiles[0], x, y, 0);
|
||||||
size,
|
drawTile(this.tiles[1], x + v + os * size, y, 0);
|
||||||
false,
|
drawTile(this.tiles[2], x + w + v + os * size, y + osy, -HALF_PI);
|
||||||
0 - 3.141592 / 2,
|
break;
|
||||||
this.tiles[1].isEqual(sf, sv),
|
default:
|
||||||
);
|
console.error(`Position non prise en charge: ${p}`);
|
||||||
this.tiles[2].drawTile(
|
}
|
||||||
ctx,
|
|
||||||
x + w + v + 3 *os * size,
|
|
||||||
y,
|
|
||||||
size,
|
|
||||||
false,
|
|
||||||
0,
|
|
||||||
this.tiles[2].isEqual(sf, sv),
|
|
||||||
);
|
|
||||||
|
|
||||||
} else if (p === 2) {
|
ctx.restore();
|
||||||
this.tiles[0].drawTile(
|
}
|
||||||
ctx,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
size,
|
|
||||||
false,
|
|
||||||
0,
|
|
||||||
this.tiles[0].isEqual(sf, sv),
|
|
||||||
);
|
|
||||||
this.tiles[1].drawTile(
|
|
||||||
ctx,
|
|
||||||
x + v + os * size,
|
|
||||||
y,
|
|
||||||
size,
|
|
||||||
false,
|
|
||||||
0,
|
|
||||||
this.tiles[1].isEqual(sf, sv),
|
|
||||||
);
|
|
||||||
this.tiles[2].drawTile(
|
|
||||||
ctx,
|
|
||||||
x + w + v + os * size,
|
|
||||||
y + osy,
|
|
||||||
size,
|
|
||||||
false,
|
|
||||||
0 - 3.141592 / 2,
|
|
||||||
this.tiles[2].isEqual(sf, sv),
|
|
||||||
);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
//TODO error
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
492
src/hand.ts
492
src/hand.ts
|
|
@ -1,200 +1,334 @@
|
||||||
import { Tile } from "./tile"
|
import { Tile } from "./tile";
|
||||||
import { Group } from "./group"
|
import { Group } from "./group";
|
||||||
|
|
||||||
|
// Constants to avoid magic numbers and improve readability
|
||||||
|
const TILE_TYPES = {
|
||||||
|
MANZU: { code: "m", family: 1 },
|
||||||
|
PINZU: { code: "p", family: 2 },
|
||||||
|
SOUZU: { code: "s", family: 3 },
|
||||||
|
WINDS: { code: "w", family: 4 },
|
||||||
|
DRAGONS: { code: "d", family: 5 }
|
||||||
|
};
|
||||||
|
|
||||||
export class Hand {
|
export class Hand {
|
||||||
private tiles: Array<Tile>;
|
private tiles: Array<Tile>;
|
||||||
public isolate: boolean = false;
|
public isolate: boolean = false;
|
||||||
|
|
||||||
public constructor(stiles: string = "") {
|
/**
|
||||||
this.tiles = [];
|
* Create a hand from string representation
|
||||||
for (let i = 0; i < stiles.length - 1; i++) {
|
* @param stiles String representation of tiles (e.g., "m1p2s3w1d1")
|
||||||
let ss = stiles.substring(i, i+2);
|
*/
|
||||||
if (ss[0] === "m") {
|
public constructor(stiles: string = "") {
|
||||||
this.tiles.push(new Tile(1, Number(ss[1]), false));
|
this.tiles = [];
|
||||||
} else if (ss[0] === "p") {
|
this.initializeFromString(stiles);
|
||||||
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 {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public getTiles(): Array<Tile> {
|
/**
|
||||||
return this.tiles;
|
* Parse string representation into tiles
|
||||||
}
|
*/
|
||||||
|
private initializeFromString(stiles: string): void {
|
||||||
|
for (let i = 0; i < stiles.length - 1; i++) {
|
||||||
|
const tileCode = stiles.substring(i, i + 2);
|
||||||
|
const type = tileCode[0];
|
||||||
|
const value = Number(tileCode[1]);
|
||||||
|
|
||||||
public length(): number {
|
if (this.isValidTileCode(type, value)) {
|
||||||
return this.tiles.length;
|
this.addTileFromCode(type, value);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public push(tile: Tile): void {
|
/**
|
||||||
this.tiles.push(tile);
|
* Check if tile code is valid
|
||||||
}
|
*/
|
||||||
|
private isValidTileCode(type: string, value: number): boolean {
|
||||||
|
return (
|
||||||
|
(type === TILE_TYPES.MANZU.code) ||
|
||||||
|
(type === TILE_TYPES.PINZU.code) ||
|
||||||
|
(type === TILE_TYPES.SOUZU.code) ||
|
||||||
|
(type === TILE_TYPES.WINDS.code) ||
|
||||||
|
(type === TILE_TYPES.DRAGONS.code)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public pop(): Tile|undefined {
|
/**
|
||||||
return this.tiles.pop();
|
* Create a tile from type code and value
|
||||||
}
|
*/
|
||||||
|
private addTileFromCode(type: string, value: number): void {
|
||||||
|
const familyMap: { [key: string]: number } = {
|
||||||
|
[TILE_TYPES.MANZU.code]: TILE_TYPES.MANZU.family,
|
||||||
|
[TILE_TYPES.PINZU.code]: TILE_TYPES.PINZU.family,
|
||||||
|
[TILE_TYPES.SOUZU.code]: TILE_TYPES.SOUZU.family,
|
||||||
|
[TILE_TYPES.WINDS.code]: TILE_TYPES.WINDS.family,
|
||||||
|
[TILE_TYPES.DRAGONS.code]: TILE_TYPES.DRAGONS.family
|
||||||
|
};
|
||||||
|
|
||||||
public find(family: number, value: number) :Tile|undefined {
|
const family = familyMap[type];
|
||||||
let n = undefined;
|
if (family !== undefined) {
|
||||||
for (let i = 0; i < this.tiles.length; i++) {
|
this.tiles.push(new Tile(family, value, false));
|
||||||
if (this.tiles[i].getFamily() === family && this.tiles[i].getValue() === value) {
|
}
|
||||||
n = i;
|
}
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (n !== undefined) {
|
|
||||||
[this.tiles[n], this.tiles[0]] = [this.tiles[0], this.tiles[n]];
|
|
||||||
let t = this.tiles.shift();
|
|
||||||
this.sort();
|
|
||||||
return t;
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public eject(idTile: number): Tile {
|
/**
|
||||||
[this.tiles[0], this.tiles[idTile]] = [this.tiles[idTile], this.tiles[0]];
|
* Get all tiles in hand
|
||||||
let tile = this.tiles.shift();
|
*/
|
||||||
this.sort();
|
public getTiles(): Array<Tile> {
|
||||||
return tile as NonNullable<Tile>;
|
return this.tiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get(idTile: number|undefined): Tile|undefined {
|
/**
|
||||||
if (idTile !== undefined) {
|
* Get number of tiles in hand
|
||||||
return this.tiles[idTile];
|
*/
|
||||||
} else {
|
public length(): number {
|
||||||
return undefined;
|
return this.tiles.length;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public sort(): undefined {
|
/**
|
||||||
this.tiles.sort((a, b) => a.isLessThan(b) ? -1 : 1);
|
* Add a tile to hand
|
||||||
}
|
*/
|
||||||
|
public push(tile: Tile): void {
|
||||||
|
this.tiles.push(tile);
|
||||||
|
}
|
||||||
|
|
||||||
public count(family: number, value: number): number {
|
/**
|
||||||
let c = 0;
|
* Remove and return the last tile
|
||||||
this.tiles.forEach(
|
*/
|
||||||
t => {
|
public pop(): Tile | undefined {
|
||||||
if (t.getFamily() === family && t.getValue() === value) {
|
return this.tiles.pop();
|
||||||
c++;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
public toGroup(pair: boolean = false): Array<Group>|undefined {
|
/**
|
||||||
if (this.tiles.length > 0) {
|
* Find and remove a specific tile by family and value
|
||||||
let t1 = this.tiles.pop() as NonNullable<Tile>;
|
*/
|
||||||
|
public find(family: number, value: number): Tile | undefined {
|
||||||
|
const index = this.findTileIndex(family, value);
|
||||||
|
|
||||||
let c = this.count(t1.getFamily(), t1.getValue());
|
if (index !== -1) {
|
||||||
if (c >= 1 && !pair) { //can do a pair
|
// Swap with first tile and remove
|
||||||
let t2 = this.find(t1.getFamily(), t1.getValue()) as NonNullable<Tile>;
|
[this.tiles[index], this.tiles[0]] = [this.tiles[0], this.tiles[index]];
|
||||||
let groups = this.toGroup(true);
|
const tile = this.tiles.shift();
|
||||||
this.tiles.push(t2);
|
this.sort();
|
||||||
this.sort();
|
return tile;
|
||||||
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);
|
return undefined;
|
||||||
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();
|
* Find index of tile with specific family and value
|
||||||
return undefined;
|
*/
|
||||||
|
private findTileIndex(family: number, value: number): number {
|
||||||
|
return this.tiles.findIndex(
|
||||||
|
tile => tile.getFamily() === family && tile.getValue() === value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
/**
|
||||||
return [];
|
* Remove tile at specific index
|
||||||
}
|
*/
|
||||||
}
|
public eject(idTile: number): Tile {
|
||||||
|
if (idTile < 0 || idTile >= this.tiles.length) {
|
||||||
|
throw new Error("Invalid tile index");
|
||||||
|
}
|
||||||
|
|
||||||
public drawHand (
|
// Swap with first tile and remove
|
||||||
ctx: CanvasRenderingContext2D,
|
[this.tiles[0], this.tiles[idTile]] = [this.tiles[idTile], this.tiles[0]];
|
||||||
x: number,
|
const tile = this.tiles.shift();
|
||||||
y: number,
|
this.sort();
|
||||||
offset: number,
|
|
||||||
size: number,
|
|
||||||
focusedTiled: number|undefined = undefined,
|
|
||||||
hidden: boolean = false,
|
|
||||||
rotation: number = 0
|
|
||||||
): void {
|
|
||||||
let v = (75 + offset) * size;
|
|
||||||
let vx = Math.cos(rotation) * v;
|
|
||||||
let vy = Math.sin(rotation) * v;
|
|
||||||
for (let i = 0; i < this.tiles.length; i++) {
|
|
||||||
let e = (i === this.tiles.length - 1 && this.isolate) ? 10 : 0;
|
|
||||||
if (i === focusedTiled) {
|
|
||||||
this.tiles[i].drawTile(
|
|
||||||
ctx,
|
|
||||||
x +
|
|
||||||
i * vx +
|
|
||||||
25 * size * Math.sin(rotation) +
|
|
||||||
e * size * Math.cos(rotation),
|
|
||||||
y +
|
|
||||||
i * vy -
|
|
||||||
25 * size * Math.cos(rotation) +
|
|
||||||
e * size * Math.sin(rotation),
|
|
||||||
size,
|
|
||||||
hidden,
|
|
||||||
rotation
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.tiles[i].drawTile(
|
|
||||||
ctx,
|
|
||||||
x + i * vx + e * size * Math.cos(rotation),
|
|
||||||
y + i * vy + e * size * Math.sin(rotation),
|
|
||||||
size,
|
|
||||||
hidden,
|
|
||||||
rotation
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async preload(): Promise<void> {
|
return tile as Tile;
|
||||||
await Promise.all(this.tiles.map(t => t.preloadImg()));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public cleanup(): void {
|
/**
|
||||||
this.tiles.forEach(tile => tile.cleanup());
|
* Get tile at specific index without removing
|
||||||
this.tiles = [];
|
*/
|
||||||
}
|
public get(idTile: number | undefined): Tile | undefined {
|
||||||
|
if (idTile === undefined || idTile < 0 || idTile >= this.tiles.length) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.tiles[idTile];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort tiles in ascending order
|
||||||
|
*/
|
||||||
|
public sort(): void {
|
||||||
|
this.tiles.sort((a, b) => a.isLessThan(b) ? -1 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count tiles with specific family and value
|
||||||
|
*/
|
||||||
|
public count(family: number, value: number): number {
|
||||||
|
return this.tiles.filter(
|
||||||
|
tile => tile.getFamily() === family && tile.getValue() === value
|
||||||
|
).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to form hand into groups (for winning detection)
|
||||||
|
*/
|
||||||
|
public toGroup(pair: boolean = false): Array<Group> | undefined {
|
||||||
|
if (this.tiles.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take last tile to try forming a group
|
||||||
|
const lastTile = this.tiles.pop() as Tile;
|
||||||
|
const family = lastTile.getFamily();
|
||||||
|
const value = lastTile.getValue();
|
||||||
|
|
||||||
|
// Try to form a pair
|
||||||
|
if (this.count(family, value) >= 1 && !pair) {
|
||||||
|
const result = this.tryFormPair(lastTile);
|
||||||
|
if (result) return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to form a triplet (pon)
|
||||||
|
if (this.count(family, value) >= 2) {
|
||||||
|
const result = this.tryFormTriplet(lastTile, pair);
|
||||||
|
if (result) return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to form a sequence (chii)
|
||||||
|
const hasMinusOne = this.count(family, value - 1) > 0;
|
||||||
|
const hasMinusTwo = this.count(family, value - 2) > 0;
|
||||||
|
|
||||||
|
if (hasMinusOne && hasMinusTwo) {
|
||||||
|
const result = this.tryFormSequence(lastTile, pair);
|
||||||
|
if (result) return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no valid group could be formed, put tile back and return undefined
|
||||||
|
this.tiles.push(lastTile);
|
||||||
|
this.sort();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to form a pair with the given tile
|
||||||
|
*/
|
||||||
|
private tryFormPair(tile: Tile): Array<Group> | undefined {
|
||||||
|
const pairTile = this.find(tile.getFamily(), tile.getValue()) as Tile;
|
||||||
|
const groups = this.toGroup(true);
|
||||||
|
|
||||||
|
// Put the tile back
|
||||||
|
this.tiles.push(pairTile);
|
||||||
|
this.sort();
|
||||||
|
|
||||||
|
if (groups !== undefined) {
|
||||||
|
this.tiles.push(tile);
|
||||||
|
this.sort();
|
||||||
|
groups.push(new Group([tile, pairTile], 0, 0));
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to form a triplet (pon) with the given tile
|
||||||
|
*/
|
||||||
|
private tryFormTriplet(tile: Tile, pair: boolean): Array<Group> | undefined {
|
||||||
|
const secondTile = this.find(tile.getFamily(), tile.getValue()) as Tile;
|
||||||
|
const thirdTile = this.find(tile.getFamily(), tile.getValue()) as Tile;
|
||||||
|
|
||||||
|
const groups = this.toGroup(pair);
|
||||||
|
|
||||||
|
// Put tiles back
|
||||||
|
this.tiles.push(secondTile);
|
||||||
|
this.tiles.push(thirdTile);
|
||||||
|
this.sort();
|
||||||
|
|
||||||
|
if (groups !== undefined) {
|
||||||
|
groups.push(new Group([tile, secondTile, thirdTile], 0, 0));
|
||||||
|
this.tiles.push(tile);
|
||||||
|
this.sort();
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to form a sequence (chii) with the given tile
|
||||||
|
*/
|
||||||
|
private tryFormSequence(tile: Tile, pair: boolean): Array<Group> | undefined {
|
||||||
|
const secondTile = this.find(tile.getFamily(), tile.getValue() - 1) as Tile;
|
||||||
|
const thirdTile = this.find(tile.getFamily(), tile.getValue() - 2) as Tile;
|
||||||
|
|
||||||
|
const groups = this.toGroup(pair);
|
||||||
|
|
||||||
|
// Put tiles back
|
||||||
|
this.tiles.push(secondTile);
|
||||||
|
this.tiles.push(thirdTile);
|
||||||
|
this.sort();
|
||||||
|
|
||||||
|
if (groups !== undefined) {
|
||||||
|
groups.push(new Group([thirdTile, secondTile, tile], 0, 0));
|
||||||
|
this.tiles.push(tile);
|
||||||
|
this.sort();
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw hand tiles on canvas
|
||||||
|
*/
|
||||||
|
public drawHand(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
offset: number,
|
||||||
|
size: number,
|
||||||
|
focusedTile: number | undefined = undefined,
|
||||||
|
hidden: boolean = false,
|
||||||
|
rotation: number = 0
|
||||||
|
): void {
|
||||||
|
const tileOffset = (75 + offset) * size;
|
||||||
|
const offsetX = Math.cos(rotation) * tileOffset;
|
||||||
|
const offsetY = Math.sin(rotation) * tileOffset;
|
||||||
|
|
||||||
|
for (let i = 0; i < this.tiles.length; i++) {
|
||||||
|
const isLastAndIsolated = (i === this.tiles.length - 1 && this.isolate) ? 10 : 0;
|
||||||
|
|
||||||
|
// Calculate position
|
||||||
|
let tileX = x + i * offsetX + isLastAndIsolated * size * Math.cos(rotation);
|
||||||
|
let tileY = y + i * offsetY + isLastAndIsolated * size * Math.sin(rotation);
|
||||||
|
|
||||||
|
// Add additional offset for focused tile
|
||||||
|
if (i === focusedTile) {
|
||||||
|
tileX += 25 * size * Math.sin(rotation);
|
||||||
|
tileY -= 25 * size * Math.cos(rotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw tile
|
||||||
|
this.tiles[i].drawTile(
|
||||||
|
ctx,
|
||||||
|
tileX,
|
||||||
|
tileY,
|
||||||
|
size,
|
||||||
|
hidden,
|
||||||
|
rotation
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preload tile images
|
||||||
|
*/
|
||||||
|
public async preload(): Promise<void> {
|
||||||
|
await Promise.all(this.tiles.map(tile => tile.preloadImg()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up resources
|
||||||
|
*/
|
||||||
|
public cleanup(): void {
|
||||||
|
this.tiles.forEach(tile => tile.cleanup());
|
||||||
|
this.tiles = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
303
src/tile.ts
303
src/tile.ts
|
|
@ -1,181 +1,156 @@
|
||||||
export class Tile {
|
export class Tile {
|
||||||
private family: number;
|
private imgFront: HTMLImageElement = new Image();
|
||||||
private value: number;
|
private imgBack: HTMLImageElement = new Image();
|
||||||
private red: boolean;
|
private imgGray: HTMLImageElement = new Image();
|
||||||
private imgSrc: string;
|
private img: HTMLImageElement = new Image();
|
||||||
private imgFront: HTMLImageElement;
|
private imgSrc: string = "";
|
||||||
private imgBack: HTMLImageElement;
|
private tilt: number = 0;
|
||||||
private imgGray: HTMLImageElement;
|
|
||||||
private img: HTMLImageElement;
|
|
||||||
private tilt: number;
|
|
||||||
|
|
||||||
public constructor(family: number, value: number , red: boolean) {
|
constructor(
|
||||||
this.family = family;
|
private family: number,
|
||||||
this.value = value;
|
private value: number,
|
||||||
this.red = red;
|
private red: boolean
|
||||||
this.imgSrc = "";
|
) {
|
||||||
this.imgFront = new Image();
|
this.setImgSrc();
|
||||||
this.imgBack = new Image();
|
}
|
||||||
this.imgGray = new Image();
|
|
||||||
this.img = new Image();
|
|
||||||
this.tilt = 0;
|
|
||||||
this.setImgSrc();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getFamily(): number {
|
public getFamily(): number {
|
||||||
return this.family;
|
return this.family;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getValue(): number {
|
public getValue(): number {
|
||||||
return this.value;
|
return this.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public isEqual(family: number, value: number): boolean {
|
public isEqual(family: number, value: number): boolean {
|
||||||
return this.family === family && this.value === value;
|
return this.family === family && this.value === value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public isRed(): boolean {
|
public isRed(): boolean {
|
||||||
return this.red;
|
return this.red;
|
||||||
}
|
}
|
||||||
|
|
||||||
public compare(t: Tile): number {
|
public compare(t: Tile): number {
|
||||||
if (this.family < t.family) {
|
// Compare d'abord par famille, puis par valeur
|
||||||
return -1;
|
if (this.family !== t.family) {
|
||||||
} else if (this.family > t.family) {
|
return this.family < t.family ? -1 : 1;
|
||||||
return 1;
|
}
|
||||||
}
|
if (this.value !== t.value) {
|
||||||
if (this.value < t.value) {
|
return this.value < t.value ? -1 : 1;
|
||||||
return -1;
|
}
|
||||||
} else if (this.value > t.value) {
|
return 0;
|
||||||
return 1;
|
}
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setTilt(): void {
|
public isLessThan(t: Tile): boolean {
|
||||||
this.tilt = (1 - 2 * Math.random()) * 0.04;
|
return this.family < t.family ||
|
||||||
}
|
(this.family === t.family && this.value <= t.value);
|
||||||
|
}
|
||||||
|
|
||||||
public drawTile(
|
public setTilt(): void {
|
||||||
ctx: CanvasRenderingContext2D,
|
this.tilt = (1 - 2 * Math.random()) * 0.04;
|
||||||
x: number,
|
}
|
||||||
y: number,
|
|
||||||
size: number,
|
|
||||||
hidden: boolean = false,
|
|
||||||
rotation: number = 0,
|
|
||||||
gray: boolean = false,
|
|
||||||
tilted: boolean = true
|
|
||||||
): void {
|
|
||||||
ctx.save();
|
|
||||||
ctx.translate(x + (75 * size) / 2, y + (100 * size) / 2);
|
|
||||||
if (tilted) {
|
|
||||||
ctx.rotate(rotation + this.tilt);
|
|
||||||
} else {
|
|
||||||
ctx.rotate(rotation);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hidden) {
|
public drawTile(
|
||||||
ctx.drawImage( // ombre
|
ctx: CanvasRenderingContext2D,
|
||||||
this.imgGray,
|
x: number,
|
||||||
-(75 * size * 0.92) / 2,
|
y: number,
|
||||||
-(100 * size * 0.91) / 2,
|
size: number,
|
||||||
75 * size,
|
hidden: boolean = false,
|
||||||
100 * size
|
rotation: number = 0,
|
||||||
);
|
gray: boolean = false,
|
||||||
ctx.drawImage( // le dos des tuiles
|
tilted: boolean = true
|
||||||
this.imgBack,
|
): void {
|
||||||
-(75 * size) / 2,
|
const tileWidth = 75 * size;
|
||||||
-(100 * size) / 2,
|
const tileHeight = 100 * size;
|
||||||
75 * size,
|
const halfWidth = tileWidth / 2;
|
||||||
100 * size
|
const halfHeight = tileHeight / 2;
|
||||||
);
|
const shadowScale = 0.92;
|
||||||
} 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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.restore();
|
// Sauvegarde du contexte et positionnement
|
||||||
}
|
ctx.save();
|
||||||
|
ctx.translate(x + halfWidth, y + halfHeight);
|
||||||
|
ctx.rotate(rotation + (tilted ? this.tilt : 0));
|
||||||
|
|
||||||
public isLessThan(t: Tile): boolean {
|
// Position de l'ombre (légèrement décalée)
|
||||||
if (this.family < t.family) {
|
const shadowX = -(tileWidth * shadowScale) / 2;
|
||||||
return true;
|
const shadowY = -(tileHeight * shadowScale) / 2;
|
||||||
} else if (this.family === t.family && this.value <= t.value) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public cleanup(): void {
|
// Dessin de l'ombre (commun aux deux cas)
|
||||||
this.imgFront.onload = null;
|
ctx.drawImage(this.imgGray, shadowX, shadowY, tileWidth, tileHeight);
|
||||||
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 {
|
if (hidden) {
|
||||||
this.imgSrc = "img/Regular/"
|
// Dessin du dos de la tuile
|
||||||
if (this.family <= 3) {
|
ctx.drawImage(this.imgBack, -halfWidth, -halfHeight, tileWidth, tileHeight);
|
||||||
this.imgSrc += ["", "Man", "Pin", "Sou"][this.family];
|
} else {
|
||||||
this.imgSrc += String(this.value);
|
// Dessin de la tuile face visible
|
||||||
if (this.red) {
|
ctx.drawImage(this.imgFront, -halfWidth, -halfHeight, tileWidth, tileHeight);
|
||||||
this.imgSrc += "-Dora";
|
|
||||||
}
|
|
||||||
this.imgSrc += ".svg";
|
|
||||||
} else if (this.family === 4) {
|
|
||||||
this.imgSrc += ["", "Ton", "Nan", "Shaa", "Pei"][this.value] + ".svg";
|
|
||||||
} else if (this.family === 5) {
|
|
||||||
this.imgSrc += ["", "Chun", "Hatsu", "Haku"][this.value] + ".svg";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async preloadImg(): Promise<void> {
|
// Dessin du motif sur la tuile (légèrement plus petit)
|
||||||
await Promise.all([
|
const patternScale = 0.9;
|
||||||
this.loadImg(this.imgFront, "img/Regular/Front.svg"),
|
const patternWidth = tileWidth * patternScale;
|
||||||
// this.loadImg(this.imgFront, "/~img/Export/Regular/Front.png"),
|
const patternHeight = tileHeight * patternScale;
|
||||||
this.loadImg(this.imgBack, "img/Regular/Back.svg"),
|
const patternX = -((75 - 7) * size) / 2;
|
||||||
this.loadImg(this.imgGray, "img/Regular/Gray.svg"),
|
const patternY = -((100 - 10) * size) / 2;
|
||||||
this.loadImg(this.img, this.imgSrc)
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private loadImg(img: HTMLImageElement, src: string): Promise<void> {
|
ctx.drawImage(this.img, patternX, patternY, patternWidth, patternHeight);
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
img.onload = () => resolve();
|
// Appliquer un filtre gris si demandé
|
||||||
img.onerror = () => reject();
|
if (gray) {
|
||||||
img.src = src;
|
ctx.drawImage(this.imgGray, -halfWidth, -halfHeight, tileWidth, tileHeight);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
public cleanup(): void {
|
||||||
|
// Supprimer tous les gestionnaires d'événements
|
||||||
|
const images = [this.imgFront, this.imgBack, this.imgGray, this.img];
|
||||||
|
images.forEach(img => {
|
||||||
|
img.onload = null;
|
||||||
|
img.onerror = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async preloadImg(): Promise<void> {
|
||||||
|
const imagesToLoad = [
|
||||||
|
{ img: this.imgFront, src: "img/Regular/Front.svg" },
|
||||||
|
{ img: this.imgBack, src: "img/Regular/Back.svg" },
|
||||||
|
{ img: this.imgGray, src: "img/Regular/Gray.svg" },
|
||||||
|
{ img: this.img, src: this.imgSrc }
|
||||||
|
];
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
imagesToLoad.map(({ img, src }) => this.loadImg(img, src))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadImg(img: HTMLImageElement, src: string): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
img.onload = () => resolve();
|
||||||
|
img.onerror = () => reject();
|
||||||
|
img.src = src;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setImgSrc(): void {
|
||||||
|
this.imgSrc = "img/Regular/";
|
||||||
|
|
||||||
|
if (this.family <= 3) {
|
||||||
|
const families = ["", "Man", "Pin", "Sou"];
|
||||||
|
this.imgSrc += families[this.family] + String(this.value);
|
||||||
|
|
||||||
|
if (this.red) {
|
||||||
|
this.imgSrc += "-Dora";
|
||||||
|
}
|
||||||
|
} else if (this.family === 4) {
|
||||||
|
const winds = ["", "Ton", "Nan", "Shaa", "Pei"];
|
||||||
|
this.imgSrc += winds[this.value];
|
||||||
|
} else if (this.family === 5) {
|
||||||
|
const dragons = ["", "Chun", "Hatsu", "Haku"];
|
||||||
|
this.imgSrc += dragons[this.value];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.imgSrc += ".svg";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es2015",
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue