Compare commits

...

22 commits

Author SHA1 Message Date
Didictateur
2b7547ac35 can now display pon correctly !!! 2026-03-25 10:31:07 +01:00
Didictateur
86e0bbd464 tilineau work in progress 2026-03-22 13:59:04 +01:00
Didictateur
67f3973913 call pon animation 2026-03-14 14:20:34 +01:00
Didictateur
980cc7bbd3 bot calling pon 2026-03-14 13:48:50 +01:00
Didictateur
d16ec925ea changed buttons positions 2026-03-14 13:46:10 +01:00
Didictateur
f50c5f7bf2 button position 2026-03-14 13:35:39 +01:00
Didictateur
bf3f3a2879 fixed spam click 2026-03-14 12:06:24 +01:00
Didictateur
8d04e99a85 distribution animation 2026-03-14 11:18:29 +01:00
Didictateur
662ee8fd10 start button and improved feeling 2026-03-14 10:56:22 +01:00
Didictateur
169aa0e72b turn indicator 2026-03-14 10:40:49 +01:00
Didictateur
b28046d935 discard in the right place 2026-03-14 10:21:16 +01:00
Didictateur
4dabff91d3 the discards stopped moving 2026-03-13 23:00:23 +01:00
Didictateur
39c1cc7d69 can technically discard tiles 2026-03-13 22:55:41 +01:00
Didictateur
655e1e5c1c new logic for Game 2026-03-13 22:38:21 +01:00
Didictateur
f205ccf70e initiate a game 2026-03-12 22:28:02 +01:00
Didictateur
4bdf3597c4 fixed tile comparaison and display draw tile appart 2026-02-28 23:21:11 +01:00
Didictateur
3e0c6e3abe hidden tiles 2026-02-28 23:09:11 +01:00
Didictateur
b131301495 draw hands 2026-02-28 23:08:13 +01:00
Didictateur
4f1d603d2e removed game implementation for a new start 2026-02-26 22:40:07 +01:00
Didictateur
52570f80b1 everyone can play ! 2026-02-26 22:22:01 +01:00
Didictateur
0c15a9e3cb removed backend 2026-02-26 21:12:56 +01:00
Didictateur
d41b6349d1 chap 1 2026-02-26 20:38:36 +01:00
24 changed files with 1618 additions and 1155 deletions

View file

@ -1,26 +1,2 @@
FROM python:3.11-slim
WORKDIR /app
# Installer les dépendances Python
COPY backend/src/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copier le code backend
COPY backend/src/ /app/
# Copier le frontend
COPY frontend /app/frontend
COPY components /app/components
COPY img /app/img
COPY index.html /app/
COPY *.json /app/
COPY vite.config.ts /app/
# Port Flask
EXPOSE 5000
# Définir PYTHONPATH pour que les imports relatifs fonctionnent
ENV PYTHONPATH=/app:$PYTHONPATH
CMD ["gunicorn", "-w", "1", "-k", "gevent", "-b", "0.0.0.0:5000", "api:app"]
FROM nginx:alpine
# Ce Dockerfile n'est plus utilisé, le service est servi par nginx via docker-compose.

View file

@ -1,221 +0,0 @@
from flask import Flask, jsonify, request
from flask_cors import CORS
from game.tile import *
from game.hand import *
from game.game import *
from game.error import *
app = Flask(__name__)
import threading
CORS(app)
tiles = Tile.generateAll()
current_game = None # Stockage de la partie actuelle
game_lock = threading.Lock()
# ============= GET ================
@app.route('/api/tiles', methods=['GET'])
def get_all_tiles():
"""Récupère toutes les tuiles"""
tiles_data = [
{
'id': tile.id,
'family': str(tile.family),
'value': str(tile.value),
'svg': tile.get_img()
}
for tile in tiles
]
return jsonify({'tiles': tiles_data})
@app.route('/api/tiles/<int:tile_id>', methods=['GET'])
def get_tile(tile_id):
if tile_id < 0 or tile_id >= len(tiles):
return jsonify({'error': 'Tuile non trouvée'}), 404
tile = tiles[tile_id]
svg = f'<svg viewBox="0 0 500 700" width="100" height="140">{tile.get_img()}</svg>'
return jsonify({
'id': tile.id,
'family': str(tile.family),
'value': str(tile.value),
'svg': svg
})
@app.route('/api/random_hand', methods=['GET'])
def get_random_hand():
"""Récupère une main aléatoire de 13 tuiles"""
hand = Hand.random()
hand_data = [
{
'id': tile.id,
'family': str(tile.family),
'value': str(tile.value),
'svg': tile.get_img()
}
for tile in hand.tiles
]
return jsonify({'hand': hand_data})
@app.route('/api/hand_with_draw', methods=['GET'])
def get_hand_with_draw():
"""Crée une partie et retourne le hand_player avec une tuile piochée"""
global current_game
with game_lock:
current_game = Game()
current_game.draw_tile(current_game.hand_player)
hand_data = [
{
'id': tile.id,
'family': str(tile.family),
'value': str(tile.value),
'svg': tile.get_img()
}
for tile in current_game.hand_player.tiles
]
draw_data = {
'id': current_game.hand_player.draw.id,
'family': str(current_game.hand_player.draw.family),
'value': str(current_game.hand_player.draw.value),
'svg': current_game.hand_player.draw.get_img()
} if getattr(current_game.hand_player, 'draw', None) else None
return jsonify({'hand': hand_data, 'draw': draw_data})
@app.route('/api/discard', methods=['POST'])
def discard_tile():
"""Défausse une tuile de la main du joueur et pioche une nouvelle"""
global current_game
with game_lock:
if current_game is None:
return jsonify({'error': 'Aucune partie en cours'}), 400
data = request.json
tile_id = data.get('tile_id')
if tile_id is None:
return jsonify({'error': 'tile_id manquant'}), 400
current_game.put_tile(current_game.hand_player, tile_id)
current_game.draw_tile(current_game.hand_player)
hand_data = [
{
'id': tile.id,
'family': str(tile.family),
'value': str(tile.value),
'svg': tile.get_img()
}
for tile in current_game.hand_player.tiles
]
draw_data = {
'id': current_game.hand_player.draw.id,
'family': str(current_game.hand_player.draw.family),
'value': str(current_game.hand_player.draw.value),
'svg': current_game.hand_player.draw.get_img()
} if getattr(current_game.hand_player, 'draw', None) else None
return jsonify({'hand': hand_data, 'draw': draw_data})
# ============== POST ================
# ============== GAME MANAGEMENT ================
# Démarrer une nouvelle partie
@app.route('/api/game/start', methods=['POST'])
def start_game():
global current_game
with game_lock:
current_game = Game()
return jsonify({'status': 'started', 'state': current_game.get_state()})
# Récupérer l'état de la partie
@app.route('/api/game/state', methods=['GET'])
def game_state():
global current_game
with game_lock:
print(f"[API] /api/game/state called. current_game is {'None' if current_game is None else 'OK'}")
if current_game is None:
return jsonify({'error': 'Aucune partie en cours'}), 400
return jsonify({'state': current_game.get_state()})
# Passer au tour suivant
@app.route('/api/game/next_turn', methods=['POST'])
def game_next_turn():
global current_game
with game_lock:
if current_game is None:
return jsonify({'error': 'Aucune partie en cours'}), 400
current_game.next_turn()
return jsonify({'state': current_game.get_state()})
# Déclarer Riichi
@app.route('/api/game/riichi', methods=['POST'])
def game_declare_riichi():
global current_game
with game_lock:
if current_game is None:
return jsonify({'error': 'Aucune partie en cours'}), 400
data = request.json
player_id = data.get('player_id')
if player_id is None:
return jsonify({'error': 'player_id manquant'}), 400
current_game.declare_riichi(player_id)
return jsonify({'state': current_game.get_state()})
# Récupérer la main d'un joueur
@app.route('/api/game/hand/<int:player_id>', methods=['GET'])
def game_get_hand(player_id):
global current_game
with game_lock:
if current_game is None:
return jsonify({'error': 'Aucune partie en cours'}), 400
hand = current_game.get_hand(player_id)
return jsonify({'hand': hand})
# Récupérer les défausses d'un joueur
@app.route('/api/game/discards/<int:player_id>', methods=['GET'])
def game_get_discards(player_id):
global current_game
with game_lock:
if current_game is None:
return jsonify({'error': 'Aucune partie en cours'}), 400
discards = current_game.get_discards(player_id)
return jsonify({'discards': discards})
# Récupérer les actions possibles pour un joueur
@app.route('/api/game/actions/<int:player_id>', methods=['GET'])
def game_get_actions(player_id):
global current_game
with game_lock:
if current_game is None:
return jsonify({'error': 'Aucune partie en cours'}), 400
actions = current_game.get_possible_actions(player_id)
return jsonify({'actions': actions})
@app.route('/api/play_draw', methods=['POST'])
def play_draw_api():
global current_game
with game_lock:
print('[API] /api/play_draw called')
if current_game is None:
return jsonify({'error': 'Aucune partie en cours'}), 400
current_game.play_draw()
return jsonify({'state': current_game.get_state()})
@app.route('/api/discard_tile', methods=['POST'])
def api_discard_tile():
global current_game
with game_lock:
if current_game is None:
return jsonify({'error': 'Aucune partie en cours'}), 400
data = request.json
tile_id = data.get('tile_id')
if tile_id is None:
return jsonify({'error': 'tile_id manquant'}), 400
discarded = current_game.discard_tile(tile_id)
if not discarded:
return jsonify({'error': 'Tuile non trouvée ou non défaussée'}), 400
return jsonify({'state': current_game.get_state(), 'discarded': {
'id': discarded.id,
'family': str(discarded.family),
'value': str(discarded.value),
'svg': discarded.get_img()
}})
if __name__ == '__main__':
app.run(debug=True, port=5000)

View file

@ -1,15 +0,0 @@
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

View file

@ -1,210 +0,0 @@
from __future__ import annotations
from .hand import *
from .wall import *
import random as rd
class Game:
def __init__(self, wind=0 , yakus=False, red=False):
self.wall = Wall()
self.hand_player = self.wall.draw_hand()
self.discard_player = []
self.hidden_discard_player = []
self.hands_bots = [self.wall.draw_hand() for _ in range(3)]
self.discards_bots = [[] for _ in range(3)]
self.hidden_discards_bots = [[] for _ in range(3)]
self.has_draw = False
self.has_played = True
self.scores = [25000] * 4
self.winds = [
wind % 4, # player
(wind+1) % 4, # right of player
(wind + 2) % 4, # front of player
(wind +3) % 4 # left of player
]
self.initial_est = (-wind) % 4
self.turn = self.winds.index(0)
self.riichi = [False] * 4
self.repeat = 0
self.end = False
# L'Est pioche une tuile dès le début
if self.turn == 0:
tile = self.wall.draw()
self.hand_player.draw_tile(tile)
self.has_draw = True
else:
tile = self.wall.draw()
self.hands_bots[self.turn-1].draw_tile(tile)
def new_turn(self, repeat=False):
"""Nouvelle manche"""
if repeat:
self.repeat += 1
else:
self.repeat = 0
self.turn = (self.turn + 1) % 4
if self.turn == self.initial_est:
self.end = True
for i in range(4):
self.winds[self.turn + i] = i
self.turn = self.winds.index(0)
self.riichi = [False] * 4
self.repeat = 0
self.wall = Wall()
self.hand_player = self.wall.draw_hand()
self.discard_player = []
self.hidden_discard_player = []
self.hands_bots = [self.wall.draw_hand() for _ in range(3)]
self.discards_bots = [[] for _ in range(3)]
self.hidden_discards_bots = [[] for _ in range(3)]
def play_draw(self):
print(f"[Game] play_draw called: has_draw={self.has_draw}, turn={self.turn}")
if not self.has_draw:
if self.turn == 0:
tile = self.wall.draw()
self.hand_player.draw_tile(tile)
self.has_draw = True
print(f"Change has_draw to {self.has_draw}")
def draw_tile(self, hand: Hand):
tile = self.wall.draw()
hand.draw_tile(tile)
def put_tile(self, hand: Hand, id: int):
self.wall.tiles.append(hand.discard_tile(id))
self.wall.shuffle()
def next_turn(self):
"""Passe au joueur suivant."""
# TODO: check for pon/chii
self.turn = (self.turn + 1) % 4
self.has_draw = False
if self.turn != 0: # a bot has to play
self.bot_turn()
else:
self.play_draw()
def get_state(self):
"""Retourne l'état actuel de la partie (joueurs, scores, main, etc.)."""
hand_tiles = list(getattr(self.hand_player, 'tiles', []))
return {
'scores': list(self.scores),
'winds': list(self.winds),
'turn': int(self.turn),
'riichi': list(self.riichi),
'repeat': int(self.repeat),
'has_draw': bool(self.has_draw),
'end': bool(self.end),
'hand_player': [
{
'id': tile.id,
'family': str(tile.family),
'value': str(tile.value),
'svg': tile.get_img()
}
for tile in hand_tiles
],
'draw': (
{
'id': self.hand_player.draw.id,
'family': str(self.hand_player.draw.family),
'value': str(self.hand_player.draw.value),
'svg': self.hand_player.draw.get_img()
}
if self.hand_player.draw is not None else None
)
}
def declare_riichi(self, player_id):
"""Déclare Riichi pour un joueur."""
pass
def get_hand(self, player_id):
"""Retourne la main d'un joueur (0 = joueur humain, 1-3 = bots)."""
if player_id == 0:
hand = list(getattr(self.hand_player, 'tiles', []))
if getattr(self.hand_player, 'draw', None) is not None:
hand = hand + [self.hand_player.draw]
elif 1 <= player_id <= 3:
hand_obj = self.hands_bots[player_id - 1]
hand = list(getattr(hand_obj, 'tiles', []))
if getattr(hand_obj, 'draw', None) is not None:
hand = hand + [hand_obj.draw]
else:
return []
return [
{
'id': tile.id,
'family': str(tile.family),
'value': str(tile.value),
'svg': tile.get_img()
}
for tile in hand
]
def get_discards(self, player_id: int):
if player_id == 0:
return [
{
'id': tile.id,
'family': str(tile.family),
'value': str(tile.value),
'svg': tile.get_img()
}
for tile in self.discard_player
]
elif 1 <= player_id <= 3:
return [
{
'id': tile.id,
'family': str(tile.family),
'value': str(tile.value),
'svg': tile.get_img()
}
for tile in self.discards_bots[player_id - 1]
]
else:
return []
def get_possible_actions(self, player_id):
"""Retourne les actions possibles pour un joueur."""
pass
def bot_turn(self):
# Joue un tour de bot sans sleep
if self.turn != 0:
# Pioche une tuile pour le bot
tile = self.wall.draw()
self.hands_bots[self.turn - 1].draw_tile(tile)
# Défausse une tuile aléatoire
hand = self.hands_bots[self.turn - 1]
n = rd.randint(0, len(hand.tiles) - 1)
tile_to_discard = hand.tiles[n]
discarded = hand.discard_tile(tile_to_discard.id)
if discarded:
self.discards_bots[self.turn - 1].append(discarded)
# Passe au joueur suivant
self.turn = (self.turn + 1) % 4
self.has_draw = True if self.turn == 0 else False
def discard_tile(self, tile_id: int):
# Défausse la tuile (de la main ou de la tuile piochée)
if self.turn == 0 and self.has_draw:
discarded = self.hand_player.discard_tile(tile_id)
if discarded:
self.discard_player.append(discarded)
self.next_turn()

View file

@ -1,45 +0,0 @@
from __future__ import annotations
from .tile import *
import random as rd
class Hand:
def __init__(self, tiles: list[Tile], draw: Tile|None = None):
self.tiles: list[Tile] = tiles
self.draw: Tile|None = draw
def sort(self) -> None:
self.tiles.sort()
def fold(self) -> None:
if self.draw is not None:
self.tiles.append(self.draw)
self.draw = None
self.sort()
def draw_tile(self, tile: Tile) -> None:
self.draw = tile
def discard_tile(self, id: int) -> Tile|None:
if self.draw is not None and self.draw.id == id:
draw = self.draw
self.draw = None
self.fold()
self.sort()
return draw
for i, tile in enumerate(self.tiles):
if tile.id == id:
t = self.tiles.pop(i)
self.fold()
self.sort()
return t
self.sort()
return None
@staticmethod
def random() -> Hand:
tiles = Tile.generateAll()
rd.shuffle(tiles)
hand = Hand(tiles[:13])
hand.sort()
return hand

View file

@ -1,116 +0,0 @@
from __future__ import annotations
from game.error import *
from game.value import *
from functools import total_ordering
@total_ordering
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:
shadow = self._load_svg(self.img_shadow, x=10, y=10)
front = self._load_svg(self.img_front, x=0, y=0)
symbol = self._load_svg(self.img_symbol, x=12, y=20, scale=0.9)
ratio = 400 / 300
width = 60
self.img = f"""<svg class="tile tile-{self.id}" data-tile-id="{self.id}" viewBox="0 0 310 410" height="{ratio * width}" width="{width}">{shadow}{front}{symbol}</svg>"""
return self.img
@staticmethod
def _load_svg(filename, x=0, y=0, scale=1):
try:
with open(f'img/Regular/{filename}', 'r', encoding='utf-8') as f:
svg_content = f.read()
return f'<g transform="translate({x}, {y}) scale({scale})">{svg_content}</g>'
except FileNotFoundError:
raise InvalidValueError(f"SVG non trouvé: {filename}")
def __eq__(self, other: Tile):
return self.value == other.value and self.family == other.family
def __lt__(self, other: Tile):
if self.family.value != other.family.value:
return self.family.value < other.family.value
if hasattr(self.value, 'value'):
return self.value.value < other.value.value
return 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

View file

@ -1,19 +0,0 @@
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

View file

@ -1,23 +0,0 @@
from __future__ import annotations
from .tile import *
from .hand import *
import random as rd
class Wall:
def __init__(self):
self.tiles = Tile.generateAll()
self.shuffle()
def shuffle(self):
rd.shuffle(self.tiles)
def draw(self) -> Tile:
if len(self.tiles) == 0:
raise Exception("No more tiles in the wall")
return self.tiles.pop()
def draw_hand(self) -> Hand:
hand_tiles = [self.draw() for _ in range(13)]
hand_tiles.sort()
return Hand(hand_tiles)

View file

@ -1,4 +0,0 @@
Flask==3.0.0
flask-cors==4.0.0
gunicorn==21.2.0
gevent>=1.4

View file

@ -1,15 +1,4 @@
services:
riichi:
build: .
ports:
- "5000:5000"
environment:
- FLASK_ENV=production
volumes:
- ./frontend:/app/frontend
- ./backend/src:/app/backend/src
restart: unless-stopped
nginx:
image: nginx:latest
ports:
@ -20,6 +9,4 @@ services:
- ${PWD}/index.html:/usr/share/nginx/html/index.html:ro
- ${PWD}/components:/usr/share/nginx/html/components
- ${PWD}/img:/usr/share/nginx/html/img
depends_on:
- riichi
restart: unless-stopped

View file

@ -46,8 +46,8 @@
}
.hand-tile-item.draw-tile {
/* Le draw s'affiche simplement à droite grâce à flex */
flex: 0 0 auto;
margin-left: 20px;
}
/* Orientation variations */

219
frontend/js/game.js Normal file
View file

@ -0,0 +1,219 @@
import "./tile.js";
class Group {
constructor(tiles, stollenTile = null, dir = null) {
this.tiles = tiles;
this.stollenTile = stollenTile;
this.dir = dir; // 1 for left, 2 for middle, 3 for right
}
}
class Hand {
constructor(tiles = []) {
this.tiles = tiles;
this.groups = [];
this.drawn= null;
}
drawTile(tile) {
this.drawn = tile;
}
discard(k) {
if (k === this.tiles.length) {
const tile = this.drawn;
this.drawn = null;
return tile;
} else {
const tile = this.tiles[k];
this.tiles.splice(k, 1);
return tile;
}
}
sort() {
this.tiles.sort((a, b) => {
if (a.lessThan(b)) return -1;
if (b.lessThan(a)) return 1;
return 0;
});
}
shuffle() {
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]];
}
}
flatten() {
if (this.drawn) {
this.tiles.push(this.drawn);
}
this.drawn = null;
this.sort();
}
makeGroup(tiles, stollenTile = null, dir = null) {
const group = new Group(tiles, stollenTile, dir);
this.groups.push(group);
for (let tile of tiles) {
const index = this.tiles.findIndex(t => t.equals(tile));
if (index !== -1) {
this.tiles.splice(index, 1);
}
}
this.flatten();
}
}
class Draw {
constructor() {
this.tiles = Tile.generateAll();
this.shuffle();
}
shuffle() {
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]];
}
}
drawTile() {
return this.tiles.pop();
}
drawHand() {
const handTiles = [];
for (let i = 0; i < 13; i++) {
handTiles.push(this.drawTile());
}
return new Hand(handTiles);
}
}
class Discard {
constructor() {
this.tiles = [];
this.hiddenTiles = [];
}
add(tile) {
this.tiles.push(tile);
}
beeingStollen() {
this.hiddenTiles.push(this.tiles.pop());
return this.hiddenTiles[this.hiddenTiles.length - 1];
}
isIn(tile) {
return this.tiles.some(t => t.equals(tile)) || this.hiddenTiles.some(t => t.equals(tile));
}
}
class Game {
constructor(skipInitialDeal = false) {
this.discards = [
new Discard(),
new Discard(),
new Discard(),
new Discard()
];
this.wall = new Draw();
if (skipInitialDeal) {
this.hands = [
new Hand(),
new Hand(),
new Hand(),
new Hand()
];
} else {
this.hands = [
this.wall.drawHand(),
this.wall.drawHand(),
this.wall.drawHand(),
this.wall.drawHand()
];
for (let hand of this.hands) {
hand.sort();
}
}
this.firstDealer = Math.floor(Math.random() * 4) * 0;
this.turn = this.firstDealer;
this.repeat = 0;
if (!skipInitialDeal) {
this.draw(this.turn);
}
}
newDeal(hasRepeat = false) {
this.discards = [
new Discard(),
new Discard(),
new Discard(),
new Discard()
];
this.wall = new Draw();
this.hands = [
this.wall.drawHand(),
this.wall.drawHand(),
this.wall.drawHand(),
this.wall.drawHand()
];
if (!hasRepeat) {
this.turn = (this.firstDealer + 1) % 4;
} else {
this.repeat++;
}
}
nextTurn() {
this.turn = (this.turn + 1) % 4;
}
draw(player) {
this.hands[player].drawTile(this.wall.drawTile());
}
discard(player, k) {
if (player === this.turn) {
const tile = this.hands[player].discard(k);
this.discards[player].add(tile);
this.hands[player].flatten();
}
}
chii(player, tiles, dir) {
const hand = this.hands[player];
dir = (player - this.turn) % 4;
console.assert(dir == 1);
this.hands[player].makeGroup(tiles, this.discards[this.turn].beeingStollen(), dir);
}
pon(player, tiles, dir) {
const hand = this.hands[player];
dir = (player - this.turn + 4) % 4;
console.assert(dir == 1 || dir == 2 ||dir == 3, `Invalid direction: ${dir}`);
this.hands[player].makeGroup(tiles, this.discards[this.turn].beeingStollen(), dir);
}
canPon(player) {
const lastTile = this.discards[this.turn].tiles[this.discards[this.turn].tiles.length - 1];
return this.hands[player].tiles.filter(t => t.equals(lastTile)).length >= 2;
}
botPlay() {
// Le bot défausse une tuile au hasard (supposant qu'il a déjà pioché)
const hand = this.hands[this.turn];
const randomIndex = Math.floor(Math.random() * (hand.tiles.length + (hand.drawn ? 1 : 0)));
this.discard(this.turn, randomIndex);
}
}
export { Discard, Draw, Game, Group, Hand };

110
frontend/js/tile.js Normal file
View file

@ -0,0 +1,110 @@
// frontend/js/tile.js
// Traduction de tile.py en JavaScript
// Enum JS pour familles et valeurs
const Family = {
MAN: 'man', PIN: 'pin', SOU: 'sou', WIND: 'wind', DRAGON: 'dragon'
};
const Wind = { EAST: 1, SOUTH: 2, WEST: 3, NORTH: 4 };
const Dragon = { RED: 1, GREEN: 2, WHITE: 3 };
class Tile {
constructor(family, value, id) {
this.family = family;
this.value = value;
this.id = id;
this.hidden = false;
this.img = null;
this.img_back = "Back.svg";
this.img_front = "Front.svg";
this.img_shadow = "Gray.svg";
if (family === Family.MAN) {
if (value >= 1 && value <= 9) this.img_symbol = `Man${value}.svg`;
else throw new Error(`Invalid value: ${value}`);
} else if (family === Family.PIN) {
if (value >= 1 && value <= 9) this.img_symbol = `Pin${value}.svg`;
else throw new Error(`Invalid value: ${value}`);
} else if (family === Family.SOU) {
if (value >= 1 && value <= 9) this.img_symbol = `Sou${value}.svg`;
else throw new Error(`Invalid value: ${value}`);
} else if (family === Family.WIND) {
if (value === Wind.EAST) this.img_symbol = "Ton.svg";
else if (value === Wind.SOUTH) this.img_symbol = "Nan.svg";
else if (value === Wind.WEST) this.img_symbol = "Shaa.svg";
else if (value === Wind.NORTH) this.img_symbol = "Pei.svg";
else throw new Error(`Invalid value: ${value}`);
} else if (family === Family.DRAGON) {
if (value === Dragon.RED) this.img_symbol = "Chun.svg";
else if (value === Dragon.GREEN) this.img_symbol = "Hatsu.svg";
else if (value === Dragon.WHITE) this.img_symbol = "Haku.svg";
else throw new Error(`Invalid value: ${value}`);
} else {
throw new Error(`Invalid family: ${family}`);
}
}
getSVG() {
if (!this.img) {
const ratio = 400 / 300;
const width = 60;
const height = ratio * width;
// Utilise <image> pour chaque couche SVG
const shadow = `<image href="/img/Regular/${this.img_shadow}" x="10" y="10" height="410" width="310"/>`;
const front = `<image href="/img/Regular/${this.img_front}" x="0" y="0" height="410" width="310"/>`;
const symbol = `<image href="/img/Regular/${this.img_symbol}" x="12" y="20" height="370" width="285"/>`;
this.img = `<svg class="tile tile-${this.id}" data-tile-id="${this.id}" viewBox="0 0 310 410" height="${height}" width="${width}">${shadow}${front}${symbol}</svg>`;
}
return this.img;
}
equals(other) {
return this.value === other.value && this.family === other.family;
}
lessThan(other) {
const order = [Family.MAN, Family.PIN, Family.SOU, Family.WIND, Family.DRAGON];
const famA = order.indexOf(this.family);
const famB = order.indexOf(other.family);
if (famA !== famB) {
return famA < famB;
}
if (typeof this.value === 'object' && 'value' in this.value) {
return this.value.value < other.value.value;
}
return this.value < other.value;
}
static generateAll() {
const tiles = [];
let id = 0;
for (let _ = 0; _ < 4; _++) {
// man
for (let i = 1; i <= 9; i++) {
tiles.push(new Tile(Family.MAN, i, id++));
}
// pin
for (let i = 1; i <= 9; i++) {
tiles.push(new Tile(Family.PIN, i, id++));
}
// sou
for (let i = 1; i <= 9; i++) {
tiles.push(new Tile(Family.SOU, i, id++));
}
// wind
for (let wind of [Wind.EAST, Wind.SOUTH, Wind.WEST, Wind.NORTH]) {
tiles.push(new Tile(Family.WIND, wind, id++));
}
// dragon
for (let dragon of [Dragon.RED, Dragon.GREEN, Dragon.WHITE]) {
tiles.push(new Tile(Family.DRAGON, dragon, id++));
}
}
return tiles;
}
}
// Expose dans le scope global pour compatibilité avec les scripts HTML classiques
window.Tile = Tile;
window.Family = Family;
window.Wind = Wind;
window.Dragon = Dragon;

View file

@ -161,6 +161,7 @@
<div id="mascotte"></div>
<script src="/frontend/js/fr/texts.js"></script>
<script src="/frontend/js/tile.js"></script>
<script>
// Charger le menu
fetch('/components/menu.html')
@ -185,116 +186,108 @@
}
});
let allTiles = [];
// Générer les tuiles localement
const allTiles = Tile.generateAll();
const tilesDisplay = document.getElementById('tiles-display');
// Récupérer les tuiles
fetch('/api/tiles')
.then(r => r.json())
.then(data => {
allTiles = data.tiles;
const tilesDisplay = document.getElementById('tiles-display');
// Man
let section = document.createElement('div');
section.className = 'tiles-family';
const manTitle = document.createElement('h3');
manTitle.textContent = 'Caractères';
const manGrid = document.createElement('div');
manGrid.className = 'tiles-grid';
section.appendChild(manTitle);
section.appendChild(manGrid);
for (let i = 0; i < 9; i++) {
const tileDiv = document.createElement('div');
tileDiv.className = 'tile-item';
tileDiv.innerHTML = allTiles[i].svg;
manGrid.appendChild(tileDiv);
}
tilesDisplay.appendChild(section);
// Pin
section = document.createElement('div');
section.className = 'tiles-family';
const pinTitle = document.createElement('h3');
pinTitle.textContent = 'Ronds';
const pinGrid = document.createElement('div');
pinGrid.className = 'tiles-grid';
section.appendChild(pinTitle);
section.appendChild(pinGrid);
for (let i = 9; i < 18; i++) {
const tileDiv = document.createElement('div');
tileDiv.className = 'tile-item';
tileDiv.innerHTML = allTiles[i].svg;
pinGrid.appendChild(tileDiv);
}
tilesDisplay.appendChild(section);
// Sou
section = document.createElement('div');
section.className = 'tiles-family';
const souTitle = document.createElement('h3');
souTitle.textContent = 'Bambous';
const souGrid = document.createElement('div');
souGrid.className = 'tiles-grid';
section.appendChild(souTitle);
section.appendChild(souGrid);
for (let i = 18; i < 27; i++) {
const tileDiv = document.createElement('div');
tileDiv.className = 'tile-item';
tileDiv.innerHTML = allTiles[i].svg;
souGrid.appendChild(tileDiv);
}
tilesDisplay.appendChild(section);
// Man
let section = document.createElement('div');
section.className = 'tiles-family';
const manTitle = document.createElement('h3');
manTitle.textContent = 'Caractères';
const manGrid = document.createElement('div');
manGrid.className = 'tiles-grid';
section.appendChild(manTitle);
section.appendChild(manGrid);
for (let i = 0; i < 9; i++) {
const tileDiv = document.createElement('div');
tileDiv.className = 'tile-item';
tileDiv.innerHTML = allTiles[i].getSVG();
manGrid.appendChild(tileDiv);
}
tilesDisplay.appendChild(section);
// Ligne des noms de vents
const windNamesDiv = document.createElement('div');
windNamesDiv.className = 'wind-names';
windNamesDiv.innerHTML = '<div>Est</div><div>Sud</div><div>Ouest</div><div>Nord</div>';
tilesDisplay.appendChild(windNamesDiv);
// Pin
section = document.createElement('div');
section.className = 'tiles-family';
const pinTitle = document.createElement('h3');
pinTitle.textContent = 'Ronds';
const pinGrid = document.createElement('div');
pinGrid.className = 'tiles-grid';
section.appendChild(pinTitle);
section.appendChild(pinGrid);
for (let i = 9; i < 18; i++) {
const tileDiv = document.createElement('div');
tileDiv.className = 'tile-item';
tileDiv.innerHTML = allTiles[i].getSVG();
pinGrid.appendChild(tileDiv);
}
tilesDisplay.appendChild(section);
// Vents
section = document.createElement('div');
section.className = 'tiles-family';
const windTitle = document.createElement('h3');
windTitle.textContent = 'Vents';
const windGrid = document.createElement('div');
windGrid.className = 'tiles-grid';
windGrid.style.gridTemplateColumns = 'repeat(4, minmax(0, 1fr))';
section.appendChild(windTitle);
section.appendChild(windGrid);
const windNames = ['Est', 'Sud', 'Ouest', 'Nord'];
for (let i = 27; i < 31; i++) {
const tileDiv = document.createElement('div');
tileDiv.className = 'tile-item';
tileDiv.innerHTML = allTiles[i].svg;
windGrid.appendChild(tileDiv);
}
tilesDisplay.appendChild(section);
// Sou
section = document.createElement('div');
section.className = 'tiles-family';
const souTitle = document.createElement('h3');
souTitle.textContent = 'Bambous';
const souGrid = document.createElement('div');
souGrid.className = 'tiles-grid';
section.appendChild(souTitle);
section.appendChild(souGrid);
for (let i = 18; i < 27; i++) {
const tileDiv = document.createElement('div');
tileDiv.className = 'tile-item';
tileDiv.innerHTML = allTiles[i].getSVG();
souGrid.appendChild(tileDiv);
}
tilesDisplay.appendChild(section);
// Ligne des noms de dragons
const dragonNamesDiv = document.createElement('div');
dragonNamesDiv.className = 'dragon-names';
dragonNamesDiv.innerHTML = '<div>Blanc</div><div>Vert</div><div>Rouge</div>';
tilesDisplay.appendChild(dragonNamesDiv);
// Ligne des noms de vents
const windNamesDiv = document.createElement('div');
windNamesDiv.className = 'wind-names';
windNamesDiv.innerHTML = '<div>Est</div><div>Sud</div><div>Ouest</div><div>Nord</div>';
tilesDisplay.appendChild(windNamesDiv);
// Dragons
section = document.createElement('div');
section.className = 'tiles-family';
const dragonTitle = document.createElement('h3');
dragonTitle.textContent = 'Dragons';
const dragonGrid = document.createElement('div');
dragonGrid.className = 'tiles-grid';
dragonGrid.style.gridTemplateColumns = 'repeat(3, minmax(0, 1fr))';
section.appendChild(dragonTitle);
section.appendChild(dragonGrid);
for (let i = 31; i < 34; i++) {
const tileDiv = document.createElement('div');
tileDiv.className = 'tile-item';
tileDiv.innerHTML = allTiles[i].svg;
dragonGrid.appendChild(tileDiv);
}
tilesDisplay.appendChild(section);
});
// Vents
section = document.createElement('div');
section.className = 'tiles-family';
const windTitle = document.createElement('h3');
windTitle.textContent = 'Vents';
const windGrid = document.createElement('div');
windGrid.className = 'tiles-grid';
windGrid.style.gridTemplateColumns = 'repeat(4, minmax(0, 1fr))';
section.appendChild(windTitle);
section.appendChild(windGrid);
for (let i = 27; i < 31; i++) {
const tileDiv = document.createElement('div');
tileDiv.className = 'tile-item';
tileDiv.innerHTML = allTiles[i].getSVG();
windGrid.appendChild(tileDiv);
}
tilesDisplay.appendChild(section);
// Ligne des noms de dragons
const dragonNamesDiv = document.createElement('div');
dragonNamesDiv.className = 'dragon-names';
dragonNamesDiv.innerHTML = '<div>Blanc</div><div>Vert</div><div>Rouge</div>';
tilesDisplay.appendChild(dragonNamesDiv);
// Dragons
section = document.createElement('div');
section.className = 'tiles-family';
const dragonTitle = document.createElement('h3');
dragonTitle.textContent = 'Dragons';
const dragonGrid = document.createElement('div');
dragonGrid.className = 'tiles-grid';
dragonGrid.style.gridTemplateColumns = 'repeat(3, minmax(0, 1fr))';
section.appendChild(dragonTitle);
section.appendChild(dragonGrid);
for (let i = 31; i < 34; i++) {
const tileDiv = document.createElement('div');
tileDiv.className = 'tile-item';
tileDiv.innerHTML = allTiles[i].getSVG();
dragonGrid.appendChild(tileDiv);
}
tilesDisplay.appendChild(section);
</script>

View file

@ -134,7 +134,7 @@
<div id="mascotte"></div>
<script src="/frontend/js/fr/texts.js"></script>
<script src="/frontend/js/hand.js"></script>
<script src="/frontend/js/tile.js"></script>
<script>
// Charger le menu
fetch('/components/menu.html')
@ -171,47 +171,52 @@
handsContainer.className = 'hands-container';
tilesDisplay.appendChild(handsContainer);
// Créer et afficher trois mains aléatoires
for (let i = 0; i < 3; i++) {
const hand = new Hand({
label: `Main ${i + 1}`,
position: 'top-label',
tilt: 'none',
containerId: 'tiles-display'
// Générer et afficher trois mains aléatoires localement
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
const allTiles = Tile.generateAll();
function sortHand(hand) {
// Ordre : caractères < ronds < bambous < vents < dragons
const familyOrder = { 'man': 0, 'pin': 1, 'sou': 2, 'wind': 3, 'dragon': 4 };
return hand.sort((a, b) => {
const famA = familyOrder[a.family] ?? 99;
const famB = familyOrder[b.family] ?? 99;
if (famA !== famB) return famA - famB;
if (a.value !== undefined && b.value !== undefined) return a.value - b.value;
if (a.wind !== undefined && b.wind !== undefined) return a.wind - b.wind;
if (a.dragon !== undefined && b.dragon !== undefined) return a.dragon - b.dragon;
return 0;
});
// Modifier temporairement le containerId pour ajouter à handsContainer
hand.containerId = null;
fetch(hand.apiUrl)
.then(r => r.json())
.then(data => {
const handDiv = document.createElement('div');
handDiv.className = `hand-display ${hand.position} tilt-${hand.tilt}`;
const label = document.createElement('div');
label.className = 'hand-label';
label.textContent = `Main ${i + 1}`;
handDiv.appendChild(label);
const container = document.createElement('div');
container.className = 'hand-container';
const handTiles = document.createElement('div');
handTiles.className = 'hand-tiles';
data.hand.forEach(tile => {
const tileDiv = document.createElement('div');
tileDiv.className = 'hand-tile-item';
tileDiv.innerHTML = tile.svg;
handTiles.appendChild(tileDiv);
});
container.appendChild(handTiles);
handDiv.appendChild(container);
handsContainer.appendChild(handDiv);
})
.catch(error => console.error('Erreur:', error));
}
for (let i = 0; i < 3; i++) {
const tiles = allTiles.slice();
shuffle(tiles);
const handTilesArr = tiles.slice(0, 13);
sortHand(handTilesArr);
const handDiv = document.createElement('div');
handDiv.className = `hand-display top-label tilt-none`;
const label = document.createElement('div');
label.className = 'hand-label';
label.textContent = `Main ${i + 1}`;
handDiv.appendChild(label);
const container = document.createElement('div');
container.className = 'hand-container';
const handTiles = document.createElement('div');
handTiles.className = 'hand-tiles';
handTilesArr.forEach(tile => {
const tileDiv = document.createElement('div');
tileDiv.className = 'hand-tile-item';
tileDiv.innerHTML = tile.getSVG();
handTiles.appendChild(tileDiv);
});
container.appendChild(handTiles);
handDiv.appendChild(container);
handsContainer.appendChild(handDiv);
}
// Créer le titre pour la main interactive
@ -220,116 +225,84 @@
interactiveTitle.textContent = 'Main interactive';
tilesDisplay.appendChild(interactiveTitle);
// Créer et afficher la main interactive avec une tuile piochée
fetch('/api/hand_with_draw')
.then(r => r.json())
.then(data => {
const handDiv = document.createElement('div');
handDiv.className = 'hand-display top-label tilt-none';
handDiv.id = 'interactive-hand';
const label = document.createElement('div');
label.className = 'hand-label';
label.textContent = 'Votre main';
handDiv.appendChild(label);
// Conteneur qui gère l'affichage avec flex (gère auto le draw si présent)
const container = document.createElement('div');
container.className = 'hand-container';
// Créer le grid des 13 tuiles
const handTiles = document.createElement('div');
handTiles.className = 'hand-tiles';
handTiles.id = 'interactive-tiles';
data.hand.forEach(tile => {
const tileDiv = document.createElement('div');
tileDiv.className = 'hand-tile-item';
tileDiv.innerHTML = tile.svg;
tileDiv.dataset.tileId = tile.id;
tileDiv.style.cursor = 'pointer';
tileDiv.addEventListener('click', () => discardTile(tile.id, tileDiv));
handTiles.appendChild(tileDiv);
});
container.appendChild(handTiles);
const drawTileDiv = document.createElement('div');
drawTileDiv.className = 'hand-tile-item draw-tile';
drawTileDiv.innerHTML = data.draw.svg;
drawTileDiv.dataset.tileId = data.draw.id;
drawTileDiv.style.cursor = 'pointer';
drawTileDiv.addEventListener('click', () => discardTile(data.draw.id, drawTileDiv));
container.appendChild(drawTileDiv);
handDiv.appendChild(container);
tilesDisplay.appendChild(handDiv);
})
.catch(error => console.error('Erreur:', error));
// Fonction pour défausser une tuile
async function discardTile(tileId, tileElement) {
try {
const response = await fetch('/api/discard', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ tile_id: tileId })
});
if (!response.ok) {
console.error('Erreur lors de la défausse');
return;
}
const data = await response.json();
// Ajouter une animation de suppression
tileElement.style.opacity = '0';
tileElement.style.transition = 'opacity 0.3s ease';
setTimeout(() => {
// Mettre à jour la main interactive avec la nouvelle main
updateInteractiveHand(data.hand, data.draw);
}, 300);
} catch (error) {
console.error('Erreur:', error);
}
}
// Fonction pour mettre à jour la main interactive
function updateInteractiveHand(handData, drawData) {
const handTiles = document.getElementById('interactive-tiles');
handTiles.innerHTML = '';
handData.forEach(tile => {
// Générer et afficher une main interactive locale avec une tuile piochée
let interactiveTiles = allTiles.slice();
shuffle(interactiveTiles);
let interactiveHand = interactiveTiles.slice(0, 13);
sortHand(interactiveHand);
let drawTile = interactiveTiles[13];
let wall = interactiveTiles.slice(14); // Le reste du paquet pour la pioche
function renderInteractiveHand() {
// Nettoyer l'affichage précédent
let oldDiv = document.getElementById('interactive-hand');
if (oldDiv) oldDiv.remove();
const handDiv = document.createElement('div');
handDiv.className = 'hand-display top-label tilt-none';
handDiv.id = 'interactive-hand';
const label = document.createElement('div');
label.className = 'hand-label';
label.textContent = 'Votre main';
handDiv.appendChild(label);
const container = document.createElement('div');
container.className = 'hand-container';
const handTiles = document.createElement('div');
handTiles.className = 'hand-tiles';
handTiles.id = 'interactive-tiles';
interactiveHand.forEach((tile, idx) => {
const tileDiv = document.createElement('div');
tileDiv.className = 'hand-tile-item';
tileDiv.innerHTML = tile.svg;
tileDiv.innerHTML = tile.getSVG();
tileDiv.dataset.tileId = tile.id;
tileDiv.style.cursor = 'pointer';
tileDiv.addEventListener('click', () => discardTile(tile.id, tileDiv));
tileDiv.addEventListener('click', () => discardTile(idx));
handTiles.appendChild(tileDiv);
});
// Mettre à jour le draw
const container = handTiles.parentElement;
const oldDrawTile = container.querySelector('.draw-tile');
if (oldDrawTile) {
oldDrawTile.remove();
}
if (drawData) {
container.appendChild(handTiles);
// Draw tile
if (drawTile) {
const drawTileDiv = document.createElement('div');
drawTileDiv.className = 'hand-tile-item draw-tile';
drawTileDiv.innerHTML = drawData.svg;
drawTileDiv.dataset.tileId = drawData.id;
drawTileDiv.innerHTML = drawTile.getSVG();
drawTileDiv.dataset.tileId = drawTile.id;
drawTileDiv.style.cursor = 'pointer';
drawTileDiv.addEventListener('click', () => discardTile(drawData.id, drawTileDiv));
drawTileDiv.addEventListener('click', () => discardTile('draw'));
container.appendChild(drawTileDiv);
}
handDiv.appendChild(container);
tilesDisplay.appendChild(handDiv);
}
function discardTile(idx) {
let discarded;
if (idx === 'draw') {
// Défausse la tuile piochée
discarded = drawTile;
drawTile = null;
} else {
// Défausse une tuile de la main
discarded = interactiveHand.splice(idx, 1)[0];
if (drawTile) {
interactiveHand.push(drawTile);
drawTile = null;
}
}
// Remettre la tuile défaussée dans la pioche (wall) et mélanger le mur
if (discarded) {
wall.push(discarded);
shuffle(wall);
}
// Trier la main après modification
sortHand(interactiveHand);
// Nouvelle pioche si possible
if (!drawTile && wall.length > 0) {
drawTile = wall.shift();
}
renderInteractiveHand();
}
// Afficher la main interactive au chargement
renderInteractiveHand();
</script>
</body>
</html>

View file

@ -85,6 +85,7 @@
<div id="mascotte"></div>
<script src="/frontend/js/fr/texts.js"></script>
<script src="/frontend/js/tile.js"></script>
<script src="/frontend/js/hand.js"></script>
<script>
// Charger le menu
@ -107,21 +108,18 @@
});
// Fonction utilitaire pour afficher les tuiles
const allTiles = Tile.generateAll();
function displayTiles(displayId, tileIndices) {
fetch('/api/tiles')
.then(r => r.json())
.then(data => {
const display = document.getElementById(displayId);
tileIndices.forEach(idx => {
const tile = data.tiles[idx];
if (tile) {
const tileDiv = document.createElement('div');
tileDiv.innerHTML = tile.svg;
display.appendChild(tileDiv);
}
});
})
.catch(error => console.error('Erreur:', error));
const display = document.getElementById(displayId);
display.innerHTML = '';
tileIndices.forEach(idx => {
const tile = allTiles[idx];
if (tile) {
const tileDiv = document.createElement('div');
tileDiv.innerHTML = tile.getSVG();
display.appendChild(tileDiv);
}
});
}
// Afficher les combinaisons

View file

@ -96,6 +96,7 @@
<div id="mascotte"></div>
<script src="/frontend/js/fr/texts.js"></script>
<script src="/frontend/js/tile.js"></script>
<script src="/frontend/js/hand.js"></script>
<script>
// Charger le menu
@ -118,21 +119,18 @@
});
// Fonction utilitaire pour afficher les tuiles
const allTiles = Tile.generateAll();
function displayTiles(displayId, tileIndices) {
fetch('/api/tiles')
.then(r => r.json())
.then(data => {
const display = document.getElementById(displayId);
tileIndices.forEach(idx => {
const tile = data.tiles[idx];
if (tile) {
const tileDiv = document.createElement('div');
tileDiv.innerHTML = tile.svg;
display.appendChild(tileDiv);
}
});
})
.catch(error => console.error('Erreur:', error));
const display = document.getElementById(displayId);
display.innerHTML = '';
tileIndices.forEach(idx => {
const tile = allTiles[idx];
if (tile) {
const tileDiv = document.createElement('div');
tileDiv.innerHTML = tile.getSVG();
display.appendChild(tileDiv);
}
});
}
// Afficher les combinaisons

File diff suppressed because it is too large Load diff

View file

@ -94,6 +94,7 @@
<div id="mascotte"></div>
<script src="/frontend/js/fr/texts.js"></script>
<script src="/frontend/js/tile.js"></script>
<script>
// Charger le menu
fetch('/components/menu.html')
@ -114,23 +115,19 @@
: '<img src="/img/tilineau/idle.png" alt="Tilineau">';
});
// Fonction utilitaire pour afficher les tuiles
function displayTiles(displayId, tileIndices) {
fetch('/api/tiles')
.then(r => r.json())
.then(data => {
const display = document.getElementById(displayId);
tileIndices.forEach(idx => {
const tile = data.tiles[idx];
if (tile) {
const tileDiv = document.createElement('div');
tileDiv.innerHTML = tile.svg;
display.appendChild(tileDiv);
}
});
})
.catch(error => console.error('Erreur:', error));
}
// Affichage local des tuiles sans backend
function displayTiles(displayId, tileIndices) {
const allTiles = Tile.generateAll();
const display = document.getElementById(displayId);
tileIndices.forEach(idx => {
const tile = allTiles[idx];
if (tile) {
const tileDiv = document.createElement('div');
tileDiv.innerHTML = tile.getSVG();
display.appendChild(tileDiv);
}
});
}
displayTiles('tiles-display', [4, 5, 6, 8, 8, 8, 14, 14, 24, 25, 26, 32, 32, 32]);
displayTiles('tiles-display-2', [4, 5, 6, 7, 7, 7, 14, 14, 20, 21, 22, 24, 24, 24]);
@ -140,7 +137,7 @@
displayTiles('tiles-display-6', [18, 18, 18, 20, 21, 22, 21, 22, 23, 25, 25, 27, 27, 27]);
displayTiles('tiles-display-7', [1, 1, 6, 6, 18, 18, 19, 19, 24, 24, 28, 28, 30, 30]);
displayTiles('tiles-display-8', [2, 2, 2, 7, 7, 7, 20, 20, 25, 25, 25, 29, 29, 29]);
displayTiles('tiles-display-9', [1, 2, 3, 9, 10, 11, 15, 16, 17, 20, 21, 28, 28])
displayTiles('tiles-display-9', [1, 2, 3, 9, 10, 11, 15, 16, 17, 20, 21, 28, 28]);
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View file

@ -73,6 +73,7 @@
<div id="mascotte"></div>
<script src="/frontend/js/fr/texts.js"></script>
<script src="/frontend/js/tile.js"></script>
<script>
// Charger le menu
fetch('/components/menu.html')
@ -95,17 +96,10 @@
: '<img src="/img/tilineau/speaking.png" alt="Tilineau">';
});
let allTiles = [];
// Récupérer les tuiles
fetch('/api/tiles')
.then(r => r.json())
.then(data => {
allTiles = data.tiles;
// Lancer la pluie de tuiles
startTileRain();
});
// Générer les tuiles localement
const allTiles = Tile.generateAll();
// Lancer la pluie de tuiles
startTileRain();
function startTileRain() {
const container = document.getElementById('tile-rain');
@ -113,18 +107,15 @@
const randomTile = allTiles[Math.floor(Math.random() * allTiles.length)];
const tileElem = document.createElement('div');
tileElem.className = 'falling-tile';
tileElem.innerHTML = randomTile.svg;
tileElem.innerHTML = randomTile.getSVG();
tileElem.style.left = Math.random() * window.innerWidth + 'px';
const duration = 8 + Math.random() * 2;
tileElem.style.animationDuration = duration + 's';
// Rotation aléatoire à droite ou à gauche
const rotation = Math.random() * 720 - 360;
const direction = Math.random() > 0.5 ? 1 : -1;
tileElem.style.setProperty('--rotation', (rotation * direction) + 'deg');
container.appendChild(tileElem);
setTimeout(() => tileElem.remove(), duration * 1000);
}, 300);
}

View file

@ -34,14 +34,7 @@ http {
add_header Cache-Control "public, immutable";
}
# Proxy vers le backend Flask
location /api/ {
proxy_pass http://riichi:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Servir les fichiers statiques la fin, après les autres locations)
location / {