Compare commits
22 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b7547ac35 | ||
|
|
86e0bbd464 | ||
|
|
67f3973913 | ||
|
|
980cc7bbd3 | ||
|
|
d16ec925ea | ||
|
|
f50c5f7bf2 | ||
|
|
bf3f3a2879 | ||
|
|
8d04e99a85 | ||
|
|
662ee8fd10 | ||
|
|
169aa0e72b | ||
|
|
b28046d935 | ||
|
|
4dabff91d3 | ||
|
|
39c1cc7d69 | ||
|
|
655e1e5c1c | ||
|
|
f205ccf70e | ||
|
|
4bdf3597c4 | ||
|
|
3e0c6e3abe | ||
|
|
b131301495 | ||
|
|
4f1d603d2e | ||
|
|
52570f80b1 | ||
|
|
0c15a9e3cb | ||
|
|
d41b6349d1 |
24 changed files with 1618 additions and 1155 deletions
28
Dockerfile
28
Dockerfile
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
Flask==3.0.0
|
||||
flask-cors==4.0.0
|
||||
gunicorn==21.2.0
|
||||
gevent>=1.4
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
219
frontend/js/game.js
Normal 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
110
frontend/js/tile.js
Normal 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;
|
||||
|
|
@ -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,14 +186,8 @@
|
|||
}
|
||||
});
|
||||
|
||||
let allTiles = [];
|
||||
|
||||
// Récupérer les tuiles
|
||||
fetch('/api/tiles')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
allTiles = data.tiles;
|
||||
|
||||
// Générer les tuiles localement
|
||||
const allTiles = Tile.generateAll();
|
||||
const tilesDisplay = document.getElementById('tiles-display');
|
||||
|
||||
// Man
|
||||
|
|
@ -207,7 +202,7 @@
|
|||
for (let i = 0; i < 9; i++) {
|
||||
const tileDiv = document.createElement('div');
|
||||
tileDiv.className = 'tile-item';
|
||||
tileDiv.innerHTML = allTiles[i].svg;
|
||||
tileDiv.innerHTML = allTiles[i].getSVG();
|
||||
manGrid.appendChild(tileDiv);
|
||||
}
|
||||
tilesDisplay.appendChild(section);
|
||||
|
|
@ -224,7 +219,7 @@
|
|||
for (let i = 9; i < 18; i++) {
|
||||
const tileDiv = document.createElement('div');
|
||||
tileDiv.className = 'tile-item';
|
||||
tileDiv.innerHTML = allTiles[i].svg;
|
||||
tileDiv.innerHTML = allTiles[i].getSVG();
|
||||
pinGrid.appendChild(tileDiv);
|
||||
}
|
||||
tilesDisplay.appendChild(section);
|
||||
|
|
@ -241,7 +236,7 @@
|
|||
for (let i = 18; i < 27; i++) {
|
||||
const tileDiv = document.createElement('div');
|
||||
tileDiv.className = 'tile-item';
|
||||
tileDiv.innerHTML = allTiles[i].svg;
|
||||
tileDiv.innerHTML = allTiles[i].getSVG();
|
||||
souGrid.appendChild(tileDiv);
|
||||
}
|
||||
tilesDisplay.appendChild(section);
|
||||
|
|
@ -262,11 +257,10 @@
|
|||
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;
|
||||
tileDiv.innerHTML = allTiles[i].getSVG();
|
||||
windGrid.appendChild(tileDiv);
|
||||
}
|
||||
tilesDisplay.appendChild(section);
|
||||
|
|
@ -290,11 +284,10 @@
|
|||
for (let i = 31; i < 34; i++) {
|
||||
const tileDiv = document.createElement('div');
|
||||
tileDiv.className = 'tile-item';
|
||||
tileDiv.innerHTML = allTiles[i].svg;
|
||||
tileDiv.innerHTML = allTiles[i].getSVG();
|
||||
dragonGrid.appendChild(tileDiv);
|
||||
}
|
||||
tilesDisplay.appendChild(section);
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
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 ${hand.position} tilt-${hand.tilt}`;
|
||||
|
||||
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';
|
||||
|
||||
data.hand.forEach(tile => {
|
||||
handTilesArr.forEach(tile => {
|
||||
const tileDiv = document.createElement('div');
|
||||
tileDiv.className = 'hand-tile-item';
|
||||
tileDiv.innerHTML = tile.svg;
|
||||
tileDiv.innerHTML = tile.getSVG();
|
||||
handTiles.appendChild(tileDiv);
|
||||
});
|
||||
|
||||
container.appendChild(handTiles);
|
||||
handDiv.appendChild(container);
|
||||
handsContainer.appendChild(handDiv);
|
||||
})
|
||||
.catch(error => console.error('Erreur:', error));
|
||||
}
|
||||
|
||||
// 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 => {
|
||||
// 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);
|
||||
|
||||
// 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 => {
|
||||
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);
|
||||
});
|
||||
|
||||
container.appendChild(handTiles);
|
||||
|
||||
// Draw tile
|
||||
if (drawTile) {
|
||||
const drawTileDiv = document.createElement('div');
|
||||
drawTileDiv.className = 'hand-tile-item draw-tile';
|
||||
drawTileDiv.innerHTML = data.draw.svg;
|
||||
drawTileDiv.dataset.tileId = data.draw.id;
|
||||
drawTileDiv.innerHTML = drawTile.getSVG();
|
||||
drawTileDiv.dataset.tileId = drawTile.id;
|
||||
drawTileDiv.style.cursor = 'pointer';
|
||||
drawTileDiv.addEventListener('click', () => discardTile(data.draw.id, drawTileDiv));
|
||||
drawTileDiv.addEventListener('click', () => discardTile('draw'));
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction pour mettre à jour la main interactive
|
||||
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();
|
||||
// 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();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
// Afficher la main interactive au chargement
|
||||
renderInteractiveHand();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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);
|
||||
display.innerHTML = '';
|
||||
tileIndices.forEach(idx => {
|
||||
const tile = data.tiles[idx];
|
||||
const tile = allTiles[idx];
|
||||
if (tile) {
|
||||
const tileDiv = document.createElement('div');
|
||||
tileDiv.innerHTML = tile.svg;
|
||||
tileDiv.innerHTML = tile.getSVG();
|
||||
display.appendChild(tileDiv);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(error => console.error('Erreur:', error));
|
||||
}
|
||||
|
||||
// Afficher les combinaisons
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
display.innerHTML = '';
|
||||
tileIndices.forEach(idx => {
|
||||
const tile = data.tiles[idx];
|
||||
const tile = allTiles[idx];
|
||||
if (tile) {
|
||||
const tileDiv = document.createElement('div');
|
||||
tileDiv.innerHTML = tile.svg;
|
||||
tileDiv.innerHTML = tile.getSVG();
|
||||
display.appendChild(tileDiv);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(error => console.error('Erreur:', error));
|
||||
}
|
||||
|
||||
// Afficher les combinaisons
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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,22 +115,18 @@
|
|||
: '<img src="/img/tilineau/idle.png" alt="Tilineau">';
|
||||
});
|
||||
|
||||
// Fonction utilitaire pour afficher les tuiles
|
||||
// Affichage local des tuiles sans backend
|
||||
function displayTiles(displayId, tileIndices) {
|
||||
fetch('/api/tiles')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const allTiles = Tile.generateAll();
|
||||
const display = document.getElementById(displayId);
|
||||
tileIndices.forEach(idx => {
|
||||
const tile = data.tiles[idx];
|
||||
const tile = allTiles[idx];
|
||||
if (tile) {
|
||||
const tileDiv = document.createElement('div');
|
||||
tileDiv.innerHTML = tile.svg;
|
||||
tileDiv.innerHTML = tile.getSVG();
|
||||
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]);
|
||||
|
|
@ -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>
|
||||
BIN
img/tilineau/workinprogress.png
Normal file
BIN
img/tilineau/workinprogress.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
17
index.html
17
index.html
|
|
@ -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;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 / {
|
||||
|
|
|
|||
Loading…
Reference in a new issue