From 1aee19e63bffa972885712bc37aeec90097ff52e Mon Sep 17 00:00:00 2001 From: Didictateur Date: Wed, 18 Feb 2026 14:41:05 +0100 Subject: [PATCH] basic project --- backend/src/api.py | 24 + backend/src/game/discard.py | 0 backend/src/game/error.py | 15 + backend/src/game/hand.py | 0 backend/src/game/tile.py | 100 ++ backend/src/game/value.py | 19 + backend/src/game/wall.py | 0 index.html | 262 ------ src/button.ts | 317 ------- src/constants/index.ts | 216 ----- src/deck.ts | 293 ------ src/display/dp0.ts | 133 --- src/display/dp1.ts | 111 --- src/display/dp2.ts | 256 ------ src/display/dp3.ts | 381 -------- src/display/dp4.ts | 275 ------ src/display/dp5.ts | 120 --- src/display/dp6.ts | 278 ------ src/display/dp7.ts | 202 ----- src/display/dp8.ts | 222 ----- src/display/template.ts | 126 --- src/game.ts | 1714 ----------------------------------- src/group.ts | 91 -- src/hand.ts | 382 -------- src/rain.ts | 125 --- src/state.ts | 91 -- src/styles/main.css | 562 ------------ src/text/default.txt | 5 - src/text/parse.ts | 229 ----- src/text/txt.ts | 20 - src/text/txt0.txt | 27 - src/text/txt1.txt | 17 - src/text/txt10.txt | 5 - src/text/txt11.txt | 5 - src/text/txt12.txt | 5 - src/text/txt13.txt | 5 - src/text/txt14.txt | 5 - src/text/txt15.txt | 5 - src/text/txt16.txt | 5 - src/text/txt17.txt | 5 - src/text/txt18.txt | 5 - src/text/txt19.txt | 5 - src/text/txt2.txt | 24 - src/text/txt20.txt | 5 - src/text/txt21.txt | 5 - src/text/txt22.txt | 5 - src/text/txt23.txt | 5 - src/text/txt3.txt | 25 - src/text/txt4.txt | 27 - src/text/txt5.txt | 24 - src/text/txt6.txt | 22 - src/text/txt7.txt | 23 - src/text/txt8.txt | 19 - src/text/txt9.txt | 5 - src/tile.ts | 156 ---- src/types/index.ts | 149 --- src/utils/index.ts | 335 ------- src/yakus/generator.ts | 328 ------- src/yakus/yaku.ts | 889 ------------------ tests/assert.ts | 9 - tests/test.ts | 2 - tests/test_hand.ts | 20 - tests/test_yakus.ts | 203 ----- 63 files changed, 158 insertions(+), 8785 deletions(-) create mode 100644 backend/src/api.py create mode 100644 backend/src/game/discard.py create mode 100644 backend/src/game/error.py create mode 100644 backend/src/game/hand.py create mode 100644 backend/src/game/tile.py create mode 100644 backend/src/game/value.py create mode 100644 backend/src/game/wall.py delete mode 100644 src/button.ts delete mode 100644 src/constants/index.ts delete mode 100644 src/deck.ts delete mode 100644 src/display/dp0.ts delete mode 100644 src/display/dp1.ts delete mode 100644 src/display/dp2.ts delete mode 100644 src/display/dp3.ts delete mode 100644 src/display/dp4.ts delete mode 100644 src/display/dp5.ts delete mode 100644 src/display/dp6.ts delete mode 100644 src/display/dp7.ts delete mode 100644 src/display/dp8.ts delete mode 100644 src/display/template.ts delete mode 100644 src/game.ts delete mode 100644 src/group.ts delete mode 100644 src/hand.ts delete mode 100644 src/rain.ts delete mode 100644 src/state.ts delete mode 100644 src/styles/main.css delete mode 100644 src/text/default.txt delete mode 100644 src/text/parse.ts delete mode 100644 src/text/txt.ts delete mode 100644 src/text/txt0.txt delete mode 100644 src/text/txt1.txt delete mode 100644 src/text/txt10.txt delete mode 100644 src/text/txt11.txt delete mode 100644 src/text/txt12.txt delete mode 100644 src/text/txt13.txt delete mode 100644 src/text/txt14.txt delete mode 100644 src/text/txt15.txt delete mode 100644 src/text/txt16.txt delete mode 100644 src/text/txt17.txt delete mode 100644 src/text/txt18.txt delete mode 100644 src/text/txt19.txt delete mode 100644 src/text/txt2.txt delete mode 100644 src/text/txt20.txt delete mode 100644 src/text/txt21.txt delete mode 100644 src/text/txt22.txt delete mode 100644 src/text/txt23.txt delete mode 100644 src/text/txt3.txt delete mode 100644 src/text/txt4.txt delete mode 100644 src/text/txt5.txt delete mode 100644 src/text/txt6.txt delete mode 100644 src/text/txt7.txt delete mode 100644 src/text/txt8.txt delete mode 100644 src/text/txt9.txt delete mode 100644 src/tile.ts delete mode 100644 src/types/index.ts delete mode 100644 src/utils/index.ts delete mode 100644 src/yakus/generator.ts delete mode 100644 src/yakus/yaku.ts delete mode 100644 tests/assert.ts delete mode 100644 tests/test.ts delete mode 100644 tests/test_hand.ts delete mode 100644 tests/test_yakus.ts diff --git a/backend/src/api.py b/backend/src/api.py new file mode 100644 index 0000000..0dc0c38 --- /dev/null +++ b/backend/src/api.py @@ -0,0 +1,24 @@ +from flask import Flask, jsonify, request +from flask_cors import CORS + +from game.tile import * + +app = Flask(__name__) +CORS(app) + +tiles = tiles = Tile.generateAll() + +# ============= GET ================ +@app.route('api/tiles', methods=['GET']) +def get_all_tiles(): + """Récupère toutes les tuiles""" + return jsonify(tiles) + +@app.route('api/tiles/', method=['GET']) +def get_tile(tile_id): + if tile_id < 0 or tile_id >= len(tiles): + return jsonify({'error': 'Tuile non trouvée'}), 404 + + return jsonify({'tile': tiles[tile_id]}) + +# ============== POST ================ diff --git a/backend/src/game/discard.py b/backend/src/game/discard.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/game/error.py b/backend/src/game/error.py new file mode 100644 index 0000000..3ee77e3 --- /dev/null +++ b/backend/src/game/error.py @@ -0,0 +1,15 @@ +class TileError(Exception): + """Exception de base pour les erreurs de tuiles""" + pass + +class InvalidFamilyError(TileError): + """Famille de tuile invalide""" + pass + +class InvalidValueError(TileError): + """Valeur de tuile invalide""" + pass + +class GameError(Exception): + """Exception de base pour les erreurs de jeu""" + pass \ No newline at end of file diff --git a/backend/src/game/hand.py b/backend/src/game/hand.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/game/tile.py b/backend/src/game/tile.py new file mode 100644 index 0000000..02412b1 --- /dev/null +++ b/backend/src/game/tile.py @@ -0,0 +1,100 @@ +from error import * +from value import * + +class Tile: + def __init__(self, family, value, id): + self.family = family + self.value = value + self.id = id + self.hidden = False + self.img = None + + self.img_back = "Back.svg" + self.img_front = "Front.svg" + self.img_shadow = "Gray.svg" + if self.family == Family.MAN: + if self.value in range(1, 10): + self.img_symbol = f"Man{self.value}.svg" + else: + raise InvalidValueError(f"Invalide value: {self.value}") + elif self.family == Family.PIN: + if self.value in range(1, 10): + self.img_symbol = f"Pin{self.value}.svg" + else: + raise InvalidValueError(f"Invalide value: {self.value}") + elif self.family == Family.SOU: + if self.value in range(1, 10): + self.img_symbol = f"Sou{self.value}.svg" + else: + raise InvalidValueError(f"Invalide value: {self.value}") + elif self.family == Family.WIND: + if self.value == Wind.EAST: + self.img_symbol = "Ton.svg" + elif self.value == Wind.SOUTH: + self.img_symbol = "Nan.svg" + elif self.value == Wind.WEST: + self.img_symbol = "Shaa.svg" + elif self.value == Wind.NORTH: + self.img_symbol = "Pei.svg" + else: + raise InvalidValueError(f"Invalide value: {self.value}") + elif self.family == Family.DRAGON: + if self.value == Dragon.RED: + self.img_symbol = "Chun.svg" + elif self.value == Dragon.GREEN: + self.img_symbol = "Hatsu.svg" + elif self.value == Dragon.WHITE: + self.img_symbol = "Haku.svg" + else: + raise InvalidValueError(f"Invalide value: {self.value}") + else: + raise InvalidFamilyError(f"Invalide family: {self.family}") + + def get_img(self): + if self.img == None: + self.img = f""" + + + + + + """ + return self.img + + def __eq__(self, other: Tile): + return self.value == other.value and self.family == other.family + + def __lt__(self, other: Tile): + return (self.family < other.family) or (self.family == other.family and self.value < other.value) + + @staticmethod + def generateAll(): + tiles = [] + id = 0 + for _ in range(4): + # man + for i in range(1, 10): + tiles.append(Tile(Family.MAN, i, id)) + id += 1 + + # pin + for i in range(1, 10): + tiles.append(Tile(Family.PIN, i, id)) + id += 1 + + # sou + for i in range(1, 10): + tiles.append(Tile(Family.SOU, i, id)) + id += 1 + + # wind + for wind in [Wind.EAST, Wind.SOUTH, Wind.WEST, Wind.NORTH]: + tiles.append(Tile(Family.WIND, wind, id)) + id += 1 + + # dragon + for dragon in [Dragon.RED, Dragon.GREEN, Dragon.WHITE]: + tiles.append(Tile(Family.DRAGON, dragon, id)) + id += 1 + + return tiles \ No newline at end of file diff --git a/backend/src/game/value.py b/backend/src/game/value.py new file mode 100644 index 0000000..a4f1c74 --- /dev/null +++ b/backend/src/game/value.py @@ -0,0 +1,19 @@ +from enum import Enum + +class Family(Enum): + MAN = 0 + PIN = 1 + SOU = 2 + WIND = 3 + DRAGON = 4 + +class Wind(Enum): + EAST : 0 + SOUTH : 1 + WEST : 2 + NORTH : 3 + +class Dragon(Enum): + RED : 0 + GREEN : 1 + WHITE : 2 \ No newline at end of file diff --git a/backend/src/game/wall.py b/backend/src/game/wall.py new file mode 100644 index 0000000..e69de29 diff --git a/index.html b/index.html index 793cb95..e69de29 100644 --- a/index.html +++ b/index.html @@ -1,262 +0,0 @@ - - - - - - - - Mahjong Riichi - Tutoriel Interactif - - - - - - - - - - - - - - -
-
- -
-
- -
-
- - - - - - diff --git a/src/button.ts b/src/button.ts deleted file mode 100644 index fbb7352..0000000 --- a/src/button.ts +++ /dev/null @@ -1,317 +0,0 @@ -import { Tile } from "./tile"; - -/** - * Type definition for button rendering functions - */ -type ButtonRenderer = ( - ctx: CanvasRenderingContext2D, - x: number, - y: number -) => void; - -/** - * Button configuration interface - */ -interface ButtonConfig { - text: string; - color: string; - action: number; -} - -// Button constants -const BUTTON_RADIUS = 8; -const BUTTON_WIDTH = 110; -const BUTTON_HEIGHT = 50; -const BUTTON_SPACING = 120; -const BUTTON_AREA_Y_MIN = 838; -const BUTTON_AREA_Y_MAX = 888; -const BUTTON_MARGIN = 10; -const BASE_X_POSITION = 850; - -// Button style configurations -const BUTTON_STYLES: Record = { - 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 }, - riichi: { text: "Riichi", color: "#66ccff", action: 6 }, - back: { text: "Retour", color: "#FF9030", action: 0 } -}; - -/** - * Determines which button was clicked based on coordinates - * @returns The action value of the clicked button or -1 if no button was clicked - */ -export function clickAction( - x: number, - y: number, - chii: boolean, - pon: boolean, - kan: boolean, - ron: boolean, - tsumo: boolean, - riichi: boolean -): number { - const activeButtons = getActiveButtons(chii, pon, kan, ron, tsumo, riichi); - - if (activeButtons.length === 0) { - return -1; - } - - // Calculate starting X position based on number of buttons - const xmin = 960 - activeButtons.length * BUTTON_SPACING; - - // Check if Y coordinate is within button area - const isYInButtonArea = BUTTON_AREA_Y_MIN < y && y < BUTTON_AREA_Y_MAX; - if (!isYInButtonArea) { - return -1; - } - - // Calculate which button was clicked - const buttonIndex = Math.floor((x - xmin) / BUTTON_SPACING); - const xOffset = (x - xmin) - BUTTON_SPACING * buttonIndex; - - if ( - buttonIndex >= 0 && - buttonIndex < activeButtons.length && - xOffset > BUTTON_MARGIN - ) { - return activeButtons[buttonIndex]; - } - - return -1; -} - -/** - * Draw all active buttons on the canvas - */ -export function drawButtons( - ctx: CanvasRenderingContext2D, - chii: boolean, - pon: boolean, - kan: boolean, - ron: boolean, - tsumo: boolean, - riichi: boolean -): void { - const buttonFunctions: [boolean, ButtonRenderer][] = [ - [chii, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.chii)], - [pon, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.pon)], - [kan, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.kan)], - [ron, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.ron)], - [tsumo, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.tsumo)], - [riichi, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.riichi)] - ]; - - // Only show the pass button if at least one other button is active - const hasActiveButtons = buttonFunctions.some(([isActive]) => isActive); - - if (hasActiveButtons) { - buttonFunctions.unshift([true, (ctx, x, y) => renderButton(ctx, x, y, BUTTON_STYLES.pass)]); - } else { - return; // No buttons to draw - } - - // Draw active buttons - let positionOffset = 0; - for (const [isActive, renderFunc] of buttonFunctions) { - if (isActive) { - renderFunc( - ctx, - BASE_X_POSITION - positionOffset * BUTTON_SPACING, - 835 - ); - positionOffset++; - } - } -} - -/** - * Determines which Chi option was clicked - * @returns The value of the clicked Chi option or -1 if no option was clicked - */ -export function clickChii( - x: number, - y: number, - chiis: Array> -): number { - if (chiis.length === 0) { - return -1; - } - - // Calculate starting X position based on number of options - const xmin = 960 - (chiis.length + 1) * BUTTON_SPACING; - - // Check if Y coordinate is within button area - const isYInButtonArea = BUTTON_AREA_Y_MIN < y && y < BUTTON_AREA_Y_MAX; - if (!isYInButtonArea) { - return -1; - } - - // Calculate which option was clicked - const optionIndex = Math.floor((x - xmin) / BUTTON_SPACING); - const xOffset = (x - xmin) - BUTTON_SPACING * optionIndex; - - if ( - optionIndex >= 0 && - optionIndex < (chiis.length + 1) && - xOffset > BUTTON_MARGIN - ) { - // Return 0 for "back" button or the value of the selected Chi option - return optionIndex === chiis.length ? 0 : chiis[optionIndex][0].getValue(); - } - - return -1; -} - -/** - * Draw Chi options on the canvas - */ -export function drawChiis( - ctx: CanvasRenderingContext2D, - chiis: Array> -): void { - // Create a copy to avoid modifying the original array - const chiiOptions = [...chiis].reverse(); - - // Draw "back" button - renderButton(ctx, BASE_X_POSITION, 835, BUTTON_STYLES.back); - - // Draw Chi options - let positionOffset = 1; - for (const tiles of chiiOptions) { - drawOneChii( - ctx, - BASE_X_POSITION - positionOffset * BUTTON_SPACING, - 835, - tiles - ); - positionOffset++; - } -} - -/** - * Draw a single Chi option - */ -function drawOneChii( - ctx: CanvasRenderingContext2D, - x: number, - y: number, - tiles: Array -): void { - const tileOffset = 32; - const tileStartX = x + 7; - const tileStartY = y + 5; - - // Draw the button background - drawButtonShape(ctx, x, y, BUTTON_RADIUS, BUTTON_WIDTH, BUTTON_HEIGHT, BUTTON_STYLES.chii.color); - - // Draw the tiles - for (let i = 0; i < tiles.length; i++) { - tiles[i].drawTile( - ctx, - tileStartX + tileOffset * i, - tileStartY, - 0.4, - false, - 0, - false, - false - ); - } -} - -/** - * Get the list of active button action values - */ -function getActiveButtons( - chii: boolean, - pon: boolean, - kan: boolean, - ron: boolean, - tsumo: boolean, - riichi: boolean -): number[] { - const buttonConfigs: [boolean, number][] = [ - [tsumo, BUTTON_STYLES.tsumo.action], - [ron, BUTTON_STYLES.ron.action], - [riichi, BUTTON_STYLES.riichi.action], - [kan, BUTTON_STYLES.kan.action], - [pon, BUTTON_STYLES.pon.action], - [chii, BUTTON_STYLES.chii.action] - ]; - - const activeButtons = buttonConfigs - .filter(([isActive]) => isActive) - .map(([, action]) => action); - - // Add pass button if any other buttons are active - if (activeButtons.length > 0) { - activeButtons.push(BUTTON_STYLES.pass.action); - } - - return activeButtons; -} - -/** - * Render a button with text - */ -function renderButton( - ctx: CanvasRenderingContext2D, - x: number, - y: number, - config: ButtonConfig -): void { - drawButtonShape(ctx, x, y, BUTTON_RADIUS, BUTTON_WIDTH, BUTTON_HEIGHT, config.color); - - // Add text to the button - ctx.fillStyle = "black"; - ctx.font = "30px garamond"; - - // Center text based on its length - const textXPosition = x + BUTTON_WIDTH * (0.25 - config.text.length * 0.025); - const textYPosition = y + BUTTON_HEIGHT/2 * 1.3; - - ctx.fillText(config.text, textXPosition, textYPosition); -} - -/** - * Draw a rounded rectangle button shape - */ -function drawButtonShape( - ctx: CanvasRenderingContext2D, - x: number, - y: number, - radius: number, - width: number, - height: number, - color: string -): void { - ctx.fillStyle = color; - ctx.beginPath(); - - // Top right corner - ctx.moveTo(x + radius, y); - ctx.lineTo(x + width - radius, y); - ctx.quadraticCurveTo(x + width, y, x + width, y + radius); - - // Bottom right corner - ctx.lineTo(x + width, y + height - radius); - ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); - - // Bottom left corner - ctx.lineTo(x + radius, y + height); - ctx.quadraticCurveTo(x, y + height, x, y + height - radius); - - // Top left corner - ctx.lineTo(x, y + radius); - ctx.quadraticCurveTo(x, y, x + radius, y); - - ctx.fill(); - - // Add border - ctx.fillStyle = "#606060"; - ctx.stroke(); -} diff --git a/src/constants/index.ts b/src/constants/index.ts deleted file mode 100644 index f771d83..0000000 --- a/src/constants/index.ts +++ /dev/null @@ -1,216 +0,0 @@ -/** - * Shared constants for the Riichi Mahjong Tutorial - */ - -// ============ Canvas Constants ============ -export const CANVAS = { - WIDTH: 1000, - HEIGHT: 1000, - GAME_AREA: { - X: 0, - Y: 0, - WIDTH: 1050, - HEIGHT: 1050, - }, -} as const; - -// ============ Colors ============ -export const COLORS = { - // Main theme - PRIMARY: '#1a7f4b', - PRIMARY_LIGHT: '#22a861', - PRIMARY_DARK: '#0d5c34', - - // Game board - TABLE_GREEN: '#007730', - TABLE_DARK: '#005520', - - // Tiles - TILE_FRONT: '#FFFFFF', - TILE_BACK: '#4A90D9', - TILE_SHADOW: 'rgba(0, 0, 0, 0.3)', - TILE_HIGHLIGHT: 'rgba(255, 215, 0, 0.5)', - - // Buttons - BUTTON_PASS: '#FF9030', - BUTTON_CALL: '#FFCC33', - BUTTON_WIN: '#FF3060', - BUTTON_RIICHI: '#66ccff', - - // Text - TEXT_LIGHT: '#FFFFFF', - TEXT_DARK: '#1A1A1A', - TEXT_MUTED: '#6B7280', - - // Status - SUCCESS: '#22C55E', - WARNING: '#F59E0B', - ERROR: '#EF4444', - INFO: '#3B82F6', -} as const; - -// ============ Tile Constants ============ -export const TILE = { - // Base dimensions (at scale 1) - WIDTH: 75, - HEIGHT: 100, - - // Families - FAMILY: { - MAN: 1, - PIN: 2, - SOU: 3, - WIND: 4, - DRAGON: 5, - }, - - // Wind values - WIND: { - EAST: 1, - SOUTH: 2, - WEST: 3, - NORTH: 4, - }, - - // Dragon values - DRAGON: { - RED: 1, // Chun - GREEN: 2, // Hatsu - WHITE: 3, // Haku - }, - - // Image paths - IMAGE_PATH: 'img/Regular/', - - // Families for numbered tiles - SUIT_FAMILIES: [1, 2, 3] as const, - HONOR_FAMILIES: [4, 5] as const, -} as const; - -// ============ Game Constants ============ -export const GAME = { - // Players - PLAYER_COUNT: 4, - - // Hand sizes - HAND_SIZE: 13, - WINNING_HAND_SIZE: 14, - - // Wall - WALL_SIZE: 136, - DEAD_WALL_SIZE: 14, - DORA_INDICATOR_COUNT: 5, - - // Timing (in ms) - TIMING: { - MIN_WAIT: 500, - DEFAULT_WAIT: 700, - MAX_WAIT: 2000, - ANIMATION_DURATION: 300, - }, - - // Display sizes (scale factors) - DISPLAY: { - HAND: 0.7, - HIDDEN_HAND: 0.6, - DISCARD: 0.6, - GROUP: 0.6, - }, -} as const; - -// ============ Math Constants ============ -export const MATH = { - PI: Math.PI, - HALF_PI: Math.PI / 2, - TWO_PI: Math.PI * 2, - DEG_TO_RAD: Math.PI / 180, - RAD_TO_DEG: 180 / Math.PI, -} as const; - -// ============ Animation Constants ============ -export const ANIMATION = { - FPS: 60, - FRAME_INTERVAL: 1000 / 60, - TIME_STEP: 1 / 60, -} as const; - -// ============ Physics Constants ============ -export const PHYSICS = { - GRAVITY: 100, - FALLING_TILE: { - INITIAL_Y: -150, - INITIAL_VELOCITY: 50, - SPAWN_CHANCE: 0.75, - ROTATION_FACTOR: Math.PI, - MOMENTUM_RANGE: 1, - MAX_Y: 1100, - }, -} as const; - -// ============ Button Constants ============ -export const BUTTON = { - RADIUS: 8, - WIDTH: 110, - HEIGHT: 50, - SPACING: 120, - MARGIN: 10, - AREA: { - Y_MIN: 838, - Y_MAX: 888, - }, - BASE_X: 850, -} as const; - -// ============ Score Constants ============ -export const SCORE = { - // Base points for different limits - MANGAN: 2000, - HANEMAN: 3000, - BAIMAN: 4000, - SANBAIMAN: 6000, - YAKUMAN: 8000, - - // Han thresholds - HAN_LIMITS: { - MANGAN: 5, - HANEMAN: 6, - BAIMAN: 8, - SANBAIMAN: 11, - YAKUMAN: 13, - }, - - // Starting score - STARTING_POINTS: 25000, - TARGET_POINTS: 30000, - - // Riichi - RIICHI_COST: 1000, -} as const; - -// ============ Wind Rotations ============ -export const ROTATIONS = [ - 0, // East (bottom) - -MATH.HALF_PI, // South (right) - -MATH.PI, // West (top) - MATH.HALF_PI, // North (left) -] as const; - -// ============ Keyboard Keys ============ -export const KEYS = { - ENTER: 'Enter', - ESCAPE: 'Escape', - SPACE: ' ', - ARROW_LEFT: 'ArrowLeft', - ARROW_RIGHT: 'ArrowRight', - ARROW_UP: 'ArrowUp', - ARROW_DOWN: 'ArrowDown', -} as const; - -// ============ CSS Class Names ============ -export const CSS_CLASSES = { - CANVAS_WRAPPER: 'canvas-wrapper', - ANIMATE_FADE_IN: 'animate-fadeIn', - ANIMATE_SLIDE_IN: 'animate-slideIn', - HIDDEN: 'hidden', - VISIBLE: 'visible', -} as const; diff --git a/src/deck.ts b/src/deck.ts deleted file mode 100644 index b9abaff..0000000 --- a/src/deck.ts +++ /dev/null @@ -1,293 +0,0 @@ -import { Tile } from "./tile"; -import { Hand } from "./hand"; - -type TileKey = `${number}-${number}`; - -export class Deck { - private tiles: Tile[]; - private tileIndexMap: Map; // Fast lookup for find() and count() - - public constructor(allowRed: boolean = false) { - this.tiles = []; - this.tileIndexMap = new Map(); - this.initTiles(allowRed); - } - - /** - * Displays all tile families on the canvas - */ - public displayFamilies( - ctx: CanvasRenderingContext2D, - x: number, - y: number, - size: number, - xOffset: number = 0, - yOffset: number = 0 - ): void { - let posX = x; - let posY = y; - - // Define tile layouts for each family - const familyLayouts = [ - { family: 1, start: 1, end: 9 }, // First suit - { family: 2, start: 1, end: 9 }, // Second suit - { family: 3, start: 1, end: 9 }, // Third suit - { family: 4, start: 1, end: 4 }, // Winds - { family: 5, start: 1, end: 3 } // Dragons - ]; - - for (const layout of familyLayouts) { - for (let j = layout.start; j <= layout.end; j++) { - const tile = this.find(layout.family, j); - if (tile) { - tile.drawTile(ctx, posX, posY, size, false, 0, false); - posX += (75 + xOffset) * size; - } - } - posX = x; - posY += (100 + yOffset) * size; - } - } - - /** - * Returns the number of tiles in the deck - */ - public length(): number { - return this.tiles.length; - } - - /** - * Removes and returns the last tile from the deck - * @throws Error if the deck is empty - */ - public pop(): Tile { - if (this.tiles.length === 0) { - throw new Error("Cannot pop from an empty deck"); - } - - const tile = this.tiles.pop()!; - // Update the index map - const key = this.getTileKey(tile.getFamily(), tile.getValue()); - const indices = this.tileIndexMap.get(key); - - if (indices && indices.length > 0) { - indices.pop(); // Remove the last index - if (indices.length === 0) { - this.tileIndexMap.delete(key); - } else { - this.tileIndexMap.set(key, indices); - } - } - - return tile; - } - - /** - * Adds a tile to the deck - */ - public push(tile: Tile): void { - const index = this.tiles.length; - this.tiles.push(tile); - - // Update the index map - const key = this.getTileKey(tile.getFamily(), tile.getValue()); - const indices = this.tileIndexMap.get(key) || []; - indices.push(index); - this.tileIndexMap.set(key, indices); - } - - /** - * Finds and removes a specific tile from the deck - */ - public find(family: number, value: number): Tile | undefined { - const key = this.getTileKey(family, value); - const indices = this.tileIndexMap.get(key); - - if (!indices || indices.length === 0) { - return undefined; - } - - // Get the first occurrence - const index = indices[0]; - if (index >= this.tiles.length) { - // Handle potential out-of-sync errors - this.rebuildIndexMap(); - return this.find(family, value); - } - - // Swap with the first element for efficient removal - [this.tiles[index], this.tiles[0]] = [this.tiles[0], this.tiles[index]]; - - // Update indices in the map - this.updateIndicesAfterSwap(0, index); - - // Remove and return the tile - const tile = this.tiles.shift(); - - // Update all indices after shift - this.decrementIndicesAfterShift(); - - return tile; - } - - /** - * Counts the number of tiles with specific family and value - */ - public count(family: number, value: number): number { - const key = this.getTileKey(family, value); - const indices = this.tileIndexMap.get(key); - return indices ? indices.length : 0; - } - - /** - * Shuffles the deck using Fisher-Yates algorithm - */ - public shuffle(): void { - // Fisher-Yates shuffle algorithm - for (let i = this.tiles.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [this.tiles[i], this.tiles[j]] = [this.tiles[j], this.tiles[i]]; - } - - // Rebuild the index map after shuffling - this.rebuildIndexMap(); - } - - /** - * Creates a random hand from the deck - */ - public getRandomHand(): Hand { - const hand = new Hand(); - this.shuffle(); - - // Handle case where deck doesn't have enough tiles - if (this.tiles.length < 13) { - throw new Error("Not enough tiles in deck to create a hand"); - } - - for (let i = 0; i < 13; i++) { - hand.push(this.pop()); - } - - return hand; - } - - /** - * Cleans up resources used by tiles and empties the deck - */ - public cleanup(): void { - this.tiles.forEach(tile => tile.cleanup()); - this.tiles = []; - this.tileIndexMap.clear(); - } - - /** - * Preloads all tile images - */ - public async preload(): Promise { - 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)); - } - } - } -} diff --git a/src/display/dp0.ts b/src/display/dp0.ts deleted file mode 100644 index 869d7a0..0000000 --- a/src/display/dp0.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { Rain } from "../rain" - -// Configuration globale -const CANVAS_ID = "myCanvas"; -const BG_RECT = { x: 0, y: 0, w: 1050, h: 1050 }; -const MOUSE = { x: 0, y: 0 }; -const FPS = 60; // Réduit de 90 à 60 pour de meilleures performances -const FRAME_INTERVAL = 1000 / FPS; -const RAIN = new Rain(); - -// Optimisation des références -let animationFrameId: number; -let lastFrameTime = 0; -let accumulatedTime = 0; -const callbacks: Array<() => void> = []; -const timeStep = 1 / FPS; - -// Pré-calcul des dimensions -const canvas = document.getElementById(CANVAS_ID) as HTMLCanvasElement; -const ctx = canvas.getContext("2d", { alpha: false }) as NonNullable; -canvas.width = BG_RECT.w; -canvas.height = BG_RECT.h; - -// Cache statique -const staticCanvas = document.createElement('canvas') as HTMLCanvasElement; -const staticCtx = staticCanvas.getContext("2d", { alpha: false }) as NonNullable; -staticCanvas.width = canvas.width; -staticCanvas.height = canvas.height; - -// Optimisation du rendu avec requestAnimationFrame -function drawFrame(deltaTime: number) { - if (!ctx) return; - - // Mise à jour avec pas de temps fixe pour stabilité physique - accumulatedTime += deltaTime / 1000; // Convertir en secondes - - // Mettre à jour la simulation avec un pas de temps fixe - while (accumulatedTime >= timeStep) { - RAIN.update(timeStep); - accumulatedTime -= timeStep; - } - - // Optimisation du rendu - staticCtx.fillStyle = "#007730"; // Couleur de fond - staticCtx.fillRect(BG_RECT.x, BG_RECT.y, BG_RECT.w, BG_RECT.h); - - // Dessin de la pluie - RAIN.drawRain(staticCtx); - - // Copier le buffer au canvas principal - ctx.drawImage(staticCanvas, 0, 0); -} - -function animationLoop(currentTime: number) { - if (!lastFrameTime) { - lastFrameTime = currentTime; - animationFrameId = requestAnimationFrame(animationLoop); - return; - } - - const deltaTime = currentTime - lastFrameTime; - lastFrameTime = currentTime; - - // Limiter la fréquence des mises à jour si le navigateur est trop lent - if (deltaTime < 100) { // Ignorer les deltas trop grands (changement d'onglet, etc.) - drawFrame(deltaTime); - } - - animationFrameId = requestAnimationFrame(animationLoop); -} - -function initEventListeners() { - // Gestion des événements de redimensionnement et de pause - const handleVisibilityChange = () => { - if (document.hidden) { - cancelAnimationFrame(animationFrameId); - lastFrameTime = 0; - } else { - lastFrameTime = performance.now(); - animationFrameId = requestAnimationFrame(animationLoop); - } - }; - - document.addEventListener('visibilitychange', handleVisibilityChange); - - // Ajouter le nettoyage de l'event listener - callbacks.push(() => { - document.removeEventListener('visibilitychange', handleVisibilityChange); - }); -} - -export function cleanup() { - cancelAnimationFrame(animationFrameId); - callbacks.forEach(fn => fn()); -} - -export async function initDisplay() { - if (!ctx) { - console.error("Context canvas indisponible"); - return; - } - - console.log("Load beginning"); - - try { - // Préchargement des ressources - await RAIN.preloadRain(); - console.log("Loading completed"); - - // Initialiser les écouteurs d'événements - initEventListeners(); - - // Démarrer la boucle d'animation avec le temps actuel - lastFrameTime = performance.now(); - animationFrameId = requestAnimationFrame(animationLoop); - - window.cleanup = cleanup; - } catch (error) { - console.error("Erreur lors de l'initialisation:", error); - } -} - -// Déclaration globale pour TypeScript -declare global { - interface Window { - cleanup: () => void; - } -} - -// Initialisation automatique si le script est chargé directement -if (typeof window !== 'undefined') { - initDisplay().catch(console.error); -} diff --git a/src/display/dp1.ts b/src/display/dp1.ts deleted file mode 100644 index fcc5dba..0000000 --- a/src/display/dp1.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { Deck } from "../deck"; - -declare global { - interface Window { - clanup: () => void; - } -} - -export { }; - -async function preloadDeck(deck: Deck) { - await deck.preload(); -} - -async function display() { - const canvas = document.getElementById("myCanvas") as HTMLCanvasElement; - - if (canvas) { - const ctx = canvas.getContext("2d"); - - if (ctx) { - // tapis - ctx.fillStyle = "#007730"; - ctx.fillRect(0, 0, 1000, 1000); - - // tuiles - let x = 130; - let y = 30; - let size = 0.75; - let xos = 40; - let yos = 100; - const deck = new Deck(false); - await preloadDeck(deck); - deck.displayFamilies(ctx, x, y, size, xos, yos); - - // texte - ctx.fillStyle = "#DFDFFF"; - ctx.font = "30px serif"; - - let delta = 50 * size; - let eps = 30 * size; - ctx.fillText("Caractère:", 5, y + delta + eps); - ctx.fillText("Rond:", 5, y + 3 * delta + yos * size + eps); - ctx.fillText("Bambou:", 5, y + 5 * delta + 2 * yos * size + eps); - ctx.fillText("Vent:", 5, y + 7 * delta + 3 * yos * size + eps); - ctx.fillText("Dragon:", 5, y + 9 * delta + 4 * yos * size + eps); - - // familles - for (let i = 0; i < 3; i++) { - for (let j = 0; j < 9; j++) { - ctx.fillText( - String(j+1), - x + j * (xos + 75) * size + 30 * size, - y + i * (yos + 100) * size + 150 * size - ); - } - } - - // vent - ctx.fillText( - "Est", - x + 0 * (xos + 75) * size, - y + 3 * (yos + 100) * size + 150 * size - ); - ctx.fillText( - "Sud", - x + 1 * (xos + 75) * size, - y + 3 * (yos + 100) * size + 150 * size - ); - ctx.fillText( - "Ouest", - x + 2 * (xos + 75) * size, - y + 3 * (yos + 100) * size + 150 * size - ); - ctx.fillText( - "Nord", - x + 3 * (xos + 75) * size, - y + 3 * (yos + 100) * size + 150 * size - ); - - // dragon - ctx.fillText( - "Rouge", - x + 0 * (xos + 75) * size, - y + 4 * (yos + 100) * size + 150 * size - ); - ctx.fillText( - "Vert", - x + 1 * (xos + 75) * size, - y + 4 * (yos + 100) * size + 150 * size - ); - ctx.fillText( - "Blanc", - x + 2 * (xos + 75) * size, - y + 4 * (yos + 100) * size + 150 * size - ); - - window.cleanup = () => { - deck.cleanup(); - } - - } else { - console.error("Impossible d'obtenir le contexte du canvas."); - } - } else { - console.error("Canvas introuvable dans le DOM."); - } -} - -display() - diff --git a/src/display/dp2.ts b/src/display/dp2.ts deleted file mode 100644 index e771d24..0000000 --- a/src/display/dp2.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { Deck } from "../deck"; -import { Hand } from "../hand"; - -declare global { - interface Window { - cleanup: () => void; - } -} - -export { }; - -class RiichiDisplay { - private canvas: HTMLCanvasElement; - private ctx: CanvasRenderingContext2D; - private offScreenCanvas: HTMLCanvasElement; - private offScreenCtx: CanvasRenderingContext2D; - - private deck: Deck; - private hands: Hand[] = []; - private edeck: Deck; - private ehand: Hand; - - private selectedTile: number | undefined = undefined; - private animationFrameId: number | null = null; - private isDirty: boolean = true; - - // Constants - private readonly FPS: number = 30; - private readonly INTERVAL: number = 1000 / this.FPS; - private readonly X: number = 100; - private readonly Y: number = 150; - private readonly OS: number = 75; - private readonly SIZE: number = 0.75; - private readonly TILE_WIDTH: number = 78 * this.SIZE; - private readonly MAX_TILES: number = 14; - - // Cache for mouse hit detection - private tileRects: Array<{x: number, y: number, width: number, height: number}> = []; - - constructor() { - const canvas = document.getElementById("myCanvas") as HTMLCanvasElement; - if (!canvas) { - throw new Error("Canvas introuvable dans le DOM."); - } - - this.canvas = canvas; - const ctx = canvas.getContext("2d"); - if (!ctx) { - throw new Error("Impossible d'obtenir le contexte du canvas."); - } - this.ctx = ctx; - - // Create off-screen canvas for double buffering - this.offScreenCanvas = document.createElement('canvas'); - this.offScreenCanvas.width = canvas.width; - this.offScreenCanvas.height = canvas.height; - const offCtx = this.offScreenCanvas.getContext('2d'); - if (!offCtx) { - throw new Error("Impossible d'obtenir le contexte du canvas hors écran."); - } - this.offScreenCtx = offCtx; - - // Initialize decks - this.deck = new Deck(false); - this.edeck = new Deck(false); - - // Initialize with empty hand (will be populated after preload) - this.ehand = new Hand(); - - // Set up event listeners - this.setupEventListeners(); - - // Calculate tile hit areas once - this.calculateTileHitAreas(); - } - - private calculateTileHitAreas(): void { - this.tileRects = []; - for (let i = 0; i < this.MAX_TILES; i++) { - this.tileRects.push({ - x: this.X + i * this.TILE_WIDTH + (i === this.MAX_TILES - 1 ? 10 : 0), - y: 800, - width: 75, - height: 100 * this.SIZE - }); - } - } - - private setupEventListeners(): void { - this.canvas.addEventListener("mousemove", this.handleMouseMove.bind(this)); - this.canvas.addEventListener("mousedown", this.handleMouseDown.bind(this)); - } - - private handleMouseMove(event: MouseEvent): void { - const rect = this.canvas.getBoundingClientRect(); - const mouseX = event.clientX - rect.left; - const mouseY = event.clientY - rect.top; - - // Check if cursor is over any tile using pre-calculated hit areas - const oldSelectedTile = this.selectedTile; - this.selectedTile = undefined; - - for (let i = 0; i < this.tileRects.length; i++) { - const tileRect = this.tileRects[i]; - if ( - mouseX >= tileRect.x && - mouseX <= tileRect.x + tileRect.width && - mouseY >= tileRect.y && - mouseY <= tileRect.y + tileRect.height - ) { - this.selectedTile = i; - break; - } - } - - // Only mark as dirty if selection changed - if (oldSelectedTile !== this.selectedTile) { - this.isDirty = true; - } - } - - private handleMouseDown(): void { - if (this.selectedTile !== undefined) { - this.edeck.push(this.ehand.eject(this.selectedTile)); - this.edeck.shuffle(); - this.ehand.sort(); - this.ehand.push(this.edeck.pop()); - this.isDirty = true; - } - } - - public async initialize(): Promise { - // 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(0, 0, 1000, 1000); - - // Draw title - ctx.fillStyle = "#DFDFFF"; - ctx.font = "50px serif"; - ctx.fillText("Exemples de main:", 15, 50); - - // 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; - } -} - -// 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(); -}; diff --git a/src/display/dp3.ts b/src/display/dp3.ts deleted file mode 100644 index 7ca20e3..0000000 --- a/src/display/dp3.ts +++ /dev/null @@ -1,381 +0,0 @@ -import { Deck } from "../deck"; -import { Hand } from "../hand"; -import { Group } from "../group"; - -// Configuration globale -const CANVAS_ID = "myCanvas"; -const BG_COLOR = "#007730"; -const BG_RECT = { x: 50, y: 50, w: 1000, h: 1000 }; -const FPS = 30; -const FRAME_INTERVAL = 1000 / FPS; - -/** - * Classe RiichiDisplay responsable de la gestion de l'affichage du jeu Riichi Mahjong - */ -class RiichiDisplay { - // Canvas principal et contexte - private canvas: HTMLCanvasElement; - private ctx: CanvasRenderingContext2D; - - // Canvas pour le double-buffering - private staticCanvas: HTMLCanvasElement; - private staticCtx: CanvasRenderingContext2D; - - // Ressources du jeu - private decks: Array = []; - private hands: Array = []; - - // État de l'interface - private selectedTile: number | undefined = undefined; - private isDirty: boolean = true; - - // Animation et événements - private animationFrameId: number | null = null; - private lastFrameTime: number = 0; - private cleanupCallbacks: Array<() => void> = []; - - // Constantes pour le rendu - private readonly BASE_X: number = 300; - private readonly BASE_Y: number = 150; - private readonly X_OFFSET: number = 250; - private readonly Y_OFFSET: number = 100; - private readonly INTERACTIVE_X: number = 100; - private readonly INTERACTIVE_Y: number = 800; - private readonly SIZE: number = 0.75; - private readonly TILE_WIDTH: number = 78 * this.SIZE; - private readonly MAX_TILES: number = 14; - - // Zones de détection pour l'interaction souris - private tileRects: Array<{ x: number, y: number, width: number, height: number }> = []; - - constructor(canvasId: string = CANVAS_ID) { - // Initialisation du canvas - const canvas = document.getElementById(canvasId) as HTMLCanvasElement; - if (!canvas) { - throw new Error(`Canvas avec ID '${canvasId}' introuvable`); - } - this.canvas = canvas; - - // Récupération du contexte 2D - const ctx = canvas.getContext("2d"); - if (!ctx) { - throw new Error("Impossible d'obtenir le contexte 2D du canvas"); - } - this.ctx = ctx; - - // Initialisation du canvas pour le double-buffering - this.staticCanvas = document.createElement('canvas'); - this.staticCanvas.width = canvas.width; - this.staticCanvas.height = canvas.height; - - const staticCtx = this.staticCanvas.getContext("2d"); - if (!staticCtx) { - throw new Error("Impossible d'obtenir le contexte du canvas statique"); - } - this.staticCtx = staticCtx; - - // Pré-calcul des zones de détection - this.calculateTileHitAreas(); - } - - /** - * Pré-calcule les zones de détection pour l'interaction souris - */ - private calculateTileHitAreas(): void { - this.tileRects = []; - for (let i = 0; i < this.MAX_TILES; i++) { - this.tileRects.push({ - x: this.INTERACTIVE_X + i * this.TILE_WIDTH + (i === this.MAX_TILES - 1 ? 10 : 0), - y: this.INTERACTIVE_Y, - width: 75, - height: 100 * this.SIZE - }); - } - } - - /** - * Pré-rendu du fond statique - */ - private prerenderBackground(): void { - this.staticCtx.clearRect(0, 0, this.staticCanvas.width, this.staticCanvas.height); - this.staticCtx.fillStyle = BG_COLOR; - this.staticCtx.fillRect(BG_RECT.x, BG_RECT.y, BG_RECT.w, BG_RECT.h); - } - - /** - * Dessine une frame complète - */ - private drawFrame(): void { - // Vérifier si un redessinage est nécessaire - if (!this.isDirty) return; - - // Pré-rendu du fond statique - this.prerenderBackground(); - - // Dessin des éléments statiques et dynamiques - this.drawHandsAndLabels(); - - // Affichage des informations sur les groupes - this.checkAndDisplayGroups(); - - // Copie du canvas statique vers le canvas principal - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - this.ctx.drawImage(this.staticCanvas, 0, 0); - - // Réinitialisation du flag de modification - this.isDirty = false; - } - - /** - * Dessine les mains et leurs étiquettes - */ - private drawHandsAndLabels(): void { - const ctx = this.staticCtx; - - // Configurer le style de texte - ctx.fillStyle = "#DFDFFF"; - ctx.font = "50px serif"; - - // Dessiner les mains "Chii" - ctx.fillText("Chii:", 75, this.BASE_Y + 100 * this.SIZE - 5); - this.hands[0].drawHand(ctx, this.BASE_X, this.BASE_Y, 5, this.SIZE); - this.hands[1].drawHand(ctx, this.BASE_X + (75 + this.X_OFFSET) * this.SIZE, this.BASE_Y, 5, this.SIZE); - - // Dessiner les mains "Pon" - ctx.fillText("Pon:", 75, this.BASE_Y + (100 + this.Y_OFFSET) * this.SIZE + 100 * this.SIZE - 5); - this.hands[2].drawHand(ctx, this.BASE_X, this.BASE_Y + (100 + this.Y_OFFSET) * this.SIZE, 5, this.SIZE); - this.hands[3].drawHand(ctx, this.BASE_X + (75 + this.X_OFFSET) * this.SIZE, this.BASE_Y + (100 + this.Y_OFFSET) * this.SIZE, 5, this.SIZE); - - // Dessiner les mains "Invalide" - ctx.fillText("Invalide:", 75, this.BASE_Y + 2 * (100 + this.Y_OFFSET) * this.SIZE + 100 * this.SIZE - 5); - this.hands[5].drawHand(ctx, this.BASE_X, this.BASE_Y + 2 * (100 + this.Y_OFFSET) * this.SIZE, 5, this.SIZE); - this.hands[6].drawHand(ctx, this.BASE_X + (75 + this.X_OFFSET) * this.SIZE, this.BASE_Y + 2 * (100 + this.Y_OFFSET) * this.SIZE, 5, this.SIZE); - this.hands[7].drawHand(ctx, this.BASE_X + 2 * (75 + this.X_OFFSET) * this.SIZE, this.BASE_Y + 2 * (100 + this.Y_OFFSET) * this.SIZE, 5, this.SIZE); - - // Main supplémentaire (position spéciale) - this.hands[4].drawHand(ctx, this.BASE_X + 2 * (75 + this.X_OFFSET) * this.SIZE, this.BASE_Y + (100 + this.Y_OFFSET) * this.SIZE, 5, this.SIZE); - - // Main interactive - this.hands[8].isolate = true; - this.hands[8].drawHand(ctx, this.INTERACTIVE_X, this.INTERACTIVE_Y, 5, this.SIZE, this.selectedTile); - } - - /** - * Vérifie et affiche les informations sur les groupes formés - */ - private checkAndDisplayGroups(): void { - const groups = this.hands[8].toGroup(); - if (groups !== undefined) { - this.staticCtx.fillStyle = "#FF0000"; - this.staticCtx.font = "50px serif"; - this.staticCtx.fillText("Tous les groupes sont formés !", 100, 750); - } - } - - /** - * Démarre la boucle d'animation - */ - private startAnimationLoop(): void { - const animationLoop = (currentTime: number) => { - this.animationFrameId = requestAnimationFrame(animationLoop); - - const deltaTime = currentTime - this.lastFrameTime; - if (deltaTime < FRAME_INTERVAL) return; - - this.lastFrameTime = currentTime - (deltaTime % FRAME_INTERVAL); - this.drawFrame(); - }; - - this.animationFrameId = requestAnimationFrame(animationLoop); - } - - /** - * Initialise les écouteurs d'événements - */ - private initEventListeners(): void { - const mouseMoveHandler = this.handleMouseMove.bind(this); - const mouseDownHandler = this.handleMouseDown.bind(this); - - this.canvas.addEventListener('mousemove', mouseMoveHandler); - this.canvas.addEventListener('mousedown', mouseDownHandler); - - // Enregistrer les callbacks de nettoyage - this.cleanupCallbacks.push(() => { - this.canvas.removeEventListener('mousemove', mouseMoveHandler); - this.canvas.removeEventListener('mousedown', mouseDownHandler); - }); - } - - /** - * Gère le mouvement de la souris - */ - private handleMouseMove(e: MouseEvent): void { - const rect = this.canvas.getBoundingClientRect(); - const mouseX = e.clientX - rect.left; - const mouseY = e.clientY - rect.top; - - // Sauvegarde de l'état précédent pour détecter les changements - const previousSelectedTile = this.selectedTile; - this.selectedTile = undefined; - - // Détection optimisée avec zones pré-calculées - for (let i = 0; i < this.tileRects.length; i++) { - const tileRect = this.tileRects[i]; - if ( - mouseX >= tileRect.x && - mouseX <= tileRect.x + tileRect.width && - mouseY >= tileRect.y && - mouseY <= tileRect.y + tileRect.height - ) { - this.selectedTile = i; - break; - } - } - - // Marquer comme "dirty" uniquement si la sélection a changé - if (previousSelectedTile !== this.selectedTile) { - this.isDirty = true; - } - } - - /** - * Gère le clic de souris - */ - private handleMouseDown(): void { - if (this.selectedTile !== undefined) { - // Exécuter l'action pour la tuile sélectionnée - this.decks[0].push(this.hands[8].eject(this.selectedTile)); - this.decks[0].shuffle(); - this.hands[8].sort(); - this.hands[8].push(this.decks[0].pop()); - - // Marquer comme "dirty" pour forcer le redessinage - this.isDirty = true; - } - } - - /** - * Précharge un deck - */ - private async preloadDeck(deck: Deck): Promise { - await deck.preload(); - } - - /** - * Précharge une main - */ - private async preloadHand(hand: Hand): Promise { - await hand.preload(); - } - - /** - * Initialise l'affichage et démarre l'application - */ - public async initialize(): Promise { - 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 { - try { - displayInstance = new RiichiDisplay(CANVAS_ID); - await displayInstance.initialize(); - window.cleanup = cleanup; - } catch (error) { - console.error("Erreur lors de l'initialisation:", error); - } -} - -// Déclaration globale pour TypeScript -declare global { - interface Window { - cleanup: () => void; - } -} - -// Initialisation automatique si le script est chargé directement -if (typeof window !== 'undefined') { - initDisplay().catch(console.error); -} diff --git a/src/display/dp4.ts b/src/display/dp4.ts deleted file mode 100644 index 4173b74..0000000 --- a/src/display/dp4.ts +++ /dev/null @@ -1,275 +0,0 @@ -import { Deck } from "../deck"; -import { Hand } from "../hand"; -import { Game } from "../game"; - -function showPlayButton() { - const button = document.createElement('button'); - button.id = 'playButton'; - button.textContent = 'Jouer'; - button.style.position = 'absolute'; - button.style.left = `${1050/2}px`; - button.style.top = `${1050/2}px`; - button.style.transform = 'translate(-50%, -50%)'; - button.style.fontSize = '2rem'; - button.style.padding = '1em 2em'; - button.style.zIndex = '1000'; - - document.body.appendChild(button); - - button.addEventListener('click', async () => { - button.disabled = true; - button.textContent = 'Chargement...'; - await initDisplay(); - button.remove(); - }); -} - -class RiichiGameManager { - // Configuration globale - private readonly CANVAS_ID: string = "myCanvas"; - private readonly BG_RECT = { x: 0, y: 0, w: 1050, h: 1050 }; - private readonly FPS: number = 60; - private readonly FRAME_INTERVAL: number = 1000 / this.FPS; - - // Singleton instance - private static instance: RiichiGameManager; - - // Canvas et contextes - private canvas: HTMLCanvasElement; - private ctx: CanvasRenderingContext2D; - private staticCanvas: HTMLCanvasElement; - private staticCtx: CanvasRenderingContext2D; - - // État du jeu - private mouse = { x: 0, y: 0 }; - private game: Game | null = null; - private decks: Array = []; - private hands: Array = []; - - // Animation et boucle de jeu - private animationFrameId: number | null = null; - private lastFrameTime: number = 0; - private isInitialized: boolean = false; - - // Nettoyage - private cleanupCallbacks: Array<() => void> = []; - - /** - * Constructeur privé pour le Singleton - */ - private constructor() { - // Initialisation du canvas principal - const canvas = document.getElementById(this.CANVAS_ID) as HTMLCanvasElement; - if (!canvas) { - throw new Error(`Canvas avec ID '${this.CANVAS_ID}' introuvable`); - } - this.canvas = canvas; - this.canvas.width = this.BG_RECT.w; - this.canvas.height = this.BG_RECT.h; - - // Récupération du contexte 2D - const ctx = canvas.getContext("2d", { alpha: false }); - if (!ctx) { - throw new Error("Impossible d'obtenir le contexte 2D du canvas"); - } - this.ctx = ctx; - - // Initialisation du canvas pour le double-buffering - this.staticCanvas = document.createElement('canvas'); - this.staticCanvas.width = canvas.width; - this.staticCanvas.height = canvas.height; - - const staticCtx = this.staticCanvas.getContext("2d", { alpha: false }); - if (!staticCtx) { - throw new Error("Impossible d'obtenir le contexte du canvas statique"); - } - this.staticCtx = staticCtx; - } - - /** - * Accès à l'instance Singleton - */ - public static getInstance(): RiichiGameManager { - if (!RiichiGameManager.instance) { - RiichiGameManager.instance = new RiichiGameManager(); - } - return RiichiGameManager.instance; - } - - /** - * Dessine une frame du jeu - */ - private drawFrame(): void { - if (this.game) { - this.game.draw(this.mouse); - } - } - - /** - * Démarre la boucle d'animation du jeu - */ - private startAnimationLoop(): void { - const animationLoop = (currentTime: number) => { - this.animationFrameId = requestAnimationFrame(animationLoop); - - const deltaTime = currentTime - this.lastFrameTime; - if (deltaTime < this.FRAME_INTERVAL) return; - - // Correction du time drift - this.lastFrameTime = currentTime - (deltaTime % this.FRAME_INTERVAL); - - // Rendu d'une frame - this.drawFrame(); - }; - - this.animationFrameId = requestAnimationFrame(animationLoop); - } - - /** - * Initialise les écouteurs d'événements - */ - private initEventListeners(): void { - // Handler pour le déplacement de la souris - const mouseMoveHandler = (e: MouseEvent) => { - // Calcul des coordonnées relatives au canvas - const rect = this.canvas.getBoundingClientRect(); - this.mouse.x = e.clientX - rect.left; - this.mouse.y = e.clientY - rect.top; - }; - - // Handler pour le clic de souris - const mouseDownHandler = (e: MouseEvent) => { - if (this.game) { - this.game.click(e); - } - }; - - // Enregistrement des écouteurs - this.canvas.addEventListener('mousemove', mouseMoveHandler); - this.canvas.addEventListener('mousedown', mouseDownHandler); - - // Enregistrement des callbacks de nettoyage - this.cleanupCallbacks.push(() => { - this.canvas.removeEventListener('mousemove', mouseMoveHandler); - this.canvas.removeEventListener('mousedown', mouseDownHandler); - }); - } - - /** - * Précharge un deck - */ - private async preloadDeck(deck: Deck): Promise { - await deck.preload(); - } - - /** - * Précharge une main - */ - private async preloadHand(hand: Hand): Promise { - await hand.preload(); - } - - /** - * Initialise le jeu - */ - public async initialize(): Promise { - if (this.isInitialized) return; - - try { - console.log("Chargement en cours..."); - - // Création du jeu - this.game = new Game( - this.ctx, - this.canvas, - this.staticCtx, - this.staticCanvas - ); - - // Préchargement parallèle des ressources - await Promise.all([ - ...this.decks.map(deck => this.preloadDeck(deck)), - ...this.hands.map(hand => this.preloadHand(hand)), - this.game.preload() - ]); - - console.log("Chargement terminé"); - - // Initialisation des écouteurs d'événements - this.initEventListeners(); - - // Démarrage de la boucle d'animation - this.startAnimationLoop(); - - // Marquer comme initialisé - this.isInitialized = true; - - // Définir la fonction de nettoyage global - window.cleanup = this.cleanup.bind(this); - - } catch (error) { - console.error("Erreur lors de l'initialisation:", error); - } - } - - /** - * Nettoie les ressources - */ - public cleanup(): void { - // Arrêter la boucle d'animation - if (this.animationFrameId !== null) { - cancelAnimationFrame(this.animationFrameId); - this.animationFrameId = null; - } - - // Exécuter tous les callbacks de nettoyage - this.cleanupCallbacks.forEach(callback => callback()); - this.cleanupCallbacks = []; - - // Nettoyer les ressources du jeu - if (this.game) { - // Supposons que Game a une méthode cleanup - (this.game as any).cleanup?.(); - this.game = null; - } - - // Nettoyer les ressources des decks et des mains - this.decks.forEach(deck => deck.cleanup()); - this.hands.forEach(hand => hand.cleanup()); - - // Vider les collections - this.decks = []; - this.hands = []; - - // Réinitialiser l'état - this.isInitialized = false; - this.lastFrameTime = 0; - this.mouse = { x: 0, y: 0 }; - - // Effacer les canvas - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - this.staticCtx.clearRect(0, 0, this.staticCanvas.width, this.staticCanvas.height); - } -} - -// Fonction de nettoyage globale exportée -export function cleanup(): void { - RiichiGameManager.getInstance().cleanup(); -} - -// Fonction d'initialisation globale exportée -export async function initDisplay(): Promise { - await RiichiGameManager.getInstance().initialize(); -} - -// Déclaration globale pour TypeScript -declare global { - interface Window { - cleanup: () => void; - } -} - -// Initialisation automatique si le script est chargé directement -if (typeof window !== 'undefined') { - showPlayButton(); -} diff --git a/src/display/dp5.ts b/src/display/dp5.ts deleted file mode 100644 index 4b11f5a..0000000 --- a/src/display/dp5.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { Deck } from "../deck"; -import { Hand } from "../hand"; -import { Game } from "../game" - -// Configuration globale -const CANVAS_ID = "myCanvas"; -const BG_RECT = { x: 0, y: 0, w: 1050, h: 1050 }; -var MOUSE = { x: 0, y: 0 }; -const FPS = 30; -const FRAME_INTERVAL = 1000 / FPS; - -// variables globales -const DECKS: Array = []; -const HANDS: Array = []; -var GAME: Game|undefined; - -// Optimisation des références -let animationFrameId: number; -let lastFrameTime = 0; -const callbacks: Array<() => void> = []; - -// Pré-calcul des dimensions -const canvas = document.getElementById(CANVAS_ID) as HTMLCanvasElement; -const ctx = canvas.getContext("2d") as NonNullable; -canvas.width = BG_RECT.w; -canvas.height = BG_RECT.h; - -// Cache statique -const staticCanvas = document.createElement('canvas') as HTMLCanvasElement; -const staticCtx = staticCanvas.getContext("2d") as NonNullable; -staticCanvas.width = canvas.width; -staticCanvas.height = canvas.height; - -function drawFrame() { - if (!ctx) return; - GAME?.draw(MOUSE); -} - -function animationLoop(currentTime: number) { - animationFrameId = requestAnimationFrame(animationLoop); - - const deltaTime = currentTime - lastFrameTime; - if (deltaTime < FRAME_INTERVAL) return; - - lastFrameTime = currentTime - (deltaTime % FRAME_INTERVAL); - drawFrame(); -} - -function initEventListeners() { - const handlers = { - mousedown: (e: MouseEvent) => { - GAME?.click(e); - }, - mousemove: (e: MouseEvent) => { - MOUSE.x = e.x; - MOUSE.y = e.y; - } - }; - - callbacks.push(() => { - canvas.removeEventListener('mousemove', handlers.mousemove); - }); - - canvas.addEventListener('mousedown', handlers.mousedown); -} - -async function preloadDeck(deck: Deck) { - await deck.preload(); -} -async function preloadHand(hand: Hand) { - await hand.preload(); -} - -export function cleanup() { - cancelAnimationFrame(animationFrameId); - callbacks.forEach(fn => fn()); -} - -export async function initDisplay() { - if (!ctx) { - console.error("Context canvas indisponible"); - return; - } - - console.log("Load begining\n"); - // Préchargement des ressources si nécessaire - // const deck = new Deck(); - // await preloadDeck(deck); - // Charge et affiche l'image "ron.png" sur la partie gauche du canvas - try { - const ronImg = new Image(); - ronImg.src = "img/ron.png"; - await new Promise((resolve) => { - ronImg.onload = () => resolve(); - ronImg.onerror = () => { - console.warn("Impossible de charger img/ron.png"); - resolve(); - }; - }); - - // Dessin de l'image en plein écran (remplit tout le canvas) - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.drawImage(ronImg, 0, 0, canvas.width, canvas.height); - } catch (err) { - console.error("Erreur lors du rendu de ron.png", err); - } -} - -// Déclaration globale pour TypeScript -declare global { - interface Window { - cleanup: () => void; - } -} - -// Initialisation automatique si le script est chargé directement -if (typeof window !== 'undefined') { - initDisplay().catch(console.error); -} - diff --git a/src/display/dp6.ts b/src/display/dp6.ts deleted file mode 100644 index b908726..0000000 --- a/src/display/dp6.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { Deck } from "../deck"; -import { Hand } from "../hand"; -import { Game } from "../game"; - -function showPlayButton() { - const button = document.createElement('button'); - button.id = 'playButton'; - button.textContent = 'Jouer'; - button.style.position = 'absolute'; - button.style.left = `${1050/2}px`; - button.style.top = `${1050/2}px`; - button.style.transform = 'translate(-50%, -50%)'; - button.style.fontSize = '2rem'; - button.style.padding = '1em 2em'; - button.style.zIndex = '1000'; - - document.body.appendChild(button); - - button.addEventListener('click', async () => { - button.disabled = true; - button.textContent = 'Chargement...'; - await initDisplay(); - button.remove(); - }); -} - -class RiichiGameManager { - // Configuration globale - private readonly CANVAS_ID: string = "myCanvas"; - private readonly BG_RECT = { x: 0, y: 0, w: 1050, h: 1050 }; - private readonly FPS: number = 60; - private readonly FRAME_INTERVAL: number = 1000 / this.FPS; - - // Singleton instance - private static instance: RiichiGameManager; - - // Canvas et contextes - private canvas: HTMLCanvasElement; - private ctx: CanvasRenderingContext2D; - private staticCanvas: HTMLCanvasElement; - private staticCtx: CanvasRenderingContext2D; - - // État du jeu - private mouse = { x: 0, y: 0 }; - private game: Game | null = null; - private decks: Array = []; - private hands: Array = []; - - // Animation et boucle de jeu - private animationFrameId: number | null = null; - private lastFrameTime: number = 0; - private isInitialized: boolean = false; - - // Nettoyage - private cleanupCallbacks: Array<() => void> = []; - - /** - * Constructeur privé pour le Singleton - */ - private constructor() { - // Initialisation du canvas principal - const canvas = document.getElementById(this.CANVAS_ID) as HTMLCanvasElement; - if (!canvas) { - throw new Error(`Canvas avec ID '${this.CANVAS_ID}' introuvable`); - } - this.canvas = canvas; - this.canvas.width = this.BG_RECT.w; - this.canvas.height = this.BG_RECT.h; - - // Récupération du contexte 2D - const ctx = canvas.getContext("2d", { alpha: false }); - if (!ctx) { - throw new Error("Impossible d'obtenir le contexte 2D du canvas"); - } - this.ctx = ctx; - - // Initialisation du canvas pour le double-buffering - this.staticCanvas = document.createElement('canvas'); - this.staticCanvas.width = canvas.width; - this.staticCanvas.height = canvas.height; - - const staticCtx = this.staticCanvas.getContext("2d", { alpha: false }); - if (!staticCtx) { - throw new Error("Impossible d'obtenir le contexte du canvas statique"); - } - this.staticCtx = staticCtx; - } - - /** - * Accès à l'instance Singleton - */ - public static getInstance(): RiichiGameManager { - if (!RiichiGameManager.instance) { - RiichiGameManager.instance = new RiichiGameManager(); - } - return RiichiGameManager.instance; - } - - /** - * Dessine une frame du jeu - */ - private drawFrame(): void { - if (this.game) { - this.game.draw(this.mouse); - } - } - - /** - * Démarre la boucle d'animation du jeu - */ - private startAnimationLoop(): void { - const animationLoop = (currentTime: number) => { - this.animationFrameId = requestAnimationFrame(animationLoop); - - const deltaTime = currentTime - this.lastFrameTime; - if (deltaTime < this.FRAME_INTERVAL) return; - - // Correction du time drift - this.lastFrameTime = currentTime - (deltaTime % this.FRAME_INTERVAL); - - // Rendu d'une frame - this.drawFrame(); - }; - - this.animationFrameId = requestAnimationFrame(animationLoop); - } - - /** - * Initialise les écouteurs d'événements - */ - private initEventListeners(): void { - // Handler pour le déplacement de la souris - const mouseMoveHandler = (e: MouseEvent) => { - // Calcul des coordonnées relatives au canvas - const rect = this.canvas.getBoundingClientRect(); - this.mouse.x = e.clientX - rect.left; - this.mouse.y = e.clientY - rect.top; - }; - - // Handler pour le clic de souris - const mouseDownHandler = (e: MouseEvent) => { - if (this.game) { - this.game.click(e); - } - }; - - // Enregistrement des écouteurs - this.canvas.addEventListener('mousemove', mouseMoveHandler); - this.canvas.addEventListener('mousedown', mouseDownHandler); - - // Enregistrement des callbacks de nettoyage - this.cleanupCallbacks.push(() => { - this.canvas.removeEventListener('mousemove', mouseMoveHandler); - this.canvas.removeEventListener('mousedown', mouseDownHandler); - }); - } - - /** - * Précharge un deck - */ - private async preloadDeck(deck: Deck): Promise { - await deck.preload(); - } - - /** - * Précharge une main - */ - private async preloadHand(hand: Hand): Promise { - await hand.preload(); - } - - /** - * Initialise le jeu - */ - public async initialize(): Promise { - 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, - false, - 0, - Math.floor(Math.random() * 4) - ); - - // Préchargement parallèle des ressources - await Promise.all([ - ...this.decks.map(deck => this.preloadDeck(deck)), - ...this.hands.map(hand => this.preloadHand(hand)), - this.game.preload() - ]); - - console.log("Chargement terminé"); - - // Initialisation des écouteurs d'événements - this.initEventListeners(); - - // Démarrage de la boucle d'animation - this.startAnimationLoop(); - - // Marquer comme initialisé - this.isInitialized = true; - - // Définir la fonction de nettoyage global - window.cleanup = this.cleanup.bind(this); - - } catch (error) { - console.error("Erreur lors de l'initialisation:", error); - } - } - - /** - * Nettoie les ressources - */ - public cleanup(): void { - // Arrêter la boucle d'animation - if (this.animationFrameId !== null) { - cancelAnimationFrame(this.animationFrameId); - this.animationFrameId = null; - } - - // Exécuter tous les callbacks de nettoyage - this.cleanupCallbacks.forEach(callback => callback()); - this.cleanupCallbacks = []; - - // Nettoyer les ressources du jeu - if (this.game) { - // Supposons que Game a une méthode cleanup - (this.game as any).cleanup?.(); - this.game = null; - } - - // Nettoyer les ressources des decks et des mains - this.decks.forEach(deck => deck.cleanup()); - this.hands.forEach(hand => hand.cleanup()); - - // Vider les collections - this.decks = []; - this.hands = []; - - // Réinitialiser l'état - this.isInitialized = false; - this.lastFrameTime = 0; - this.mouse = { x: 0, y: 0 }; - - // Effacer les canvas - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - this.staticCtx.clearRect(0, 0, this.staticCanvas.width, this.staticCanvas.height); - } -} - -// Fonction de nettoyage globale exportée -export function cleanup(): void { - RiichiGameManager.getInstance().cleanup(); -} - -// Fonction d'initialisation globale exportée -export async function initDisplay(): Promise { - await RiichiGameManager.getInstance().initialize(); -} - -// Déclaration globale pour TypeScript -declare global { - interface Window { - cleanup: () => void; - } -} - -// Initialisation automatique si le script est chargé directement -if (typeof window !== 'undefined') { - showPlayButton(); -} diff --git a/src/display/dp7.ts b/src/display/dp7.ts deleted file mode 100644 index d9a15f9..0000000 --- a/src/display/dp7.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { Hand } from "../hand"; -import { function_generator } from "../yakus/generator" - -// Configuration globale -const CANVAS_ID = "myCanvas"; -const FPS = 30; -const FRAME_INTERVAL = 1000 / FPS; - -/** - * Classe RiichiDisplay responsable de la gestion de l'affichage du jeu Riichi Mahjong - */ -class RiichiDisplay { - // Canvas principal et contexte - private canvas: HTMLCanvasElement; - private ctx: CanvasRenderingContext2D; - - // Ressources du jeu - private hands: Array = []; - private handTexts: Array = []; - private wind: number = Math.floor(Math.random() * 4); - - // Animation et événements - private animationFrameId: number | null = null; - private lastFrameTime: number = 0; - private cleanupCallbacks: Array<() => void> = []; - - // Constantes pour le rendu - private readonly BASE_X: number = 50; - private readonly BASE_Y: number = 90; - private readonly DY = 160; - private readonly SIZE: number = 0.75; - - constructor(canvasId: string = CANVAS_ID) { - // Initialisation du canvas - const canvas = document.getElementById(canvasId) as HTMLCanvasElement; - if (!canvas) { - throw new Error(`Canvas avec ID '${canvasId}' introuvable`); - } - this.canvas = canvas; - - // Récupération du contexte 2D - const ctx = canvas.getContext("2d"); - if (!ctx) { - throw new Error("Impossible d'obtenir le contexte 2D du canvas"); - } - this.ctx = ctx; - } - - /** - * Dessine une frame complète - */ - private drawFrame(): void { - // Copie du canvas statique vers le canvas principal - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - - // Dessin des éléments statiques et dynamiques - this.drawHandsAndLabels(); - } - - /** - * Dessine les mains et leurs étiquettes - */ - private drawHandsAndLabels(): void { - for (let i = 0; i < this.hands.length; i++) { - - this.ctx.fillStyle = "#DFDFFF"; - this.ctx.font = "40px serif"; - this.ctx.fillText(this.handTexts[i], this.BASE_X, this.BASE_Y - 20 + i * this.DY); - - this.hands[i].drawHand( - this.ctx, - this.BASE_X, - this.BASE_Y + i * this.DY, - 5, - this.SIZE - ); - } - } - - /** - * Démarre la boucle d'animation - */ - private startAnimationLoop(): void { - const animationLoop = (currentTime: number) => { - this.animationFrameId = requestAnimationFrame(animationLoop); - - const deltaTime = currentTime - this.lastFrameTime; - if (deltaTime < FRAME_INTERVAL) return; - - this.lastFrameTime = currentTime - (deltaTime % FRAME_INTERVAL); - this.drawFrame(); - }; - - this.animationFrameId = requestAnimationFrame(animationLoop); - } - - /** - * Précharge une main - */ - private async preloadHand(hand: Hand): Promise { - await hand.preload(); - } - - /** - * Initialise l'affichage et démarre l'application - */ - public async initialize(): Promise { - try { - // Création des mains prédéfinies - this.hands.push( - function_generator.ordinaires(), - function_generator.brelan_valeur(this.wind), - function_generator.main_pure(), - function_generator.main_semi_pure(), - function_generator.double_suite(), - function_generator.sept_pairs() - ); - - this.handTexts.push( - "Tout ordinaires :", - "Brelan de valeur (" + ["Est", "Sud", "Ouest", "Nord"][this.wind] + ") :", - "Main pure :", - "Main semi-pure :", - "Double suite :", - "Sept pairs :" - ); - - // Préchargement parallèle des ressources - await Promise.all([ - ...this.hands.map(hand => this.preloadHand(hand)) - ]); - - // Premier rendu - 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.hands.forEach(hand => hand.cleanup()); - - // Vider les collections - this.hands = []; - } -} - -// 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 { - try { - displayInstance = new RiichiDisplay(CANVAS_ID); - await displayInstance.initialize(); - window.cleanup = cleanup; - } catch (error) { - console.error("Erreur lors de l'initialisation:", error); - } -} - -// Déclaration globale pour TypeScript -declare global { - interface Window { - cleanup: () => void; - } -} - -// Initialisation automatique si le script est chargé directement -if (typeof window !== 'undefined') { - initDisplay().catch(console.error); -} diff --git a/src/display/dp8.ts b/src/display/dp8.ts deleted file mode 100644 index c5e231b..0000000 --- a/src/display/dp8.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { Deck } from "../deck"; -import { Hand } from "../hand"; -import { Game } from "../game"; - -function showPlayButton() { - const button = document.createElement('button'); - button.id = 'playButton'; - button.textContent = 'Jouer'; - button.style.position = 'absolute'; - button.style.left = `${1050/2}px`; - button.style.top = `${1050/2}px`; - button.style.transform = 'translate(-50%, -50%)'; - button.style.fontSize = '2rem'; - button.style.padding = '1em 2em'; - button.style.zIndex = '1000'; - - document.body.appendChild(button); - - button.addEventListener('click', async () => { - button.disabled = true; - button.textContent = 'Chargement...'; - await initDisplay(); - button.remove(); - }); -} - -class RiichiGameManagerDP8 { - private readonly CANVAS_ID: string = "myCanvas"; - private readonly BG_RECT = { x: 0, y: 0, w: 1050, h: 1050 }; - private readonly FPS: number = 60; - private readonly FRAME_INTERVAL: number = 1000 / this.FPS; - - private static instance: RiichiGameManagerDP8; - - private canvas: HTMLCanvasElement; - private ctx: CanvasRenderingContext2D; - private staticCanvas: HTMLCanvasElement; - private staticCtx: CanvasRenderingContext2D; - - private mouse = { x: 0, y: 0 }; - private game: Game | null = null; - private decks: Array = []; - private hands: Array = []; - - private animationFrameId: number | null = null; - private lastFrameTime: number = 0; - private isInitialized: boolean = false; - - private cleanupCallbacks: Array<() => void> = []; - - private constructor() { - const canvas = document.getElementById(this.CANVAS_ID) as HTMLCanvasElement; - if (!canvas) { - throw new Error(`Canvas with ID '${this.CANVAS_ID}' not found`); - } - this.canvas = canvas; - this.canvas.width = this.BG_RECT.w; - this.canvas.height = this.BG_RECT.h; - - const ctx = canvas.getContext("2d", { alpha: false }); - if (!ctx) { - throw new Error("Unable to get 2D context"); - } - this.ctx = ctx; - - this.staticCanvas = document.createElement('canvas'); - this.staticCanvas.width = canvas.width; - this.staticCanvas.height = canvas.height; - - const staticCtx = this.staticCanvas.getContext("2d", { alpha: false }); - if (!staticCtx) { - throw new Error("Unable to get static context"); - } - this.staticCtx = staticCtx; - } - - public static getInstance(): RiichiGameManagerDP8 { - if (!RiichiGameManagerDP8.instance) { - RiichiGameManagerDP8.instance = new RiichiGameManagerDP8(); - } - return RiichiGameManagerDP8.instance; - } - - private drawFrame(): void { - if (this.game) { - this.game.draw(this.mouse); - - // If game finished and yakus enabled, draw the detected yakus on the left - if (this.game.isFinished()) { - const yakus = this.game.getLastYakus(); - const total = this.game.getLastTotalHan(); - if (yakus && yakus.length > 0) { - const ctx = this.staticCtx; - ctx.save(); - ctx.fillStyle = 'rgba(0,0,0,0.6)'; - ctx.fillRect(20, 60, 300, 24 + yakus.length * 24 + 30); - ctx.fillStyle = '#ffffff'; - ctx.font = '18px garamond'; - ctx.fillText('Yakus detected:', 30, 86); - for (let i = 0; i < yakus.length; i++) { - const y = 110 + i * 24; - ctx.fillText(`${yakus[i].name} (${yakus[i].han})`, 30, y); - } - ctx.fillText(`Total han: ${total}`, 30, 110 + yakus.length * 24 + 8); - ctx.restore(); - - // Blit to visible canvas - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - this.ctx.drawImage(this.staticCanvas, 0, 0); - } - } - } - } - - private startAnimationLoop(): void { - const animationLoop = (currentTime: number) => { - this.animationFrameId = requestAnimationFrame(animationLoop); - const deltaTime = currentTime - this.lastFrameTime; - if (deltaTime < this.FRAME_INTERVAL) return; - this.lastFrameTime = currentTime - (deltaTime % this.FRAME_INTERVAL); - this.drawFrame(); - }; - this.animationFrameId = requestAnimationFrame(animationLoop); - } - - private initEventListeners(): void { - const mouseMoveHandler = (e: MouseEvent) => { - const rect = this.canvas.getBoundingClientRect(); - this.mouse.x = e.clientX - rect.left; - this.mouse.y = e.clientY - rect.top; - }; - - const mouseDownHandler = (e: MouseEvent) => { - if (this.game) this.game.click(e as any); - }; - - this.canvas.addEventListener('mousemove', mouseMoveHandler); - this.canvas.addEventListener('mousedown', mouseDownHandler); - - this.cleanupCallbacks.push(() => { - this.canvas.removeEventListener('mousemove', mouseMoveHandler); - this.canvas.removeEventListener('mousedown', mouseDownHandler); - }); - } - - private async preloadDeck(deck: Deck): Promise { - await deck.preload(); - } - - private async preloadHand(hand: Hand): Promise { - await hand.preload(); - } - - public async initialize(): Promise { - if (this.isInitialized) return; - - try { - // Create the game with yakus enabled (dp8) - this.game = new Game( - this.ctx, - this.canvas, - this.staticCtx, - this.staticCanvas, - false, - 8, - Math.floor(Math.random() * 4), - true // enableYakus - ); - - await Promise.all([ - ...this.decks.map(d => this.preloadDeck(d)), - ...this.hands.map(h => this.preloadHand(h)), - this.game.preload() - ]); - - this.initEventListeners(); - this.startAnimationLoop(); - this.isInitialized = true; - (window as any).cleanup = this.cleanup.bind(this); - } catch (e) { - console.error('Initialization error dp8', e); - } - } - - public cleanup(): void { - if (this.animationFrameId !== null) { - cancelAnimationFrame(this.animationFrameId); - this.animationFrameId = null; - } - this.cleanupCallbacks.forEach(cb => cb()); - this.cleanupCallbacks = []; - if (this.game) (this.game as any).cleanup?.(); - this.game = null; - this.decks.forEach(d => d.cleanup()); - this.hands.forEach(h => h.cleanup()); - this.decks = []; - this.hands = []; - this.isInitialized = false; - this.lastFrameTime = 0; - this.mouse = { x: 0, y: 0 }; - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - this.staticCtx.clearRect(0, 0, this.staticCanvas.width, this.staticCanvas.height); - } -} - -export function cleanup(): void { - RiichiGameManagerDP8.getInstance().cleanup(); -} - -export async function initDisplay(): Promise { - await RiichiGameManagerDP8.getInstance().initialize(); -} - -declare global { - interface Window { - cleanup: () => void; - } -} - -if (typeof window !== 'undefined') { - showPlayButton(); -} diff --git a/src/display/template.ts b/src/display/template.ts deleted file mode 100644 index d9e71c3..0000000 --- a/src/display/template.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { Deck } from "../deck"; -import { Hand } from "../hand"; - -// Configuration globale -const CANVAS_ID = "myCanvas"; -const BG_COLOR = "#007730"; -const BG_RECT = { x: 0, y: 0, w: 1000, h: 1000 }; -const FPS = 30; -const FRAME_INTERVAL = 1000 / FPS; - -// variables globales -const DECKS: Array = []; -const HANDS: Array = []; - -// Optimisation des références -let animationFrameId: number; -let lastFrameTime = 0; -const callbacks: Array<() => void> = []; - -// Pré-calcul des dimensions -const canvas = document.getElementById(CANVAS_ID) as HTMLCanvasElement; -const ctx = canvas.getContext("2d") as NonNullable; - -// Cache statique -const staticCanvas = document.createElement('canvas') as HTMLCanvasElement; -const staticCtx = staticCanvas.getContext("2d") as NonNullable; -staticCanvas.width = canvas.width; -staticCanvas.height = canvas.height; - -// Pré-rendu du fond -function prerenderBackground() { - staticCtx.clearRect(0, 0, canvas.width, canvas.height); - staticCtx.fillStyle = BG_COLOR; - staticCtx.fillRect(BG_RECT.x, BG_RECT.y, BG_RECT.w, BG_RECT.h); -}; - -function drawFrame() { - if (!ctx) return; - - // Effacement intelligent (uniquement la zone nécessaire) - prerenderBackground(); - - // Ici viendrait le dessin des éléments dynamiques - // Par exemple: - // drawDeck(); - // drawHands(); - - // Dessin du cache statique - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.drawImage(staticCanvas, 0, 0); -} - -function animationLoop(currentTime: number) { - animationFrameId = requestAnimationFrame(animationLoop); - - const deltaTime = currentTime - lastFrameTime; - if (deltaTime < FRAME_INTERVAL) return; - - lastFrameTime = currentTime - (deltaTime % FRAME_INTERVAL); - drawFrame(); -} - -function initEventListeners() { - const handlers = { - mousemove: (e: MouseEvent) => { - // Logique de gestion du mouvement de la souris - }, - mousedown: (e: MouseEvent) => { - // Logique de gestion du clic de souris - } - }; - - callbacks.push(() => { - canvas.removeEventListener('mousemove', handlers.mousemove); - canvas.removeEventListener('mousedown', handlers.mousedown); - }); - - canvas.addEventListener('mousemove', handlers.mousemove); - canvas.addEventListener('mousedown', handlers.mousedown); -} - -async function preloadDeck(deck: Deck) { - await deck.preload(); -} -async function preloadHand(hand: Hand) { - await hand.preload(); -} - -export function cleanup() { - cancelAnimationFrame(animationFrameId); - callbacks.forEach(fn => fn()); -} - -export async function initDisplay() { - if (!ctx) { - console.error("Context canvas indisponible"); - return; - } - - // Préchargement des ressources si nécessaire - // const deck = new Deck(); - // await preloadDeck(deck); - DECKS.push( - ); - HANDS.push( - ); - await Promise.all(DECKS.map(d => preloadDeck(d))); - await Promise.all(HANDS.map(h => preloadHand(h))); - - initEventListeners(); - requestAnimationFrame(animationLoop); - window.cleanup = cleanup; -} - -// Déclaration globale pour TypeScript -declare global { - interface Window { - cleanup: () => void; - } -} - -// Initialisation automatique si le script est chargé directement -if (typeof window !== 'undefined') { - initDisplay().catch(console.error); -} - diff --git a/src/game.ts b/src/game.ts deleted file mode 100644 index 3ab6f3e..0000000 --- a/src/game.ts +++ /dev/null @@ -1,1714 +0,0 @@ -import { clickAction, clickChii, drawButtons, drawChiis } from "./button"; -import { Deck } from "./deck"; -import { Group } from "./group"; -import { Hand } from "./hand"; -import { drawState } from "./state"; -import { Tile } from "./tile"; -import { yakus } from "./yakus/yaku"; - -export type MousePos = { x: number, y: number }; - -// Game constants to avoid magic numbers and improve readability -const GAME_CONSTANTS = { - PLAYERS: 4, - BACKGROUND: { color: "#007730", x: 0, y: 0, w: 1050, h: 1050 }, - DEAD_WALL_SIZE: 14, - WAITING_TIME: { - MIN: 500, - MAX: 2000, - DEFAULT: 700 - }, - DISPLAY: { - HAND_SIZE: 0.7, - HIDDEN_HAND_SIZE: 0.6, - DISCARD_SIZE: 0.6, - GROUP_SIZE: 0.6 - }, - PI: Math.PI -}; - -export class Game { - private deck: Deck; - private deadWall: Array = []; - private hands: Array = []; - private discards: Array> = []; - private lastDiscard: number | undefined; - private groups: Array> = []; - private riichiDiscardIndex: number[] = []; - - // Game state - private level: number; - private turn = 0; - private windPlayer: number; - private waitingTime = GAME_CONSTANTS.WAITING_TIME.DEFAULT; - private selectedTile: number | undefined = undefined; - private canCall: boolean = false; - private declaredRiichi: boolean[] = []; - // Version counters to detect when a hand changed (to avoid recomputing expensive sims every frame) - private handVersion: number[] = []; - // Tenpai cache per player - private tenpaiCache: boolean[] = []; - private tenpaiCacheVersion: number[] = []; - // canRon cache for player 0 relative to lastDiscard - private canRonCache: { lastDiscard?: number; handVer?: number; result: boolean } = { result: false }; - private hasPicked: boolean = false; - private hasPlayed: boolean = false; - private lastPlayed: number = Date.now(); - private chooseChii: boolean = false; - private choosingRiichiDiscard: boolean = false; - private validRiichiDiscards: boolean[] = []; // Which tiles can be discarded while keeping tenpai - private turnCount: number = 0; // Count of full rounds (each player played once) - private end: boolean = false; - private result: number = -1; - - // Canvas elements - private ctx: CanvasRenderingContext2D; - private cv: HTMLCanvasElement; - private staticCtx: CanvasRenderingContext2D; - private staticCv: HTMLCanvasElement; - - // Cached values to avoid recalculations - private readonly BG_RECT = GAME_CONSTANTS.BACKGROUND; - private readonly rotations = [0, -GAME_CONSTANTS.PI / 2, -GAME_CONSTANTS.PI, GAME_CONSTANTS.PI / 2]; - - // When enableYakus is true the game will evaluate yakus on any win and - // store the detected yakus for display. - constructor( - ctx: CanvasRenderingContext2D, - cv: HTMLCanvasElement, - staticCtx: CanvasRenderingContext2D, - staticCv: HTMLCanvasElement, - red: boolean = false, - level: number = 0, - windPlayer: number = 0, - private enableYakus: boolean = false - ) { - this.ctx = ctx; - this.cv = cv; - this.staticCtx = staticCtx; - this.staticCv = staticCv; - this.level = level; - this.windPlayer = windPlayer; - this.turn = windPlayer % 2 === 0 ? windPlayer : 4 - windPlayer; - - // Initialize game elements - this.deck = new Deck(red); - this.initializeGame(); - } - - private drawDynamic(ctx: CanvasRenderingContext2D): void { - // Redraw player's hand on top to show focused tile - const { HAND_SIZE } = GAME_CONSTANTS.DISPLAY; - - // In Riichi selection mode, draw tiles with graying for invalid discards - if (this.choosingRiichiDiscard) { - this.drawHandWithRiichiSelection(ctx, HAND_SIZE); - } else { - this.hands[0].drawHand( - ctx, - 2.5 * 75 * 0.75, - 1000 - 150 * HAND_SIZE, - 5 * HAND_SIZE, - 0.75, - this.selectedTile, - false, - 0 - ); - } - - // If a tile is selected, highlight matching discarded tiles - if (this.selectedTile !== undefined) { - const highlighted = this.hands[0].get(this.selectedTile) as Tile; - const sizeDiscard = GAME_CONSTANTS.DISPLAY.DISCARD_SIZE; - for (let p = 0; p < GAME_CONSTANTS.PLAYERS; p++) { - const d = this.discards[p]; - if (!d || d.length === 0) continue; - const riichiIdx = this.riichiDiscardIndex[p]; - ctx.save(); - ctx.translate(525, 525); - ctx.rotate(this.rotations[p]); - for (let i = 0; i < d.length; i++) { - const tile = d[i]; - if (!highlighted.isEqual(tile.getFamily(), tile.getValue())) continue; - - const isRiichiTile = (i === riichiIdx); - const row = i < 6 ? 0 : (i < 12 ? 1 : 2); - const col = i < 12 ? (i % 6) : (i - 12); - - let tx = -sizeDiscard * 475 / 2 + col * 80 * sizeDiscard; - let ty = sizeDiscard * (475 / 2 + 5) + row * 105 * sizeDiscard; - - // Add extra offset for tiles after riichi tile in same row - if (riichiIdx >= 0 && i > riichiIdx) { - const riichiRow = riichiIdx < 6 ? 0 : (riichiIdx < 12 ? 1 : 2); - if (row === riichiRow) { - tx += 25 * sizeDiscard; - } - } - - if (isRiichiTile) { - // Draw riichi tile sideways (90 degree rotation) - const adjustX = tx + 12 * sizeDiscard; - const adjustY = ty - 12 * sizeDiscard; - tile.drawTile(ctx, adjustX, adjustY, sizeDiscard, false, GAME_CONSTANTS.PI / 2, true); - } else { - tile.drawTile(ctx, tx, ty, sizeDiscard, false, 0, true); - } - } - ctx.restore(); - } - } - - // Draw call buttons / chiis on top - if (this.choosingRiichiDiscard) { - // Draw cancel button and Riichi instruction - this.drawRiichiSelectionUI(ctx); - } else if (this.chooseChii) { - drawChiis(ctx, this.getChii(0)); - } else { - const canTsumoLocal = this.hasPicked && this.hasWin(0) && (!this.enableYakus || this.handHasAnyYaku(this.hands[0], this.groups[0])); - let canRonLocal = false; - if (this.lastDiscard !== undefined && this.lastDiscard !== 0) { - if (this.canRonCache.lastDiscard === this.lastDiscard && this.canRonCache.handVer === this.handVersion[0]) { - canRonLocal = this.canRonCache.result; - } else { - const d = this.discards[this.lastDiscard]; - if (d && d.length > 0) { - const last = d[d.length - 1]; - const sim = new Hand(); - for (const t of this.hands[0].getTiles()) { - sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed())); - } - sim.push(new Tile(last.getFamily(), last.getValue(), last.isRed())); - sim.sort(); - const simGroups = sim.toGroup() as Array | undefined; - const combinedGroups = this.groups[0].concat(simGroups ? simGroups : []); - canRonLocal = sim.toGroup() !== undefined && (!this.enableYakus || this.handHasAnyYaku(sim, combinedGroups)); - } else { - canRonLocal = false; - } - this.canRonCache = { lastDiscard: this.lastDiscard, handVer: this.handVersion[0], result: canRonLocal }; - } - } - - const canRiichiLocal = this.canDeclareRiichi(0); - - // When in Riichi, player cannot call chii/pon but can still Ron/Tsumo - const inRiichi = this.declaredRiichi[0]; - const canChiiDisplay = !inRiichi && this.canDoAChii().length > 0; - const canPonDisplay = !inRiichi && this.canDoAPon(); - - drawButtons( - ctx, - canChiiDisplay, - canPonDisplay, - false && this.level > 1, - canRonLocal, - canTsumoLocal, - canRiichiLocal - ); - } - } - - /** - * Draw player's hand with grayed out invalid Riichi discards - */ - private drawHandWithRiichiSelection(ctx: CanvasRenderingContext2D, handSize: number): void { - const tiles = this.hands[0].getTiles(); - const x = 2.5 * 75 * 0.75; - const y = 1000 - 150 * handSize; - const tileOffset = (75 + 5 * handSize) * 0.75; - - for (let i = 0; i < tiles.length; i++) { - const isLastAndIsolated = (i === tiles.length - 1 && this.hands[0].isolate) ? 10 : 0; - let tileX = x + i * tileOffset + isLastAndIsolated * 0.75; - let tileY = y; - - // Lift tile if selected - if (i === this.selectedTile) { - tileY -= 25 * 0.75; - } - - // Gray out tiles that would break tenpai - const isValidDiscard = this.validRiichiDiscards[i] || false; - tiles[i].drawTile( - ctx, - tileX, - tileY, - 0.75, - false, - 0, - !isValidDiscard // gray if not valid - ); - } - } - - /** - * Draw Riichi selection UI (cancel button and instruction text) - */ - private drawRiichiSelectionUI(ctx: CanvasRenderingContext2D): void { - // Draw instruction text - ctx.save(); - ctx.fillStyle = "#66ccff"; - ctx.font = "bold 24px garamond"; - ctx.textAlign = "center"; - ctx.fillText("Choisissez une tuile à défausser pour déclarer Riichi", 525, 820); - ctx.restore(); - - // Draw cancel button - ctx.fillStyle = "#FF9030"; - ctx.beginPath(); - const btnX = 800, btnY = 835, btnW = 110, btnH = 50, r = 8; - ctx.moveTo(btnX + r, btnY); - ctx.lineTo(btnX + btnW - r, btnY); - ctx.quadraticCurveTo(btnX + btnW, btnY, btnX + btnW, btnY + r); - ctx.lineTo(btnX + btnW, btnY + btnH - r); - ctx.quadraticCurveTo(btnX + btnW, btnY + btnH, btnX + btnW - r, btnY + btnH); - ctx.lineTo(btnX + r, btnY + btnH); - ctx.quadraticCurveTo(btnX, btnY + btnH, btnX, btnY + btnH - r); - ctx.lineTo(btnX, btnY + r); - ctx.quadraticCurveTo(btnX, btnY, btnX + r, btnY); - ctx.fill(); - ctx.strokeStyle = "#606060"; - ctx.stroke(); - - ctx.fillStyle = "black"; - ctx.font = "30px garamond"; - ctx.fillText("Annuler", btnX + 15, btnY + 35); - } - - // Last detected yakus for the most recent win (player index => list) - private lastYakus: Array<{ name: string; han: number }> = []; - private lastTotalHan: number = 0; - // Static/dynamic rendering optimization: when true we redraw the static buffer - private staticDirty: boolean = true; - - /** - * Compute yakus for a given player and populate lastYakus/lastTotalHan. - * This uses the exported `yakus` table where each entry returns han (0 if not present). - */ - private computeYakusForPlayer(player: number): void { - this.lastYakus = []; - this.lastTotalHan = 0; - - try { - const hand = this.hands[player]; - const groups = this.groups[player] || []; - - for (const k of Object.keys(yakus)) { - // Call each yaku detector with (hand, groups, wind) - // Some detector functions expect the player's wind as an index - const fn = (yakus as any)[k]; - try { - const han = fn(hand, groups, this.windPlayer); - if (han && han > 0) { - this.lastYakus.push({ name: k, han }); - this.lastTotalHan += han; - } - } catch (e) { - // Ignore errors in any individual detector to avoid breaking the flow - console.warn(`Yaku ${k} threw:`, e); - } - } - } catch (e) { - console.warn("Failed to compute yakus:", e); - this.lastYakus = []; - this.lastTotalHan = 0; - } - } - - private resolveWin(player: number, resultCode: number, fromPlayer?: number): void { - if (this.enableYakus) { - this.computeYakusForPlayer(player); - } else { - this.lastYakus = []; - this.lastTotalHan = 0; - } - - this.end = true; - this.result = resultCode; - this.staticDirty = true; - } - - /** - * Check whether a given hand (possibly simulated) yields at least one yaku. - * Does not modify `lastYakus`/`lastTotalHan` — only inspects using yakus detectors. - */ - private handHasAnyYaku(hand: Hand, groups: Array = []): boolean { - try { - for (const k of Object.keys(yakus)) { - const fn = (yakus as any)[k]; - try { - const han = fn(hand, groups, this.windPlayer); - if (han && han > 0) return true; - } catch (e) { - // ignore detector errors - } - } - } catch (e) { - // ignore - } - return false; - } - - /** - * Return full list of yakus for a given hand+groups (doesn't mutate state) - */ - private getYakusForHand(hand: Hand, groups: Array = []): Array<{ name: string; han: number }> { - const res: Array<{ name: string; han: number }> = []; - try { - for (const k of Object.keys(yakus)) { - const fn = (yakus as any)[k]; - try { - const han = fn(hand, groups, this.windPlayer); - if (han && han > 0) res.push({ name: k, han }); - } catch (e) { - // ignore individual detector errors - } - } - } catch (e) { - // ignore - } - return res; - } - - // Helper: human-readable tile label for logging - private formatTile(t: Tile): string { - const fam = t.getFamily(); - const val = t.getValue(); - if (fam >= 1 && fam <= 3) { - const names = ["", "Man", "Pin", "Sou"]; - return `${names[fam]}${val}${t.isRed() ? "(r)" : ""}`; - } - if (fam === 4) { - const winds = ["", "Ton", "Nan", "Shaa", "Pei"]; - return winds[val]; - } - if (fam === 5) { - const dr = ["", "Chun", "Hatsu", "Haku"]; - return dr[val]; - } - return `F${fam}V${val}`; - } - - private initializeGame(): void { - this.deck.shuffle(); - - // Set up dead wall - for (let i = 0; i < GAME_CONSTANTS.DEAD_WALL_SIZE; i++) { - this.deadWall.push(this.deck.pop()); - } - - // Create player hands - for (let i = 0; i < GAME_CONSTANTS.PLAYERS; i++) { - this.hands.push(this.deck.getRandomHand()); - this.discards.push([]); - this.groups.push([]); - this.declaredRiichi.push(false); - this.riichiDiscardIndex.push(-1); - this.handVersion.push(0); - this.tenpaiCache.push(false); - this.tenpaiCacheVersion.push(-1); - } - - this.lastDiscard = undefined; - this.hands[0].sort(); - if (this.turn === 0) this.pick(0); - this.staticDirty = true; - } - - public draw(mp: MousePos): void { - // Update selection and game state - this.getSelected(mp); - this.play(); - - // Rebuild static buffer only when needed - if (this.staticDirty) { - this.staticCtx.clearRect(0, 0, this.cv.width, this.cv.height); - // Draw background - this.staticCtx.fillStyle = this.BG_RECT.color; - this.staticCtx.fillRect( - this.BG_RECT.x, - this.BG_RECT.y, - this.BG_RECT.w, - this.BG_RECT.h - ); - - // Draw mostly-static elements - drawState(this.staticCtx, this.turn, GAME_CONSTANTS.PI / 2 * this.windPlayer); - this.drawDiscardSize(); - this.drawResult(); - // Draw hands without focused (focused tile rendered in dynamic overlay) - const showHands = false; - const { HAND_SIZE, HIDDEN_HAND_SIZE } = GAME_CONSTANTS.DISPLAY; - this.hands[0].drawHand( - this.staticCtx, - 2.5 * 75 * 0.75, - 1000 - 150 * HAND_SIZE, - 5 * HAND_SIZE, - 0.75, - undefined, - false, - 0, - this.selectedTile - ); - this.hands[1].drawHand( - this.staticCtx, - 1000 - 150 * HIDDEN_HAND_SIZE, - 1000 - 75 * 5 * HIDDEN_HAND_SIZE, - 5 * HIDDEN_HAND_SIZE, - HIDDEN_HAND_SIZE, - undefined, - !showHands, - -GAME_CONSTANTS.PI / 2 - ); - this.hands[2].drawHand( - this.staticCtx, - 1000 - 75 * 5 * HIDDEN_HAND_SIZE, - 150 * HIDDEN_HAND_SIZE, - 5 * HIDDEN_HAND_SIZE, - HIDDEN_HAND_SIZE, - undefined, - !showHands, - -GAME_CONSTANTS.PI - ); - this.hands[3].drawHand( - this.staticCtx, - 150 * HIDDEN_HAND_SIZE, - 75 * 5 * HIDDEN_HAND_SIZE, - 5 * HIDDEN_HAND_SIZE, - HIDDEN_HAND_SIZE, - undefined, - !showHands, - GAME_CONSTANTS.PI / 2 - ); - - // Draw groups and discards (no per-frame highlights) - this.drawGroups(GAME_CONSTANTS.DISPLAY.GROUP_SIZE); - for (let i = 0; i < GAME_CONSTANTS.PLAYERS; i++) { - this.drawDiscard(i, undefined); - } - - // Draw Riichi indicators (sticks and status) - this.drawRiichiIndicators(); - - this.staticDirty = false; - } - - // Blit static buffer - this.ctx.clearRect(0, 0, this.cv.width, this.cv.height); - this.ctx.drawImage(this.staticCv, 0, 0); - - // Draw dynamic overlay: selection, highlights, buttons, chiis - this.drawDynamic(this.ctx); - } - - public getDeck(): Deck { - return this.deck; - } - - public getHands(): Array { - return this.hands; - } - - public isFinished(): boolean { - return this.end; - } - - public click(mp: MousePos): void { - const rect = this.cv.getBoundingClientRect(); - - if (this.hasWin(0) && (!this.enableYakus || this.handHasAnyYaku(this.hands[0], this.groups[0]))) { - this.resolveWin(0, 1); - return; - } - - // Handle Riichi selection mode - if (this.choosingRiichiDiscard) { - this.handleRiichiSelection(mp, rect); - return; - } - - if (this.chooseChii) { - this.handleChiiSelection(mp, rect); - return; - } - - // Determine if Ron/Tsumo should be available for player 0 - const canTsumo = this.hasPicked && this.hasWin(0) && (!this.enableYakus || this.handHasAnyYaku(this.hands[0], this.groups[0])); - let canRon = false; - if (this.lastDiscard !== undefined && this.lastDiscard !== 0) { - const d = this.discards[this.lastDiscard]; - if (d && d.length > 0) { - const last = d[d.length - 1]; - const sim = new Hand(); - for (const t of this.hands[0].getTiles()) { - sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed())); - } - sim.push(new Tile(last.getFamily(), last.getValue(), last.isRed())); - sim.sort(); - const simGroups = sim.toGroup() as Array | undefined; - const combinedGroups = this.groups[0].concat(simGroups ? simGroups : []); - canRon = sim.toGroup() !== undefined && (!this.enableYakus || this.handHasAnyYaku(sim, combinedGroups)); - } - } - - const canRiichi = this.canDeclareRiichi(0); - - // When in Riichi, player cannot call chii/pon but can still Ron - const inRiichi = this.declaredRiichi[0]; - const canChiiClick = !inRiichi && this.canDoAChii().length > 0; - const canPonClick = !inRiichi && this.canDoAPon(); - - const action = clickAction( - mp.x - rect.x, - mp.y - rect.y, - canChiiClick, - canPonClick, - false && this.level > 1, - canRon, - canTsumo, - canRiichi - ); - - if (this.canCall && action !== -1) { - // In Riichi, only allow pass (0), ron (4), or tsumo (5) - if (inRiichi && action !== 0 && action !== 4 && action !== 5) { - return; // Ignore chii/pon/kan actions when in Riichi - } - this.handleCallAction(action); - } else if (action === 6) { // Riichi action - if (canRiichi) { - this.enterRiichiSelectionMode(); - } - } else if (this.turn === 0 && this.selectedTile !== undefined) { - this.handlePlayerDiscard(); - } - } - - /** - * Handle clicks during Riichi tile selection mode - */ - private handleRiichiSelection(mp: MousePos, rect: DOMRect): void { - const x = mp.x - rect.x; - const y = mp.y - rect.y; - - // Check if cancel button was clicked (button at x=800, y=835, w=110, h=50) - if (x >= 800 && x <= 910 && y >= 835 && y <= 885) { - this.cancelRiichiSelection(); - return; - } - - // Check if a valid tile was clicked - if (this.selectedTile !== undefined && this.validRiichiDiscards[this.selectedTile]) { - this.confirmRiichiDiscard(this.selectedTile); - } - } - - /** - * Enter Riichi selection mode - calculate valid discards - */ - private enterRiichiSelectionMode(): void { - this.choosingRiichiDiscard = true; - this.validRiichiDiscards = this.calculateValidRiichiDiscards(); - this.selectedTile = undefined; - this.staticDirty = true; - } - - /** - * Cancel Riichi selection and return to normal state - */ - private cancelRiichiSelection(): void { - this.choosingRiichiDiscard = false; - this.validRiichiDiscards = []; - this.selectedTile = undefined; - this.staticDirty = true; - } - - /** - * Confirm Riichi declaration with selected tile - */ - private confirmRiichiDiscard(tileIndex: number): void { - this.choosingRiichiDiscard = false; - this.validRiichiDiscards = []; - - // Set Riichi state - this.declaredRiichi[0] = true; - this.riichiDiscardIndex[0] = this.discards[0].length; - - // Discard the selected tile - this.discard(0, tileIndex); - - // Continue game flow - if (!this.checkPon()) { - const chiis = this.canDoAChii(1); - if (chiis.length > 0) { - const i = Math.floor(Math.random() * chiis.length); - this.chii(chiis[i], 1); - } else { - this.advanceTurn(); - } - } - - this.staticDirty = true; - } - - /** - * Calculate which tiles can be discarded while maintaining tenpai - */ - private calculateValidRiichiDiscards(): boolean[] { - const tiles = this.hands[0].getTiles(); - const valid: boolean[] = []; - - for (let i = 0; i < tiles.length; i++) { - // Create a simulated hand without tile i - const sim = new Hand(); - for (let j = 0; j < tiles.length; j++) { - if (j === i) continue; - const t = tiles[j]; - sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed())); - } - sim.sort(); - - // Check if this hand is in tenpai - valid[i] = this.handIsTenpai(sim); - } - - return valid; - } - - private handleChiiSelection(mp: MousePos, rect: DOMRect): void { - const allChii = this.getChii(0); - const selection = clickChii( - mp.x - rect.x, - mp.y - rect.y, - allChii - ); - - if (selection === 0) { - this.chooseChii = false; - } else { - this.chooseChii = false; - this.chii(selection, 0); - } - } - - private handleCallAction(action: number): void { - if (action === 0) { // Pass - this.canCall = false; - this.advanceTurn(); - } else if (action === 1) { // Chii - const chiis = this.canDoAChii(); - if (chiis.length === 1) { - this.chii(chiis[0], 0); - } else { - this.chooseChii = true; - this.staticDirty = true; - } - } else if (action === 2) { // Pon - this.pon(this.turn); - } - } - - private handlePlayerDiscard(): void { - this.discard(0, this.selectedTile as number); - - // Level 8: Bots don't make calls - if (this.level === 8) { - this.advanceTurn(); - return; - } - - if (!this.checkPon()) { - const chiis = this.canDoAChii(1); - if (chiis.length > 0) { - const i = Math.floor(Math.random() * chiis.length); - this.chii(chiis[i], 1); - } else { - this.advanceTurn(); - } - } - } - - private advanceTurn(): void { - this.updateWaitingTime(); - this.turn = (this.turn + 1) % GAME_CONSTANTS.PLAYERS; - this.pick(this.turn); - this.hasPicked = true; - this.hasPlayed = false; - } - - private getSelected(mp: MousePos): void { - const rect = this.cv.getBoundingClientRect(); - const x = 2.5 * 75 * 0.75; - const y = 1050 - 250 * 0.6; - const sizeHand = GAME_CONSTANTS.DISPLAY.HAND_SIZE; - - const mouseX = mp.x - x; - const mouseY = mp.y; - const tileWidth = 83.9; - - const tileIndex = Math.floor(mouseX / (tileWidth * sizeHand)); - const relativeX = mouseX - tileIndex * tileWidth * sizeHand; - // Determine whether selection changed and only mark static buffer dirty - // when it did. This prevents ghosting where the static layer still - // contains the tile at its original position while the dynamic layer - // draws the lifted tile. - const inBounds = ( - relativeX <= (tileWidth - 3) * sizeHand && - tileIndex >= 0 && - tileIndex < this.hands[0].length() && - mouseY >= y && - mouseY <= y + 100 * sizeHand - ); - - let newSelected = inBounds ? tileIndex : undefined; - - // When choosing Riichi discard, only allow selecting valid tiles - if (this.choosingRiichiDiscard && newSelected !== undefined) { - if (!this.validRiichiDiscards[newSelected]) { - newSelected = undefined; - } - } - // When already in Riichi, player can only select and discard the last drawn tile - else if (this.declaredRiichi[0] && newSelected !== undefined) { - const lastTileIndex = this.hands[0].length() - 1; - // Only allow selecting the isolated (last drawn) tile - if (newSelected !== lastTileIndex) { - newSelected = undefined; - } - } - - if (newSelected !== this.selectedTile) { - this.selectedTile = newSelected; - // Force static buffer rebuild so the static hand skips drawing the - // selected tile (we pass skipIndex when drawing static hand). - this.staticDirty = true; - } - } - - private updateWaitingTime(): void { - this.waitingTime = Math.floor( - Math.random() * - (GAME_CONSTANTS.WAITING_TIME.MAX - GAME_CONSTANTS.WAITING_TIME.MIN) + - GAME_CONSTANTS.WAITING_TIME.MIN - ); - } - - private play(): void { - if (this.turn !== 0 && !this.end) { - if (!this.hasPicked) { - // Begin of turn - this.lastPlayed = Date.now(); - this.pick(this.turn); - this.hasPicked = true; - } else if (!this.hasPlayed) { - // Middle of turn - this.handleBotTurn(); - } else if (!this.canCall) { - // End of turn - this.advanceBotTurn(); - } - } - } - - private handleBotTurn(): void { - if (this.hasWin(this.turn) && (!this.enableYakus || this.handHasAnyYaku(this.hands[this.turn], this.groups[this.turn]))) { - this.resolveWin(this.turn, 2); - return; - } - - if (Date.now() - this.lastPlayed > this.waitingTime) { - this.lastPlayed = Date.now(); - - // Level 8: Bots might declare fake Riichi after 9 turns - if (this.level === 8 && this.turnCount >= 9 && !this.declaredRiichi[this.turn]) { - if (Math.random() < 1/9) { - this.declareBotRiichi(this.turn); - } - } - - // Choose random tile to discard - const n = Math.floor(this.hands[this.turn].length() * Math.random()); - this.discard(this.turn, n); - this.hasPlayed = true; - - if (this.deck.length() <= 0) { - // Draw / no tiles left - this.result = 0; - this.end = true; - return; - } - - if (!this.end) { - // If any player (including player 0) can Ron on this discard, open the call window - const anyRon = this.canAnyPlayerRon(this.turn); - if (anyRon) { - this.canCall = true; - return; - } - - // Level 8: Bots don't make calls (pon/chii) - if (this.level === 8) { - this.canCall = this.canDoAChii().length > 0 || this.canDoAPon(); - return; - } - - // Otherwise proceed with normal automatic pon/chii resolution - this.checkPon(); - this.checkForChii(); - this.canCall = this.canDoAChii().length > 0 || this.canDoAPon(); - } - } - } - - /** - * Bot declares Riichi (can be fake - just for visual effect in level 8) - */ - private declareBotRiichi(player: number): void { - this.declaredRiichi[player] = true; - this.riichiDiscardIndex[player] = this.discards[player].length; - this.staticDirty = true; - } - - /** - * Check if any other player can Ron on the last discard performed by discardPlayer. - * This builds a simulated hand for each candidate player to avoid mutating state. - */ - private canAnyPlayerRon(discardPlayer: number): boolean { - const d = this.discards[discardPlayer]; - if (!d || d.length === 0) return false; - const last = d[d.length - 1]; - - for (let p = 0; p < GAME_CONSTANTS.PLAYERS; p++) { - if (p === discardPlayer) continue; - const sim = new Hand(); - for (const t of this.hands[p].getTiles()) { - sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed())); - } - sim.push(new Tile(last.getFamily(), last.getValue(), last.isRed())); - sim.sort(); - const simGroups = sim.toGroup() as Array | undefined; - const combinedGroups = this.groups[p].concat(simGroups ? simGroups : []); - if (sim.toGroup() !== undefined && (!this.enableYakus || this.handHasAnyYaku(sim, combinedGroups))) return true; - } - - return false; - } - - private checkForChii(): void { - if (this.turn === 3) return; - - const nextPlayer = this.turn + 1; - const chiis = this.canDoAChii(nextPlayer); - - if (chiis.length > 0) { - const i = Math.floor(Math.random() * chiis.length); - this.chii(chiis[i], nextPlayer); - } - } - - private advanceBotTurn(): void { - if (this.turn === 3) { - this.turn = 0; - this.turnCount++; - this.pick(0); - this.hasPicked = true; - this.hasPlayed = false; - if (this.hasWin(0) && (!this.enableYakus || this.handHasAnyYaku(this.hands[0], this.groups[0]))) { - this.resolveWin(0, 1); - } - } else { - this.turn++; - this.hasPicked = false; - this.hasPlayed = false; - } - - this.updateWaitingTime(); - } - - private pick(player: number): void { - this.hands[player].push(this.deck.pop()); - this.hands[player].isolate = true; - // mark hand as changed - this.handVersion[player] = (this.handVersion[player] || 0) + 1; - this.tenpaiCacheVersion[player] = -1; - // changing hand invalidates ron cache - this.canRonCache = { result: false }; - this.staticDirty = true; - } - - private discard(player: number, n: number): void { - const tile = this.hands[player].eject(n); - this.hands[player].sort(); - - tile.setTilt(); - this.discards[player].push(tile); - - this.hands[player].isolate = false; - this.hands[player].sort(); - - this.lastDiscard = player; - this.lastPlayed = Date.now(); - // mark hand as changed - this.handVersion[player] = (this.handVersion[player] || 0) + 1; - this.tenpaiCacheVersion[player] = -1; - this.canRonCache = { result: false }; - this.staticDirty = true; - } - - private canDoAChii(p: number = 0): Array { - const chii: number[] = []; - - // Check if chii is possible - if ( - this.lastDiscard === undefined || - (this.lastDiscard + 1) % 4 !== p || - (this.turn + 1) % 4 !== p || - this.discards[this.lastDiscard][this.discards[this.lastDiscard].length - 1].getFamily() >= 4 - ) { - return chii; - } - - const t = this.discards[this.lastDiscard][this.discards[this.lastDiscard].length - 1]; - const h = this.hands[p]; - const family = t.getFamily(); - const value = t.getValue(); - - // Check for three possible chii patterns - if (h.count(family, value - 2) > 0 && h.count(family, value - 1) > 0) { - chii.push(value - 2); - } - - if (h.count(family, value - 1) > 0 && h.count(family, value + 1) > 0) { - chii.push(value - 1); - } - - if (h.count(family, value + 1) > 0 && h.count(family, value + 2) > 0) { - chii.push(value); - } - - return chii; - } - - private chii(minValue: number, p: number): void { - const discardPlayer = p === 0 ? 3 : p - 1; - const t = this.discards[discardPlayer].pop() as Tile; - this.lastDiscard = undefined; - - const tt: Tile[] = []; - let tn = 0; - let v = minValue; - - // Find the right tiles for the chii - while (tn < 2) { - if (v === t.getValue()) { - v++; - } else { - tt[tn] = this.hands[p].find(t.getFamily(), v) as Tile; - tn++; - v++; - } - } - - // Set tilt for all tiles in the group - [t, tt[0], tt[1]].forEach(tile => tile.setTilt()); - - // Create new group - this.groups[p].push(new Group([t, tt[0], tt[1]], discardPlayer, p)); - - // mark hand changed for player p - this.handVersion[p] = (this.handVersion[p] || 0) + 1; - this.tenpaiCacheVersion[p] = -1; - this.canRonCache = { result: false }; - - if (this.hasWin(p) && (!this.enableYakus || this.handHasAnyYaku(this.hands[p], this.groups[p]))) { - this.resolveWin(p, p === 0 ? 1 : 2); - } - - this.updateWaitingTime(); - this.turn = p; - this.hasPicked = true; - this.hasPlayed = false; - } - - private getChii(p: number): Array> { - const chiis: Array> = []; - const numChii = this.canDoAChii(); - const discardPlayer = p === 0 ? 3 : p - 1; - const d = this.discards[discardPlayer]; - const t = d[d.length - 1]; - - for (let i = 0; i < numChii.length; i++) { - const v = numChii[i]; - const chii: Tile[] = []; - - for (let dv = 0; dv < 3; dv++) { - if (v + dv === t.getValue()) { - chii.push(t); - } else { - const tt = this.hands[p].find(t.getFamily(), v + dv) as Tile; - chii.push(tt); - this.hands[p].push(tt); - this.hands[p].sort(); - } - } - - chiis.push(chii); - } - - return chiis; - } - - private checkPon(): boolean { - for (let p = 1; p < GAME_CONSTANTS.PLAYERS; p++) { - if (this.canDoAPon(p)) { - this.pon(this.lastDiscard as number, p); - return true; - } - } - return false; - } - - private canDoAPon(player: number = 0): boolean { - if ( - this.lastDiscard === undefined || - this.lastDiscard === player || - this.turn === player || - (this.hasPicked && !this.hasPlayed) - ) { - return false; - } - - const t = this.discards[this.lastDiscard][this.discards[this.lastDiscard].length - 1]; - return this.hands[player].count(t.getFamily(), t.getValue()) >= 2; - } - - /** - * Check whether the given player's closed hand is in tenpai (waiting). - * We simulate adding any possible tile and check if it yields a winning hand. - */ - private isTenpai(player: number): boolean { - // Use cached result when hand hasn't changed - if (this.tenpaiCacheVersion[player] === this.handVersion[player]) { - return this.tenpaiCache[player]; - } - - const h = this.hands[player]; - // Note: For a 13-tile hand, we test if adding any tile makes it win - // No early guard needed - just test all possibilities - - // Try all possible tile types - // suits 1..3 values 1..9 - for (let fam = 1; fam <= 3; fam++) { - for (let val = 1; val <= 9; val++) { - const sim = new Hand(); - for (const t of h.getTiles()) { - sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed())); - } - sim.push(new Tile(fam, val, false)); - sim.sort(); - if (sim.toGroup() !== undefined) { - this.tenpaiCache[player] = true; - this.tenpaiCacheVersion[player] = this.handVersion[player]; - return true; - } - // also try red five - if (val === 5) { - const simR = new Hand(); - for (const t of h.getTiles()) simR.push(new Tile(t.getFamily(), t.getValue(), t.isRed())); - simR.push(new Tile(fam, val, true)); - simR.sort(); - if (simR.toGroup() !== undefined) { - this.tenpaiCache[player] = true; - this.tenpaiCacheVersion[player] = this.handVersion[player]; - return true; - } - } - } - } - - // winds family 4 values 1..4 - for (let val = 1; val <= 4; val++) { - const sim = new Hand(); - for (const t of h.getTiles()) { - sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed())); - } - sim.push(new Tile(4, val, false)); - sim.sort(); - if (sim.toGroup() !== undefined) { - this.tenpaiCache[player] = true; - this.tenpaiCacheVersion[player] = this.handVersion[player]; - return true; - } - } - - // dragons family 5 values 1..3 - for (let val = 1; val <= 3; val++) { - const sim = new Hand(); - for (const t of h.getTiles()) { - sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed())); - } - sim.push(new Tile(5, val, false)); - sim.sort(); - if (sim.toGroup() !== undefined) { - this.tenpaiCache[player] = true; - this.tenpaiCacheVersion[player] = this.handVersion[player]; - return true; - } - // try red dragon? dragons don't have red variants, skip - } - - // update cache - this.tenpaiCache[player] = false; - this.tenpaiCacheVersion[player] = this.handVersion[player]; - return false; - } - - private canDeclareRiichi(player: number): boolean { - // Riichi rules: player's turn, just drawn (hasPicked), hasn't discarded yet (hasPlayed false), - // hand is closed (no open groups), in tenpai, and hasn't already declared Riichi. - // Riichi itself IS a yaku, so no need to check for other yakus! - if (player !== 0) return false; - if (this.turn !== player) return false; - if (!this.hasPicked || this.hasPlayed) return false; - if (this.groups[player] && this.groups[player].length > 0) return false; // hand must be closed - if (this.declaredRiichi[player]) return false; - - const hand = this.hands[player]; - - // With 14 tiles (just drawn), check if discarding any tile leaves us in tenpai - if (hand.length && hand.length() === 14) { - const tiles = hand.getTiles(); - for (let i = 0; i < 14; i++) { - const sim = new Hand(); - for (let j = 0; j < tiles.length; j++) { - if (j === i) continue; - const t = tiles[j]; - sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed())); - } - sim.sort(); - if (this.handIsTenpai(sim)) { - return true; // At least one discard leaves us in tenpai - } - } - return false; - } - - // With 13 tiles, just check if we're in tenpai - return this.isTenpai(player); - } - - // Helper: test tenpai for an arbitrary Hand instance (no caching) - private handIsTenpai(h: Hand): boolean { - // If hand is already a winning hand, it's technically "tenpai" on any tile it's waiting on - // But for 13 tiles we want to find if adding ANY tile makes it win - // Note: a 13-tile hand should not already be winning (needs 14 for complete hand) - - // suits 1..3 values 1..9 - for (let fam = 1; fam <= 3; fam++) { - for (let val = 1; val <= 9; val++) { - const sim = new Hand(); - for (const t of h.getTiles()) sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed())); - sim.push(new Tile(fam, val, false)); - sim.sort(); - if (sim.toGroup() !== undefined) return true; - if (val === 5) { - const simR = new Hand(); - for (const t of h.getTiles()) simR.push(new Tile(t.getFamily(), t.getValue(), t.isRed())); - simR.push(new Tile(fam, val, true)); - simR.sort(); - if (simR.toGroup() !== undefined) return true; - } - } - } - - // winds family 4 values 1..4 - for (let val = 1; val <= 4; val++) { - const sim = new Hand(); - for (const t of h.getTiles()) sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed())); - sim.push(new Tile(4, val, false)); - sim.sort(); - if (sim.toGroup() !== undefined) return true; - } - - // dragons family 5 values 1..3 - for (let val = 1; val <= 3; val++) { - const sim = new Hand(); - for (const t of h.getTiles()) sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed())); - sim.push(new Tile(5, val, false)); - sim.sort(); - if (sim.toGroup() !== undefined) return true; - } - - return false; - } - - // Helper: for an arbitrary Hand, check if any winning tile produces >=1 yaku - private handIsTenpaiWithYaku(h: Hand, groups: Array): boolean { - // If already winning and has yaku - if (h.toGroup() !== undefined) { - const simGroups = h.toGroup() as Array | undefined; - const combined = groups.concat(simGroups ? simGroups : []); - return this.handHasAnyYaku(h, combined); - } - - // suits - for (let fam = 1; fam <= 3; fam++) { - for (let val = 1; val <= 9; val++) { - const sim = new Hand(); - for (const t of h.getTiles()) sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed())); - sim.push(new Tile(fam, val, false)); - sim.sort(); - let simGroups = sim.toGroup() as Array | undefined; - if (simGroups !== undefined) { - const combined = groups.concat(simGroups); - if (this.handHasAnyYaku(sim, combined)) return true; - } - // try red five - if (val === 5) { - const simR = new Hand(); - for (const t of h.getTiles()) simR.push(new Tile(t.getFamily(), t.getValue(), t.isRed())); - simR.push(new Tile(fam, val, true)); - simR.sort(); - simGroups = simR.toGroup() as Array | undefined; - if (simGroups !== undefined) { - const combined = groups.concat(simGroups); - if (this.handHasAnyYaku(simR, combined)) return true; - } - } - } - } - - // winds - for (let val = 1; val <= 4; val++) { - const sim = new Hand(); - for (const t of h.getTiles()) sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed())); - sim.push(new Tile(4, val, false)); - sim.sort(); - const simGroups = sim.toGroup() as Array | undefined; - if (simGroups !== undefined) { - const combined = groups.concat(simGroups); - if (this.handHasAnyYaku(sim, combined)) return true; - } - } - - // dragons - for (let val = 1; val <= 3; val++) { - const sim = new Hand(); - for (const t of h.getTiles()) sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed())); - sim.push(new Tile(5, val, false)); - sim.sort(); - const simGroups = sim.toGroup() as Array | undefined; - if (simGroups !== undefined) { - const combined = groups.concat(simGroups); - if (this.handHasAnyYaku(sim, combined)) return true; - } - } - - return false; - } - - /** - * Return true if the player's closed hand is waiting on at least one tile - * that would produce a winning hand containing at least one yaku. - */ - private isTenpaiWithYaku(player: number): boolean { - const h = this.hands[player]; - // If hand already winning and has yaku, it's trivially true - if (h.toGroup() !== undefined && this.handHasAnyYaku(h, this.groups[player])) return true; - - // Try all possible tile types - for (let fam = 1; fam <= 3; fam++) { - for (let val = 1; val <= 9; val++) { - const sim = new Hand(); - for (const t of h.getTiles()) sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed())); - sim.push(new Tile(fam, val, false)); - sim.sort(); - const simGroups = sim.toGroup() as Array | undefined; - if (simGroups !== undefined) { - const combined = this.groups[player].concat(simGroups); - if (this.handHasAnyYaku(sim, combined)) return true; - } - } - } - - for (let val = 1; val <= 4; val++) { - const sim = new Hand(); - for (const t of h.getTiles()) sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed())); - sim.push(new Tile(4, val, false)); - sim.sort(); - const simGroups = sim.toGroup() as Array | undefined; - if (simGroups !== undefined) { - const combined = this.groups[player].concat(simGroups); - if (this.handHasAnyYaku(sim, combined)) return true; - } - } - - for (let val = 1; val <= 3; val++) { - const sim = new Hand(); - for (const t of h.getTiles()) sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed())); - sim.push(new Tile(5, val, false)); - sim.sort(); - const simGroups = sim.toGroup() as Array | undefined; - if (simGroups !== undefined) { - const combined = this.groups[player].concat(simGroups); - if (this.handHasAnyYaku(sim, combined)) return true; - } - } - - return false; - } - - private pon(p: number, thief: number = 0): void { - const t = this.discards[p].pop() as Tile; - this.lastDiscard = undefined; - - const t2 = this.hands[thief].find(t.getFamily(), t.getValue()) as Tile; - const t3 = this.hands[thief].find(t.getFamily(), t.getValue()) as Tile; - - [t, t2, t3].forEach(tile => tile.setTilt()); - - this.groups[thief].push(new Group([t, t2, t3], p, thief)); - - // mark thief hand changed - this.handVersion[thief] = (this.handVersion[thief] || 0) + 1; - this.tenpaiCacheVersion[thief] = -1; - this.canRonCache = { result: false }; - - if (this.hasWin(thief) && (!this.enableYakus || this.handHasAnyYaku(this.hands[thief], this.groups[thief]))) { - this.resolveWin(thief, thief === 0 ? 1 : 2); - } - - this.updateWaitingTime(); - this.turn = thief; - this.hasPicked = true; - this.hasPlayed = false; - } - - private hasWin(p: number): boolean { - return this.hands[p].toGroup() !== undefined; - } - - private drawGame(): void { - // Update game state - this.play(); - - // Draw game elements - drawState(this.staticCtx, this.turn, GAME_CONSTANTS.PI / 2 * this.windPlayer); - this.drawDiscardSize(); - this.drawResult(); - this.drawHands(); - this.drawGroups(GAME_CONSTANTS.DISPLAY.GROUP_SIZE); - - // Draw discards for all players - for (let i = 0; i < GAME_CONSTANTS.PLAYERS; i++) { - this.drawDiscard( - i, - this.selectedTile !== undefined ? this.hands[0].get(this.selectedTile) : undefined - ); - } - - // Draw UI elements - if (this.chooseChii) { - drawChiis(this.staticCtx, this.getChii(0)); - } else { - // Compute availability of ron/tsumo for player 0 - const canTsumoLocal = this.hasPicked && this.hasWin(0) && (!this.enableYakus || this.handHasAnyYaku(this.hands[0], this.groups[0])); - let canRonLocal = false; - if (this.lastDiscard !== undefined && this.lastDiscard !== 0) { - // Use cache when lastDiscard and player's hand version are unchanged - if (this.canRonCache.lastDiscard === this.lastDiscard && this.canRonCache.handVer === this.handVersion[0]) { - canRonLocal = this.canRonCache.result; - } else { - const d = this.discards[this.lastDiscard]; - if (d && d.length > 0) { - const last = d[d.length - 1]; - const sim = new Hand(); - for (const t of this.hands[0].getTiles()) { - sim.push(new Tile(t.getFamily(), t.getValue(), t.isRed())); - } - sim.push(new Tile(last.getFamily(), last.getValue(), last.isRed())); - sim.sort(); - const simGroups = sim.toGroup() as Array | undefined; - const combinedGroups = this.groups[0].concat(simGroups ? simGroups : []); - canRonLocal = sim.toGroup() !== undefined && (!this.enableYakus || this.handHasAnyYaku(sim, combinedGroups)); - } else { - canRonLocal = false; - } - // update cache - this.canRonCache = { lastDiscard: this.lastDiscard, handVer: this.handVersion[0], result: canRonLocal }; - } - } - - const canRiichiLocal = this.canDeclareRiichi(0); - drawButtons( - this.staticCtx, - this.canDoAChii().length > 0, - this.canDoAPon(), - false && this.level > 1, - canRonLocal, - canTsumoLocal, - canRiichiLocal - ); - } - } - - private drawHands(): void { - const showHands = false; - const { HAND_SIZE, HIDDEN_HAND_SIZE } = GAME_CONSTANTS.DISPLAY; - - // Draw player's hand - this.hands[0].drawHand( - this.staticCtx, - 2.5 * 75 * 0.75, - 1000 - 150 * HAND_SIZE, - 5 * HAND_SIZE, - 0.75, - this.selectedTile, - false, - 0 - ); - - // Draw opponents' hands - this.hands[1].drawHand( - this.staticCtx, - 1000 - 150 * HIDDEN_HAND_SIZE, - 1000 - 75 * 5 * HIDDEN_HAND_SIZE, - 5 * HIDDEN_HAND_SIZE, - HIDDEN_HAND_SIZE, - undefined, - !showHands, - -GAME_CONSTANTS.PI / 2 - ); - - this.hands[2].drawHand( - this.staticCtx, - 1000 - 75 * 5 * HIDDEN_HAND_SIZE, - 150 * HIDDEN_HAND_SIZE, - 5 * HIDDEN_HAND_SIZE, - HIDDEN_HAND_SIZE, - undefined, - !showHands, - -GAME_CONSTANTS.PI - ); - - this.hands[3].drawHand( - this.staticCtx, - 150 * HIDDEN_HAND_SIZE, - 75 * 5 * HIDDEN_HAND_SIZE, - 5 * HIDDEN_HAND_SIZE, - HIDDEN_HAND_SIZE, - undefined, - !showHands, - GAME_CONSTANTS.PI / 2 - ); - } - - private drawGroups(size: number): void { - const offset = 25; - - for (let p = 0; p < GAME_CONSTANTS.PLAYERS; p++) { - const rotation = this.rotations[p]; - const groups = this.groups[p]; - - if (groups.length > 0) { - for (let i = groups.length - 1; i >= 0; i--) { - groups[i].drawGroup( - this.staticCtx, - 1050 - 240 - (260 + offset) * size * i, - 1050 - 62, - 5, - 0.6, - rotation, - this.selectedTile !== undefined ? this.hands[0].get(this.selectedTile) : undefined - ); - } - } - } - } - - private drawDiscard(p: number, highlightedTile: Tile | undefined): void { - const sizeDiscard = GAME_CONSTANTS.DISPLAY.DISCARD_SIZE; - - this.staticCtx.save(); - this.staticCtx.translate(525, 525); - this.staticCtx.rotate(this.rotations[p]); - - const x = -sizeDiscard * 475 / 2; - const y = sizeDiscard * (475 / 2 + 5); - - // Track extra offset caused by sideways riichi tiles - let extraOffset = 0; - const riichiIdx = this.riichiDiscardIndex[p]; - - // Draw all discarded tiles - for (let i = 0; i < this.discards[p].length; i++) { - const tile = this.discards[p][i]; - const isRiichiTile = (i === riichiIdx); - let tx, ty; - - // Calculate row and column - const row = i < 6 ? 0 : (i < 12 ? 1 : 2); - const col = i < 12 ? (i % 6) : (i - 12); - - // Base position - tx = x + col * 80 * sizeDiscard; - ty = y + row * 105 * sizeDiscard; - - // Add extra offset for tiles after riichi tile in same row - if (riichiIdx >= 0 && i > riichiIdx) { - const riichiRow = riichiIdx < 6 ? 0 : (riichiIdx < 12 ? 1 : 2); - if (row === riichiRow) { - // Sideways tile is wider: add offset (height - width difference) - tx += 25 * sizeDiscard; - } - } - - if (isRiichiTile) { - // Draw riichi tile sideways (90 degree rotation) - // Adjust position to account for rotation center - const adjustX = tx + 12 * sizeDiscard; - const adjustY = ty - 12 * sizeDiscard; - tile.drawTile( - this.staticCtx, - adjustX, - adjustY, - sizeDiscard, - false, - GAME_CONSTANTS.PI / 2, // 90 degrees - highlightedTile?.isEqual(tile.getFamily(), tile.getValue()) - ); - } else { - tile.drawTile( - this.staticCtx, - tx, - ty, - sizeDiscard, - false, - 0, - highlightedTile?.isEqual(tile.getFamily(), tile.getValue()) - ); - } - } - - // Draw indicator for last discard - if (this.lastDiscard === p) { - this.drawLastDiscardIndicator(p); - } - - this.staticCtx.restore(); - } - - private drawLastDiscardIndicator(p: number): void { - const j = this.discards[p].length - 1; - const sizeDiscard = GAME_CONSTANTS.DISPLAY.DISCARD_SIZE; - const x = -sizeDiscard * 475 / 2; - const y = sizeDiscard * (475 / 2 + 5); - - let tx = j < 12 - ? x + (j % 6) * 80 * sizeDiscard - : x + (j - 12) * 80 * sizeDiscard; - - let ty = j < 12 - ? y + Math.floor(j / 6) * 105 * sizeDiscard - : y + 2 * 105 * sizeDiscard; - - tx += 75 / 2 * sizeDiscard; - ty += 115 * sizeDiscard; - - const triangleSize = 10; - this.staticCtx.fillStyle = "#ff0000"; - this.staticCtx.beginPath(); - this.staticCtx.moveTo(tx, ty); - this.staticCtx.lineTo(tx + triangleSize / 2, ty + 0.866 * triangleSize); - this.staticCtx.lineTo(tx - triangleSize / 2, ty + 0.866 * triangleSize); - this.staticCtx.lineTo(tx, ty); - this.staticCtx.fill(); - this.staticCtx.stroke(); - } - - private drawDiscardSize(): void { - this.staticCtx.fillStyle = "#f070f0"; - this.staticCtx.font = "40px garamond"; - - const remainingTiles = this.deck.length(); - const x = remainingTiles < 10 ? 517 : 507; - - this.staticCtx.fillText(remainingTiles.toString(), x, 537); - } - - /** - * Draw Riichi indicators: sticks for players who have declared Riichi - * and a visual indicator showing locked hand status - */ - private drawRiichiIndicators(): void { - const ctx = this.staticCtx; - const center = 525; - - for (let p = 0; p < GAME_CONSTANTS.PLAYERS; p++) { - if (!this.declaredRiichi[p]) continue; - - ctx.save(); - ctx.translate(center, center); - ctx.rotate(this.rotations[p]); - - // Draw riichi stick (1000 point stick) - horizontal bar near discard area - const stickY = 120; - const stickWidth = 80; - const stickHeight = 8; - - // White stick background - ctx.fillStyle = "#ffffff"; - ctx.strokeStyle = "#333333"; - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.roundRect(-stickWidth / 2, stickY, stickWidth, stickHeight, 3); - ctx.fill(); - ctx.stroke(); - - // Red dot in center (1000 point stick marking) - ctx.fillStyle = "#cc0000"; - ctx.beginPath(); - ctx.arc(0, stickY + stickHeight / 2, 3, 0, Math.PI * 2); - ctx.fill(); - - ctx.restore(); - } - } - - private drawResult(): void { - if (this.result === -1) return; - - // Draw result background - this.staticCtx.fillStyle = "#e0e0f0"; - this.staticCtx.fillRect(450, 430, 150, 190); - this.staticCtx.fillRect(430, 450, 190, 150); - - // Draw result text - this.staticCtx.fillStyle = "#ff0000"; - this.staticCtx.font = "45px garamond"; - - if (this.result === 0) { - this.staticCtx.fillText("Égalité", 450, 535); - } else if (this.result === 1) { - this.staticCtx.fillText("Victoire !", 440, 535); - } else if (this.result === 2) { - this.staticCtx.fillText("Défaite...", 440, 535); - } - } - - public async preload(): Promise { - await this.deck.preload(); - await Promise.all(this.hands.map(h => h.preload())); - } - - // Expose last yakus for UI/display when enabled - public getLastYakus(): Array<{ name: string; han: number }> { - return this.lastYakus; - } - - public getLastTotalHan(): number { - return this.lastTotalHan; - } -} diff --git a/src/group.ts b/src/group.ts deleted file mode 100644 index 54f08a9..0000000 --- a/src/group.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Tile } from "./tile"; - -export class Group { - constructor( - private tiles: Array, - private stolenFrom: number, - private belongsTo: number - ) {} - - public push(tile: Tile): void { - this.tiles.push(tile); - } - - public pop(): Tile | undefined { - return this.tiles.pop(); - } - - public getTiles(): Array { - return this.tiles; - } - - public compare(g: Group): number { - // Compare les premiers tiles, puis les seconds si égalité - const firstComparison = this.tiles[0].compare(g.tiles[0]); - return firstComparison !== 0 ? firstComparison : this.tiles[1].compare(g.tiles[1]); - } - - public drawGroup( - ctx: CanvasRenderingContext2D, - x: number, - y: number, - os: number, - size: number, - rotation: number, - selectedTile?: Tile - ): void { - // Sauvegarde et rotation du contexte - ctx.save(); - ctx.translate(525, 525); - ctx.rotate(rotation); - ctx.translate(-525, -525); - - // Calcul des paramètres de dessin - const v = 75 * size; - const w = 90 * size; - const osy = 25 * size / 2; - const p = (this.belongsTo - this.stolenFrom - 1 + 4) % 4; - - // Détermination du tile sélectionné - const sf = selectedTile === undefined ? -1 : selectedTile.getFamily(); - const sv = selectedTile === undefined ? 0 : selectedTile.getValue(); - - // Fonction helper pour éviter la répétition de code - const drawTile = (tile: Tile, tx: number, ty: number, angle: number) => { - tile.drawTile( - ctx, - tx, - ty, - size, - false, - angle, - tile.isEqual(sf, sv) - ); - }; - - const HALF_PI = Math.PI / 2; - - // Dessin selon la position - switch (p) { - case 0: - drawTile(this.tiles[0], x, y + osy, HALF_PI); - drawTile(this.tiles[1], x + w, y, 0); - drawTile(this.tiles[2], x + w + v + os * size, y, 0); - break; - case 1: - drawTile(this.tiles[0], x, y, 0); - drawTile(this.tiles[1], x + w, y + osy, -HALF_PI); - drawTile(this.tiles[2], x + w + v + 3 * os * size, y, 0); - break; - case 2: - drawTile(this.tiles[0], x, y, 0); - drawTile(this.tiles[1], x + v + os * size, y, 0); - drawTile(this.tiles[2], x + w + v + os * size, y + osy, -HALF_PI); - break; - default: - console.error(`Position non prise en charge: ${p}`); - } - - ctx.restore(); - } -} diff --git a/src/hand.ts b/src/hand.ts deleted file mode 100644 index 375b5ea..0000000 --- a/src/hand.ts +++ /dev/null @@ -1,382 +0,0 @@ -import { Tile } from "./tile"; -import { Group } from "./group"; - -// Constants to avoid magic numbers and improve readability -const TILE_TYPES = { - MANZU: { code: "m", family: 1 }, - PINZU: { code: "p", family: 2 }, - SOUZU: { code: "s", family: 3 }, - WINDS: { code: "w", family: 4 }, - DRAGONS: { code: "d", family: 5 } -}; - -// Helper class for grouping operations -class GroupFinder { - private tiles: Array; - - constructor(tiles: Array) { - // Create deep copies of the tiles to avoid modifying originals - this.tiles = tiles.map(t => new Tile(t.getFamily(), t.getValue(), t.isRed())); - this.sort(); - } - - public sort(): void { - this.tiles.sort((a, b) => a.isLessThan(b) ? -1 : 1); - } - - public find(family: number, value: number): Tile | undefined { - const index = this.findTileIndex(family, value); - - if (index !== -1) { - [this.tiles[index], this.tiles[0]] = [this.tiles[0], this.tiles[index]]; - const tile = this.tiles.shift(); - this.sort(); - return tile; - } - - return undefined; - } - - private findTileIndex(family: number, value: number): number { - return this.tiles.findIndex( - tile => tile.getFamily() === family && tile.getValue() === value - ); - } - - public count(family: number, value: number): number { - return this.tiles.filter( - tile => tile.getFamily() === family && tile.getValue() === value - ).length; - } - - // Find groups recursively without modifying original tiles - public findGroups(pair: boolean = false): Array | 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) - if (family <= 3) { // Only suit tiles can form sequences - 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; - } - - private tryFormPair(tile: Tile): Array | undefined { - const pairTile = this.find(tile.getFamily(), tile.getValue()) as Tile; - const groups = this.findGroups(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; - } - - private tryFormTriplet(tile: Tile, pair: boolean): Array | undefined { - const secondTile = this.find(tile.getFamily(), tile.getValue()) as Tile; - const thirdTile = this.find(tile.getFamily(), tile.getValue()) as Tile; - - const groups = this.findGroups(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; - } - - private tryFormSequence(tile: Tile, pair: boolean): Array | 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.findGroups(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; - } -} - -export class Hand { - private tiles: Array; - public isolate: boolean = false; - - /** - * Create a hand from string representation - * @param stiles String representation of tiles (e.g., "m1p2s3w1d1") - */ - public constructor(stiles: string = "") { - this.tiles = []; - this.initializeFromString(stiles); - } - - /** - * Parse string representation into tiles - */ - private initializeFromString(stiles: string): void { - for (let i = 0; i < stiles.length - 1; i++) { - const tileCode = stiles.substring(i, i + 2); - const type = tileCode[0]; - const value = Number(tileCode[1]); - - if (this.isValidTileCode(type, value)) { - this.addTileFromCode(type, value); - } - } - } - - /** - * Check if tile code is valid - */ - private isValidTileCode(type: string, value: number): boolean { - return ( - (type === TILE_TYPES.MANZU.code) || - (type === TILE_TYPES.PINZU.code) || - (type === TILE_TYPES.SOUZU.code) || - (type === TILE_TYPES.WINDS.code) || - (type === TILE_TYPES.DRAGONS.code) - ); - } - - /** - * Create a tile from type code and value - */ - private addTileFromCode(type: string, value: number): void { - const familyMap: { [key: string]: number } = { - [TILE_TYPES.MANZU.code]: TILE_TYPES.MANZU.family, - [TILE_TYPES.PINZU.code]: TILE_TYPES.PINZU.family, - [TILE_TYPES.SOUZU.code]: TILE_TYPES.SOUZU.family, - [TILE_TYPES.WINDS.code]: TILE_TYPES.WINDS.family, - [TILE_TYPES.DRAGONS.code]: TILE_TYPES.DRAGONS.family - }; - - const family = familyMap[type]; - if (family !== undefined) { - this.tiles.push(new Tile(family, value, false)); - } - } - - /** - * Get all tiles in hand - */ - public getTiles(): Array { - return this.tiles; - } - - /** - * Get number of tiles in hand - */ - public length(): number { - return this.tiles.length; - } - - /** - * Add a tile to hand - */ - public push(tile: Tile): void { - this.tiles.push(tile); - } - - /** - * Remove and return the last tile - */ - public pop(): Tile | undefined { - return this.tiles.pop(); - } - - /** - * Find and remove a specific tile by family and value - */ - public find(family: number, value: number): Tile | undefined { - const index = this.findTileIndex(family, value); - - if (index !== -1) { - // Swap with first tile and remove - [this.tiles[index], this.tiles[0]] = [this.tiles[0], this.tiles[index]]; - const tile = this.tiles.shift(); - this.sort(); - return tile; - } - - return undefined; - } - - /** - * Find index of tile with specific family and value - */ - private findTileIndex(family: number, value: number): number { - return this.tiles.findIndex( - tile => tile.getFamily() === family && tile.getValue() === value - ); - } - - /** - * Remove tile at specific index - */ - public eject(idTile: number): Tile { - if (idTile < 0 || idTile >= this.tiles.length) { - throw new Error("Invalid tile index"); - } - - // Swap with first tile and remove - [this.tiles[0], this.tiles[idTile]] = [this.tiles[idTile], this.tiles[0]]; - const tile = this.tiles.shift(); - this.sort(); - - return tile as Tile; - } - - /** - * Get tile at specific index without removing - */ - public get(idTile: number | undefined): Tile | undefined { - if (idTile === undefined || idTile < 0 || idTile >= this.tiles.length) { - return undefined; - } - - return this.tiles[idTile]; - } - - /** - * 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) - * This version preserves the original hand - */ - public toGroup(pair: boolean = false): Array | undefined { - // Create a helper instance with copied tiles - const groupFinder = new GroupFinder(this.tiles); - return groupFinder.findGroups(pair); - } - - /** - * 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, - // If provided, skip drawing this tile index (useful when the focused - // tile is rendered on a dynamic overlay to avoid seeing the unshifted - // tile under the lifted one). - skipIndex?: number - ): 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++) { - // Optionally skip drawing a specific tile (prevents ghosting when the - // focused tile is drawn separately in a dynamic overlay). - if (skipIndex !== undefined && i === skipIndex) continue; - 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 { - await Promise.all(this.tiles.map(tile => tile.preloadImg())); - } - - /** - * Clean up resources - */ - public cleanup(): void { - this.tiles.forEach(tile => tile.cleanup()); - this.tiles = []; - } -} diff --git a/src/rain.ts b/src/rain.ts deleted file mode 100644 index 30371c6..0000000 --- a/src/rain.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { Tile } from "./tile" -import { Deck } from "./deck" - -const G: number = 100; -const SIZE: number = 1; -const LIMIT_MAX = 1100; -const SPAWN_CHANCE = 0.75; -const INITIAL_Y = -150; -const INITIAL_VY = 50; -const MAX_X = 1000; -const ROTATION_FACTOR = Math.PI; -const MOMENTUM_RANGE = 1; - -class FallingTile { - private tile: Tile; - private x: number; - private y: number; - private vy: number = INITIAL_VY; - private orientation: number; - private momentum: number; - - public constructor( - tile: Tile, - x: number, - y: number, - orientation: number, - momentum: number - ) { - this.tile = tile; - this.x = x; - this.y = y; - this.orientation = orientation; - this.momentum = momentum; - } - - public update(dt: number): void { - this.vy += dt * G; - this.y += dt * this.vy; - this.orientation += dt * this.momentum; - } - - public isOutside(): boolean { - return this.y > LIMIT_MAX; - } - - public getTile(): Tile { - return this.tile; - } - - public drawFallingTile(ctx: CanvasRenderingContext2D): void { - this.tile?.drawTile( - ctx, - this.x, - this.y, - SIZE, - false, - this.orientation, - false, - false - ); - } -} - -export class Rain { - private deck: Deck; - private tiles: FallingTile[] = []; - private tileAddTimer: number = 0; - - public constructor() { - this.deck = new Deck(true); - } - - public update(dt: number): void { - let i = this.tiles.length; - - while (i--) { - const tile = this.tiles[i]; - tile.update(dt); - - if (tile.isOutside()) { - this.deck.push(tile.getTile()); - this.tiles.splice(i, 1); - } - } - - this.tileAddTimer += dt; - if (this.tileAddTimer >= (1 / SPAWN_CHANCE)) { - this.addFallingTile(); - this.tileAddTimer = 0; - } else if (Math.random() < SPAWN_CHANCE * dt) { - this.addFallingTile(); - } - } - - public addFallingTile(): void { - if (this.deck.length() === 0) { - return; - } - - if (this.deck.length() > 1) { - this.deck.shuffle(); - } - - const newTile = new FallingTile( - this.deck.pop()!, - Math.floor(Math.random() * MAX_X), - INITIAL_Y, - Math.random() * ROTATION_FACTOR, - (Math.random() * 2 - 1) * MOMENTUM_RANGE - ); - - this.tiles.push(newTile); - } - - public drawRain(ctx: CanvasRenderingContext2D): void { - for (const tile of this.tiles) { - tile.drawFallingTile(ctx); - } - } - - public async preloadRain(): Promise { - console.log("preload rain"); - await this.deck.preload(); - } -} diff --git a/src/state.ts b/src/state.ts deleted file mode 100644 index c70ebf6..0000000 --- a/src/state.ts +++ /dev/null @@ -1,91 +0,0 @@ -export function drawState( - ctx: CanvasRenderingContext2D, - turn: number = 0, - rotation: number = 0 -): void { - let color = "#e0e0f0"; - let r = 30; - let s = 100; - let c = 525; - let pi = Math.PI; - - // turn - let w = 150; - let h = 4; - let rd = 2; - let y = 525 + s + 5; - - ctx.save(); - ctx.translate(525, 525); - ctx.rotate([0, -pi/2, pi, pi/2][turn]); - ctx.translate(-525, -525); - - ctx.fillStyle = "#ffcc33"; - ctx.beginPath(); - ctx.moveTo(c, y); - ctx.lineTo(c - w/2 + rd, y); - ctx.quadraticCurveTo(c - w/2, y, c - w/2, y + rd); - ctx.lineTo(c - w/2, y + h - rd); - ctx.quadraticCurveTo(c - w/2, y + h, c - w/2 + rd, y + h); - ctx.lineTo(c + w/2 - rd, y + h); - ctx.quadraticCurveTo(c + w/2, y + h, c + w/2, y + h - rd); - ctx.lineTo(c + w/2, y + rd); - ctx.quadraticCurveTo(c + w/2, y, c + w/2 - rd, y); - ctx.lineTo(c, y); - - ctx.fill(); - ctx.stroke(); - - ctx.restore(); - - // big rectangle - ctx.save(); - ctx.translate(525, 525); - ctx.rotate(rotation); - ctx.translate(-525, -525); - - ctx.fillStyle = color; - ctx.beginPath(); - ctx.moveTo(c - s, c); - ctx.lineTo(c - s, c + s - r); - ctx.quadraticCurveTo(c - s, c + s, c - s + r, c + s); - ctx.lineTo(c + s - r, c + s); - ctx.quadraticCurveTo(c + s, c + s, c + s, c + s - r); - ctx.lineTo(c + s, c - s + r); - ctx.quadraticCurveTo(c + s, c - s, c + s - r, c - s); - ctx.lineTo(c - s + r, c - s); - ctx.quadraticCurveTo(c - s, c - s, c - s, c - s + r); - ctx.lineTo(c - s, c); - - ctx.fill(); - ctx.stroke(); - - // winds - ctx.fillStyle = "#000000"; - ctx.font = "40px garamond"; - - ctx.fillText("Est", 505, 515 + s); - - ctx.save(); - ctx.translate(525, 525); - ctx.rotate(-3.141592/2); - ctx.translate(-525, -525); - ctx.fillText("Sud", 500, 515 + s); - ctx.restore(); - - ctx.save(); - ctx.translate(525, 525); - ctx.rotate(3.141592); - ctx.translate(-525, -525); - ctx.fillText("Ouest", 480, 515 + s); - ctx.restore(); - - ctx.save(); - ctx.translate(525, 525); - ctx.rotate(3.141592/2); - ctx.translate(-525, -525); - ctx.fillText("Nord", 485, 515 + s); - ctx.restore(); - - ctx.restore(); -} diff --git a/src/styles/main.css b/src/styles/main.css deleted file mode 100644 index 2cf8c7a..0000000 --- a/src/styles/main.css +++ /dev/null @@ -1,562 +0,0 @@ -/* ============ CSS Variables ============ */ -:root { - /* Colors - Green theme */ - --color-primary: #1a7f4b; - --color-primary-light: #22a861; - --color-primary-dark: #0d5c34; - --color-accent: #ffd700; - --color-accent-hover: #ffed4a; - - /* Background colors */ - --bg-main: linear-gradient(135deg, #0d3d23 0%, #1a5c3a 50%, #0d3d23 100%); - --bg-navbar: linear-gradient(180deg, #1a5c3a 0%, #0d3d23 100%); - --bg-dropdown: rgba(255, 255, 255, 0.98); - --bg-glass: rgba(255, 255, 255, 0.1); - - /* Text colors */ - --text-light: #ffffff; - --text-dark: #1a1a1a; - --text-muted: #6b7280; - - /* Shadows */ - --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.1); - --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.15); - --shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.25); - --shadow-xl: 0 20px 40px rgba(0, 0, 0, 0.3); - - /* Spacing */ - --spacing-xs: 0.25rem; - --spacing-sm: 0.5rem; - --spacing-md: 1rem; - --spacing-lg: 1.5rem; - --spacing-xl: 2rem; - --spacing-2xl: 3rem; - - /* Border radius */ - --radius-sm: 4px; - --radius-md: 8px; - --radius-lg: 12px; - --radius-xl: 16px; - --radius-full: 9999px; - - /* Transitions */ - --transition-fast: 150ms ease; - --transition-normal: 250ms ease; - --transition-slow: 400ms ease; - - /* Typography */ - --font-sans: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif; - --font-display: 'Poppins', var(--font-sans); - - /* Z-index layers */ - --z-dropdown: 100; - --z-navbar: 200; - --z-modal: 300; - --z-tooltip: 400; -} - -/* ============ Reset & Base ============ */ -*, -*::before, -*::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -html { - font-size: 16px; - scroll-behavior: smooth; -} - -body { - font-family: var(--font-sans); - background: var(--bg-main); - min-height: 100vh; - color: var(--text-light); - line-height: 1.6; - overflow-x: hidden; -} - -/* ============ Typography ============ */ -h1, h2, h3, h4, h5, h6 { - font-family: var(--font-display); - font-weight: 600; - line-height: 1.3; -} - -a { - color: inherit; - text-decoration: none; - transition: color var(--transition-fast); -} - -/* ============ Navbar ============ */ -.navbar { - position: sticky; - top: 0; - z-index: var(--z-navbar); - background: var(--bg-navbar); - box-shadow: var(--shadow-lg); - backdrop-filter: blur(10px); -} - -.navbar__container { - display: flex; - align-items: center; - justify-content: center; - gap: var(--spacing-sm); - max-width: 1400px; - margin: 0 auto; - padding: 0 var(--spacing-md); -} - -.navbar__item { - position: relative; - list-style: none; -} - -.navbar__link { - display: flex; - align-items: center; - gap: var(--spacing-xs); - padding: var(--spacing-md) var(--spacing-lg); - font-size: 1rem; - font-weight: 500; - color: var(--text-light); - transition: all var(--transition-normal); - border-radius: var(--radius-md); - position: relative; -} - -.navbar__link::after { - content: ''; - position: absolute; - bottom: 0; - left: 50%; - width: 0; - height: 3px; - background: var(--color-accent); - transition: all var(--transition-normal); - transform: translateX(-50%); - border-radius: var(--radius-full); -} - -.navbar__link:hover { - background: var(--bg-glass); - color: var(--color-accent); -} - -.navbar__link:hover::after { - width: 60%; -} - -.navbar__link--active { - color: var(--color-accent); -} - -/* Dropdown icon */ -.navbar__link--dropdown::before { - content: ''; - position: absolute; - right: var(--spacing-sm); - top: 50%; - transform: translateY(-50%); - border: 5px solid transparent; - border-top-color: currentColor; - transition: transform var(--transition-fast); -} - -.navbar__item:hover .navbar__link--dropdown::before { - transform: translateY(-50%) rotate(180deg); -} - -/* ============ Dropdown ============ */ -.dropdown { - position: absolute; - top: 100%; - left: 50%; - transform: translateX(-50%) translateY(10px); - background: var(--bg-dropdown); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-xl); - min-width: 280px; - padding: var(--spacing-sm); - opacity: 0; - visibility: hidden; - transition: all var(--transition-normal); - z-index: var(--z-dropdown); -} - -.navbar__item:hover .dropdown { - opacity: 1; - visibility: visible; - transform: translateX(-50%) translateY(0); -} - -.dropdown__link { - display: flex; - align-items: center; - gap: var(--spacing-sm); - padding: var(--spacing-sm) var(--spacing-md); - color: var(--text-dark); - border-radius: var(--radius-md); - font-size: 0.95rem; - transition: all var(--transition-fast); -} - -.dropdown__link:hover { - background: var(--color-primary); - color: var(--text-light); - transform: translateX(5px); -} - -.dropdown__link--chapter { - padding-left: var(--spacing-lg); -} - -.dropdown__link--chapter::before { - margin-right: var(--spacing-xs); -} - -/* ============ Main Container ============ */ -.main-container { - display: flex; - justify-content: center; - align-items: flex-start; - gap: var(--spacing-lg); - padding: var(--spacing-lg); - min-height: calc(100vh - 70px); - flex-wrap: nowrap; - overflow-x: auto; -} - -/* ============ Canvas Containers ============ */ -.canvas-wrapper { - background: rgba(0, 0, 0, 0.2); - border-radius: var(--radius-xl); - padding: 0; - margin: 0; - box-shadow: var(--shadow-xl); - backdrop-filter: blur(5px); - border: 1px solid rgba(255, 255, 255, 0.1); - transition: transform var(--transition-normal), box-shadow var(--transition-normal); - flex-shrink: 0; - overflow: hidden; - line-height: 0; - font-size: 0; -} - -/* Canvas de jeu (gauche) */ -.canvas-wrapper:first-child { - order: 1; -} - -/* Canvas de texte (droite) */ -.canvas-wrapper:last-child { - order: 2; -} - -/* ============ Mode Introduction ============ */ -.intro-mode { - justify-content: flex-start; - padding-left: var(--spacing-xl); -} - -.canvas-wrapper.hidden { - display: none; -} - -.canvas-wrapper:hover { - transform: translateY(-2px); - box-shadow: 0 25px 50px rgba(0, 0, 0, 0.35); -} - -.canvas-wrapper canvas { - display: block; - border-radius: var(--radius-xl); - margin: 0; - padding: 0; -} - -/* ============ Buttons ============ */ -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: var(--spacing-sm); - padding: var(--spacing-sm) var(--spacing-lg); - font-size: 1rem; - font-weight: 600; - font-family: inherit; - border: none; - border-radius: var(--radius-md); - cursor: pointer; - transition: all var(--transition-fast); - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.btn--primary { - background: linear-gradient(135deg, var(--color-primary-light), var(--color-primary)); - color: var(--text-light); - box-shadow: var(--shadow-md); -} - -.btn--primary:hover { - background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark)); - transform: translateY(-2px); - box-shadow: var(--shadow-lg); -} - -.btn--accent { - background: linear-gradient(135deg, var(--color-accent-hover), var(--color-accent)); - color: var(--text-dark); - box-shadow: var(--shadow-md); -} - -.btn--accent:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-lg); -} - -.btn--danger { - background: linear-gradient(135deg, #ff6b6b, #ee5a5a); - color: var(--text-light); -} - -.btn--ghost { - background: transparent; - border: 2px solid var(--text-light); - color: var(--text-light); -} - -.btn--ghost:hover { - background: var(--text-light); - color: var(--color-primary-dark); -} - -/* ============ Loading Indicator ============ */ -.loader { - display: flex; - align-items: center; - justify-content: center; - gap: var(--spacing-sm); -} - -.loader__dot { - width: 12px; - height: 12px; - background: var(--color-accent); - border-radius: var(--radius-full); - animation: bounce 1.4s ease-in-out infinite; -} - -.loader__dot:nth-child(1) { animation-delay: 0s; } -.loader__dot:nth-child(2) { animation-delay: 0.2s; } -.loader__dot:nth-child(3) { animation-delay: 0.4s; } - -@keyframes bounce { - 0%, 80%, 100% { - transform: scale(0.8); - opacity: 0.5; - } - 40% { - transform: scale(1.2); - opacity: 1; - } -} - -/* ============ Tooltips ============ */ -.tooltip { - position: relative; -} - -.tooltip::after { - content: attr(data-tooltip); - position: absolute; - bottom: 100%; - left: 50%; - transform: translateX(-50%) translateY(-5px); - background: var(--text-dark); - color: var(--text-light); - padding: var(--spacing-xs) var(--spacing-sm); - border-radius: var(--radius-sm); - font-size: 0.85rem; - white-space: nowrap; - opacity: 0; - visibility: hidden; - transition: all var(--transition-fast); - z-index: var(--z-tooltip); -} - -.tooltip:hover::after { - opacity: 1; - visibility: visible; - transform: translateX(-50%) translateY(0); -} - -/* ============ Utility Classes ============ */ -.text-center { text-align: center; } -.text-left { text-align: left; } -.text-right { text-align: right; } - -.mt-sm { margin-top: var(--spacing-sm); } -.mt-md { margin-top: var(--spacing-md); } -.mt-lg { margin-top: var(--spacing-lg); } - -.mb-sm { margin-bottom: var(--spacing-sm); } -.mb-md { margin-bottom: var(--spacing-md); } -.mb-lg { margin-bottom: var(--spacing-lg); } - -.hidden { display: none !important; } -.visible { display: block !important; } - -/* ============ Animations ============ */ -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes slideInLeft { - from { - opacity: 0; - transform: translateX(-30px); - } - to { - opacity: 1; - transform: translateX(0); - } -} - -@keyframes pulse { - 0%, 100% { - transform: scale(1); - } - 50% { - transform: scale(1.05); - } -} - -.animate-fadeIn { - animation: fadeIn var(--transition-slow) ease-out; -} - -.animate-slideIn { - animation: slideInLeft var(--transition-slow) ease-out; -} - -.animate-pulse { - animation: pulse 2s ease-in-out infinite; -} - -/* ============ Scrollbar Styling ============ */ -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background: rgba(0, 0, 0, 0.1); - border-radius: var(--radius-full); -} - -::-webkit-scrollbar-thumb { - background: var(--color-primary); - border-radius: var(--radius-full); -} - -::-webkit-scrollbar-thumb:hover { - background: var(--color-primary-light); -} - -/* ============ Responsive Design ============ */ -@media (max-width: 2100px) { - .main-container { - flex-wrap: nowrap; - } -} - -@media (max-width: 1300px) { - .main-container { - flex-direction: column; - align-items: center; - flex-wrap: wrap; - } -} - -@media (max-width: 768px) { - :root { - font-size: 14px; - } - - .navbar__container { - flex-wrap: wrap; - justify-content: space-around; - } - - .navbar__link { - padding: var(--spacing-sm) var(--spacing-md); - font-size: 0.9rem; - } - - .dropdown { - min-width: 220px; - } - - .main-container { - padding: var(--spacing-md); - gap: var(--spacing-md); - } - - .canvas-wrapper { - overflow-x: auto; - } -} - -@media (max-width: 480px) { - .navbar__container { - gap: 0; - } - - .navbar__link { - padding: var(--spacing-xs) var(--spacing-sm); - font-size: 0.8rem; - } -} - -/* ============ Print Styles ============ */ -@media print { - .navbar, - .btn { - display: none !important; - } - - body { - background: white; - color: black; - } -} - -/* ============ Focus Styles (Accessibility) ============ */ -:focus-visible { - outline: 3px solid var(--color-accent); - outline-offset: 2px; -} - -/* ============ Reduced Motion ============ */ -@media (prefers-reduced-motion: reduce) { - *, - *::before, - *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - } -} diff --git a/src/text/default.txt b/src/text/default.txt deleted file mode 100644 index 2472189..0000000 --- a/src/text/default.txt +++ /dev/null @@ -1,5 +0,0 @@ -Cette page n'a pas *encore* été implémentée. - -Merci de votre compréhension et de votre patience. - - diff --git a/src/text/parse.ts b/src/text/parse.ts deleted file mode 100644 index 10c55e2..0000000 --- a/src/text/parse.ts +++ /dev/null @@ -1,229 +0,0 @@ -export async function drawText( - filePath: string, - ctx: CanvasRenderingContext2D, -): Promise { - // New implementation: word-based wrapping, DPR-aware canvas resize, - // simple inline formatting markers: *bold*, ~italic~, #rrggbb{color}... - const raw = await fetch(filePath).then(r => r.text()); - - const DEFAULT_COLOR = "#ffffff"; - const BASE_FONT_SIZE = 30; // in CSS px - const FONT_FAMILY = "Garamond, serif"; - const MARGIN_X = 10; - const MARGIN_Y = 10; - const LINE_SPACING = 1.25; // multiplier - - const canvas = ctx.canvas; - const dpr = (window.devicePixelRatio || 1); - - // Helper to build font string for canvas context - const fontFor = (size: number, bold: boolean, italic: boolean) => { - return `${italic ? 'italic ' : ''}${bold ? 'bold ' : ''}${size}px ${FONT_FAMILY}`; - }; - - // Tokenize raw text into words/newlines while preserving inline styles - type Style = { bold: boolean; italic: boolean; color?: string }; - type Token = { text: string; style: Style }; - - function tokenize(input: string): Token[] { - const tokens: Token[] = []; - let i = 0; - let currentStyle: Style = { bold: false, italic: false }; - while (i < input.length) { - const ch = input[i]; - if (ch === '*') { currentStyle = { ...currentStyle, bold: !currentStyle.bold }; i++; continue; } - if (ch === '~') { currentStyle = { ...currentStyle, italic: !currentStyle.italic }; i++; continue; } - if (ch === '#') { - // read hex up to '{' - let hex = '#'; - i++; - while (i < input.length && input[i] !== '{') { hex += input[i++]; } - if (i < input.length && input[i] === '{') { - // consume '{' - i++; - // read until closing '}' or newline - let inner = ''; - while (i < input.length && input[i] !== '}') { inner += input[i++]; } - // consume '}' if present - if (i < input.length && input[i] === '}') i++; - // push inner text as a token with color hex - if (inner.length > 0) { - tokens.push({ text: inner, style: { ...currentStyle, color: hex } }); - } - continue; - } else { - // fallback: just treat '#' as normal char - tokens.push({ text: '#', style: { ...currentStyle } }); - continue; - } - } - - if (ch === '\n') { - tokens.push({ text: '\n', style: { ...currentStyle } }); - i++; continue; - } - - // read a word (until whitespace or newline) - if (ch === ' ' || ch === '\t' || ch === '\r') { - // collapse sequences of spaces into single space token - let spaces = ''; - while (i < input.length && (input[i] === ' ' || input[i] === '\t' || input[i] === '\r')) { spaces += input[i++]; } - tokens.push({ text: ' ', style: { ...currentStyle } }); - continue; - } - - // regular word - let w = ''; - while (i < input.length && input[i] !== ' ' && input[i] !== '\n' && input[i] !== '\t' && input[i] !== '\r') { - // handle formatting markers inside words by breaking - if (input[i] === '*' || input[i] === '~' || input[i] === '#') break; - w += input[i++]; - } - if (w.length > 0) tokens.push({ text: w, style: { ...currentStyle } }); - } - return tokens; - } - - const tokens = tokenize(raw); - - // Use an offscreen canvas to measure text widths - const measureCanvas = document.createElement('canvas'); - const mctx = measureCanvas.getContext('2d') as CanvasRenderingContext2D; - - // Determine max allowed width (allow text to expand but cap to viewport) - const MAX_ALLOWED_CSS_WIDTH = Math.max(200, Math.min(window.innerWidth - 40, 1050)); - - // Build lines by measuring tokens and wrapping - const lines: Token[][] = []; - let currentLine: Token[] = []; - let currentLineWidth = 0; - let measuredMaxLineWidth = 0; - - const spaceWidthCache = new Map(); - - function measureTokenWidth(tok: Token) { - const font = fontFor(BASE_FONT_SIZE, tok.style.bold, tok.style.italic); - mctx.font = font; - return mctx.measureText(tok.text).width; - } - - for (let i = 0; i < tokens.length; i++) { - const tok = tokens[i]; - if (tok.text === '\n') { - // push current line and start a new one - lines.push(currentLine); - measuredMaxLineWidth = Math.max(measuredMaxLineWidth, currentLineWidth); - currentLine = []; - currentLineWidth = 0; - continue; - } - - // measure token width - let w = measureTokenWidth(tok); - - // treat spaces as small width - if (tok.text === ' ') { - currentLine.push(tok); - currentLineWidth += w; - continue; - } - - // If token is wider than the allowed width, try to break it into smaller tokens (characters) - const availableWidth = MAX_ALLOWED_CSS_WIDTH - 2 * MARGIN_X; - if (w > availableWidth) { - // break the token into single-character tokens (preserve style) - for (const ch of tok.text) { - const charTok: Token = { text: ch, style: { ...tok.style } }; - const cw = measureTokenWidth(charTok); - if (currentLine.length > 0 && (currentLineWidth + cw) > availableWidth) { - lines.push(currentLine); - measuredMaxLineWidth = Math.max(measuredMaxLineWidth, currentLineWidth); - currentLine = [charTok]; - currentLineWidth = cw; - } else { - currentLine.push(charTok); - currentLineWidth += cw; - } - } - continue; - } - - // If adding this word would exceed max width, wrap - if (currentLine.length > 0 && (currentLineWidth + w) > availableWidth) { - lines.push(currentLine); - measuredMaxLineWidth = Math.max(measuredMaxLineWidth, currentLineWidth); - currentLine = [tok]; - currentLineWidth = w; - } else { - currentLine.push(tok); - currentLineWidth += w; - } - } - // push last line - if (currentLine.length > 0) { - lines.push(currentLine); - measuredMaxLineWidth = Math.max(measuredMaxLineWidth, currentLineWidth); - } - - const cssWidth = Math.min(MAX_ALLOWED_CSS_WIDTH, Math.ceil(measuredMaxLineWidth + 2 * MARGIN_X)); - const lineHeight = Math.ceil(BASE_FONT_SIZE * LINE_SPACING); - const cssHeight = Math.ceil(lines.length * lineHeight + 2 * MARGIN_Y); - - // Resize canvas with DPR awareness. Re-get context after setting width/height (context is reset). - canvas.style.width = cssWidth + 'px'; - canvas.style.height = cssHeight + 'px'; - canvas.width = Math.max(1, Math.round(cssWidth * dpr)); - canvas.height = Math.max(1, Math.round(cssHeight * dpr)); - - const drawCtx = canvas.getContext('2d') as CanvasRenderingContext2D; - // reset transform and scale for DPR - drawCtx.setTransform(1, 0, 0, 1, 0, 0); - drawCtx.scale(dpr, dpr); - - // Do not clear background here — caller may have drawn background. But - // to avoid artifacts on increased canvas size, we can clear the interior area. - drawCtx.clearRect(0, 0, cssWidth, cssHeight); - - // Draw lines - let y = MARGIN_Y + BASE_FONT_SIZE; // baseline for first line - for (const lineTokens of lines) { - let x = MARGIN_X; - for (const tok of lineTokens) { - if (tok.text === ' ') { - // measure and advance - drawCtx.font = fontFor(BASE_FONT_SIZE, tok.style.bold, tok.style.italic); - const sw = drawCtx.measureText(' ').width; - x += sw; - continue; - } - drawCtx.font = fontFor(BASE_FONT_SIZE, tok.style.bold, tok.style.italic); - drawCtx.fillStyle = tok.style.color || DEFAULT_COLOR; - drawCtx.fillText(tok.text, x, y); - const w = drawCtx.measureText(tok.text).width; - x += w; - } - y += lineHeight; - } - - // update an offscreen accessible copy for screen readers - try { - let htmlCopy = document.getElementById('textHtmlCopy'); - if (!htmlCopy) { - htmlCopy = document.createElement('div'); - htmlCopy.id = 'textHtmlCopy'; - htmlCopy.style.position = 'absolute'; - htmlCopy.style.left = '-9999px'; - htmlCopy.style.top = 'auto'; - htmlCopy.style.width = '1px'; - htmlCopy.style.height = '1px'; - htmlCopy.style.overflow = 'hidden'; - document.body.appendChild(htmlCopy); - } - // plain text variant: strip formatting markers - const plain = raw.replace(/\*|~/g, '').replace(/#([0-9a-fA-F]+)\{([^}]*)\}/g, '$2'); - htmlCopy.textContent = plain; - } catch (e) { - // non-fatal - console.warn('Could not create accessible text copy', e); - } -} diff --git a/src/text/txt.ts b/src/text/txt.ts deleted file mode 100644 index 8864686..0000000 --- a/src/text/txt.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { drawText } from "./parse" - -const CANVAS_ID = "myTextCanvas"; -const BG_RECT = { x: 0, y: 0, w: 800, h: 1050, color: "#007733" }; - -const canvas = document.getElementById(CANVAS_ID) as HTMLCanvasElement; -const ctx = canvas.getContext("2d") as NonNullable; -canvas.width = BG_RECT.w; -canvas.height = BG_RECT.h; -const number = (window as any).txtNumber; - -const path = "src/text/" - -ctx.fillStyle = BG_RECT.color; -ctx.fillRect(BG_RECT.x, BG_RECT.y, BG_RECT.w, BG_RECT.h); - -const filePath = path + "txt" + number + ".txt"; -drawText(filePath, ctx).catch(error => console.error(error)); - -export {}; diff --git a/src/text/txt0.txt b/src/text/txt0.txt deleted file mode 100644 index 5190504..0000000 --- a/src/text/txt0.txt +++ /dev/null @@ -1,27 +0,0 @@ - -~Introduction~ - -Bienvenue sur ce site dédié au *Riichi Mahjong* ! - -Ce site s'adresse principalement aux personnes qui découvrent ce -formidable jeu. - -Le nombre de chapitres par section peut sembler important -au premier abord, mais chacun d'eux reste court et facile -à aborder. - -Vous y trouverez notamment : - -- *Initiation au Riichi Mahjong* (onglet #ff00ff{Riichi Mahjong}) - -- *Explication d'une partie réelle* (onglet #ff00ff{Partie réelle}) - pour apprendre à jouer en conditions réelles. - -- *Présentation d'une variante* (onglet #ff00ff{Riichi à trois}) - -- *Entraînement aux différents Yakus* (onglet #ff00ff{Yaku trainer}) - -- *Introduction au MCR* (variante chinoise) (onglet #ff00ff{MCR}) - -Ce site est en développement — certaines pages sont encore incomplètes. -N'hésitez pas à poster vos retours sur le dépôt GitHub pour contribuer. diff --git a/src/text/txt1.txt b/src/text/txt1.txt deleted file mode 100644 index 98409bb..0000000 --- a/src/text/txt1.txt +++ /dev/null @@ -1,17 +0,0 @@ - -~Chapitre 1: Présentation des tuiles~ - -Le *Riichi Mahjong* se joue avec des *tuiles*. Elles se divisent en -deux grandes catégories : - -- Les #ff00ff{familles} - Le jeu comporte trois familles principales : - - Les #ff00ff{Caractères} (appelées *Man* en japonais) - - Les #ff00ff{Ronds} (appelées *Pin* en japonais) - - Les #ff00ff{Bambous} (appelées *Sou* en japonais) - Chaque famille comprend les nombres de *1* à *9*. - -- Les #ff00ff{honneurs} - Ils se divisent en deux catégories : - - Les *Dragons* : #ff0000{Rouge}, *Blanc* et #00ff00{Vert} - - Les *Vents* : #a0a0ff{Est}, #a0a0ff{Sud}, #a0a0ff{Ouest} et #a0a0ff{Nord} diff --git a/src/text/txt10.txt b/src/text/txt10.txt deleted file mode 100644 index 2472189..0000000 --- a/src/text/txt10.txt +++ /dev/null @@ -1,5 +0,0 @@ -Cette page n'a pas *encore* été implémentée. - -Merci de votre compréhension et de votre patience. - - diff --git a/src/text/txt11.txt b/src/text/txt11.txt deleted file mode 100644 index 2472189..0000000 --- a/src/text/txt11.txt +++ /dev/null @@ -1,5 +0,0 @@ -Cette page n'a pas *encore* été implémentée. - -Merci de votre compréhension et de votre patience. - - diff --git a/src/text/txt12.txt b/src/text/txt12.txt deleted file mode 100644 index 2472189..0000000 --- a/src/text/txt12.txt +++ /dev/null @@ -1,5 +0,0 @@ -Cette page n'a pas *encore* été implémentée. - -Merci de votre compréhension et de votre patience. - - diff --git a/src/text/txt13.txt b/src/text/txt13.txt deleted file mode 100644 index 2472189..0000000 --- a/src/text/txt13.txt +++ /dev/null @@ -1,5 +0,0 @@ -Cette page n'a pas *encore* été implémentée. - -Merci de votre compréhension et de votre patience. - - diff --git a/src/text/txt14.txt b/src/text/txt14.txt deleted file mode 100644 index 2472189..0000000 --- a/src/text/txt14.txt +++ /dev/null @@ -1,5 +0,0 @@ -Cette page n'a pas *encore* été implémentée. - -Merci de votre compréhension et de votre patience. - - diff --git a/src/text/txt15.txt b/src/text/txt15.txt deleted file mode 100644 index 2472189..0000000 --- a/src/text/txt15.txt +++ /dev/null @@ -1,5 +0,0 @@ -Cette page n'a pas *encore* été implémentée. - -Merci de votre compréhension et de votre patience. - - diff --git a/src/text/txt16.txt b/src/text/txt16.txt deleted file mode 100644 index 2472189..0000000 --- a/src/text/txt16.txt +++ /dev/null @@ -1,5 +0,0 @@ -Cette page n'a pas *encore* été implémentée. - -Merci de votre compréhension et de votre patience. - - diff --git a/src/text/txt17.txt b/src/text/txt17.txt deleted file mode 100644 index 2472189..0000000 --- a/src/text/txt17.txt +++ /dev/null @@ -1,5 +0,0 @@ -Cette page n'a pas *encore* été implémentée. - -Merci de votre compréhension et de votre patience. - - diff --git a/src/text/txt18.txt b/src/text/txt18.txt deleted file mode 100644 index 2472189..0000000 --- a/src/text/txt18.txt +++ /dev/null @@ -1,5 +0,0 @@ -Cette page n'a pas *encore* été implémentée. - -Merci de votre compréhension et de votre patience. - - diff --git a/src/text/txt19.txt b/src/text/txt19.txt deleted file mode 100644 index 2472189..0000000 --- a/src/text/txt19.txt +++ /dev/null @@ -1,5 +0,0 @@ -Cette page n'a pas *encore* été implémentée. - -Merci de votre compréhension et de votre patience. - - diff --git a/src/text/txt2.txt b/src/text/txt2.txt deleted file mode 100644 index 1944d90..0000000 --- a/src/text/txt2.txt +++ /dev/null @@ -1,24 +0,0 @@ -~Chapitre 2: La main~ - -Durant une partie, un joueur possède généralement *13 tuiles* -dans sa main. - -Lors de son tour, il commence par *piocher* (récupérer) une tuile, -puis il *défausse* une tuile (qui peut être celle qu'il vient de -piocher). - - - - - - - - - - - - - - -~Une main interactive est mise à disposition pour s'entraîner -à cette mécanique~ diff --git a/src/text/txt20.txt b/src/text/txt20.txt deleted file mode 100644 index 2472189..0000000 --- a/src/text/txt20.txt +++ /dev/null @@ -1,5 +0,0 @@ -Cette page n'a pas *encore* été implémentée. - -Merci de votre compréhension et de votre patience. - - diff --git a/src/text/txt21.txt b/src/text/txt21.txt deleted file mode 100644 index 2472189..0000000 --- a/src/text/txt21.txt +++ /dev/null @@ -1,5 +0,0 @@ -Cette page n'a pas *encore* été implémentée. - -Merci de votre compréhension et de votre patience. - - diff --git a/src/text/txt22.txt b/src/text/txt22.txt deleted file mode 100644 index 2472189..0000000 --- a/src/text/txt22.txt +++ /dev/null @@ -1,5 +0,0 @@ -Cette page n'a pas *encore* été implémentée. - -Merci de votre compréhension et de votre patience. - - diff --git a/src/text/txt23.txt b/src/text/txt23.txt deleted file mode 100644 index 2472189..0000000 --- a/src/text/txt23.txt +++ /dev/null @@ -1,5 +0,0 @@ -Cette page n'a pas *encore* été implémentée. - -Merci de votre compréhension et de votre patience. - - diff --git a/src/text/txt3.txt b/src/text/txt3.txt deleted file mode 100644 index ae3b461..0000000 --- a/src/text/txt3.txt +++ /dev/null @@ -1,25 +0,0 @@ -~Chapitre 3: Les groupes~ - -Pour gagner, un joueur doit former une main suivant une forme -particulière. En effet, une main standard est composée de : - -- *4 groupes* de *3 tuiles* (chaque groupe peut être une suite ou - un brelan) -- *1 paire* - -~Ci‑contre : exemples de groupes valides et non valides~ - -On distingue notamment : -- Les #ff00ff{Chii} : suites de trois tuiles consécutives (même - famille) -- Les #ff00ff{Pon} : brelans (trois tuiles identiques) - -Ainsi, lorsqu'un joueur gagne, sa main contient *14 tuiles* au total. -Il est donc possible d'annoncer la victoire uniquement après avoir -récupéré une tuile (pioche ou défausse). - -Lorsque tous ces éléments sont réunis, la main est considérée -#ff00ff{Valide}. - -~Pour s'entraîner, piochez et défaussez des tuiles jusqu'à obtenir -une main valide — un message vous indiquera le succès.~ diff --git a/src/text/txt4.txt b/src/text/txt4.txt deleted file mode 100644 index f71ad95..0000000 --- a/src/text/txt4.txt +++ /dev/null @@ -1,27 +0,0 @@ -~Chapitre 4: Avoir une main valide en jeu~ - -Voici votre *première partie* ! -Il est temps de mettre en œuvre tout ce que vous avez appris. - -Il est important de connaître la mécanique suivante pour obtenir -des tuiles supplémentaires : - -- Lorsqu'un joueur défausse une tuile qui complète un *brelan* - pour un autre joueur, ce dernier peut l'annoncer avec - #ff00ff{Pon !} et la prendre pour compléter son brelan. -- Lorsque le joueur précédent défausse une tuile qui complète une - *suite* pour le joueur actif, celui-ci peut l'annoncer avec - #ff00ff{Chii !} et la récupérer pour former la suite. - -#ff0000{Attention !} -Cette action est appelée un #ff00ff{appel}. Ce dernier est *définitif* et est -*visible par tout le monde* - -Note : si un joueur veut faire un *Chii* en même temps qu'un autre -veut faire un *Pon*, alors le *Pon* a la priorité - - - - - -~Il est maintenant temps de choisir une tuile à défausser !~ diff --git a/src/text/txt5.txt b/src/text/txt5.txt deleted file mode 100644 index 93d3acc..0000000 --- a/src/text/txt5.txt +++ /dev/null @@ -1,24 +0,0 @@ -~Chapitre 5: Annoncer la victoire~ - -Nous avons vu qu'un *Chii* ne peut être déclaré que sur la défausse -du joueur précédent (sauf exception). - -Une exception importante : lorsqu'il ne manque plus qu'une seule tuile -pour compléter la main, on dit que le joueur est en #ff00ff{Tenpai}. -Dans cet état, si quelqu'un défausse une tuile qui complète la main -du joueur en Tenpai, ce dernier peut la récupérer et annoncer la -victoire. - -Il existe deux façons de gagner : -- Si le joueur en *Tenpai* pioche la tuile gagnante lui‑même, il - annonce #ff00ff{Tsumo !} -- Si le joueur en *Tenpai* gagne sur la défausse d'un autre joueur, - il annonce #ff00ff{Ron !} - -Après une victoire, des commandes apparaîtront pour signaler la -victoire. Les appels de victoire sont #ff00ff{Ron} et -#ff00ff{Tsumo} — ce sont eux qui déclarent la fin de la main. - -~Dans l'exemple ci‑contre, le joueur peut annoncer sa victoire -avec *Ron* sur la défausse du joueur en face en complétant sa -combinaison.~ \ No newline at end of file diff --git a/src/text/txt6.txt b/src/text/txt6.txt deleted file mode 100644 index 58c297c..0000000 --- a/src/text/txt6.txt +++ /dev/null @@ -1,22 +0,0 @@ -~Chapitre 6: Le vent du joueur~ - -Vous avez peut‑être remarqué des indications de direction affichées -au centre. Chaque joueur se voit assigner un vent au début d'une -manche : l'un des joueurs est #ff00ff{Est}, le joueur à sa droite -est #ff00ff{Sud}, puis #ff00ff{Ouest} et #ff00ff{Nord}. - -Le #ff00ff{Est} commence la manche. À la fin de la manche, les vents -peuvent tourner (généralement dans le sens anti‑horaire). - -Dans cette démo, la partie s'arrête lorsque *chaque joueur* a été -#ff00ff{Est} au moins une fois. - -#ff0000{Remarque} : contrairement à une boussole, l'*Est* et l'*Ouest* -peuvent sembler inversés selon la représentation utilisée ici. - -#ff0000{Remarque 2} : si le joueur #ff00ff{Est} remporte la manche, -il peut y avoir une #ff00ff{répétition} (la manche est rejouée) et -les vents ne tournent pas. - -~Dans le jeu ci‑contre, le vent initial est tiré aléatoirement et -change en fonction du résultat de chaque manche.~ \ No newline at end of file diff --git a/src/text/txt7.txt b/src/text/txt7.txt deleted file mode 100644 index 0ea7e4b..0000000 --- a/src/text/txt7.txt +++ /dev/null @@ -1,23 +0,0 @@ - -~Chapitre 7: Les yakus~ - -Une main *Valide* (forme correcte) n'est pas suffisante pour gagner : -il faut aussi que la main remplisse au moins une condition de -victoire, appelée un #ff00ff{Yaku}. - -Il existe de nombreux Yakus (environ *39*), mais en pratique une -dizaine des plus courants suffit pour la majorité des parties. - -Exemples de Yakus fréquents : -- #ff00ff{Tout ordinaire} : aucune tuile terminale ni honneur -- #ff00ff{Brelan de valeur} : par exemple brelan de dragon, brelan - d'Est ou brelan du vent du joueur -- #ff00ff{Main pure} : une seule famille -- #ff00ff{Main semi‑pure} : une seule famille + des honneurs -- #ff00ff{Double suite} : deux suites identiques - #ff0000{Ne fonctionne qu'en main fermée !} -- #ff00ff{Sept paires} : sept paires distinctes - #ff0000{Exception sur la forme des groupes} - -(Liste non exhaustive — référez‑vous à la fiche Yaku pour les règles -détaillées.) diff --git a/src/text/txt8.txt b/src/text/txt8.txt deleted file mode 100644 index d7177c0..0000000 --- a/src/text/txt8.txt +++ /dev/null @@ -1,19 +0,0 @@ -~Chapitre 8 — Le Riichi~ - -Le #ff00ff{Riichi} est le yaku le plus important du riichi mahjong -(d'où son nom). - -Pour le réaliser, la main doit *obligatoirement être fermée*. -Lorsque le joueur est en situation de tenpai, il peut déclarer -#ff00ff{Riichi} ! À ce moment-là, il défausse une tuile (qu'il -placera de côté) et ne pourra plus choisir sa défausse : - -- Il *pioche* -- Soit il *gagne* -- Soit il *défausse* la tuile piochée - -Cela peut sembler contraignant, mais permet d'obtenir un yaku, -quelle que soit la composition de la main (sous réserve d'être -en tenpai) - -~Il est temps d'essayer de déclarer un Riichi !~ \ No newline at end of file diff --git a/src/text/txt9.txt b/src/text/txt9.txt deleted file mode 100644 index 2472189..0000000 --- a/src/text/txt9.txt +++ /dev/null @@ -1,5 +0,0 @@ -Cette page n'a pas *encore* été implémentée. - -Merci de votre compréhension et de votre patience. - - diff --git a/src/tile.ts b/src/tile.ts deleted file mode 100644 index c3ccda7..0000000 --- a/src/tile.ts +++ /dev/null @@ -1,156 +0,0 @@ -export class Tile { - private imgFront: HTMLImageElement = new Image(); - private imgBack: HTMLImageElement = new Image(); - private imgGray: HTMLImageElement = new Image(); - private img: HTMLImageElement = new Image(); - private imgSrc: string = ""; - private tilt: number = 0; - - constructor( - private family: number, - private value: number, - private red: boolean - ) { - this.setImgSrc(); - } - - public getFamily(): number { - return this.family; - } - - public getValue(): number { - return this.value; - } - - public isEqual(family: number, value: number): boolean { - return this.family === family && this.value === value; - } - - public isRed(): boolean { - return this.red; - } - - public compare(t: Tile): number { - // Compare d'abord par famille, puis par valeur - if (this.family !== t.family) { - return this.family < t.family ? -1 : 1; - } - if (this.value !== t.value) { - return this.value < t.value ? -1 : 1; - } - return 0; - } - - public isLessThan(t: Tile): boolean { - return this.family < t.family || - (this.family === t.family && this.value <= t.value); - } - - public setTilt(): void { - this.tilt = (1 - 2 * Math.random()) * 0.04; - } - - public drawTile( - ctx: CanvasRenderingContext2D, - x: number, - y: number, - size: number, - hidden: boolean = false, - rotation: number = 0, - gray: boolean = false, - tilted: boolean = true - ): void { - const tileWidth = 75 * size; - const tileHeight = 100 * size; - const halfWidth = tileWidth / 2; - const halfHeight = tileHeight / 2; - const shadowScale = 0.92; - - // Sauvegarde du contexte et positionnement - ctx.save(); - ctx.translate(x + halfWidth, y + halfHeight); - ctx.rotate(rotation + (tilted ? this.tilt : 0)); - - // Position de l'ombre (légèrement décalée) - const shadowX = -(tileWidth * shadowScale) / 2; - const shadowY = -(tileHeight * shadowScale) / 2; - - // Dessin de l'ombre (commun aux deux cas) - ctx.drawImage(this.imgGray, shadowX, shadowY, tileWidth, tileHeight); - - if (hidden) { - // Dessin du dos de la tuile - ctx.drawImage(this.imgBack, -halfWidth, -halfHeight, tileWidth, tileHeight); - } else { - // Dessin de la tuile face visible - ctx.drawImage(this.imgFront, -halfWidth, -halfHeight, tileWidth, tileHeight); - - // Dessin du motif sur la tuile (légèrement plus petit) - const patternScale = 0.9; - const patternWidth = tileWidth * patternScale; - const patternHeight = tileHeight * patternScale; - const patternX = -((75 - 7) * size) / 2; - const patternY = -((100 - 10) * size) / 2; - - ctx.drawImage(this.img, patternX, patternY, patternWidth, patternHeight); - - // Appliquer un filtre gris si demandé - if (gray) { - ctx.drawImage(this.imgGray, -halfWidth, -halfHeight, tileWidth, tileHeight); - } - } - - ctx.restore(); - } - - 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 { - 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 { - 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"; - } -} diff --git a/src/types/index.ts b/src/types/index.ts deleted file mode 100644 index 9328f55..0000000 --- a/src/types/index.ts +++ /dev/null @@ -1,149 +0,0 @@ -/** - * Shared type definitions for the Riichi Mahjong Tutorial - */ - -// ============ Canvas Types ============ -export interface CanvasContext { - ctx: CanvasRenderingContext2D; - canvas: HTMLCanvasElement; -} - -export interface Point { - x: number; - y: number; -} - -export interface Rectangle { - x: number; - y: number; - width: number; - height: number; -} - -export interface Size { - width: number; - height: number; -} - -// ============ Tile Types ============ -export type TileFamily = 1 | 2 | 3 | 4 | 5; - -export interface TileIdentifier { - family: TileFamily; - value: number; - isRed?: boolean; -} - -export type TileFamilyName = 'man' | 'pin' | 'sou' | 'wind' | 'dragon'; - -export const TILE_FAMILY_MAP: Record = { - 1: 'man', - 2: 'pin', - 3: 'sou', - 4: 'wind', - 5: 'dragon', -}; - -// ============ Game Types ============ -export type PlayerPosition = 0 | 1 | 2 | 3; - -export type WindType = 'east' | 'south' | 'west' | 'north'; - -export const WIND_ORDER: WindType[] = ['east', 'south', 'west', 'north']; - -export interface PlayerState { - position: PlayerPosition; - wind: WindType; - score: number; - isDealer: boolean; - hasRiichi: boolean; -} - -export type GamePhase = - | 'waiting' - | 'drawing' - | 'discarding' - | 'calling' - | 'winning' - | 'end'; - -export interface GameState { - phase: GamePhase; - currentPlayer: PlayerPosition; - roundWind: WindType; - roundNumber: number; - honba: number; - riichiSticks: number; -} - -// ============ Group Types ============ -export type GroupType = 'pair' | 'triplet' | 'sequence' | 'quad'; - -export interface GroupDefinition { - type: GroupType; - tiles: TileIdentifier[]; - isOpen: boolean; -} - -// ============ Yaku Types ============ -export interface YakuResult { - name: string; - han: number; - isYakuman?: boolean; -} - -export interface ScoreResult { - yakus: YakuResult[]; - han: number; - fu: number; - basePoints: number; - totalPoints: number; -} - -// ============ UI Types ============ -export type ButtonAction = - | 'pass' - | 'chii' - | 'pon' - | 'kan' - | 'ron' - | 'tsumo' - | 'riichi'; - -export interface ButtonState { - action: ButtonAction; - enabled: boolean; - highlighted?: boolean; -} - -// ============ Animation Types ============ -export interface AnimationConfig { - duration: number; - easing: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out'; - delay?: number; -} - -export interface AnimationState { - startTime: number; - progress: number; - isComplete: boolean; -} - -// ============ Event Types ============ -export interface TileClickEvent { - tileIndex: number; - tile: TileIdentifier; - position: Point; -} - -export interface GameEvent { - type: string; - player: PlayerPosition; - data?: unknown; - timestamp: number; -} - -// ============ Callback Types ============ -export type CleanupFunction = () => void; -export type RenderFunction = (deltaTime: number) => void; -export type UpdateFunction = (deltaTime: number) => void; diff --git a/src/utils/index.ts b/src/utils/index.ts deleted file mode 100644 index cdd36e8..0000000 --- a/src/utils/index.ts +++ /dev/null @@ -1,335 +0,0 @@ -/** - * Utility functions for the Riichi Mahjong Tutorial - */ - -import type { AnimationConfig, AnimationState, Point, Rectangle } from '../types'; - -// ============ Canvas Utilities ============ - -/** - * Get a canvas context with optimizations - */ -export function getOptimizedContext( - canvas: HTMLCanvasElement, - alpha: boolean = false, -): CanvasRenderingContext2D | null { - return canvas.getContext('2d', { - alpha, - desynchronized: true, - willReadFrequently: false, - }); -} - -/** - * Clear a canvas with a solid color - */ -export function clearCanvas( - ctx: CanvasRenderingContext2D, - color: string, - width: number, - height: number, -): void { - ctx.fillStyle = color; - ctx.fillRect(0, 0, width, height); -} - -/** - * Create an offscreen canvas for double buffering - */ -export function createOffscreenCanvas( - width: number, - height: number, -): { canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D } | null { - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - - const ctx = getOptimizedContext(canvas); - if (!ctx) { - return null; - } - - return { canvas, ctx }; -} - -// ============ Math Utilities ============ - -/** - * Clamp a value between min and max - */ -export function clamp(value: number, min: number, max: number): number { - return Math.min(Math.max(value, min), max); -} - -/** - * Linear interpolation - */ -export function lerp(start: number, end: number, t: number): number { - return start + (end - start) * clamp(t, 0, 1); -} - -/** - * Convert degrees to radians - */ -export function degToRad(degrees: number): number { - return degrees * (Math.PI / 180); -} - -/** - * Convert radians to degrees - */ -export function radToDeg(radians: number): number { - return radians * (180 / Math.PI); -} - -/** - * Get a random number between min and max - */ -export function randomRange(min: number, max: number): number { - return min + Math.random() * (max - min); -} - -/** - * Get a random integer between min and max (inclusive) - */ -export function randomInt(min: number, max: number): number { - return Math.floor(randomRange(min, max + 1)); -} - -// ============ Geometry Utilities ============ - -/** - * Check if a point is inside a rectangle - */ -export function pointInRect(point: Point, rect: Rectangle): boolean { - return ( - point.x >= rect.x && - point.x <= rect.x + rect.width && - point.y >= rect.y && - point.y <= rect.y + rect.height - ); -} - -/** - * Get the distance between two points - */ -export function distance(p1: Point, p2: Point): number { - const dx = p2.x - p1.x; - const dy = p2.y - p1.y; - return Math.sqrt(dx * dx + dy * dy); -} - -/** - * Normalize an angle to be between 0 and 2π - */ -export function normalizeAngle(angle: number): number { - const TWO_PI = Math.PI * 2; - return ((angle % TWO_PI) + TWO_PI) % TWO_PI; -} - -// ============ Animation Utilities ============ - -/** - * Easing functions - */ -export const Easing = { - linear: (t: number): number => t, - - easeIn: (t: number): number => t * t, - - easeOut: (t: number): number => t * (2 - t), - - easeInOut: (t: number): number => - t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t, - - easeInCubic: (t: number): number => t * t * t, - - easeOutCubic: (t: number): number => (--t) * t * t + 1, - - easeInOutCubic: (t: number): number => - t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1, - - easeOutBounce: (t: number): number => { - if (t < 1 / 2.75) { - return 7.5625 * t * t; - } else if (t < 2 / 2.75) { - return 7.5625 * (t -= 1.5 / 2.75) * t + 0.75; - } else if (t < 2.5 / 2.75) { - return 7.5625 * (t -= 2.25 / 2.75) * t + 0.9375; - } else { - return 7.5625 * (t -= 2.625 / 2.75) * t + 0.984375; - } - }, -}; - -/** - * Create an animation state - */ -export function createAnimation(config: AnimationConfig): AnimationState & { config: AnimationConfig } { - return { - config, - startTime: 0, - progress: 0, - isComplete: false, - }; -} - -/** - * Update an animation state - */ -export function updateAnimation( - state: AnimationState & { config: AnimationConfig }, - currentTime: number, -): void { - if (state.startTime === 0) { - state.startTime = currentTime + (state.config.delay ?? 0); - } - - const elapsed = currentTime - state.startTime; - state.progress = clamp(elapsed / state.config.duration, 0, 1); - state.isComplete = state.progress >= 1; -} - -// ============ Array Utilities ============ - -/** - * Shuffle an array in place (Fisher-Yates) - */ -export function shuffleArray(array: T[]): T[] { - for (let i = array.length - 1; i > 0; i--) { - const j = randomInt(0, i); - [array[i], array[j]] = [array[j], array[i]]; - } - return array; -} - -/** - * Remove an item from an array by index - */ -export function removeAt(array: T[], index: number): T | undefined { - if (index >= 0 && index < array.length) { - return array.splice(index, 1)[0]; - } - return undefined; -} - -/** - * Get unique values from an array - */ -export function unique(array: T[]): T[] { - return [...new Set(array)]; -} - -// ============ DOM Utilities ============ - -/** - * Get element by ID with type safety - */ -export function getElementById(id: string): T | null { - return document.getElementById(id) as T | null; -} - -/** - * Create an element with attributes - */ -export function createElement( - tag: K, - attributes?: Partial, -): HTMLElementTagNameMap[K] { - const element = document.createElement(tag); - if (attributes) { - Object.assign(element, attributes); - } - return element; -} - -// ============ Image Utilities ============ - -/** - * Load an image as a Promise - */ -export function loadImage(src: string): Promise { - return new Promise((resolve, reject) => { - const img = new Image(); - img.onload = () => resolve(img); - img.onerror = () => reject(new Error(`Failed to load image: ${src}`)); - img.src = src; - }); -} - -/** - * Preload multiple images - */ -export async function preloadImages(sources: string[]): Promise { - return Promise.all(sources.map(loadImage)); -} - -// ============ Time Utilities ============ - -/** - * Create a debounced function - */ -export function debounce void>( - fn: T, - delay: number, -): (...args: Parameters) => void { - let timeoutId: ReturnType | null = null; - - return (...args: Parameters) => { - if (timeoutId) { - clearTimeout(timeoutId); - } - timeoutId = setTimeout(() => fn(...args), delay); - }; -} - -/** - * Create a throttled function - */ -export function throttle void>( - fn: T, - limit: number, -): (...args: Parameters) => void { - let inThrottle = false; - - return (...args: Parameters) => { - if (!inThrottle) { - fn(...args); - inThrottle = true; - setTimeout(() => { inThrottle = false; }, limit); - } - }; -} - -/** - * Wait for a specified duration - */ -export function wait(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -// ============ Storage Utilities ============ - -/** - * Safe localStorage get with JSON parsing - */ -export function getStorageItem(key: string, defaultValue: T): T { - try { - const item = localStorage.getItem(key); - return item ? JSON.parse(item) as T : defaultValue; - } catch { - return defaultValue; - } -} - -/** - * Safe localStorage set with JSON stringify - */ -export function setStorageItem(key: string, value: T): void { - try { - localStorage.setItem(key, JSON.stringify(value)); - } catch { - // Storage might be full or disabled - console.warn(`Failed to save to localStorage: ${key}`); - } -} diff --git a/src/yakus/generator.ts b/src/yakus/generator.ts deleted file mode 100644 index 2ebce88..0000000 --- a/src/yakus/generator.ts +++ /dev/null @@ -1,328 +0,0 @@ -import { Hand } from "../hand" -import { Deck } from "../deck" -import { Tile } from "../tile"; - -export const function_generator = { - - ordinaires(wind: number = 0): Hand { - let h = new Hand(); - let deck = new Deck(); - deck.shuffle(); - - // choix d'une paire - let t1 = deck.pop(); - let t2 = deck.find(t1.getFamily(), t1.getValue()) as NonNullable; - h.push(t1); - h.push(t2); - - let count = 2; - while (count !== 14) { - if (Math.random() < 0.5) { // pon - let t1 = deck.pop(); - let f = t1.getFamily(); - let v = t1.getValue(); - if ( - f < 4 && v !== 1 && v !== 9 && - deck.count(f, v) > 1 - ) { - let t2 = deck.find(f, v) as NonNullable; - let t3 = deck.find(f, v) as NonNullable; - h.push(t1); - h.push(t2); - h.push(t3); - count += 3; - } else { - deck.push(t1); - deck.shuffle(); - } - } else { // chii - let t1 = deck.pop(); - let f = t1.getFamily(); - let v = t1.getValue(); - if ( - f < 4 && v !== 1 && v < 6 && - deck.count(f, v + 1) > 0 && - deck.count(f, v + 2) > 0 - ) { - let t2 = deck.find(f, v + 1) as NonNullable; - let t3 = deck.find(f, v + 2) as NonNullable; - h.push(t1); - h.push(t2); - h.push(t3); - count += 3; - } else { - deck.push(t1); - deck.shuffle(); - } - } - } - h.sort(); - return h; - }, - - brelan_valeur(wind: number = 0): Hand { - let h = new Hand(); - let deck = new Deck(); - deck.shuffle(); - - // brelan - let f: number; - let v: number; - if (Math.random() < 1/3) { // dragons - f = 5; - v = Math.floor(Math.random() * 3) + 1; - } else if (Math.random() < 0.5) { // est - f = 4; - v = 1; - } else { // vent du joueur - f = 4; - v = wind + 1; - } - h.push(deck.find(f, v) as NonNullable); - h.push(deck.find(f, v) as NonNullable); - h.push(deck.find(f, v) as NonNullable); - let count = 3; - - // pair - f = Math.floor(Math.random() * 3) + 1; - v = Math.floor(Math.random() * 9) + 1; - while (deck.count(f, v) < 2) { - f = Math.floor(Math.random() * 3) + 1; - v = Math.floor(Math.random() * 9) + 1; - } - h.push(deck.find(f, v) as NonNullable); - h.push(deck.find(f, v) as NonNullable); - count += 2; - - while (count !== 14) { - if (Math.random() < 0.5) { // pon - let t1 = deck.pop(); - let f = t1.getFamily(); - let v = t1.getValue(); - if ( - deck.count(f, v) > 1 - ) { - h.push(t1); - h.push(deck.find(f, v) as NonNullable); - h.push(deck.find(f, v) as NonNullable); - count += 3; - } else { - deck.push(t1); - deck.shuffle(); - } - - } else { // chii - let f = Math.floor(Math.random() * 3) + 1; - let v = Math.floor(Math.random() * 7) + 1; - if ( - deck.count(f, v) > 1 && - deck.count(f, v + 1) > 1 && - deck.count(f, v + 2) > 1 - ) { - h.push(deck.find(f, v) as NonNullable); - h.push(deck.find(f, v + 1) as NonNullable); - h.push(deck.find(f, v + 2) as NonNullable); - count += 3; - } - - } - } - h.sort(); - return h; - }, - - main_pure(wind: number = 0): Hand { - let h = new Hand(); - let deck = new Deck(); - deck.shuffle(); - let f = Math.floor(Math.random() * 3) + 1; - - let v = Math.floor(Math.random() * 9) + 1; - h.push(deck.find(f, v) as NonNullable); - h.push(deck.find(f, v) as NonNullable); - let count = 2; - - while (count < 14) { - if (Math.random() < 0.5) { // pon - let v = Math.floor(Math.random() * 9) + 1; - if (deck.count(f, v) > 2) { - for (let i = 0; i < 3; i++) { - h.push(deck.find(f, v) as NonNullable); - count++; - } - } - - } else { // chii - let v = Math.floor(Math.random() * 7) + 1; - if ( - deck.count(f, v) > 0 && - deck.count(f, v+1) > 0 && - deck.count(f, v+2) > 0 - ) { - for (let i = 0; i < 3; i++) { - h.push(deck.find(f, v + i) as NonNullable); - count++; - } - } - } - } - h.sort(); - return h; - }, - - main_semi_pure(wind: number = 0): Hand { - let h = new Hand(); - let deck = new Deck(); - deck.shuffle(); - let f = Math.floor(Math.random() * 3) + 1; - - let v = Math.floor(Math.random() * 9) + 1; - h.push(deck.find(f, v) as NonNullable); - h.push(deck.find(f, v) as NonNullable); - let count = 2; - - let family = Math.floor(Math.random() * 2) + 4; - let value = Math.floor(Math.random() * 3) + 1; - h.push(deck.find(family, value) as NonNullable); - h.push(deck.find(family, value) as NonNullable); - h.push(deck.find(family, value) as NonNullable); - count += 3; - - while (count < 14) { - if (Math.random() < 0.5) { // pon - - if (Math.random() < 0.5) { // famille - let v = Math.floor(Math.random() * 9) + 1; - if (deck.count(f, v) > 2) { - for (let i = 0; i < 3; i++) { - h.push(deck.find(f, v) as NonNullable); - count++; - } - } - } else if (Math.random() < 0.5) { // vent - let v = Math.floor(Math.random() * 4) + 1; - if (deck.count(4, v) > 2) { - for (let i = 0; i < 3; i++) { - h.push(deck.find(4, v) as NonNullable); - count++; - } - } - } else { // dragon - let v = Math.floor(Math.random() * 3) + 1; - if (deck.count(5, v) > 2) { - for (let i = 0; i < 3; i++) { - h.push(deck.find(5, v) as NonNullable); - count++; - } - } - } - - } else { // chii - let v = Math.floor(Math.random() * 7) + 1; - if ( - deck.count(f, v) > 0 && - deck.count(f, v+1) > 0 && - deck.count(f, v+2) > 0 - ) { - for (let i = 0; i < 3; i++) { - h.push(deck.find(f, v + i) as NonNullable); - count++; - } - } - } - } - h.sort(); - return h; - }, - - double_suite(wind: number = 0): Hand { - let h = new Hand(); - let deck = new Deck(); - deck.shuffle(); - - // choix d'une paire - let t1 = deck.pop(); - let t2 = deck.find(t1.getFamily(), t1.getValue()) as NonNullable; - h.push(t1); - h.push(t2); - let count = 2; - - // double suite - let f = Math.floor(Math.random() * 3) + 1; - let v = Math.floor(Math.random() * 7) + 1; - while ( - deck.count(f, v) < 2 || - deck.count(f, v + 1) < 2 || - deck.count(f, v + 2 ) < 2 - ) { - f = Math.floor(Math.random() * 3) + 1; - v = Math.floor(Math.random() * 7) + 1; - } - h.push(deck.find(f, v) as NonNullable); - h.push(deck.find(f, v) as NonNullable); - h.push(deck.find(f, v + 1) as NonNullable); - h.push(deck.find(f, v + 1) as NonNullable); - h.push(deck.find(f, v + 2) as NonNullable); - h.push(deck.find(f, v + 2) as NonNullable); - count += 6; - - while (count !== 14) { - if (Math.random() < 0.5) { // pon - let t1 = deck.pop(); - let f = t1.getFamily(); - let v = t1.getValue(); - if ( - deck.count(f, v) > 1 - ) { - h.push(t1); - h.push(deck.find(f, v) as NonNullable); - h.push(deck.find(f, v) as NonNullable); - count += 3; - } else { - deck.push(t1); - deck.shuffle(); - } - - } else { // chii - let f = Math.floor(Math.random() * 3) + 1; - let v = Math.floor(Math.random() * 7) + 1; - if ( - deck.count(f, v) > 1 && - deck.count(f, v + 1) > 1 && - deck.count(f, v + 2) > 1 - ) { - h.push(deck.find(f, v) as NonNullable); - h.push(deck.find(f, v + 1) as NonNullable); - h.push(deck.find(f, v + 2) as NonNullable); - count += 3; - } - - } - } - h.sort(); - return h; - - }, - - sept_pairs(wind: number = 0): Hand { - let h = new Hand(); - let deck = new Deck(); - deck.shuffle(); - let count = 0; - while (count !== 7) { - let tile = deck.pop(); - if (h.getTiles().every( - t => t.compare(tile) !== 0 - )) { - count++; - h.push(tile); - h.push(new Tile(tile.getFamily(), tile.getValue(), false)); - } - } - h.sort(); - return h; - } - -} - - diff --git a/src/yakus/yaku.ts b/src/yakus/yaku.ts deleted file mode 100644 index 8c51204..0000000 --- a/src/yakus/yaku.ts +++ /dev/null @@ -1,889 +0,0 @@ -import { Hand } from "../hand"; -import { Group } from "../group"; -import { Tile } from "../tile" - -function ord(g: Group): boolean { - return g.getTiles().every( - t => t.getFamily() < 4 && - t.getValue() > 1 && - t.getValue() < 9 - ) -} - -function term(g: Group): boolean { - return g.getTiles().every( - t => - t.getFamily() < 4 && - (t.getValue() === 1 || - t.getValue() === 9) - ); -} - -function honn(g: Group): boolean { - return g.getTiles().every( - t => - t.getFamily() >= 4 - ); -} - -function chii(g: Group): boolean { - let t = g.getTiles(); - return t[0].getValue() !== t[1].getValue(); -} - -function pon(g: Group): boolean { - let t = g.getTiles(); - return t[0].getValue() === t[1].getValue(); -} - -function green(t: Tile): boolean { - let f = t.getFamily(); - let v = t.getValue(); - if (f !== 5 && f !== 3) { - return false; - } - - if (f === 5) { - return v === 2; - } - - return v % 2 === 0 || v === 3; -} - - - - - -export const yakus = { - -/** - * double suite pure - * 0/1 - */ -lipekou: function ( - hand: Hand, - groups: Array, - wind: number -): number { - if (groups.length > 0 && !yakus.ryanpeikou(hand, groups, wind)) { // ouvert - return 0; - } - let gr = hand.toGroup() as NonNullable>; - gr.sort((g1, g2) => g1.compare(g2)); - for (let i = 0; i < 3; i++) { - let g1 = gr[i].getTiles(); - let g2 = gr[i+1].getTiles(); - if ( - g1[0].isEqual(g2[0].getFamily(), g2[0].getValue()) && - chii(gr[i]) && chii(gr[i+1]) - ) { - return 1; - } - } - return 0; -}, - -/** - * deux doubles suites pures - * 0/3 - */ -ryanpeikou: function ( - hand: Hand, - groups: Array, - wind: number -): number { - if (groups.length > 0) { - return 0; - } - let gr = hand.toGroup() as NonNullable>; - gr.filter(g => chii(g)); - if (gr.length < 4) { // pas assez de suite - return 0; - } - gr.sort((g1, g2) => g1.compare(g2)); - let t1 = gr[0].getTiles()[0]; - let t2 = gr[1].getTiles()[0]; - let t3 = gr[2].getTiles()[0]; - let t4 = gr[3].getTiles()[0]; - if ( - t1.compare(t2) === 0 && - t3.compare(t4) === 0 - ) { - return 3; - } - return 0; -}, - -/** - * tout suite - * 0/1 - */ -pinfu: function( - hand: Hand, - groups: Array, - wind: number -): number { //TODO: double attente - if (groups.length > 0) { - return 0; - } - let h = hand.toGroup(); - if ( - h !== undefined && - h.every( - g => { - let tiles = g.getTiles(); - return (chii(g) || tiles.length === 2) - } - ) - ) { - return 1; - } - return 0; -}, - -/** - * triple suite - * 1/2 - */ -sanshokuDoujun: function( - hand: Hand, - groups: Array, - wind: number -): number { - let h = hand.toGroup(); - let gr = []; - if (h !== undefined) { - gr = groups.concat(h); - } else { - gr = groups; - } - gr = gr.filter(g => chii(g)); - gr.sort((g1, g2) => g1.compare(g2)) - if (gr.length < 3) { // pas assez de chii - return 0; - } else if(gr.length === 3) { - let t0 = gr[0].getTiles(); - let t1 = gr[1].getTiles(); - let t2 = gr[2].getTiles(); - if ( - t0[0].getValue() === t1[0].getValue() && - t0[0].getValue() === t2[0].getValue() && - t0[0].getFamily() !== t1[0].getFamily() && - t0[0].getFamily() !== t2[0].getFamily() && - t1[0].getFamily() !== t2[0].getFamily() - ) { - return groups.length > 0 ? 1 : 2; - } - return 0; - } else {// il y a un intrus - for (let i = 0; i < 4; i++) { - let index = [] - for (let j = 0; j < 4; j++) { - if (j !== i) { - index.push(j); - } - } - let t0 = gr[index[0]].getTiles(); - let t1 = gr[index[1]].getTiles(); - let t2 = gr[index[2]].getTiles(); - if ( - t0[0].getValue() === t1[0].getValue() && - t0[0].getValue() === t2[0].getValue() && - t0[0].getFamily() !== t1[0].getFamily() && - t0[0].getFamily() !== t2[0].getFamily() && - t1[0].getFamily() !== t2[0].getFamily() - ) { - return groups.length > 0 ? 1 : 2; - } - } - return 0; - } -}, - -/** - * grande suite pure - * 1/2 - */ -ittsuu: function( - hand: Hand, - groups: Array, - wind: number -): number { - let gr = groups.concat(hand.toGroup() as NonNullable>); - gr = gr.filter(g => chii(g)); - gr.sort((g1, g2) => g1.compare(g2)); - if (gr.length < 3) { // trop peu de suite - return 0; - } else if (gr.length === 3) { // pile le bon nombre - let g1 = gr[0].getTiles(); - let g2 = gr[1].getTiles(); - let g3 = gr[2].getTiles(); - if ( - g1[0].getFamily() === g2[0].getFamily() && - g2[0].getFamily() === g3[0].getFamily() && - g1[0].getValue() === 1 && - g2[0].getValue() === 4 && - g3[0].getValue() === 7 - ) { - return groups.length > 0 ? 1 : 2; - } - } else { // il y a un intrus - for (let i = 0; i < 4; i++) { - let index = [] - for (let j = 0; j < 4; j++) { - if (j !== i) { - index.push(j); - } - } - let t0 = gr[index[0]].getTiles(); - let t1 = gr[index[1]].getTiles(); - let t2 = gr[index[2]].getTiles(); - if ( - t0[0].getValue() === 1 && - t1[0].getValue() === 4 && - t2[0].getValue() === 7 && - t0[0].getFamily() === t1[0].getFamily() && - t0[0].getFamily() === t2[0].getFamily() - ) { - return groups.length > 0 ? 1 : 2; - } - } - return 0; - } - return 0; -}, - -/** - * tout ordinaire - * 1/1 - */ -tanyao: function( - hand: Hand, - groups: Array, - wind: number -): number { - let h = hand.toGroup(); - if (h === undefined) { - return 0; - } - if ( - groups.every(g => ord(g)) && - h.every(g => ord(g)) - ) { - return 1; - } - return 0; -}, - -/** - * brelan de valeur - * 1/1 - */ -yakuhai: function( - hand: Hand, - groups: Array, - wind: number -): number { - let gr = groups.concat(hand.toGroup() as NonNullable>); - gr = gr.filter(g => pon(g) && g.getTiles().length === 3); - let han = 0; - gr.forEach( - g => { - let t = g.getTiles(); - let f = t[0].getFamily(); - let v = t[0].getValue(); - if (f === 5) { // brelan de dragon - han++; - } - if (f === 4 && v === 0) { // vent d'est - han++; - } - if (f === 4 && v === wind) { // vent du joueur - han++; - } - } - ); - return han; -}, - -/** - * trois petits dragons - * 2/2 - */ -shousangen: function( - hand: Hand, - groups: Array, - wind: number -): number { - let gr = groups.concat(hand.toGroup() as NonNullable>); - gr = gr.filter(g => pon(g)); - let nbPon = 0; - let nbPair = 0; - gr.forEach( - g => { - let t = g.getTiles(); - if (t[0].getFamily() === 5) { - if (t.length === 3) { - nbPon++; - } else { - nbPair++; - } - } - } - ) - if (nbPon == 2 && nbPair == 1) { - return 2; - } - return 0; -}, - -/** - * trois grands dragons - * 13/13 - */ -daisangen: function( - hand: Hand, - groups: Array, - wind: number -): number { - let gr = groups.concat(hand.toGroup() as NonNullable>); - gr = gr.filter( - g => pon(g) && - g.getTiles().length === 3 && - g.getTiles()[0].getFamily() === 5 - ); - if (gr.length === 3) { - return 13; - } - return 0; -}, - -/** - * quatre petits vents - * 13/13 - */ -shousuushii: function( - hand: Hand, - groups: Array, - wind: number -): number { - let gr = groups.concat(hand.toGroup() as NonNullable>); - gr = gr.filter(g => pon(g)); - let nbPon = 0; - let nbPair = 0; - gr.forEach( - g => { - let t = g.getTiles(); - if (t[0].getFamily() === 4) { - if (t.length === 3) { - nbPon++; - } else { - nbPair++; - } - } - } - ) - if (nbPon == 3 && nbPair == 1) { - return 13; - } - return 0; -}, - -/** - * quatre grands vents - * 13/13 - */ -daisuushi: function( - hand: Hand, - groups: Array, - wind: number -): number { - let gr = groups.concat(hand.toGroup() as NonNullable>); - gr = gr.filter(g => pon(g)); - let nbPon = 0; - let nbPair = 0; - gr.forEach( - g => { - let t = g.getTiles(); - if (t[0].getFamily() === 4) { - if (t.length === 3) { - nbPon++; - } else { - nbPair++; - } - } - } - ) - if (nbPon == 4 && nbPair == 0) { - return 13; - } - return 0; -}, - -/** - * terminales et honneurs partout - * 1/2 - */ -chanta: function( - hand: Hand, - groups: Array, - wind: number -): number { - let gr = groups.concat(hand.toGroup() as NonNullable>); - gr = gr.filter( - g => { - let tiles = g.getTiles(); - let f = tiles[0].getFamily(); - let v = tiles[0].getValue(); - let vd = tiles[tiles.length - 1].getValue(); - return f < 4 && v !== 1 && vd !== 9 - } - ); - if (gr.length > 0) { - return 0; - } - return groups.length > 0 ? 1 : 2; -}, - -/** - * terminales partout - * 2/3 - */ -junchan: function( - hand: Hand, - groups: Array, - wind: number -): number { - let gr = groups.concat(hand.toGroup() as NonNullable>); - gr = gr.filter( - g => { - let tiles = g.getTiles(); - let f = tiles[0].getFamily(); - let v = tiles[0].getValue(); - let vd = tiles[tiles.length - 1].getValue(); - return f > 3 || (v !== 1 && vd !== 9) - } - ); - if (gr.length > 0) { - return 0; - } - return groups.length > 0 ? 2 : 3; -}, - -/** - * tout terminale et honneur - * 2/2 - */ -honroutou: function( - hand: Hand, - groups: Array, - wind: number -): number { - let gr = groups.concat(hand.toGroup() as NonNullable>); - gr = gr.filter( - g => { - let tiles = g.getTiles(); - let f = tiles[0].getFamily(); - let v = tiles[1].getValue(); - return f < 4 && v !== 1 && v !== 9 - } - ); - if (gr.length > 0) { - return 0; - } - return 2; -}, - -/** - * tout terminale - * 13/13 - */ -chinroutou: function( - hand: Hand, - groups: Array, - wind: number -): number { - let gr = groups.concat(hand.toGroup() as NonNullable>); - if ( - gr.every(g => term(g)) - ) { - return 13; - } - return 0; -}, - -/** - * tout honneur - * 13/13 - */ -tsuuiisou: function( - hand: Hand, - groups: Array, - wind: number -): number { - let h = hand.toGroup(); - if (h === undefined) { - return 0; - } - if ( - groups.every(g => honn(g)) && - h.every(g => honn(g)) - ) { - return 13; - } - return 0; -}, - -/** - * treize orphelins - * 0/13 - */ -kokushiMusou: function( - hand: Hand, - groups: Array, - wind: number -): number { - if (groups.length > 0) { - return 0; - } - let h = hand.getTiles(); - if ( - h.some(t => - t.getFamily() < 4 && - t.getValue() > 1 && - t.getValue() < 9 - ) - ) { - return 0; - } - let count = 0; - for (let i = 0; i < h.length - 1; i++) { - if (h[i].isEqual(h[i+1].getFamily(), h[i+1].getValue())) { - count++; - } - if (count > 1) { - break; - } - } - if (count === 1) { - return 13; - } - return 0; -}, - -/** - * sept paires - * 0/2 - */ -chiitoitsu: function( - hand: Hand, - groups: Array, - wind: number -): number { - if (groups.length > 0) { - return 0; - } - let h = hand.getTiles(); - if (h.length !== 14) { - return 0; - } - for (let i = 0; i < 14; i += 2) { - if ( - h[i].compare(h[i+1]) !== 0 || - (i > 0 && h[i].compare(h[i-1]) === 0) - ) { - return 0 - } - } - return 2; -}, - -/** - * tout brelans - * 2/2 - */ -toitoi: function( - hand: Hand, - groups: Array, - wind: number -): number { - let h = hand.toGroup(); - if ( - groups.every(g => pon(g)) && - h?.every(g => pon(g)) - ) { - return 2; - } - return 0; -}, - -/** - * trois brelan cachés - * 2/2 - */ -sanankou: function( - hand: Hand, - groups: Array, - wind: number -): number { - let h = hand.toGroup(); - let count = 0; - h?.forEach( - g => { - if (pon(g) && g.getTiles().length === 3) { - count++; - } - } - ); - if (count === 3) { - return 2; - } - return 0; -}, - -/** - * quatre brelans cachés - * 0/13 - */ -suuankou: function( - hand: Hand, - groups: Array, - wind: number -): number { - let h = hand.toGroup(); - let count = 0; - h?.forEach( - g => { - if (pon(g) && g.getTiles().length === 3) { - count++; - } - } - ); - if (count === 4) { - return 13; - } - return 0; -}, - -/** - * triple brelan - * 2/2 - */ -sanshokuDoukou: function( - hand: Hand, - groups: Array, - wind: number -): number { - let h = hand.toGroup(); - let gr = []; - if (h !== undefined) { - gr = groups.concat(h); - } else { - gr = groups; - } - gr = gr.filter(g => pon(g) && g.getTiles().length === 3); - gr.sort((g1, g2) => g1.compare(g2)) - if (gr.length < 3) { // pas assez de chii - return 0; - } else if(gr.length === 3) { - let t0 = gr[0].getTiles(); - let t1 = gr[1].getTiles(); - let t2 = gr[2].getTiles(); - if ( - t0[0].getValue() === t1[0].getValue() && - t0[0].getValue() === t2[0].getValue() && - t0[0].getFamily() !== t1[0].getFamily() && - t0[0].getFamily() !== t2[0].getFamily() && - t1[0].getFamily() !== t2[0].getFamily() - ) { - return groups.length > 0 ? 1 : 2; - } - return 0; - } else {// il y a un intrus - for (let i = 0; i < 4; i++) { - let index = [] - for (let j = 0; j < 4; j++) { - if (j !== i) { - index.push(j); - } - } - let t0 = gr[index[0]].getTiles(); - let t1 = gr[index[1]].getTiles(); - let t2 = gr[index[2]].getTiles(); - if ( - t0[0].getValue() === t1[0].getValue() && - t0[0].getValue() === t2[0].getValue() && - t0[0].getFamily() !== t1[0].getFamily() && - t0[0].getFamily() !== t2[0].getFamily() && - t1[0].getFamily() !== t2[0].getFamily() - ) { - return groups.length > 0 ? 1 : 2; - } - } - return 0; - } -}, - -/** - * trois carrés - * 2/2 - */ -sankantsu: function( //TODO - hand: Hand, - groups: Array, - wind: number -): number { - return 0; -}, - -/** - * quatre carrés - * 13/13 - */ -suukantsu: function( //TODO - hand: Hand, - groups: Array, - wind: number -): number { - return 0; -}, - -/** - * semie pure - * 2/3 - */ -honitsu: function( - hand: Hand, - groups: Array, - wind: number -): number { - let h = hand.toGroup(); - let gr = []; - if (h !== undefined) { - gr = groups.concat(h); - } else { - gr = groups; - } - gr.sort((g1, g2) => g1.compare(g2)) - if (gr.length === 0) { - return 0; - } - if (gr[gr.length - 1].getTiles()[0].getFamily() < 4) { // main pure - return 0; - } - let f = gr[0].getTiles()[0].getFamily(); - if (f > 3) { // tout honneur - return 0; - } - if (gr.every( - g => { - let ff = g.getTiles()[0].getFamily(); - return ff > 3 || f === ff - } - )) { - return groups.length > 0 ? 2 : 3; - } - return 0; -}, - -/** - * main pure - * 5/6 - */ -chinitsu: function( - hand: Hand, - groups: Array, - wind: number -): number { - let h = hand.getTiles(); - let t0 = h[0]; - if ( - h.every(t => t.getFamily() === t0.getFamily()) && - groups.every(g => g.getTiles().every(t => t.getFamily() === t0.getFamily())) - ) { - return groups.length > 0 ? 5 : 6; - } - return 0; -}, - -/** - * main verte - * 13/13 - */ -ryuuisou: function( - hand: Hand, - groups: Array, - wind: number -): number { - if (groups.length > 0) { - return 0; - } - let h = hand.toGroup(); - if (h?.every( - g => g.getTiles().every( - t => green(t) - ) - )) { - return 13; - } - return 0; -}, - -/** - * neuf portes - * 0/13 - */ -chuurenPoutou: function( - hand: Hand, - groups: Array, - wind: number -): number { - if (groups.length > 0 || yakus.chinitsu(hand, groups, wind) === 0) { - return 0; - } - let tiles = hand.getTiles(); - if (tiles[0].getFamily() >= 4) { - return 0; - } - - let pureHand = [tiles[0].getValue()]; - let count = 1; - for (let i = 1; i < tiles.length; i++) { - let v = tiles[i].getValue(); - let lastv = pureHand[pureHand.length - 1]; - - if (v === 1 || v === 9) { - if (v === lastv) { - count++; - if (count < 4) { - pureHand.push(v); - } else if (count > 4) { - return 0; - } - } else { - count = 1; - pureHand.push(v); - } - - } else { - if (v === lastv) { - count ++; - if (count < 2) { - pureHand.push(v); - } else if (count > 2) { - return 0; - } - } else { - count = 1; - pureHand.push(v); - } - } - } - - if (pureHand.toString() === [1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 9].toString()) { - return 13; - } - return 0; -} - -} diff --git a/tests/assert.ts b/tests/assert.ts deleted file mode 100644 index 57ca753..0000000 --- a/tests/assert.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function assert(b: boolean, msg: string): number { - if (b) { - console.log("%c[SUCCES] " + msg, "color: green"); - return 1; - } else { - console.log("%c[ECHEC] " + msg, "color: red"); - return 0; - } -} diff --git a/tests/test.ts b/tests/test.ts deleted file mode 100644 index 2591aae..0000000 --- a/tests/test.ts +++ /dev/null @@ -1,2 +0,0 @@ -import "./test_hand" -import "./test_yakus" diff --git a/tests/test_hand.ts b/tests/test_hand.ts deleted file mode 100644 index 667c55b..0000000 --- a/tests/test_hand.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Hand } from "../src/hand" -import { assert } from "./assert"; - -let h0 = new Hand("s1s1"); -let h1 = new Hand("m1m2m3"); -let h2 = new Hand("m2m2m2 p1p1"); -let h3 = new Hand("m1m2m3 p1p2p3 s1s2s3 m9m9m9 p9p9"); -let h4 = new Hand("m2m2m2 p1p1p1"); - -let count = 0; -let total = 0; - -count += assert(h0.toGroup()?.length === 1, "s11 has 1 group"); -count += assert(h1.toGroup()?.length === 1, "m123 has 1 group"); -count += assert(h2.toGroup()?.length === 2, "m222 p11 has 2 groups"); -count == assert(h3.toGroup()?.length === 5, "m123 p123 s123 m999 p99 has 5 groups"); -count += assert(h4.toGroup()?.length === 2, "m222 p111 has 2 groups"); -total += 4; - -console.log("Succès: " + count.toString() + "/" + total.toString()); diff --git a/tests/test_yakus.ts b/tests/test_yakus.ts deleted file mode 100644 index 23ae5f4..0000000 --- a/tests/test_yakus.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { assert } from "./assert" -import { yakus } from "../src/yakus/yaku" -import { Hand } from "../src/hand" -import { Group } from "../src/group"; -import { Tile } from "../src/tile"; - -let count = 0; -let total = 0; - -let h1 = new Hand("m1m2m3 m4m5m6 m7m8m9 m1m2m3 m5m5"); -let h2 = new Hand("m1m1m1 m4m4m4 m7m7m7 p1p1p1 p5p5"); -let h3 = new Hand("m1m1m1 p9p9p9 s1s1s1 w2w2w2 d1d1"); -let h4 = new Hand("m2m3m4 p3p3p3 p4p5p6 s4s4 s6s7s8"); -let h5 = new Hand("m1m2m3 m1m2m3 p7p8p9 p7p8p9 w3w3"); -let h6 = new Hand("m1m2m3 m1m2m3 p6p7p8 p7p8p9 w3w3"); -let h7 = new Hand("m1m2m3 p1p2p3 s1s2s3 m7m8m9 p9p9"); -let h8 = new Hand("m1m2m3 m1m2m3 p6p6 w0w0w0 w3w3w3"); -let h9 = new Hand("m1m2m3 m1m2m3 d1d1d1 d2d2d2 d3d3"); -let h10 = new Hand("m1m2m3 p6p6 d1d1d1 d2d2d2 d3d3d3"); -let h11 = new Hand("m1m2m3 w1w1w1 w2w2w2 w3w3w3 w4w4"); -let h12 = new Hand("m1m1 w1w1w1 w2w2w2 w3w3w3 w4w4w4"); -let h13 = new Hand("d1d1 w1w1w1 w2w2w2 w3w3w3 w4w4w4"); -let h14 = new Hand("m1m1 m9m9m9 p1p1p1 p9p9p9 s1s1s1"); -let h15 = new Hand("m1m9m9 p1p9 s1s9 w1w2w3w4 d1d2d3"); -let h16 = new Hand("m1m1 m2m2 m3m3 m4m4 m5m5 m6m6 m7m7"); -let h17 = new Hand("m1m1 m2m2 m3m3 m4m4 m5m5 m6m6 m6m6"); -let h18 = new Hand("m1m2m3 m4m4m4 m7m7m7 p1p1p1 p5p5"); -let h19 = new Hand("m1m1m1 p1p1p1 s1s1s1 s1s2s3 p5p5"); -let h20 = new Hand("m1m1m1 p1p1p1 s1s1 s1s2s3 p5p5p5"); -let h21 = new Hand("m1m1m1 m2m3m4 m5m6m7 m8m8 m9m9m9"); -let h22 = new Hand("s2s2s2 s8s8s8 s4s4s4 s6s6 d2d2d2"); -let h23 = new Hand("m1m1m1 m2m3m4 m5m6m7 m8m8m8 m9m9m9"); -let h24 = new Hand("m1m1m1 m2m3m4 m6m6 m5m6m7 m9m9m9"); - -// lipeikou -count += assert(yakus.lipekou(h5, [], 0) === 1, "m123 m123 p789 p789 w33 is Lipeikou"); -count += assert(yakus.lipekou(h3, [], 0) === 0, "m111 p999 s111 w222 d11 is not Lipeikou"); -total += 2; - -// ryanpeikou -count += assert(yakus.ryanpeikou(h5, [], 0) === 3, "m123 m123 p789 p789 w33 is Ryanpeikou"); -count += assert(yakus.ryanpeikou(h6, [], 0) === 0, "m1123 m123 p678 p789 w33 is not Ryanpeikou"); -total += 2; - -// pinfu -count += assert(yakus.pinfu(h1, [], 0) === 1, "m123 m456 m789 m123 p55 is Pinfu"); -count += assert(yakus.pinfu(h2, [], 0) === 0, "m111 m444 m777 p111 p55 is not Pinfu"); -total += 2; - -// sanshoku doujun -count += assert(yakus.sanshokuDoujun(h7, [], 0) === 2, "m123 p123 s123 m789 p99 is Shanshokou Doujun"); -count += assert(yakus.sanshokuDoujun(h2, [], 0) === 0, "m111 m444 m777 p111 p55 is not Shanshokou Doujun"); -total += 2; - -// ittsuu -count += assert(yakus.ittsuu(h1, [], 0) === 2, "m123 m456 m789 m123 m55 is Ittsuu"); -count += assert(yakus.ittsuu(h2, [], 0) === 0, "m111 m444 m777 p111 p55 is not Ittsu"); -total += 2; - -// tanyao -count += assert(yakus.tanyao(h4, [], 0) === 1, "m234 p333 p456 s44 s678 is Tanyao"); -count += assert(yakus.tanyao(h1, [], 0) === 0, "m123 m456 m789 m123 p55 is not Tanyao"); -total += 2; - -// yakuhai -count += assert(yakus.yakuhai(h3, [], 2) === 1, "m111 p999 s111 w222 d11 West is Yakuhai"); -count += assert(yakus.yakuhai(h8, [], 3) === 2, "m123 m123 p66 w000 w333 North is double Yakuhai"); -count += assert(yakus.yakuhai(h3, [], 0) === 0, "m111 p999 s111 w222 d11 Est is not Yakuhai"); -total += 3; - -// shousangen -count += assert(yakus.shousangen(h9, [], 0) === 2, "m123 m123 d111 d222 d33 is Shousangen"); -count += assert(yakus.shousangen(h10, [], 0) === 0, "m123 p66 d111 d222 d333 is not Shousangen"); -count += assert(yakus.shousangen(h3, [], 0) === 0, "m111 p999 s111 w222 d11 is not Shousangen"); -total += 3; - -// daisangen -count += assert(yakus.daisangen(h10, [], 0) === 13, "m123 p66 d111 d222 d333 is Daisangen"); -count += assert(yakus.daisangen(h9, [], 0) === 0, "m123 m123 d111 d222 d33 is not Daisangen"); -count += assert(yakus.daisangen(h3, [], 0) === 0, "m111 p999 s111 w222 d11 is not Daisangen"); -total += 3; - -// shousuushi -count += assert(yakus.shousuushii(h11, [], 0) === 13, "m123 w111 w222 w333 w44 is Shousuushi"); -count += assert(yakus.shousuushii(h12, [], 0) === 0, "m11 w111 w222 w333 w444 is not Shousuushi"); -count += assert(yakus.shousuushii(h3, [], 0) === 0, "m111 p999 s111 w222 d11 is not Shousuushi"); -total += 3; - -// daisuushi -count += assert(yakus.daisuushi(h12, [], 0) === 13, "m11 w111 w222 w333 w444 is Daisuushi"); -count += assert(yakus.daisuushi(h11, [], 0) === 0, "m123 w111 w222 w333 w44 is not Daisuushi"); -count += assert(yakus.daisuushi(h3, [], 0) === 0, "m111 p999 s111 w222 d11 is not Daisuushi"); -total += 3; - -// chanta -count += assert(yakus.chanta(h13, [], 0) === 2, "d11 w111 w222 w333 w444 is Chanta"); -count += assert(yakus.chanta(h12, [], 0) === 2, "m11 w111 w222 w333 w444 is Chanta"); -count += assert(yakus.chanta(h10, [], 0) === 0, "m123 p66 d111 d222 d333 is not Chanta"); -total += 3; - -// junchan -count += assert(yakus.junchan(h7, [], 0) === 3, "m123 p123 s123 m789 p99 is Junchan"); -count += assert(yakus.junchan(h10, [], 0) === 0, "m123 p66 d111 d222 d333 is not Junchan"); -total += 2; - -// honroutou -count += assert(yakus.honroutou(h14, [], 0) === 2, "m11 m999 p111 p999 s111 is Honroutou"); -count += assert(yakus.honroutou(h3, [], 0) === 2, "m111 p999 s111 w222 d11 is Honroutou"); -count += assert(yakus.honroutou(h10, [], 0) === 0, "m123 p66 d111 d222 d333 is not Honroutou"); -total += 3; - -// chinroutou -count += assert(yakus.chinroutou(h14, [], 0) === 13, "m11 m999 p111 p999 s111 is Chinroutou"); -count += assert(yakus.chinroutou(h3, [], 0) === 0, "m111 p999 s111 w222 d11 is not Chinroutou"); -count += assert(yakus.chinroutou(h10, [], 0) === 0, "m123 p66 d111 d222 d333 is not Chinroutou"); -total += 3; - -// tsuuisou -count += assert(yakus.tsuuiisou(h13, [], 0) === 13, "d11 w111 w222 w333 w444 is Tsuuisou"); -count += assert(yakus.tsuuiisou(h3, [], 0) === 0, "m111 p999 s111 w222 d11 is not Tsuuisou"); -total += 2; - -// kokushiMusou -count += assert(yakus.kokushiMusou(h15, [], 0) === 13, "m199 p19 s19 w1234 d123 is Kokushi Musou"); -count += assert(yakus.kokushiMusou(h3, [], 0) === 0, "m111 p999 s111 w222 d11 is not Kokushi Musou"); -total += 2; - -// chiitoitsu -count += assert(yakus.chiitoitsu(h16, [], 0) === 2, "m11 m22 m33 m44 m55 m66 m77 is Chiitoitsu"); -count += assert(yakus.chiitoitsu(h17, [], 0) === 0, "m11 m22 m33 m44 m55 m66 m66 is not Chiitoitsu"); -count += assert(yakus.chiitoitsu(h3, [], 0) === 0, "m111 p999 s111 w222 d11 is not Chiitoitsu"); -total += 3; - -// toitoi -count += assert(yakus.toitoi(h2, [], 0) === 2, "m111 m444 m777 p111 p55 is Toitoi"); -count += assert(yakus.toitoi(h7, [], 0) === 0, "m123 p123 s123 m789 p99 is not Toitoi"); -total += 2; - -// sanankou -count += assert(yakus.sanankou(h18, [], 0) === 2, "m123 m444 m777 p111 p55 is Sanankou"); -count += assert(yakus.sanankou(h2, [], 0) === 0, "m111 m444 m777 p111 p55 is not Sanankou"); -count += assert(yakus.sanankou(h7, [], 0) === 0, "m123 p123 s123 m789 p99 is not Sanankou"); -total += 3; - -// sanankou -count += assert(yakus.suuankou(h2, [], 0) === 13, "m111 m444 m777 p111 p55 is Sanankou"); -count += assert(yakus.suuankou(h18, [], 0) === 0, "m123 m444 m777 p111 p55 is not Sanankou"); -count += assert(yakus.suuankou(h7, [], 0) === 0, "m123 p123 s123 m789 p99 is not Sanankou"); -total += 3; - -// sanshoku doukou -count += assert(yakus.sanshokuDoukou(h19, [], 0) === 2, "m111 p111 s111 s123 p55 is Shanshoku Doukou"); -count += assert(yakus.sanshokuDoukou(h20, [], 0) === 0, "m11 p111 s111 s123 p555 is not Shanshoku Doukou"); -count += assert(yakus.sanshokuDoukou(h7, [], 0) === 0, "m123 p123 s123 m789 p99 is not Shanshoku Doukou"); -total += 3; - -// sankantsu -// count += assert( -// yakus.sankantsu( -// new Hand(), -// [ -// new Group([new Tile(1, 1, false), new Tile(1, 1, false), new Tile(1, 1, false), new Tile(1, 1, false)], 0, 0), -// new Group([new Tile(1, 3, false), new Tile(1, 3, false), new Tile(1, 3, false), new Tile(1, 3, false)], 0, 0), -// new Group([new Tile(1, 5, false), new Tile(1, 5, false), new Tile(1, 5, false), new Tile(1, 5, false)], 0, 0), -// ], -// 0 -// ) === 2, -// " is Sankantsu"); -count += assert(false, "sankantsu not implemented"); -total += 1; - -// suukantsu -count += assert(false, "suukantsu not implemented"); -total += 1; - -// honitsu -count += assert(yakus.honitsu(h9, [], 0) === 3, "m123 m123 d111 d222 d33 is Honitsu"); -count += assert(yakus.honitsu(h13, [], 0) === 0, "d11 w111 w222 w333 w444 is not Honitsu"); -count += assert(yakus.honitsu(h1, [], 0) === 0, "m123 m456 m789 m123 m55 is not Honitsu"); -count += assert(yakus.honitsu(h7, [], 0) === 0, "m123 p123 s123 m789 p99 is not Honitsu"); -total += 4; - -// chinitsu -count += assert(yakus.chinitsu(h1, [], 0) === 6, "m123 m456 m789 m123 m55 is Chinitsu"); -count += assert(yakus.chinitsu(h9, [], 0) === 0, "m123 m123 d111 d222 d33 is not Chinitsu"); -count += assert(yakus.chinitsu(h13, [], 0) === 0, "d11 w111 w222 w333 w444 is not Chinitsu"); -count += assert(yakus.chinitsu(h7, [], 0) === 0, "m123 p123 s123 m789 p99 is not Chinitsu"); -total += 4; - -// ryuuisou -count += assert(yakus.ryuuisou(h22, [], 0) === 13, "s222 s888 s444 s66 d222 is Ryuuisou"); -count += assert(yakus.ryuuisou(h1, [], 0) === 0, "m123 m456 m789 m123 m55 is not Ryuuisou"); -total += 2; - -// chuuren poutou -count += assert(yakus.chuurenPoutou(h21, [], 0) === 13, "m111 m234 m567 m88 m999 is Chuuren Poutou"); -count += assert(yakus.chuurenPoutou(h23, [], 0) === 0, "m111 m234 m567 m888 m999 is not Chuuren Poutou"); -count += assert(yakus.chuurenPoutou(h24, [], 0) === 0, "m111 m234 m66 m567 m999 is not Chuuren Poutou"); -count += assert(yakus.chuurenPoutou(h1, [], 0) === 0, "m123 m456 m789 m123 m55 is not Chuuren Poutou"); -total += 4; - -// total -console.log("Succès: " + count.toString() + "/" + total.toString());