Compare commits

..

1 commit

Author SHA1 Message Date
20147b32af Téléverser les fichiers vers "/" 2026-07-25 08:59:15 +00:00
25 changed files with 1283 additions and 1625 deletions

View file

@ -1,2 +1,26 @@
FROM nginx:alpine FROM python:3.11-slim
# Ce Dockerfile n'est plus utilisé, le service est servi par nginx via docker-compose.
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"]

121
LICENSE Normal file
View file

@ -0,0 +1,121 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.

221
backend/src/api.py Normal file
View file

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

View file

15
backend/src/game/error.py Normal file
View file

@ -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

210
backend/src/game/game.py Normal file
View file

@ -0,0 +1,210 @@
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()

45
backend/src/game/hand.py Normal file
View file

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

116
backend/src/game/tile.py Normal file
View file

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

19
backend/src/game/value.py Normal file
View file

@ -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

23
backend/src/game/wall.py Normal file
View file

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

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

View file

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

View file

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

View file

@ -1,219 +0,0 @@
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 };

View file

@ -1,110 +0,0 @@
// 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,7 +161,6 @@
<div id="mascotte"></div> <div id="mascotte"></div>
<script src="/frontend/js/fr/texts.js"></script> <script src="/frontend/js/fr/texts.js"></script>
<script src="/frontend/js/tile.js"></script>
<script> <script>
// Charger le menu // Charger le menu
fetch('/components/menu.html') fetch('/components/menu.html')
@ -186,8 +185,14 @@
} }
}); });
// Générer les tuiles localement let allTiles = [];
const allTiles = Tile.generateAll();
// Récupérer les tuiles
fetch('/api/tiles')
.then(r => r.json())
.then(data => {
allTiles = data.tiles;
const tilesDisplay = document.getElementById('tiles-display'); const tilesDisplay = document.getElementById('tiles-display');
// Man // Man
@ -202,7 +207,7 @@
for (let i = 0; i < 9; i++) { for (let i = 0; i < 9; i++) {
const tileDiv = document.createElement('div'); const tileDiv = document.createElement('div');
tileDiv.className = 'tile-item'; tileDiv.className = 'tile-item';
tileDiv.innerHTML = allTiles[i].getSVG(); tileDiv.innerHTML = allTiles[i].svg;
manGrid.appendChild(tileDiv); manGrid.appendChild(tileDiv);
} }
tilesDisplay.appendChild(section); tilesDisplay.appendChild(section);
@ -219,7 +224,7 @@
for (let i = 9; i < 18; i++) { for (let i = 9; i < 18; i++) {
const tileDiv = document.createElement('div'); const tileDiv = document.createElement('div');
tileDiv.className = 'tile-item'; tileDiv.className = 'tile-item';
tileDiv.innerHTML = allTiles[i].getSVG(); tileDiv.innerHTML = allTiles[i].svg;
pinGrid.appendChild(tileDiv); pinGrid.appendChild(tileDiv);
} }
tilesDisplay.appendChild(section); tilesDisplay.appendChild(section);
@ -236,7 +241,7 @@
for (let i = 18; i < 27; i++) { for (let i = 18; i < 27; i++) {
const tileDiv = document.createElement('div'); const tileDiv = document.createElement('div');
tileDiv.className = 'tile-item'; tileDiv.className = 'tile-item';
tileDiv.innerHTML = allTiles[i].getSVG(); tileDiv.innerHTML = allTiles[i].svg;
souGrid.appendChild(tileDiv); souGrid.appendChild(tileDiv);
} }
tilesDisplay.appendChild(section); tilesDisplay.appendChild(section);
@ -257,10 +262,11 @@
windGrid.style.gridTemplateColumns = 'repeat(4, minmax(0, 1fr))'; windGrid.style.gridTemplateColumns = 'repeat(4, minmax(0, 1fr))';
section.appendChild(windTitle); section.appendChild(windTitle);
section.appendChild(windGrid); section.appendChild(windGrid);
const windNames = ['Est', 'Sud', 'Ouest', 'Nord'];
for (let i = 27; i < 31; i++) { for (let i = 27; i < 31; i++) {
const tileDiv = document.createElement('div'); const tileDiv = document.createElement('div');
tileDiv.className = 'tile-item'; tileDiv.className = 'tile-item';
tileDiv.innerHTML = allTiles[i].getSVG(); tileDiv.innerHTML = allTiles[i].svg;
windGrid.appendChild(tileDiv); windGrid.appendChild(tileDiv);
} }
tilesDisplay.appendChild(section); tilesDisplay.appendChild(section);
@ -284,10 +290,11 @@
for (let i = 31; i < 34; i++) { for (let i = 31; i < 34; i++) {
const tileDiv = document.createElement('div'); const tileDiv = document.createElement('div');
tileDiv.className = 'tile-item'; tileDiv.className = 'tile-item';
tileDiv.innerHTML = allTiles[i].getSVG(); tileDiv.innerHTML = allTiles[i].svg;
dragonGrid.appendChild(tileDiv); dragonGrid.appendChild(tileDiv);
} }
tilesDisplay.appendChild(section); tilesDisplay.appendChild(section);
});
</script> </script>

View file

@ -134,7 +134,7 @@
<div id="mascotte"></div> <div id="mascotte"></div>
<script src="/frontend/js/fr/texts.js"></script> <script src="/frontend/js/fr/texts.js"></script>
<script src="/frontend/js/tile.js"></script> <script src="/frontend/js/hand.js"></script>
<script> <script>
// Charger le menu // Charger le menu
fetch('/components/menu.html') fetch('/components/menu.html')
@ -171,52 +171,47 @@
handsContainer.className = 'hands-container'; handsContainer.className = 'hands-container';
tilesDisplay.appendChild(handsContainer); tilesDisplay.appendChild(handsContainer);
// Générer et afficher trois mains aléatoires localement // Créer et afficher trois mains aléatoires
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;
});
}
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
const tiles = allTiles.slice(); const hand = new Hand({
shuffle(tiles); label: `Main ${i + 1}`,
const handTilesArr = tiles.slice(0, 13); position: 'top-label',
sortHand(handTilesArr); tilt: 'none',
containerId: 'tiles-display'
});
// Modifier temporairement le containerId pour ajouter à handsContainer
hand.containerId = null;
fetch(hand.apiUrl)
.then(r => r.json())
.then(data => {
const handDiv = document.createElement('div'); const handDiv = document.createElement('div');
handDiv.className = `hand-display top-label tilt-none`; handDiv.className = `hand-display ${hand.position} tilt-${hand.tilt}`;
const label = document.createElement('div'); const label = document.createElement('div');
label.className = 'hand-label'; label.className = 'hand-label';
label.textContent = `Main ${i + 1}`; label.textContent = `Main ${i + 1}`;
handDiv.appendChild(label); handDiv.appendChild(label);
const container = document.createElement('div'); const container = document.createElement('div');
container.className = 'hand-container'; container.className = 'hand-container';
const handTiles = document.createElement('div'); const handTiles = document.createElement('div');
handTiles.className = 'hand-tiles'; handTiles.className = 'hand-tiles';
handTilesArr.forEach(tile => {
data.hand.forEach(tile => {
const tileDiv = document.createElement('div'); const tileDiv = document.createElement('div');
tileDiv.className = 'hand-tile-item'; tileDiv.className = 'hand-tile-item';
tileDiv.innerHTML = tile.getSVG(); tileDiv.innerHTML = tile.svg;
handTiles.appendChild(tileDiv); handTiles.appendChild(tileDiv);
}); });
container.appendChild(handTiles); container.appendChild(handTiles);
handDiv.appendChild(container); handDiv.appendChild(container);
handsContainer.appendChild(handDiv); handsContainer.appendChild(handDiv);
})
.catch(error => console.error('Erreur:', error));
} }
// Créer le titre pour la main interactive // Créer le titre pour la main interactive
@ -225,84 +220,116 @@
interactiveTitle.textContent = 'Main interactive'; interactiveTitle.textContent = 'Main interactive';
tilesDisplay.appendChild(interactiveTitle); tilesDisplay.appendChild(interactiveTitle);
// Générer et afficher une main interactive locale avec une tuile piochée // Créer et afficher la main interactive avec une tuile piochée
let interactiveTiles = allTiles.slice(); fetch('/api/hand_with_draw')
shuffle(interactiveTiles); .then(r => r.json())
let interactiveHand = interactiveTiles.slice(0, 13); .then(data => {
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'); const handDiv = document.createElement('div');
handDiv.className = 'hand-display top-label tilt-none'; handDiv.className = 'hand-display top-label tilt-none';
handDiv.id = 'interactive-hand'; handDiv.id = 'interactive-hand';
const label = document.createElement('div'); const label = document.createElement('div');
label.className = 'hand-label'; label.className = 'hand-label';
label.textContent = 'Votre main'; label.textContent = 'Votre main';
handDiv.appendChild(label); handDiv.appendChild(label);
// Conteneur qui gère l'affichage avec flex (gère auto le draw si présent)
const container = document.createElement('div'); const container = document.createElement('div');
container.className = 'hand-container'; container.className = 'hand-container';
// Créer le grid des 13 tuiles
const handTiles = document.createElement('div'); const handTiles = document.createElement('div');
handTiles.className = 'hand-tiles'; handTiles.className = 'hand-tiles';
handTiles.id = 'interactive-tiles'; handTiles.id = 'interactive-tiles';
interactiveHand.forEach((tile, idx) => {
data.hand.forEach(tile => {
const tileDiv = document.createElement('div'); const tileDiv = document.createElement('div');
tileDiv.className = 'hand-tile-item'; tileDiv.className = 'hand-tile-item';
tileDiv.innerHTML = tile.getSVG(); tileDiv.innerHTML = tile.svg;
tileDiv.dataset.tileId = tile.id; tileDiv.dataset.tileId = tile.id;
tileDiv.style.cursor = 'pointer'; tileDiv.style.cursor = 'pointer';
tileDiv.addEventListener('click', () => discardTile(idx)); tileDiv.addEventListener('click', () => discardTile(tile.id, tileDiv));
handTiles.appendChild(tileDiv); handTiles.appendChild(tileDiv);
}); });
container.appendChild(handTiles); container.appendChild(handTiles);
// Draw tile
if (drawTile) {
const drawTileDiv = document.createElement('div'); const drawTileDiv = document.createElement('div');
drawTileDiv.className = 'hand-tile-item draw-tile'; drawTileDiv.className = 'hand-tile-item draw-tile';
drawTileDiv.innerHTML = drawTile.getSVG(); drawTileDiv.innerHTML = data.draw.svg;
drawTileDiv.dataset.tileId = drawTile.id; drawTileDiv.dataset.tileId = data.draw.id;
drawTileDiv.style.cursor = 'pointer'; drawTileDiv.style.cursor = 'pointer';
drawTileDiv.addEventListener('click', () => discardTile('draw')); drawTileDiv.addEventListener('click', () => discardTile(data.draw.id, drawTileDiv));
container.appendChild(drawTileDiv); container.appendChild(drawTileDiv);
}
handDiv.appendChild(container); handDiv.appendChild(container);
tilesDisplay.appendChild(handDiv); 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;
} }
function discardTile(idx) { const data = await response.json();
let discarded;
if (idx === 'draw') { // Ajouter une animation de suppression
// Défausse la tuile piochée tileElement.style.opacity = '0';
discarded = drawTile; tileElement.style.transition = 'opacity 0.3s ease';
drawTile = null;
} else { setTimeout(() => {
// Défausse une tuile de la main // Mettre à jour la main interactive avec la nouvelle main
discarded = interactiveHand.splice(idx, 1)[0]; updateInteractiveHand(data.hand, data.draw);
if (drawTile) { }, 300);
interactiveHand.push(drawTile); } catch (error) {
drawTile = null; console.error('Erreur:', error);
} }
} }
// 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 // Fonction pour mettre à jour la main interactive
renderInteractiveHand(); function updateInteractiveHand(handData, drawData) {
const handTiles = document.getElementById('interactive-tiles');
handTiles.innerHTML = '';
handData.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);
});
// Mettre à jour le draw
const container = handTiles.parentElement;
const oldDrawTile = container.querySelector('.draw-tile');
if (oldDrawTile) {
oldDrawTile.remove();
}
if (drawData) {
const drawTileDiv = document.createElement('div');
drawTileDiv.className = 'hand-tile-item draw-tile';
drawTileDiv.innerHTML = drawData.svg;
drawTileDiv.dataset.tileId = drawData.id;
drawTileDiv.style.cursor = 'pointer';
drawTileDiv.addEventListener('click', () => discardTile(drawData.id, drawTileDiv));
container.appendChild(drawTileDiv);
}
}
</script> </script>
</body> </body>
</html> </html>

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

View file

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

View file

@ -34,7 +34,14 @@ http {
add_header Cache-Control "public, immutable"; 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) # Servir les fichiers statiques la fin, après les autres locations)
location / { location / {