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
|
FROM nginx:alpine
|
||||||
|
# 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"]
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
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:
|
||||||
|
|
@ -20,6 +9,4 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
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>
|
<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')
|
||||||
|
|
@ -185,116 +186,108 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let allTiles = [];
|
// Générer les tuiles localement
|
||||||
|
const allTiles = Tile.generateAll();
|
||||||
|
const tilesDisplay = document.getElementById('tiles-display');
|
||||||
|
|
||||||
// Récupérer les tuiles
|
// Man
|
||||||
fetch('/api/tiles')
|
let section = document.createElement('div');
|
||||||
.then(r => r.json())
|
section.className = 'tiles-family';
|
||||||
.then(data => {
|
const manTitle = document.createElement('h3');
|
||||||
allTiles = data.tiles;
|
manTitle.textContent = 'Caractères';
|
||||||
|
const manGrid = document.createElement('div');
|
||||||
const tilesDisplay = document.getElementById('tiles-display');
|
manGrid.className = 'tiles-grid';
|
||||||
|
section.appendChild(manTitle);
|
||||||
// Man
|
section.appendChild(manGrid);
|
||||||
let section = document.createElement('div');
|
for (let i = 0; i < 9; i++) {
|
||||||
section.className = 'tiles-family';
|
const tileDiv = document.createElement('div');
|
||||||
const manTitle = document.createElement('h3');
|
tileDiv.className = 'tile-item';
|
||||||
manTitle.textContent = 'Caractères';
|
tileDiv.innerHTML = allTiles[i].getSVG();
|
||||||
const manGrid = document.createElement('div');
|
manGrid.appendChild(tileDiv);
|
||||||
manGrid.className = 'tiles-grid';
|
}
|
||||||
section.appendChild(manTitle);
|
tilesDisplay.appendChild(section);
|
||||||
section.appendChild(manGrid);
|
|
||||||
for (let i = 0; i < 9; i++) {
|
|
||||||
const tileDiv = document.createElement('div');
|
|
||||||
tileDiv.className = 'tile-item';
|
|
||||||
tileDiv.innerHTML = allTiles[i].svg;
|
|
||||||
manGrid.appendChild(tileDiv);
|
|
||||||
}
|
|
||||||
tilesDisplay.appendChild(section);
|
|
||||||
|
|
||||||
// Pin
|
|
||||||
section = document.createElement('div');
|
|
||||||
section.className = 'tiles-family';
|
|
||||||
const pinTitle = document.createElement('h3');
|
|
||||||
pinTitle.textContent = 'Ronds';
|
|
||||||
const pinGrid = document.createElement('div');
|
|
||||||
pinGrid.className = 'tiles-grid';
|
|
||||||
section.appendChild(pinTitle);
|
|
||||||
section.appendChild(pinGrid);
|
|
||||||
for (let i = 9; i < 18; i++) {
|
|
||||||
const tileDiv = document.createElement('div');
|
|
||||||
tileDiv.className = 'tile-item';
|
|
||||||
tileDiv.innerHTML = allTiles[i].svg;
|
|
||||||
pinGrid.appendChild(tileDiv);
|
|
||||||
}
|
|
||||||
tilesDisplay.appendChild(section);
|
|
||||||
|
|
||||||
// Sou
|
|
||||||
section = document.createElement('div');
|
|
||||||
section.className = 'tiles-family';
|
|
||||||
const souTitle = document.createElement('h3');
|
|
||||||
souTitle.textContent = 'Bambous';
|
|
||||||
const souGrid = document.createElement('div');
|
|
||||||
souGrid.className = 'tiles-grid';
|
|
||||||
section.appendChild(souTitle);
|
|
||||||
section.appendChild(souGrid);
|
|
||||||
for (let i = 18; i < 27; i++) {
|
|
||||||
const tileDiv = document.createElement('div');
|
|
||||||
tileDiv.className = 'tile-item';
|
|
||||||
tileDiv.innerHTML = allTiles[i].svg;
|
|
||||||
souGrid.appendChild(tileDiv);
|
|
||||||
}
|
|
||||||
tilesDisplay.appendChild(section);
|
|
||||||
|
|
||||||
// Ligne des noms de vents
|
// Pin
|
||||||
const windNamesDiv = document.createElement('div');
|
section = document.createElement('div');
|
||||||
windNamesDiv.className = 'wind-names';
|
section.className = 'tiles-family';
|
||||||
windNamesDiv.innerHTML = '<div>Est</div><div>Sud</div><div>Ouest</div><div>Nord</div>';
|
const pinTitle = document.createElement('h3');
|
||||||
tilesDisplay.appendChild(windNamesDiv);
|
pinTitle.textContent = 'Ronds';
|
||||||
|
const pinGrid = document.createElement('div');
|
||||||
|
pinGrid.className = 'tiles-grid';
|
||||||
|
section.appendChild(pinTitle);
|
||||||
|
section.appendChild(pinGrid);
|
||||||
|
for (let i = 9; i < 18; i++) {
|
||||||
|
const tileDiv = document.createElement('div');
|
||||||
|
tileDiv.className = 'tile-item';
|
||||||
|
tileDiv.innerHTML = allTiles[i].getSVG();
|
||||||
|
pinGrid.appendChild(tileDiv);
|
||||||
|
}
|
||||||
|
tilesDisplay.appendChild(section);
|
||||||
|
|
||||||
// Vents
|
// Sou
|
||||||
section = document.createElement('div');
|
section = document.createElement('div');
|
||||||
section.className = 'tiles-family';
|
section.className = 'tiles-family';
|
||||||
const windTitle = document.createElement('h3');
|
const souTitle = document.createElement('h3');
|
||||||
windTitle.textContent = 'Vents';
|
souTitle.textContent = 'Bambous';
|
||||||
const windGrid = document.createElement('div');
|
const souGrid = document.createElement('div');
|
||||||
windGrid.className = 'tiles-grid';
|
souGrid.className = 'tiles-grid';
|
||||||
windGrid.style.gridTemplateColumns = 'repeat(4, minmax(0, 1fr))';
|
section.appendChild(souTitle);
|
||||||
section.appendChild(windTitle);
|
section.appendChild(souGrid);
|
||||||
section.appendChild(windGrid);
|
for (let i = 18; i < 27; i++) {
|
||||||
const windNames = ['Est', 'Sud', 'Ouest', 'Nord'];
|
const tileDiv = document.createElement('div');
|
||||||
for (let i = 27; i < 31; i++) {
|
tileDiv.className = 'tile-item';
|
||||||
const tileDiv = document.createElement('div');
|
tileDiv.innerHTML = allTiles[i].getSVG();
|
||||||
tileDiv.className = 'tile-item';
|
souGrid.appendChild(tileDiv);
|
||||||
tileDiv.innerHTML = allTiles[i].svg;
|
}
|
||||||
windGrid.appendChild(tileDiv);
|
tilesDisplay.appendChild(section);
|
||||||
}
|
|
||||||
tilesDisplay.appendChild(section);
|
|
||||||
|
|
||||||
// Ligne des noms de dragons
|
// Ligne des noms de vents
|
||||||
const dragonNamesDiv = document.createElement('div');
|
const windNamesDiv = document.createElement('div');
|
||||||
dragonNamesDiv.className = 'dragon-names';
|
windNamesDiv.className = 'wind-names';
|
||||||
dragonNamesDiv.innerHTML = '<div>Blanc</div><div>Vert</div><div>Rouge</div>';
|
windNamesDiv.innerHTML = '<div>Est</div><div>Sud</div><div>Ouest</div><div>Nord</div>';
|
||||||
tilesDisplay.appendChild(dragonNamesDiv);
|
tilesDisplay.appendChild(windNamesDiv);
|
||||||
|
|
||||||
// Dragons
|
// Vents
|
||||||
section = document.createElement('div');
|
section = document.createElement('div');
|
||||||
section.className = 'tiles-family';
|
section.className = 'tiles-family';
|
||||||
const dragonTitle = document.createElement('h3');
|
const windTitle = document.createElement('h3');
|
||||||
dragonTitle.textContent = 'Dragons';
|
windTitle.textContent = 'Vents';
|
||||||
const dragonGrid = document.createElement('div');
|
const windGrid = document.createElement('div');
|
||||||
dragonGrid.className = 'tiles-grid';
|
windGrid.className = 'tiles-grid';
|
||||||
dragonGrid.style.gridTemplateColumns = 'repeat(3, minmax(0, 1fr))';
|
windGrid.style.gridTemplateColumns = 'repeat(4, minmax(0, 1fr))';
|
||||||
section.appendChild(dragonTitle);
|
section.appendChild(windTitle);
|
||||||
section.appendChild(dragonGrid);
|
section.appendChild(windGrid);
|
||||||
for (let i = 31; i < 34; 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].svg;
|
tileDiv.innerHTML = allTiles[i].getSVG();
|
||||||
dragonGrid.appendChild(tileDiv);
|
windGrid.appendChild(tileDiv);
|
||||||
}
|
}
|
||||||
tilesDisplay.appendChild(section);
|
tilesDisplay.appendChild(section);
|
||||||
});
|
|
||||||
|
// Ligne des noms de dragons
|
||||||
|
const dragonNamesDiv = document.createElement('div');
|
||||||
|
dragonNamesDiv.className = 'dragon-names';
|
||||||
|
dragonNamesDiv.innerHTML = '<div>Blanc</div><div>Vert</div><div>Rouge</div>';
|
||||||
|
tilesDisplay.appendChild(dragonNamesDiv);
|
||||||
|
|
||||||
|
// Dragons
|
||||||
|
section = document.createElement('div');
|
||||||
|
section.className = 'tiles-family';
|
||||||
|
const dragonTitle = document.createElement('h3');
|
||||||
|
dragonTitle.textContent = 'Dragons';
|
||||||
|
const dragonGrid = document.createElement('div');
|
||||||
|
dragonGrid.className = 'tiles-grid';
|
||||||
|
dragonGrid.style.gridTemplateColumns = 'repeat(3, minmax(0, 1fr))';
|
||||||
|
section.appendChild(dragonTitle);
|
||||||
|
section.appendChild(dragonGrid);
|
||||||
|
for (let i = 31; i < 34; i++) {
|
||||||
|
const tileDiv = document.createElement('div');
|
||||||
|
tileDiv.className = 'tile-item';
|
||||||
|
tileDiv.innerHTML = allTiles[i].getSVG();
|
||||||
|
dragonGrid.appendChild(tileDiv);
|
||||||
|
}
|
||||||
|
tilesDisplay.appendChild(section);
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -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/hand.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')
|
||||||
|
|
@ -171,47 +171,52 @@
|
||||||
handsContainer.className = 'hands-container';
|
handsContainer.className = 'hands-container';
|
||||||
tilesDisplay.appendChild(handsContainer);
|
tilesDisplay.appendChild(handsContainer);
|
||||||
|
|
||||||
// Créer et afficher trois mains aléatoires
|
// Générer et afficher trois mains aléatoires localement
|
||||||
for (let i = 0; i < 3; i++) {
|
function shuffle(array) {
|
||||||
const hand = new Hand({
|
for (let i = array.length - 1; i > 0; i--) {
|
||||||
label: `Main ${i + 1}`,
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
position: 'top-label',
|
[array[i], array[j]] = [array[j], array[i]];
|
||||||
tilt: 'none',
|
}
|
||||||
containerId: 'tiles-display'
|
}
|
||||||
|
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;
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const tiles = allTiles.slice();
|
||||||
fetch(hand.apiUrl)
|
shuffle(tiles);
|
||||||
.then(r => r.json())
|
const handTilesArr = tiles.slice(0, 13);
|
||||||
.then(data => {
|
sortHand(handTilesArr);
|
||||||
const handDiv = document.createElement('div');
|
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');
|
||||||
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');
|
||||||
|
container.className = 'hand-container';
|
||||||
const container = document.createElement('div');
|
const handTiles = document.createElement('div');
|
||||||
container.className = 'hand-container';
|
handTiles.className = 'hand-tiles';
|
||||||
|
handTilesArr.forEach(tile => {
|
||||||
const handTiles = document.createElement('div');
|
const tileDiv = document.createElement('div');
|
||||||
handTiles.className = 'hand-tiles';
|
tileDiv.className = 'hand-tile-item';
|
||||||
|
tileDiv.innerHTML = tile.getSVG();
|
||||||
data.hand.forEach(tile => {
|
handTiles.appendChild(tileDiv);
|
||||||
const tileDiv = document.createElement('div');
|
});
|
||||||
tileDiv.className = 'hand-tile-item';
|
container.appendChild(handTiles);
|
||||||
tileDiv.innerHTML = tile.svg;
|
handDiv.appendChild(container);
|
||||||
handTiles.appendChild(tileDiv);
|
handsContainer.appendChild(handDiv);
|
||||||
});
|
|
||||||
|
|
||||||
container.appendChild(handTiles);
|
|
||||||
handDiv.appendChild(container);
|
|
||||||
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
|
||||||
|
|
@ -220,116 +225,84 @@
|
||||||
interactiveTitle.textContent = 'Main interactive';
|
interactiveTitle.textContent = 'Main interactive';
|
||||||
tilesDisplay.appendChild(interactiveTitle);
|
tilesDisplay.appendChild(interactiveTitle);
|
||||||
|
|
||||||
// Créer et afficher la main interactive avec une tuile piochée
|
// Générer et afficher une main interactive locale avec une tuile piochée
|
||||||
fetch('/api/hand_with_draw')
|
let interactiveTiles = allTiles.slice();
|
||||||
.then(r => r.json())
|
shuffle(interactiveTiles);
|
||||||
.then(data => {
|
let interactiveHand = interactiveTiles.slice(0, 13);
|
||||||
const handDiv = document.createElement('div');
|
sortHand(interactiveHand);
|
||||||
handDiv.className = 'hand-display top-label tilt-none';
|
let drawTile = interactiveTiles[13];
|
||||||
handDiv.id = 'interactive-hand';
|
let wall = interactiveTiles.slice(14); // Le reste du paquet pour la pioche
|
||||||
|
|
||||||
const label = document.createElement('div');
|
function renderInteractiveHand() {
|
||||||
label.className = 'hand-label';
|
// Nettoyer l'affichage précédent
|
||||||
label.textContent = 'Votre main';
|
let oldDiv = document.getElementById('interactive-hand');
|
||||||
handDiv.appendChild(label);
|
if (oldDiv) oldDiv.remove();
|
||||||
|
const handDiv = document.createElement('div');
|
||||||
// Conteneur qui gère l'affichage avec flex (gère auto le draw si présent)
|
handDiv.className = 'hand-display top-label tilt-none';
|
||||||
const container = document.createElement('div');
|
handDiv.id = 'interactive-hand';
|
||||||
container.className = 'hand-container';
|
const label = document.createElement('div');
|
||||||
|
label.className = 'hand-label';
|
||||||
// Créer le grid des 13 tuiles
|
label.textContent = 'Votre main';
|
||||||
const handTiles = document.createElement('div');
|
handDiv.appendChild(label);
|
||||||
handTiles.className = 'hand-tiles';
|
const container = document.createElement('div');
|
||||||
handTiles.id = 'interactive-tiles';
|
container.className = 'hand-container';
|
||||||
|
const handTiles = document.createElement('div');
|
||||||
data.hand.forEach(tile => {
|
handTiles.className = 'hand-tiles';
|
||||||
const tileDiv = document.createElement('div');
|
handTiles.id = 'interactive-tiles';
|
||||||
tileDiv.className = 'hand-tile-item';
|
interactiveHand.forEach((tile, idx) => {
|
||||||
tileDiv.innerHTML = tile.svg;
|
|
||||||
tileDiv.dataset.tileId = tile.id;
|
|
||||||
tileDiv.style.cursor = 'pointer';
|
|
||||||
tileDiv.addEventListener('click', () => discardTile(tile.id, tileDiv));
|
|
||||||
handTiles.appendChild(tileDiv);
|
|
||||||
});
|
|
||||||
|
|
||||||
container.appendChild(handTiles);
|
|
||||||
|
|
||||||
const drawTileDiv = document.createElement('div');
|
|
||||||
drawTileDiv.className = 'hand-tile-item draw-tile';
|
|
||||||
drawTileDiv.innerHTML = data.draw.svg;
|
|
||||||
drawTileDiv.dataset.tileId = data.draw.id;
|
|
||||||
drawTileDiv.style.cursor = 'pointer';
|
|
||||||
drawTileDiv.addEventListener('click', () => discardTile(data.draw.id, drawTileDiv));
|
|
||||||
container.appendChild(drawTileDiv);
|
|
||||||
|
|
||||||
handDiv.appendChild(container);
|
|
||||||
tilesDisplay.appendChild(handDiv);
|
|
||||||
})
|
|
||||||
.catch(error => console.error('Erreur:', error));
|
|
||||||
|
|
||||||
// Fonction pour défausser une tuile
|
|
||||||
async function discardTile(tileId, tileElement) {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/discard', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ tile_id: tileId })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error('Erreur lors de la défausse');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
// Ajouter une animation de suppression
|
|
||||||
tileElement.style.opacity = '0';
|
|
||||||
tileElement.style.transition = 'opacity 0.3s ease';
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
// Mettre à jour la main interactive avec la nouvelle main
|
|
||||||
updateInteractiveHand(data.hand, data.draw);
|
|
||||||
}, 300);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erreur:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fonction pour mettre à jour la main interactive
|
|
||||||
function updateInteractiveHand(handData, drawData) {
|
|
||||||
const handTiles = document.getElementById('interactive-tiles');
|
|
||||||
handTiles.innerHTML = '';
|
|
||||||
|
|
||||||
handData.forEach(tile => {
|
|
||||||
const tileDiv = document.createElement('div');
|
const tileDiv = document.createElement('div');
|
||||||
tileDiv.className = 'hand-tile-item';
|
tileDiv.className = 'hand-tile-item';
|
||||||
tileDiv.innerHTML = tile.svg;
|
tileDiv.innerHTML = tile.getSVG();
|
||||||
tileDiv.dataset.tileId = tile.id;
|
tileDiv.dataset.tileId = tile.id;
|
||||||
tileDiv.style.cursor = 'pointer';
|
tileDiv.style.cursor = 'pointer';
|
||||||
tileDiv.addEventListener('click', () => discardTile(tile.id, tileDiv));
|
tileDiv.addEventListener('click', () => discardTile(idx));
|
||||||
handTiles.appendChild(tileDiv);
|
handTiles.appendChild(tileDiv);
|
||||||
});
|
});
|
||||||
|
container.appendChild(handTiles);
|
||||||
// Mettre à jour le draw
|
// Draw tile
|
||||||
const container = handTiles.parentElement;
|
if (drawTile) {
|
||||||
const oldDrawTile = container.querySelector('.draw-tile');
|
|
||||||
if (oldDrawTile) {
|
|
||||||
oldDrawTile.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (drawData) {
|
|
||||||
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 = drawData.svg;
|
drawTileDiv.innerHTML = drawTile.getSVG();
|
||||||
drawTileDiv.dataset.tileId = drawData.id;
|
drawTileDiv.dataset.tileId = drawTile.id;
|
||||||
drawTileDiv.style.cursor = 'pointer';
|
drawTileDiv.style.cursor = 'pointer';
|
||||||
drawTileDiv.addEventListener('click', () => discardTile(drawData.id, drawTileDiv));
|
drawTileDiv.addEventListener('click', () => discardTile('draw'));
|
||||||
container.appendChild(drawTileDiv);
|
container.appendChild(drawTileDiv);
|
||||||
}
|
}
|
||||||
|
handDiv.appendChild(container);
|
||||||
|
tilesDisplay.appendChild(handDiv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function discardTile(idx) {
|
||||||
|
let discarded;
|
||||||
|
if (idx === 'draw') {
|
||||||
|
// Défausse la tuile piochée
|
||||||
|
discarded = drawTile;
|
||||||
|
drawTile = null;
|
||||||
|
} else {
|
||||||
|
// Défausse une tuile de la main
|
||||||
|
discarded = interactiveHand.splice(idx, 1)[0];
|
||||||
|
if (drawTile) {
|
||||||
|
interactiveHand.push(drawTile);
|
||||||
|
drawTile = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remettre la tuile défaussée dans la pioche (wall) et mélanger le mur
|
||||||
|
if (discarded) {
|
||||||
|
wall.push(discarded);
|
||||||
|
shuffle(wall);
|
||||||
|
}
|
||||||
|
// Trier la main après modification
|
||||||
|
sortHand(interactiveHand);
|
||||||
|
// Nouvelle pioche si possible
|
||||||
|
if (!drawTile && wall.length > 0) {
|
||||||
|
drawTile = wall.shift();
|
||||||
|
}
|
||||||
|
renderInteractiveHand();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Afficher la main interactive au chargement
|
||||||
|
renderInteractiveHand();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -85,6 +85,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 src="/frontend/js/hand.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Charger le menu
|
// Charger le menu
|
||||||
|
|
@ -107,21 +108,18 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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')
|
const display = document.getElementById(displayId);
|
||||||
.then(r => r.json())
|
display.innerHTML = '';
|
||||||
.then(data => {
|
tileIndices.forEach(idx => {
|
||||||
const display = document.getElementById(displayId);
|
const tile = allTiles[idx];
|
||||||
tileIndices.forEach(idx => {
|
if (tile) {
|
||||||
const tile = data.tiles[idx];
|
const tileDiv = document.createElement('div');
|
||||||
if (tile) {
|
tileDiv.innerHTML = tile.getSVG();
|
||||||
const tileDiv = document.createElement('div');
|
display.appendChild(tileDiv);
|
||||||
tileDiv.innerHTML = tile.svg;
|
}
|
||||||
display.appendChild(tileDiv);
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(error => console.error('Erreur:', error));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Afficher les combinaisons
|
// Afficher les combinaisons
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,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 src="/frontend/js/hand.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Charger le menu
|
// Charger le menu
|
||||||
|
|
@ -118,21 +119,18 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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')
|
const display = document.getElementById(displayId);
|
||||||
.then(r => r.json())
|
display.innerHTML = '';
|
||||||
.then(data => {
|
tileIndices.forEach(idx => {
|
||||||
const display = document.getElementById(displayId);
|
const tile = allTiles[idx];
|
||||||
tileIndices.forEach(idx => {
|
if (tile) {
|
||||||
const tile = data.tiles[idx];
|
const tileDiv = document.createElement('div');
|
||||||
if (tile) {
|
tileDiv.innerHTML = tile.getSVG();
|
||||||
const tileDiv = document.createElement('div');
|
display.appendChild(tileDiv);
|
||||||
tileDiv.innerHTML = tile.svg;
|
}
|
||||||
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
|
|
@ -94,6 +94,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>
|
<script>
|
||||||
// Charger le menu
|
// Charger le menu
|
||||||
fetch('/components/menu.html')
|
fetch('/components/menu.html')
|
||||||
|
|
@ -114,23 +115,19 @@
|
||||||
: '<img src="/img/tilineau/idle.png" alt="Tilineau">';
|
: '<img src="/img/tilineau/idle.png" alt="Tilineau">';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fonction utilitaire pour afficher les tuiles
|
// Affichage local des tuiles sans backend
|
||||||
function displayTiles(displayId, tileIndices) {
|
function displayTiles(displayId, tileIndices) {
|
||||||
fetch('/api/tiles')
|
const allTiles = Tile.generateAll();
|
||||||
.then(r => r.json())
|
const display = document.getElementById(displayId);
|
||||||
.then(data => {
|
tileIndices.forEach(idx => {
|
||||||
const display = document.getElementById(displayId);
|
const tile = allTiles[idx];
|
||||||
tileIndices.forEach(idx => {
|
if (tile) {
|
||||||
const tile = data.tiles[idx];
|
const tileDiv = document.createElement('div');
|
||||||
if (tile) {
|
tileDiv.innerHTML = tile.getSVG();
|
||||||
const tileDiv = document.createElement('div');
|
display.appendChild(tileDiv);
|
||||||
tileDiv.innerHTML = tile.svg;
|
}
|
||||||
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]);
|
||||||
displayTiles('tiles-display-2', [4, 5, 6, 7, 7, 7, 14, 14, 20, 21, 22, 24, 24, 24]);
|
displayTiles('tiles-display-2', [4, 5, 6, 7, 7, 7, 14, 14, 20, 21, 22, 24, 24, 24]);
|
||||||
|
|
@ -140,7 +137,7 @@
|
||||||
displayTiles('tiles-display-6', [18, 18, 18, 20, 21, 22, 21, 22, 23, 25, 25, 27, 27, 27]);
|
displayTiles('tiles-display-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>
|
||||||
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 |
21
index.html
21
index.html
|
|
@ -73,6 +73,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>
|
<script>
|
||||||
// Charger le menu
|
// Charger le menu
|
||||||
fetch('/components/menu.html')
|
fetch('/components/menu.html')
|
||||||
|
|
@ -95,17 +96,10 @@
|
||||||
: '<img src="/img/tilineau/speaking.png" alt="Tilineau">';
|
: '<img src="/img/tilineau/speaking.png" alt="Tilineau">';
|
||||||
});
|
});
|
||||||
|
|
||||||
let allTiles = [];
|
// Générer les tuiles localement
|
||||||
|
const allTiles = Tile.generateAll();
|
||||||
// Récupérer les tuiles
|
// Lancer la pluie de tuiles
|
||||||
fetch('/api/tiles')
|
startTileRain();
|
||||||
.then(r => r.json())
|
|
||||||
.then(data => {
|
|
||||||
allTiles = data.tiles;
|
|
||||||
|
|
||||||
// Lancer la pluie de tuiles
|
|
||||||
startTileRain();
|
|
||||||
});
|
|
||||||
|
|
||||||
function startTileRain() {
|
function startTileRain() {
|
||||||
const container = document.getElementById('tile-rain');
|
const container = document.getElementById('tile-rain');
|
||||||
|
|
@ -113,18 +107,15 @@
|
||||||
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.svg;
|
tileElem.innerHTML = randomTile.getSVG();
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,14 +34,7 @@ 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 / {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue