Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
| 20147b32af |
25 changed files with 1283 additions and 1625 deletions
28
Dockerfile
28
Dockerfile
|
|
@ -1,2 +1,26 @@
|
||||||
FROM nginx:alpine
|
FROM python:3.11-slim
|
||||||
# Ce Dockerfile n'est plus utilisé, le service est servi par nginx via docker-compose.
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Installer les dépendances Python
|
||||||
|
COPY backend/src/requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copier le code backend
|
||||||
|
COPY backend/src/ /app/
|
||||||
|
|
||||||
|
# Copier le frontend
|
||||||
|
COPY frontend /app/frontend
|
||||||
|
COPY components /app/components
|
||||||
|
COPY img /app/img
|
||||||
|
COPY index.html /app/
|
||||||
|
COPY *.json /app/
|
||||||
|
COPY vite.config.ts /app/
|
||||||
|
|
||||||
|
# Port Flask
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Définir PYTHONPATH pour que les imports relatifs fonctionnent
|
||||||
|
ENV PYTHONPATH=/app:$PYTHONPATH
|
||||||
|
|
||||||
|
CMD ["gunicorn", "-w", "1", "-k", "gevent", "-b", "0.0.0.0:5000", "api:app"]
|
||||||
|
|
|
||||||
121
LICENSE
Normal file
121
LICENSE
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
Creative Commons Legal Code
|
||||||
|
|
||||||
|
CC0 1.0 Universal
|
||||||
|
|
||||||
|
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||||
|
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||||
|
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||||
|
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||||
|
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
||||||
|
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
||||||
|
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
||||||
|
HEREUNDER.
|
||||||
|
|
||||||
|
Statement of Purpose
|
||||||
|
|
||||||
|
The laws of most jurisdictions throughout the world automatically confer
|
||||||
|
exclusive Copyright and Related Rights (defined below) upon the creator
|
||||||
|
and subsequent owner(s) (each and all, an "owner") of an original work of
|
||||||
|
authorship and/or a database (each, a "Work").
|
||||||
|
|
||||||
|
Certain owners wish to permanently relinquish those rights to a Work for
|
||||||
|
the purpose of contributing to a commons of creative, cultural and
|
||||||
|
scientific works ("Commons") that the public can reliably and without fear
|
||||||
|
of later claims of infringement build upon, modify, incorporate in other
|
||||||
|
works, reuse and redistribute as freely as possible in any form whatsoever
|
||||||
|
and for any purposes, including without limitation commercial purposes.
|
||||||
|
These owners may contribute to the Commons to promote the ideal of a free
|
||||||
|
culture and the further production of creative, cultural and scientific
|
||||||
|
works, or to gain reputation or greater distribution for their Work in
|
||||||
|
part through the use and efforts of others.
|
||||||
|
|
||||||
|
For these and/or other purposes and motivations, and without any
|
||||||
|
expectation of additional consideration or compensation, the person
|
||||||
|
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
||||||
|
is an owner of Copyright and Related Rights in the Work, voluntarily
|
||||||
|
elects to apply CC0 to the Work and publicly distribute the Work under its
|
||||||
|
terms, with knowledge of his or her Copyright and Related Rights in the
|
||||||
|
Work and the meaning and intended legal effect of CC0 on those rights.
|
||||||
|
|
||||||
|
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||||
|
protected by copyright and related or neighboring rights ("Copyright and
|
||||||
|
Related Rights"). Copyright and Related Rights include, but are not
|
||||||
|
limited to, the following:
|
||||||
|
|
||||||
|
i. the right to reproduce, adapt, distribute, perform, display,
|
||||||
|
communicate, and translate a Work;
|
||||||
|
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||||
|
iii. publicity and privacy rights pertaining to a person's image or
|
||||||
|
likeness depicted in a Work;
|
||||||
|
iv. rights protecting against unfair competition in regards to a Work,
|
||||||
|
subject to the limitations in paragraph 4(a), below;
|
||||||
|
v. rights protecting the extraction, dissemination, use and reuse of data
|
||||||
|
in a Work;
|
||||||
|
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||||
|
European Parliament and of the Council of 11 March 1996 on the legal
|
||||||
|
protection of databases, and under any national implementation
|
||||||
|
thereof, including any amended or successor version of such
|
||||||
|
directive); and
|
||||||
|
vii. other similar, equivalent or corresponding rights throughout the
|
||||||
|
world based on applicable law or treaty, and any national
|
||||||
|
implementations thereof.
|
||||||
|
|
||||||
|
2. Waiver. To the greatest extent permitted by, but not in contravention
|
||||||
|
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
||||||
|
irrevocably and unconditionally waives, abandons, and surrenders all of
|
||||||
|
Affirmer's Copyright and Related Rights and associated claims and causes
|
||||||
|
of action, whether now known or unknown (including existing as well as
|
||||||
|
future claims and causes of action), in the Work (i) in all territories
|
||||||
|
worldwide, (ii) for the maximum duration provided by applicable law or
|
||||||
|
treaty (including future time extensions), (iii) in any current or future
|
||||||
|
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
||||||
|
including without limitation commercial, advertising or promotional
|
||||||
|
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
||||||
|
member of the public at large and to the detriment of Affirmer's heirs and
|
||||||
|
successors, fully intending that such Waiver shall not be subject to
|
||||||
|
revocation, rescission, cancellation, termination, or any other legal or
|
||||||
|
equitable action to disrupt the quiet enjoyment of the Work by the public
|
||||||
|
as contemplated by Affirmer's express Statement of Purpose.
|
||||||
|
|
||||||
|
3. Public License Fallback. Should any part of the Waiver for any reason
|
||||||
|
be judged legally invalid or ineffective under applicable law, then the
|
||||||
|
Waiver shall be preserved to the maximum extent permitted taking into
|
||||||
|
account Affirmer's express Statement of Purpose. In addition, to the
|
||||||
|
extent the Waiver is so judged Affirmer hereby grants to each affected
|
||||||
|
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
||||||
|
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
||||||
|
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
||||||
|
maximum duration provided by applicable law or treaty (including future
|
||||||
|
time extensions), (iii) in any current or future medium and for any number
|
||||||
|
of copies, and (iv) for any purpose whatsoever, including without
|
||||||
|
limitation commercial, advertising or promotional purposes (the
|
||||||
|
"License"). The License shall be deemed effective as of the date CC0 was
|
||||||
|
applied by Affirmer to the Work. Should any part of the License for any
|
||||||
|
reason be judged legally invalid or ineffective under applicable law, such
|
||||||
|
partial invalidity or ineffectiveness shall not invalidate the remainder
|
||||||
|
of the License, and in such case Affirmer hereby affirms that he or she
|
||||||
|
will not (i) exercise any of his or her remaining Copyright and Related
|
||||||
|
Rights in the Work or (ii) assert any associated claims and causes of
|
||||||
|
action with respect to the Work, in either case contrary to Affirmer's
|
||||||
|
express Statement of Purpose.
|
||||||
|
|
||||||
|
4. Limitations and Disclaimers.
|
||||||
|
|
||||||
|
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||||
|
surrendered, licensed or otherwise affected by this document.
|
||||||
|
b. Affirmer offers the Work as-is and makes no representations or
|
||||||
|
warranties of any kind concerning the Work, express, implied,
|
||||||
|
statutory or otherwise, including without limitation warranties of
|
||||||
|
title, merchantability, fitness for a particular purpose, non
|
||||||
|
infringement, or the absence of latent or other defects, accuracy, or
|
||||||
|
the present or absence of errors, whether or not discoverable, all to
|
||||||
|
the greatest extent permissible under applicable law.
|
||||||
|
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||||
|
that may apply to the Work or any use thereof, including without
|
||||||
|
limitation any person's Copyright and Related Rights in the Work.
|
||||||
|
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||||
|
consents, permissions or other rights required for any use of the
|
||||||
|
Work.
|
||||||
|
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||||
|
party to this document and has no duty or obligation with respect to
|
||||||
|
this CC0 or use of the Work.
|
||||||
221
backend/src/api.py
Normal file
221
backend/src/api.py
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
from flask import Flask, jsonify, request
|
||||||
|
from flask_cors import CORS
|
||||||
|
|
||||||
|
from game.tile import *
|
||||||
|
from game.hand import *
|
||||||
|
from game.game import *
|
||||||
|
from game.error import *
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
import threading
|
||||||
|
CORS(app)
|
||||||
|
|
||||||
|
tiles = Tile.generateAll()
|
||||||
|
current_game = None # Stockage de la partie actuelle
|
||||||
|
game_lock = threading.Lock()
|
||||||
|
|
||||||
|
# ============= GET ================
|
||||||
|
@app.route('/api/tiles', methods=['GET'])
|
||||||
|
def get_all_tiles():
|
||||||
|
"""Récupère toutes les tuiles"""
|
||||||
|
tiles_data = [
|
||||||
|
{
|
||||||
|
'id': tile.id,
|
||||||
|
'family': str(tile.family),
|
||||||
|
'value': str(tile.value),
|
||||||
|
'svg': tile.get_img()
|
||||||
|
}
|
||||||
|
for tile in tiles
|
||||||
|
]
|
||||||
|
return jsonify({'tiles': tiles_data})
|
||||||
|
|
||||||
|
@app.route('/api/tiles/<int:tile_id>', methods=['GET'])
|
||||||
|
def get_tile(tile_id):
|
||||||
|
if tile_id < 0 or tile_id >= len(tiles):
|
||||||
|
return jsonify({'error': 'Tuile non trouvée'}), 404
|
||||||
|
|
||||||
|
tile = tiles[tile_id]
|
||||||
|
svg = f'<svg viewBox="0 0 500 700" width="100" height="140">{tile.get_img()}</svg>'
|
||||||
|
return jsonify({
|
||||||
|
'id': tile.id,
|
||||||
|
'family': str(tile.family),
|
||||||
|
'value': str(tile.value),
|
||||||
|
'svg': svg
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.route('/api/random_hand', methods=['GET'])
|
||||||
|
def get_random_hand():
|
||||||
|
"""Récupère une main aléatoire de 13 tuiles"""
|
||||||
|
hand = Hand.random()
|
||||||
|
hand_data = [
|
||||||
|
{
|
||||||
|
'id': tile.id,
|
||||||
|
'family': str(tile.family),
|
||||||
|
'value': str(tile.value),
|
||||||
|
'svg': tile.get_img()
|
||||||
|
}
|
||||||
|
for tile in hand.tiles
|
||||||
|
]
|
||||||
|
return jsonify({'hand': hand_data})
|
||||||
|
|
||||||
|
@app.route('/api/hand_with_draw', methods=['GET'])
|
||||||
|
def get_hand_with_draw():
|
||||||
|
"""Crée une partie et retourne le hand_player avec une tuile piochée"""
|
||||||
|
global current_game
|
||||||
|
with game_lock:
|
||||||
|
current_game = Game()
|
||||||
|
current_game.draw_tile(current_game.hand_player)
|
||||||
|
hand_data = [
|
||||||
|
{
|
||||||
|
'id': tile.id,
|
||||||
|
'family': str(tile.family),
|
||||||
|
'value': str(tile.value),
|
||||||
|
'svg': tile.get_img()
|
||||||
|
}
|
||||||
|
for tile in current_game.hand_player.tiles
|
||||||
|
]
|
||||||
|
draw_data = {
|
||||||
|
'id': current_game.hand_player.draw.id,
|
||||||
|
'family': str(current_game.hand_player.draw.family),
|
||||||
|
'value': str(current_game.hand_player.draw.value),
|
||||||
|
'svg': current_game.hand_player.draw.get_img()
|
||||||
|
} if getattr(current_game.hand_player, 'draw', None) else None
|
||||||
|
return jsonify({'hand': hand_data, 'draw': draw_data})
|
||||||
|
|
||||||
|
@app.route('/api/discard', methods=['POST'])
|
||||||
|
def discard_tile():
|
||||||
|
"""Défausse une tuile de la main du joueur et pioche une nouvelle"""
|
||||||
|
global current_game
|
||||||
|
with game_lock:
|
||||||
|
if current_game is None:
|
||||||
|
return jsonify({'error': 'Aucune partie en cours'}), 400
|
||||||
|
data = request.json
|
||||||
|
tile_id = data.get('tile_id')
|
||||||
|
if tile_id is None:
|
||||||
|
return jsonify({'error': 'tile_id manquant'}), 400
|
||||||
|
current_game.put_tile(current_game.hand_player, tile_id)
|
||||||
|
current_game.draw_tile(current_game.hand_player)
|
||||||
|
hand_data = [
|
||||||
|
{
|
||||||
|
'id': tile.id,
|
||||||
|
'family': str(tile.family),
|
||||||
|
'value': str(tile.value),
|
||||||
|
'svg': tile.get_img()
|
||||||
|
}
|
||||||
|
for tile in current_game.hand_player.tiles
|
||||||
|
]
|
||||||
|
draw_data = {
|
||||||
|
'id': current_game.hand_player.draw.id,
|
||||||
|
'family': str(current_game.hand_player.draw.family),
|
||||||
|
'value': str(current_game.hand_player.draw.value),
|
||||||
|
'svg': current_game.hand_player.draw.get_img()
|
||||||
|
} if getattr(current_game.hand_player, 'draw', None) else None
|
||||||
|
return jsonify({'hand': hand_data, 'draw': draw_data})
|
||||||
|
|
||||||
|
# ============== POST ================
|
||||||
|
# ============== GAME MANAGEMENT ================
|
||||||
|
|
||||||
|
# Démarrer une nouvelle partie
|
||||||
|
@app.route('/api/game/start', methods=['POST'])
|
||||||
|
def start_game():
|
||||||
|
global current_game
|
||||||
|
with game_lock:
|
||||||
|
current_game = Game()
|
||||||
|
return jsonify({'status': 'started', 'state': current_game.get_state()})
|
||||||
|
|
||||||
|
# Récupérer l'état de la partie
|
||||||
|
@app.route('/api/game/state', methods=['GET'])
|
||||||
|
def game_state():
|
||||||
|
global current_game
|
||||||
|
with game_lock:
|
||||||
|
print(f"[API] /api/game/state called. current_game is {'None' if current_game is None else 'OK'}")
|
||||||
|
if current_game is None:
|
||||||
|
return jsonify({'error': 'Aucune partie en cours'}), 400
|
||||||
|
return jsonify({'state': current_game.get_state()})
|
||||||
|
|
||||||
|
# Passer au tour suivant
|
||||||
|
@app.route('/api/game/next_turn', methods=['POST'])
|
||||||
|
def game_next_turn():
|
||||||
|
global current_game
|
||||||
|
with game_lock:
|
||||||
|
if current_game is None:
|
||||||
|
return jsonify({'error': 'Aucune partie en cours'}), 400
|
||||||
|
current_game.next_turn()
|
||||||
|
return jsonify({'state': current_game.get_state()})
|
||||||
|
|
||||||
|
# Déclarer Riichi
|
||||||
|
@app.route('/api/game/riichi', methods=['POST'])
|
||||||
|
def game_declare_riichi():
|
||||||
|
global current_game
|
||||||
|
with game_lock:
|
||||||
|
if current_game is None:
|
||||||
|
return jsonify({'error': 'Aucune partie en cours'}), 400
|
||||||
|
data = request.json
|
||||||
|
player_id = data.get('player_id')
|
||||||
|
if player_id is None:
|
||||||
|
return jsonify({'error': 'player_id manquant'}), 400
|
||||||
|
current_game.declare_riichi(player_id)
|
||||||
|
return jsonify({'state': current_game.get_state()})
|
||||||
|
|
||||||
|
# Récupérer la main d'un joueur
|
||||||
|
@app.route('/api/game/hand/<int:player_id>', methods=['GET'])
|
||||||
|
def game_get_hand(player_id):
|
||||||
|
global current_game
|
||||||
|
with game_lock:
|
||||||
|
if current_game is None:
|
||||||
|
return jsonify({'error': 'Aucune partie en cours'}), 400
|
||||||
|
hand = current_game.get_hand(player_id)
|
||||||
|
return jsonify({'hand': hand})
|
||||||
|
|
||||||
|
# Récupérer les défausses d'un joueur
|
||||||
|
@app.route('/api/game/discards/<int:player_id>', methods=['GET'])
|
||||||
|
def game_get_discards(player_id):
|
||||||
|
global current_game
|
||||||
|
with game_lock:
|
||||||
|
if current_game is None:
|
||||||
|
return jsonify({'error': 'Aucune partie en cours'}), 400
|
||||||
|
discards = current_game.get_discards(player_id)
|
||||||
|
return jsonify({'discards': discards})
|
||||||
|
|
||||||
|
# Récupérer les actions possibles pour un joueur
|
||||||
|
@app.route('/api/game/actions/<int:player_id>', methods=['GET'])
|
||||||
|
def game_get_actions(player_id):
|
||||||
|
global current_game
|
||||||
|
with game_lock:
|
||||||
|
if current_game is None:
|
||||||
|
return jsonify({'error': 'Aucune partie en cours'}), 400
|
||||||
|
actions = current_game.get_possible_actions(player_id)
|
||||||
|
return jsonify({'actions': actions})
|
||||||
|
|
||||||
|
@app.route('/api/play_draw', methods=['POST'])
|
||||||
|
def play_draw_api():
|
||||||
|
global current_game
|
||||||
|
with game_lock:
|
||||||
|
print('[API] /api/play_draw called')
|
||||||
|
if current_game is None:
|
||||||
|
return jsonify({'error': 'Aucune partie en cours'}), 400
|
||||||
|
current_game.play_draw()
|
||||||
|
return jsonify({'state': current_game.get_state()})
|
||||||
|
|
||||||
|
@app.route('/api/discard_tile', methods=['POST'])
|
||||||
|
def api_discard_tile():
|
||||||
|
global current_game
|
||||||
|
with game_lock:
|
||||||
|
if current_game is None:
|
||||||
|
return jsonify({'error': 'Aucune partie en cours'}), 400
|
||||||
|
data = request.json
|
||||||
|
tile_id = data.get('tile_id')
|
||||||
|
if tile_id is None:
|
||||||
|
return jsonify({'error': 'tile_id manquant'}), 400
|
||||||
|
discarded = current_game.discard_tile(tile_id)
|
||||||
|
if not discarded:
|
||||||
|
return jsonify({'error': 'Tuile non trouvée ou non défaussée'}), 400
|
||||||
|
return jsonify({'state': current_game.get_state(), 'discarded': {
|
||||||
|
'id': discarded.id,
|
||||||
|
'family': str(discarded.family),
|
||||||
|
'value': str(discarded.value),
|
||||||
|
'svg': discarded.get_img()
|
||||||
|
}})
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(debug=True, port=5000)
|
||||||
0
backend/src/game/__init__.py
Normal file
0
backend/src/game/__init__.py
Normal file
0
backend/src/game/discard.py
Normal file
0
backend/src/game/discard.py
Normal file
15
backend/src/game/error.py
Normal file
15
backend/src/game/error.py
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
class TileError(Exception):
|
||||||
|
"""Exception de base pour les erreurs de tuiles"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class InvalidFamilyError(TileError):
|
||||||
|
"""Famille de tuile invalide"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class InvalidValueError(TileError):
|
||||||
|
"""Valeur de tuile invalide"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class GameError(Exception):
|
||||||
|
"""Exception de base pour les erreurs de jeu"""
|
||||||
|
pass
|
||||||
210
backend/src/game/game.py
Normal file
210
backend/src/game/game.py
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from .hand import *
|
||||||
|
from .wall import *
|
||||||
|
|
||||||
|
import random as rd
|
||||||
|
|
||||||
|
class Game:
|
||||||
|
def __init__(self, wind=0 , yakus=False, red=False):
|
||||||
|
self.wall = Wall()
|
||||||
|
|
||||||
|
self.hand_player = self.wall.draw_hand()
|
||||||
|
self.discard_player = []
|
||||||
|
self.hidden_discard_player = []
|
||||||
|
self.hands_bots = [self.wall.draw_hand() for _ in range(3)]
|
||||||
|
self.discards_bots = [[] for _ in range(3)]
|
||||||
|
self.hidden_discards_bots = [[] for _ in range(3)]
|
||||||
|
|
||||||
|
self.has_draw = False
|
||||||
|
self.has_played = True
|
||||||
|
|
||||||
|
self.scores = [25000] * 4
|
||||||
|
|
||||||
|
self.winds = [
|
||||||
|
wind % 4, # player
|
||||||
|
(wind+1) % 4, # right of player
|
||||||
|
(wind + 2) % 4, # front of player
|
||||||
|
(wind +3) % 4 # left of player
|
||||||
|
]
|
||||||
|
|
||||||
|
self.initial_est = (-wind) % 4
|
||||||
|
self.turn = self.winds.index(0)
|
||||||
|
|
||||||
|
self.riichi = [False] * 4
|
||||||
|
self.repeat = 0
|
||||||
|
|
||||||
|
self.end = False
|
||||||
|
|
||||||
|
# L'Est pioche une tuile dès le début
|
||||||
|
if self.turn == 0:
|
||||||
|
tile = self.wall.draw()
|
||||||
|
self.hand_player.draw_tile(tile)
|
||||||
|
self.has_draw = True
|
||||||
|
else:
|
||||||
|
tile = self.wall.draw()
|
||||||
|
self.hands_bots[self.turn-1].draw_tile(tile)
|
||||||
|
|
||||||
|
def new_turn(self, repeat=False):
|
||||||
|
"""Nouvelle manche"""
|
||||||
|
if repeat:
|
||||||
|
self.repeat += 1
|
||||||
|
else:
|
||||||
|
self.repeat = 0
|
||||||
|
self.turn = (self.turn + 1) % 4
|
||||||
|
if self.turn == self.initial_est:
|
||||||
|
self.end = True
|
||||||
|
for i in range(4):
|
||||||
|
self.winds[self.turn + i] = i
|
||||||
|
|
||||||
|
self.turn = self.winds.index(0)
|
||||||
|
|
||||||
|
self.riichi = [False] * 4
|
||||||
|
self.repeat = 0
|
||||||
|
|
||||||
|
self.wall = Wall()
|
||||||
|
|
||||||
|
self.hand_player = self.wall.draw_hand()
|
||||||
|
self.discard_player = []
|
||||||
|
self.hidden_discard_player = []
|
||||||
|
self.hands_bots = [self.wall.draw_hand() for _ in range(3)]
|
||||||
|
self.discards_bots = [[] for _ in range(3)]
|
||||||
|
self.hidden_discards_bots = [[] for _ in range(3)]
|
||||||
|
|
||||||
|
def play_draw(self):
|
||||||
|
print(f"[Game] play_draw called: has_draw={self.has_draw}, turn={self.turn}")
|
||||||
|
if not self.has_draw:
|
||||||
|
if self.turn == 0:
|
||||||
|
tile = self.wall.draw()
|
||||||
|
self.hand_player.draw_tile(tile)
|
||||||
|
self.has_draw = True
|
||||||
|
print(f"Change has_draw to {self.has_draw}")
|
||||||
|
|
||||||
|
def draw_tile(self, hand: Hand):
|
||||||
|
tile = self.wall.draw()
|
||||||
|
hand.draw_tile(tile)
|
||||||
|
|
||||||
|
def put_tile(self, hand: Hand, id: int):
|
||||||
|
self.wall.tiles.append(hand.discard_tile(id))
|
||||||
|
self.wall.shuffle()
|
||||||
|
|
||||||
|
def next_turn(self):
|
||||||
|
"""Passe au joueur suivant."""
|
||||||
|
# TODO: check for pon/chii
|
||||||
|
self.turn = (self.turn + 1) % 4
|
||||||
|
self.has_draw = False
|
||||||
|
|
||||||
|
if self.turn != 0: # a bot has to play
|
||||||
|
self.bot_turn()
|
||||||
|
else:
|
||||||
|
self.play_draw()
|
||||||
|
|
||||||
|
def get_state(self):
|
||||||
|
"""Retourne l'état actuel de la partie (joueurs, scores, main, etc.)."""
|
||||||
|
hand_tiles = list(getattr(self.hand_player, 'tiles', []))
|
||||||
|
return {
|
||||||
|
'scores': list(self.scores),
|
||||||
|
'winds': list(self.winds),
|
||||||
|
'turn': int(self.turn),
|
||||||
|
'riichi': list(self.riichi),
|
||||||
|
'repeat': int(self.repeat),
|
||||||
|
'has_draw': bool(self.has_draw),
|
||||||
|
'end': bool(self.end),
|
||||||
|
'hand_player': [
|
||||||
|
{
|
||||||
|
'id': tile.id,
|
||||||
|
'family': str(tile.family),
|
||||||
|
'value': str(tile.value),
|
||||||
|
'svg': tile.get_img()
|
||||||
|
}
|
||||||
|
for tile in hand_tiles
|
||||||
|
],
|
||||||
|
'draw': (
|
||||||
|
{
|
||||||
|
'id': self.hand_player.draw.id,
|
||||||
|
'family': str(self.hand_player.draw.family),
|
||||||
|
'value': str(self.hand_player.draw.value),
|
||||||
|
'svg': self.hand_player.draw.get_img()
|
||||||
|
}
|
||||||
|
if self.hand_player.draw is not None else None
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def declare_riichi(self, player_id):
|
||||||
|
"""Déclare Riichi pour un joueur."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_hand(self, player_id):
|
||||||
|
"""Retourne la main d'un joueur (0 = joueur humain, 1-3 = bots)."""
|
||||||
|
if player_id == 0:
|
||||||
|
hand = list(getattr(self.hand_player, 'tiles', []))
|
||||||
|
if getattr(self.hand_player, 'draw', None) is not None:
|
||||||
|
hand = hand + [self.hand_player.draw]
|
||||||
|
elif 1 <= player_id <= 3:
|
||||||
|
hand_obj = self.hands_bots[player_id - 1]
|
||||||
|
hand = list(getattr(hand_obj, 'tiles', []))
|
||||||
|
if getattr(hand_obj, 'draw', None) is not None:
|
||||||
|
hand = hand + [hand_obj.draw]
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'id': tile.id,
|
||||||
|
'family': str(tile.family),
|
||||||
|
'value': str(tile.value),
|
||||||
|
'svg': tile.get_img()
|
||||||
|
}
|
||||||
|
for tile in hand
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_discards(self, player_id: int):
|
||||||
|
if player_id == 0:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'id': tile.id,
|
||||||
|
'family': str(tile.family),
|
||||||
|
'value': str(tile.value),
|
||||||
|
'svg': tile.get_img()
|
||||||
|
}
|
||||||
|
for tile in self.discard_player
|
||||||
|
]
|
||||||
|
elif 1 <= player_id <= 3:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'id': tile.id,
|
||||||
|
'family': str(tile.family),
|
||||||
|
'value': str(tile.value),
|
||||||
|
'svg': tile.get_img()
|
||||||
|
}
|
||||||
|
for tile in self.discards_bots[player_id - 1]
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_possible_actions(self, player_id):
|
||||||
|
"""Retourne les actions possibles pour un joueur."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def bot_turn(self):
|
||||||
|
# Joue un tour de bot sans sleep
|
||||||
|
if self.turn != 0:
|
||||||
|
# Pioche une tuile pour le bot
|
||||||
|
tile = self.wall.draw()
|
||||||
|
self.hands_bots[self.turn - 1].draw_tile(tile)
|
||||||
|
# Défausse une tuile aléatoire
|
||||||
|
hand = self.hands_bots[self.turn - 1]
|
||||||
|
n = rd.randint(0, len(hand.tiles) - 1)
|
||||||
|
tile_to_discard = hand.tiles[n]
|
||||||
|
discarded = hand.discard_tile(tile_to_discard.id)
|
||||||
|
if discarded:
|
||||||
|
self.discards_bots[self.turn - 1].append(discarded)
|
||||||
|
# Passe au joueur suivant
|
||||||
|
self.turn = (self.turn + 1) % 4
|
||||||
|
self.has_draw = True if self.turn == 0 else False
|
||||||
|
|
||||||
|
def discard_tile(self, tile_id: int):
|
||||||
|
# Défausse la tuile (de la main ou de la tuile piochée)
|
||||||
|
if self.turn == 0 and self.has_draw:
|
||||||
|
discarded = self.hand_player.discard_tile(tile_id)
|
||||||
|
if discarded:
|
||||||
|
self.discard_player.append(discarded)
|
||||||
|
self.next_turn()
|
||||||
45
backend/src/game/hand.py
Normal file
45
backend/src/game/hand.py
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from .tile import *
|
||||||
|
|
||||||
|
import random as rd
|
||||||
|
|
||||||
|
class Hand:
|
||||||
|
def __init__(self, tiles: list[Tile], draw: Tile|None = None):
|
||||||
|
self.tiles: list[Tile] = tiles
|
||||||
|
self.draw: Tile|None = draw
|
||||||
|
|
||||||
|
def sort(self) -> None:
|
||||||
|
self.tiles.sort()
|
||||||
|
|
||||||
|
def fold(self) -> None:
|
||||||
|
if self.draw is not None:
|
||||||
|
self.tiles.append(self.draw)
|
||||||
|
self.draw = None
|
||||||
|
self.sort()
|
||||||
|
|
||||||
|
def draw_tile(self, tile: Tile) -> None:
|
||||||
|
self.draw = tile
|
||||||
|
|
||||||
|
def discard_tile(self, id: int) -> Tile|None:
|
||||||
|
if self.draw is not None and self.draw.id == id:
|
||||||
|
draw = self.draw
|
||||||
|
self.draw = None
|
||||||
|
self.fold()
|
||||||
|
self.sort()
|
||||||
|
return draw
|
||||||
|
for i, tile in enumerate(self.tiles):
|
||||||
|
if tile.id == id:
|
||||||
|
t = self.tiles.pop(i)
|
||||||
|
self.fold()
|
||||||
|
self.sort()
|
||||||
|
return t
|
||||||
|
self.sort()
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def random() -> Hand:
|
||||||
|
tiles = Tile.generateAll()
|
||||||
|
rd.shuffle(tiles)
|
||||||
|
hand = Hand(tiles[:13])
|
||||||
|
hand.sort()
|
||||||
|
return hand
|
||||||
116
backend/src/game/tile.py
Normal file
116
backend/src/game/tile.py
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from game.error import *
|
||||||
|
from game.value import *
|
||||||
|
from functools import total_ordering
|
||||||
|
|
||||||
|
@total_ordering
|
||||||
|
class Tile:
|
||||||
|
def __init__(self, family, value, id):
|
||||||
|
self.family = family
|
||||||
|
self.value = value
|
||||||
|
self.id = id
|
||||||
|
self.hidden = False
|
||||||
|
self.img = None
|
||||||
|
|
||||||
|
self.img_back = "Back.svg"
|
||||||
|
self.img_front = "Front.svg"
|
||||||
|
self.img_shadow = "Gray.svg"
|
||||||
|
if self.family == Family.MAN:
|
||||||
|
if self.value in range(1, 10):
|
||||||
|
self.img_symbol = f"Man{self.value}.svg"
|
||||||
|
else:
|
||||||
|
raise InvalidValueError(f"Invalide value: {self.value}")
|
||||||
|
elif self.family == Family.PIN:
|
||||||
|
if self.value in range(1, 10):
|
||||||
|
self.img_symbol = f"Pin{self.value}.svg"
|
||||||
|
else:
|
||||||
|
raise InvalidValueError(f"Invalide value: {self.value}")
|
||||||
|
elif self.family == Family.SOU:
|
||||||
|
if self.value in range(1, 10):
|
||||||
|
self.img_symbol = f"Sou{self.value}.svg"
|
||||||
|
else:
|
||||||
|
raise InvalidValueError(f"Invalide value: {self.value}")
|
||||||
|
elif self.family == Family.WIND:
|
||||||
|
if self.value == Wind.EAST:
|
||||||
|
self.img_symbol = "Ton.svg"
|
||||||
|
elif self.value == Wind.SOUTH:
|
||||||
|
self.img_symbol = "Nan.svg"
|
||||||
|
elif self.value == Wind.WEST:
|
||||||
|
self.img_symbol = "Shaa.svg"
|
||||||
|
elif self.value == Wind.NORTH:
|
||||||
|
self.img_symbol = "Pei.svg"
|
||||||
|
else:
|
||||||
|
raise InvalidValueError(f"Invalide value: {self.value}")
|
||||||
|
elif self.family == Family.DRAGON:
|
||||||
|
if self.value == Dragon.RED:
|
||||||
|
self.img_symbol = "Chun.svg"
|
||||||
|
elif self.value == Dragon.GREEN:
|
||||||
|
self.img_symbol = "Hatsu.svg"
|
||||||
|
elif self.value == Dragon.WHITE:
|
||||||
|
self.img_symbol = "Haku.svg"
|
||||||
|
else:
|
||||||
|
raise InvalidValueError(f"Invalide value: {self.value}")
|
||||||
|
else:
|
||||||
|
raise InvalidFamilyError(f"Invalide family: {self.family}")
|
||||||
|
|
||||||
|
def get_img(self):
|
||||||
|
if self.img == None:
|
||||||
|
shadow = self._load_svg(self.img_shadow, x=10, y=10)
|
||||||
|
front = self._load_svg(self.img_front, x=0, y=0)
|
||||||
|
symbol = self._load_svg(self.img_symbol, x=12, y=20, scale=0.9)
|
||||||
|
|
||||||
|
ratio = 400 / 300
|
||||||
|
width = 60
|
||||||
|
self.img = f"""<svg class="tile tile-{self.id}" data-tile-id="{self.id}" viewBox="0 0 310 410" height="{ratio * width}" width="{width}">{shadow}{front}{symbol}</svg>"""
|
||||||
|
return self.img
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _load_svg(filename, x=0, y=0, scale=1):
|
||||||
|
try:
|
||||||
|
with open(f'img/Regular/{filename}', 'r', encoding='utf-8') as f:
|
||||||
|
svg_content = f.read()
|
||||||
|
return f'<g transform="translate({x}, {y}) scale({scale})">{svg_content}</g>'
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise InvalidValueError(f"SVG non trouvé: {filename}")
|
||||||
|
|
||||||
|
def __eq__(self, other: Tile):
|
||||||
|
return self.value == other.value and self.family == other.family
|
||||||
|
|
||||||
|
def __lt__(self, other: Tile):
|
||||||
|
if self.family.value != other.family.value:
|
||||||
|
return self.family.value < other.family.value
|
||||||
|
if hasattr(self.value, 'value'):
|
||||||
|
return self.value.value < other.value.value
|
||||||
|
return self.value < other.value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generateAll():
|
||||||
|
tiles = []
|
||||||
|
id = 0
|
||||||
|
for _ in range(4):
|
||||||
|
# man
|
||||||
|
for i in range(1, 10):
|
||||||
|
tiles.append(Tile(Family.MAN, i, id))
|
||||||
|
id += 1
|
||||||
|
|
||||||
|
# pin
|
||||||
|
for i in range(1, 10):
|
||||||
|
tiles.append(Tile(Family.PIN, i, id))
|
||||||
|
id += 1
|
||||||
|
|
||||||
|
# sou
|
||||||
|
for i in range(1, 10):
|
||||||
|
tiles.append(Tile(Family.SOU, i, id))
|
||||||
|
id += 1
|
||||||
|
|
||||||
|
# wind
|
||||||
|
for wind in [Wind.EAST, Wind.SOUTH, Wind.WEST, Wind.NORTH]:
|
||||||
|
tiles.append(Tile(Family.WIND, wind, id))
|
||||||
|
id += 1
|
||||||
|
|
||||||
|
# dragon
|
||||||
|
for dragon in [Dragon.RED, Dragon.GREEN, Dragon.WHITE]:
|
||||||
|
tiles.append(Tile(Family.DRAGON, dragon, id))
|
||||||
|
id += 1
|
||||||
|
|
||||||
|
return tiles
|
||||||
19
backend/src/game/value.py
Normal file
19
backend/src/game/value.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class Family(Enum):
|
||||||
|
MAN = 0
|
||||||
|
PIN = 1
|
||||||
|
SOU = 2
|
||||||
|
WIND = 3
|
||||||
|
DRAGON = 4
|
||||||
|
|
||||||
|
class Wind(Enum):
|
||||||
|
EAST = 0
|
||||||
|
SOUTH = 1
|
||||||
|
WEST = 2
|
||||||
|
NORTH = 3
|
||||||
|
|
||||||
|
class Dragon(Enum):
|
||||||
|
RED = 0
|
||||||
|
GREEN = 1
|
||||||
|
WHITE = 2
|
||||||
23
backend/src/game/wall.py
Normal file
23
backend/src/game/wall.py
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from .tile import *
|
||||||
|
from .hand import *
|
||||||
|
|
||||||
|
import random as rd
|
||||||
|
|
||||||
|
class Wall:
|
||||||
|
def __init__(self):
|
||||||
|
self.tiles = Tile.generateAll()
|
||||||
|
self.shuffle()
|
||||||
|
|
||||||
|
def shuffle(self):
|
||||||
|
rd.shuffle(self.tiles)
|
||||||
|
|
||||||
|
def draw(self) -> Tile:
|
||||||
|
if len(self.tiles) == 0:
|
||||||
|
raise Exception("No more tiles in the wall")
|
||||||
|
return self.tiles.pop()
|
||||||
|
|
||||||
|
def draw_hand(self) -> Hand:
|
||||||
|
hand_tiles = [self.draw() for _ in range(13)]
|
||||||
|
hand_tiles.sort()
|
||||||
|
return Hand(hand_tiles)
|
||||||
4
backend/src/requirements.txt
Normal file
4
backend/src/requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
Flask==3.0.0
|
||||||
|
flask-cors==4.0.0
|
||||||
|
gunicorn==21.2.0
|
||||||
|
gevent>=1.4
|
||||||
|
|
@ -1,4 +1,15 @@
|
||||||
services:
|
services:
|
||||||
|
riichi:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
environment:
|
||||||
|
- FLASK_ENV=production
|
||||||
|
volumes:
|
||||||
|
- ./frontend:/app/frontend
|
||||||
|
- ./backend/src:/app/backend/src
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:latest
|
image: nginx:latest
|
||||||
ports:
|
ports:
|
||||||
|
|
@ -9,4 +20,6 @@ services:
|
||||||
- ${PWD}/index.html:/usr/share/nginx/html/index.html:ro
|
- ${PWD}/index.html:/usr/share/nginx/html/index.html:ro
|
||||||
- ${PWD}/components:/usr/share/nginx/html/components
|
- ${PWD}/components:/usr/share/nginx/html/components
|
||||||
- ${PWD}/img:/usr/share/nginx/html/img
|
- ${PWD}/img:/usr/share/nginx/html/img
|
||||||
|
depends_on:
|
||||||
|
- riichi
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -1,219 +0,0 @@
|
||||||
import "./tile.js";
|
|
||||||
|
|
||||||
class Group {
|
|
||||||
constructor(tiles, stollenTile = null, dir = null) {
|
|
||||||
this.tiles = tiles;
|
|
||||||
this.stollenTile = stollenTile;
|
|
||||||
this.dir = dir; // 1 for left, 2 for middle, 3 for right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Hand {
|
|
||||||
constructor(tiles = []) {
|
|
||||||
this.tiles = tiles;
|
|
||||||
this.groups = [];
|
|
||||||
this.drawn= null;
|
|
||||||
}
|
|
||||||
|
|
||||||
drawTile(tile) {
|
|
||||||
this.drawn = tile;
|
|
||||||
}
|
|
||||||
|
|
||||||
discard(k) {
|
|
||||||
if (k === this.tiles.length) {
|
|
||||||
const tile = this.drawn;
|
|
||||||
this.drawn = null;
|
|
||||||
return tile;
|
|
||||||
} else {
|
|
||||||
const tile = this.tiles[k];
|
|
||||||
this.tiles.splice(k, 1);
|
|
||||||
return tile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort() {
|
|
||||||
this.tiles.sort((a, b) => {
|
|
||||||
if (a.lessThan(b)) return -1;
|
|
||||||
if (b.lessThan(a)) return 1;
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
shuffle() {
|
|
||||||
for (let i = this.tiles.length - 1; i > 0; i--) {
|
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
|
||||||
[this.tiles[i], this.tiles[j]] = [this.tiles[j], this.tiles[i]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
flatten() {
|
|
||||||
if (this.drawn) {
|
|
||||||
this.tiles.push(this.drawn);
|
|
||||||
}
|
|
||||||
this.drawn = null;
|
|
||||||
this.sort();
|
|
||||||
}
|
|
||||||
|
|
||||||
makeGroup(tiles, stollenTile = null, dir = null) {
|
|
||||||
const group = new Group(tiles, stollenTile, dir);
|
|
||||||
this.groups.push(group);
|
|
||||||
for (let tile of tiles) {
|
|
||||||
const index = this.tiles.findIndex(t => t.equals(tile));
|
|
||||||
if (index !== -1) {
|
|
||||||
this.tiles.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.flatten();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Draw {
|
|
||||||
constructor() {
|
|
||||||
this.tiles = Tile.generateAll();
|
|
||||||
this.shuffle();
|
|
||||||
}
|
|
||||||
|
|
||||||
shuffle() {
|
|
||||||
for (let i = this.tiles.length - 1; i > 0; i--) {
|
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
|
||||||
[this.tiles[i], this.tiles[j]] = [this.tiles[j], this.tiles[i]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
drawTile() {
|
|
||||||
return this.tiles.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
drawHand() {
|
|
||||||
const handTiles = [];
|
|
||||||
for (let i = 0; i < 13; i++) {
|
|
||||||
handTiles.push(this.drawTile());
|
|
||||||
}
|
|
||||||
return new Hand(handTiles);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Discard {
|
|
||||||
constructor() {
|
|
||||||
this.tiles = [];
|
|
||||||
this.hiddenTiles = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
add(tile) {
|
|
||||||
this.tiles.push(tile);
|
|
||||||
}
|
|
||||||
|
|
||||||
beeingStollen() {
|
|
||||||
this.hiddenTiles.push(this.tiles.pop());
|
|
||||||
return this.hiddenTiles[this.hiddenTiles.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
isIn(tile) {
|
|
||||||
return this.tiles.some(t => t.equals(tile)) || this.hiddenTiles.some(t => t.equals(tile));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Game {
|
|
||||||
constructor(skipInitialDeal = false) {
|
|
||||||
this.discards = [
|
|
||||||
new Discard(),
|
|
||||||
new Discard(),
|
|
||||||
new Discard(),
|
|
||||||
new Discard()
|
|
||||||
];
|
|
||||||
this.wall = new Draw();
|
|
||||||
|
|
||||||
if (skipInitialDeal) {
|
|
||||||
this.hands = [
|
|
||||||
new Hand(),
|
|
||||||
new Hand(),
|
|
||||||
new Hand(),
|
|
||||||
new Hand()
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
this.hands = [
|
|
||||||
this.wall.drawHand(),
|
|
||||||
this.wall.drawHand(),
|
|
||||||
this.wall.drawHand(),
|
|
||||||
this.wall.drawHand()
|
|
||||||
];
|
|
||||||
for (let hand of this.hands) {
|
|
||||||
hand.sort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.firstDealer = Math.floor(Math.random() * 4) * 0;
|
|
||||||
this.turn = this.firstDealer;
|
|
||||||
this.repeat = 0;
|
|
||||||
|
|
||||||
if (!skipInitialDeal) {
|
|
||||||
this.draw(this.turn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newDeal(hasRepeat = false) {
|
|
||||||
this.discards = [
|
|
||||||
new Discard(),
|
|
||||||
new Discard(),
|
|
||||||
new Discard(),
|
|
||||||
new Discard()
|
|
||||||
];
|
|
||||||
this.wall = new Draw();
|
|
||||||
this.hands = [
|
|
||||||
this.wall.drawHand(),
|
|
||||||
this.wall.drawHand(),
|
|
||||||
this.wall.drawHand(),
|
|
||||||
this.wall.drawHand()
|
|
||||||
];
|
|
||||||
if (!hasRepeat) {
|
|
||||||
this.turn = (this.firstDealer + 1) % 4;
|
|
||||||
} else {
|
|
||||||
this.repeat++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nextTurn() {
|
|
||||||
this.turn = (this.turn + 1) % 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
draw(player) {
|
|
||||||
this.hands[player].drawTile(this.wall.drawTile());
|
|
||||||
}
|
|
||||||
|
|
||||||
discard(player, k) {
|
|
||||||
if (player === this.turn) {
|
|
||||||
const tile = this.hands[player].discard(k);
|
|
||||||
this.discards[player].add(tile);
|
|
||||||
this.hands[player].flatten();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
chii(player, tiles, dir) {
|
|
||||||
const hand = this.hands[player];
|
|
||||||
dir = (player - this.turn) % 4;
|
|
||||||
console.assert(dir == 1);
|
|
||||||
this.hands[player].makeGroup(tiles, this.discards[this.turn].beeingStollen(), dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
pon(player, tiles, dir) {
|
|
||||||
const hand = this.hands[player];
|
|
||||||
dir = (player - this.turn + 4) % 4;
|
|
||||||
console.assert(dir == 1 || dir == 2 ||dir == 3, `Invalid direction: ${dir}`);
|
|
||||||
this.hands[player].makeGroup(tiles, this.discards[this.turn].beeingStollen(), dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
canPon(player) {
|
|
||||||
const lastTile = this.discards[this.turn].tiles[this.discards[this.turn].tiles.length - 1];
|
|
||||||
return this.hands[player].tiles.filter(t => t.equals(lastTile)).length >= 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
botPlay() {
|
|
||||||
// Le bot défausse une tuile au hasard (supposant qu'il a déjà pioché)
|
|
||||||
const hand = this.hands[this.turn];
|
|
||||||
const randomIndex = Math.floor(Math.random() * (hand.tiles.length + (hand.drawn ? 1 : 0)));
|
|
||||||
this.discard(this.turn, randomIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Discard, Draw, Game, Group, Hand };
|
|
||||||
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
// frontend/js/tile.js
|
|
||||||
// Traduction de tile.py en JavaScript
|
|
||||||
|
|
||||||
// Enum JS pour familles et valeurs
|
|
||||||
const Family = {
|
|
||||||
MAN: 'man', PIN: 'pin', SOU: 'sou', WIND: 'wind', DRAGON: 'dragon'
|
|
||||||
};
|
|
||||||
const Wind = { EAST: 1, SOUTH: 2, WEST: 3, NORTH: 4 };
|
|
||||||
const Dragon = { RED: 1, GREEN: 2, WHITE: 3 };
|
|
||||||
|
|
||||||
class Tile {
|
|
||||||
constructor(family, value, id) {
|
|
||||||
this.family = family;
|
|
||||||
this.value = value;
|
|
||||||
this.id = id;
|
|
||||||
this.hidden = false;
|
|
||||||
this.img = null;
|
|
||||||
this.img_back = "Back.svg";
|
|
||||||
this.img_front = "Front.svg";
|
|
||||||
this.img_shadow = "Gray.svg";
|
|
||||||
if (family === Family.MAN) {
|
|
||||||
if (value >= 1 && value <= 9) this.img_symbol = `Man${value}.svg`;
|
|
||||||
else throw new Error(`Invalid value: ${value}`);
|
|
||||||
} else if (family === Family.PIN) {
|
|
||||||
if (value >= 1 && value <= 9) this.img_symbol = `Pin${value}.svg`;
|
|
||||||
else throw new Error(`Invalid value: ${value}`);
|
|
||||||
} else if (family === Family.SOU) {
|
|
||||||
if (value >= 1 && value <= 9) this.img_symbol = `Sou${value}.svg`;
|
|
||||||
else throw new Error(`Invalid value: ${value}`);
|
|
||||||
} else if (family === Family.WIND) {
|
|
||||||
if (value === Wind.EAST) this.img_symbol = "Ton.svg";
|
|
||||||
else if (value === Wind.SOUTH) this.img_symbol = "Nan.svg";
|
|
||||||
else if (value === Wind.WEST) this.img_symbol = "Shaa.svg";
|
|
||||||
else if (value === Wind.NORTH) this.img_symbol = "Pei.svg";
|
|
||||||
else throw new Error(`Invalid value: ${value}`);
|
|
||||||
} else if (family === Family.DRAGON) {
|
|
||||||
if (value === Dragon.RED) this.img_symbol = "Chun.svg";
|
|
||||||
else if (value === Dragon.GREEN) this.img_symbol = "Hatsu.svg";
|
|
||||||
else if (value === Dragon.WHITE) this.img_symbol = "Haku.svg";
|
|
||||||
else throw new Error(`Invalid value: ${value}`);
|
|
||||||
} else {
|
|
||||||
throw new Error(`Invalid family: ${family}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getSVG() {
|
|
||||||
if (!this.img) {
|
|
||||||
const ratio = 400 / 300;
|
|
||||||
const width = 60;
|
|
||||||
const height = ratio * width;
|
|
||||||
// Utilise <image> pour chaque couche SVG
|
|
||||||
const shadow = `<image href="/img/Regular/${this.img_shadow}" x="10" y="10" height="410" width="310"/>`;
|
|
||||||
const front = `<image href="/img/Regular/${this.img_front}" x="0" y="0" height="410" width="310"/>`;
|
|
||||||
const symbol = `<image href="/img/Regular/${this.img_symbol}" x="12" y="20" height="370" width="285"/>`;
|
|
||||||
this.img = `<svg class="tile tile-${this.id}" data-tile-id="${this.id}" viewBox="0 0 310 410" height="${height}" width="${width}">${shadow}${front}${symbol}</svg>`;
|
|
||||||
}
|
|
||||||
return this.img;
|
|
||||||
}
|
|
||||||
|
|
||||||
equals(other) {
|
|
||||||
return this.value === other.value && this.family === other.family;
|
|
||||||
}
|
|
||||||
|
|
||||||
lessThan(other) {
|
|
||||||
const order = [Family.MAN, Family.PIN, Family.SOU, Family.WIND, Family.DRAGON];
|
|
||||||
const famA = order.indexOf(this.family);
|
|
||||||
const famB = order.indexOf(other.family);
|
|
||||||
if (famA !== famB) {
|
|
||||||
return famA < famB;
|
|
||||||
}
|
|
||||||
if (typeof this.value === 'object' && 'value' in this.value) {
|
|
||||||
return this.value.value < other.value.value;
|
|
||||||
}
|
|
||||||
return this.value < other.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
static generateAll() {
|
|
||||||
const tiles = [];
|
|
||||||
let id = 0;
|
|
||||||
for (let _ = 0; _ < 4; _++) {
|
|
||||||
// man
|
|
||||||
for (let i = 1; i <= 9; i++) {
|
|
||||||
tiles.push(new Tile(Family.MAN, i, id++));
|
|
||||||
}
|
|
||||||
// pin
|
|
||||||
for (let i = 1; i <= 9; i++) {
|
|
||||||
tiles.push(new Tile(Family.PIN, i, id++));
|
|
||||||
}
|
|
||||||
// sou
|
|
||||||
for (let i = 1; i <= 9; i++) {
|
|
||||||
tiles.push(new Tile(Family.SOU, i, id++));
|
|
||||||
}
|
|
||||||
// wind
|
|
||||||
for (let wind of [Wind.EAST, Wind.SOUTH, Wind.WEST, Wind.NORTH]) {
|
|
||||||
tiles.push(new Tile(Family.WIND, wind, id++));
|
|
||||||
}
|
|
||||||
// dragon
|
|
||||||
for (let dragon of [Dragon.RED, Dragon.GREEN, Dragon.WHITE]) {
|
|
||||||
tiles.push(new Tile(Family.DRAGON, dragon, id++));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tiles;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expose dans le scope global pour compatibilité avec les scripts HTML classiques
|
|
||||||
window.Tile = Tile;
|
|
||||||
window.Family = Family;
|
|
||||||
window.Wind = Wind;
|
|
||||||
window.Dragon = Dragon;
|
|
||||||
|
|
@ -161,7 +161,6 @@
|
||||||
<div id="mascotte"></div>
|
<div id="mascotte"></div>
|
||||||
|
|
||||||
<script src="/frontend/js/fr/texts.js"></script>
|
<script src="/frontend/js/fr/texts.js"></script>
|
||||||
<script src="/frontend/js/tile.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
// Charger le menu
|
// Charger le menu
|
||||||
fetch('/components/menu.html')
|
fetch('/components/menu.html')
|
||||||
|
|
@ -186,108 +185,116 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Générer les tuiles localement
|
let allTiles = [];
|
||||||
const allTiles = Tile.generateAll();
|
|
||||||
const tilesDisplay = document.getElementById('tiles-display');
|
|
||||||
|
|
||||||
// Man
|
// Récupérer les tuiles
|
||||||
let section = document.createElement('div');
|
fetch('/api/tiles')
|
||||||
section.className = 'tiles-family';
|
.then(r => r.json())
|
||||||
const manTitle = document.createElement('h3');
|
.then(data => {
|
||||||
manTitle.textContent = 'Caractères';
|
allTiles = data.tiles;
|
||||||
const manGrid = document.createElement('div');
|
|
||||||
manGrid.className = 'tiles-grid';
|
|
||||||
section.appendChild(manTitle);
|
|
||||||
section.appendChild(manGrid);
|
|
||||||
for (let i = 0; i < 9; i++) {
|
|
||||||
const tileDiv = document.createElement('div');
|
|
||||||
tileDiv.className = 'tile-item';
|
|
||||||
tileDiv.innerHTML = allTiles[i].getSVG();
|
|
||||||
manGrid.appendChild(tileDiv);
|
|
||||||
}
|
|
||||||
tilesDisplay.appendChild(section);
|
|
||||||
|
|
||||||
// Pin
|
const tilesDisplay = document.getElementById('tiles-display');
|
||||||
section = document.createElement('div');
|
|
||||||
section.className = 'tiles-family';
|
|
||||||
const pinTitle = document.createElement('h3');
|
|
||||||
pinTitle.textContent = 'Ronds';
|
|
||||||
const pinGrid = document.createElement('div');
|
|
||||||
pinGrid.className = 'tiles-grid';
|
|
||||||
section.appendChild(pinTitle);
|
|
||||||
section.appendChild(pinGrid);
|
|
||||||
for (let i = 9; i < 18; i++) {
|
|
||||||
const tileDiv = document.createElement('div');
|
|
||||||
tileDiv.className = 'tile-item';
|
|
||||||
tileDiv.innerHTML = allTiles[i].getSVG();
|
|
||||||
pinGrid.appendChild(tileDiv);
|
|
||||||
}
|
|
||||||
tilesDisplay.appendChild(section);
|
|
||||||
|
|
||||||
// Sou
|
// Man
|
||||||
section = document.createElement('div');
|
let section = document.createElement('div');
|
||||||
section.className = 'tiles-family';
|
section.className = 'tiles-family';
|
||||||
const souTitle = document.createElement('h3');
|
const manTitle = document.createElement('h3');
|
||||||
souTitle.textContent = 'Bambous';
|
manTitle.textContent = 'Caractères';
|
||||||
const souGrid = document.createElement('div');
|
const manGrid = document.createElement('div');
|
||||||
souGrid.className = 'tiles-grid';
|
manGrid.className = 'tiles-grid';
|
||||||
section.appendChild(souTitle);
|
section.appendChild(manTitle);
|
||||||
section.appendChild(souGrid);
|
section.appendChild(manGrid);
|
||||||
for (let i = 18; i < 27; i++) {
|
for (let i = 0; i < 9; i++) {
|
||||||
const tileDiv = document.createElement('div');
|
const tileDiv = document.createElement('div');
|
||||||
tileDiv.className = 'tile-item';
|
tileDiv.className = 'tile-item';
|
||||||
tileDiv.innerHTML = allTiles[i].getSVG();
|
tileDiv.innerHTML = allTiles[i].svg;
|
||||||
souGrid.appendChild(tileDiv);
|
manGrid.appendChild(tileDiv);
|
||||||
}
|
}
|
||||||
tilesDisplay.appendChild(section);
|
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].svg;
|
||||||
|
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++) {
|
||||||
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();
|
souGrid.appendChild(tileDiv);
|
||||||
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++) {
|
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';
|
||||||
dragonGrid.appendChild(tileDiv);
|
tileDiv.innerHTML = allTiles[i].svg;
|
||||||
}
|
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].svg;
|
||||||
|
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/tile.js"></script>
|
<script src="/frontend/js/hand.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Charger le menu
|
// Charger le menu
|
||||||
fetch('/components/menu.html')
|
fetch('/components/menu.html')
|
||||||
|
|
@ -171,52 +171,47 @@
|
||||||
handsContainer.className = 'hands-container';
|
handsContainer.className = 'hands-container';
|
||||||
tilesDisplay.appendChild(handsContainer);
|
tilesDisplay.appendChild(handsContainer);
|
||||||
|
|
||||||
// Générer et afficher trois mains aléatoires localement
|
// Créer et afficher trois mains aléatoires
|
||||||
function shuffle(array) {
|
|
||||||
for (let i = array.length - 1; i > 0; i--) {
|
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
|
||||||
[array[i], array[j]] = [array[j], array[i]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const allTiles = Tile.generateAll();
|
|
||||||
function sortHand(hand) {
|
|
||||||
// Ordre : caractères < ronds < bambous < vents < dragons
|
|
||||||
const familyOrder = { 'man': 0, 'pin': 1, 'sou': 2, 'wind': 3, 'dragon': 4 };
|
|
||||||
return hand.sort((a, b) => {
|
|
||||||
const famA = familyOrder[a.family] ?? 99;
|
|
||||||
const famB = familyOrder[b.family] ?? 99;
|
|
||||||
if (famA !== famB) return famA - famB;
|
|
||||||
if (a.value !== undefined && b.value !== undefined) return a.value - b.value;
|
|
||||||
if (a.wind !== undefined && b.wind !== undefined) return a.wind - b.wind;
|
|
||||||
if (a.dragon !== undefined && b.dragon !== undefined) return a.dragon - b.dragon;
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
const tiles = allTiles.slice();
|
const hand = new Hand({
|
||||||
shuffle(tiles);
|
label: `Main ${i + 1}`,
|
||||||
const handTilesArr = tiles.slice(0, 13);
|
position: 'top-label',
|
||||||
sortHand(handTilesArr);
|
tilt: 'none',
|
||||||
const handDiv = document.createElement('div');
|
containerId: 'tiles-display'
|
||||||
handDiv.className = `hand-display top-label tilt-none`;
|
|
||||||
const label = document.createElement('div');
|
|
||||||
label.className = 'hand-label';
|
|
||||||
label.textContent = `Main ${i + 1}`;
|
|
||||||
handDiv.appendChild(label);
|
|
||||||
const container = document.createElement('div');
|
|
||||||
container.className = 'hand-container';
|
|
||||||
const handTiles = document.createElement('div');
|
|
||||||
handTiles.className = 'hand-tiles';
|
|
||||||
handTilesArr.forEach(tile => {
|
|
||||||
const tileDiv = document.createElement('div');
|
|
||||||
tileDiv.className = 'hand-tile-item';
|
|
||||||
tileDiv.innerHTML = tile.getSVG();
|
|
||||||
handTiles.appendChild(tileDiv);
|
|
||||||
});
|
});
|
||||||
container.appendChild(handTiles);
|
|
||||||
handDiv.appendChild(container);
|
// Modifier temporairement le containerId pour ajouter à handsContainer
|
||||||
handsContainer.appendChild(handDiv);
|
hand.containerId = null;
|
||||||
|
|
||||||
|
fetch(hand.apiUrl)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
const handDiv = document.createElement('div');
|
||||||
|
handDiv.className = `hand-display ${hand.position} tilt-${hand.tilt}`;
|
||||||
|
|
||||||
|
const label = document.createElement('div');
|
||||||
|
label.className = 'hand-label';
|
||||||
|
label.textContent = `Main ${i + 1}`;
|
||||||
|
handDiv.appendChild(label);
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'hand-container';
|
||||||
|
|
||||||
|
const handTiles = document.createElement('div');
|
||||||
|
handTiles.className = 'hand-tiles';
|
||||||
|
|
||||||
|
data.hand.forEach(tile => {
|
||||||
|
const tileDiv = document.createElement('div');
|
||||||
|
tileDiv.className = 'hand-tile-item';
|
||||||
|
tileDiv.innerHTML = tile.svg;
|
||||||
|
handTiles.appendChild(tileDiv);
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(handTiles);
|
||||||
|
handDiv.appendChild(container);
|
||||||
|
handsContainer.appendChild(handDiv);
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Erreur:', error));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Créer le titre pour la main interactive
|
// Créer le titre pour la main interactive
|
||||||
|
|
@ -225,84 +220,116 @@
|
||||||
interactiveTitle.textContent = 'Main interactive';
|
interactiveTitle.textContent = 'Main interactive';
|
||||||
tilesDisplay.appendChild(interactiveTitle);
|
tilesDisplay.appendChild(interactiveTitle);
|
||||||
|
|
||||||
// Générer et afficher une main interactive locale avec une tuile piochée
|
// Créer et afficher la main interactive avec une tuile piochée
|
||||||
let interactiveTiles = allTiles.slice();
|
fetch('/api/hand_with_draw')
|
||||||
shuffle(interactiveTiles);
|
.then(r => r.json())
|
||||||
let interactiveHand = interactiveTiles.slice(0, 13);
|
.then(data => {
|
||||||
sortHand(interactiveHand);
|
const handDiv = document.createElement('div');
|
||||||
let drawTile = interactiveTiles[13];
|
handDiv.className = 'hand-display top-label tilt-none';
|
||||||
let wall = interactiveTiles.slice(14); // Le reste du paquet pour la pioche
|
handDiv.id = 'interactive-hand';
|
||||||
|
|
||||||
|
const label = document.createElement('div');
|
||||||
|
label.className = 'hand-label';
|
||||||
|
label.textContent = 'Votre main';
|
||||||
|
handDiv.appendChild(label);
|
||||||
|
|
||||||
|
// Conteneur qui gère l'affichage avec flex (gère auto le draw si présent)
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'hand-container';
|
||||||
|
|
||||||
|
// Créer le grid des 13 tuiles
|
||||||
|
const handTiles = document.createElement('div');
|
||||||
|
handTiles.className = 'hand-tiles';
|
||||||
|
handTiles.id = 'interactive-tiles';
|
||||||
|
|
||||||
|
data.hand.forEach(tile => {
|
||||||
|
const tileDiv = document.createElement('div');
|
||||||
|
tileDiv.className = 'hand-tile-item';
|
||||||
|
tileDiv.innerHTML = tile.svg;
|
||||||
|
tileDiv.dataset.tileId = tile.id;
|
||||||
|
tileDiv.style.cursor = 'pointer';
|
||||||
|
tileDiv.addEventListener('click', () => discardTile(tile.id, tileDiv));
|
||||||
|
handTiles.appendChild(tileDiv);
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(handTiles);
|
||||||
|
|
||||||
function renderInteractiveHand() {
|
|
||||||
// Nettoyer l'affichage précédent
|
|
||||||
let oldDiv = document.getElementById('interactive-hand');
|
|
||||||
if (oldDiv) oldDiv.remove();
|
|
||||||
const handDiv = document.createElement('div');
|
|
||||||
handDiv.className = 'hand-display top-label tilt-none';
|
|
||||||
handDiv.id = 'interactive-hand';
|
|
||||||
const label = document.createElement('div');
|
|
||||||
label.className = 'hand-label';
|
|
||||||
label.textContent = 'Votre main';
|
|
||||||
handDiv.appendChild(label);
|
|
||||||
const container = document.createElement('div');
|
|
||||||
container.className = 'hand-container';
|
|
||||||
const handTiles = document.createElement('div');
|
|
||||||
handTiles.className = 'hand-tiles';
|
|
||||||
handTiles.id = 'interactive-tiles';
|
|
||||||
interactiveHand.forEach((tile, idx) => {
|
|
||||||
const tileDiv = document.createElement('div');
|
|
||||||
tileDiv.className = 'hand-tile-item';
|
|
||||||
tileDiv.innerHTML = tile.getSVG();
|
|
||||||
tileDiv.dataset.tileId = tile.id;
|
|
||||||
tileDiv.style.cursor = 'pointer';
|
|
||||||
tileDiv.addEventListener('click', () => discardTile(idx));
|
|
||||||
handTiles.appendChild(tileDiv);
|
|
||||||
});
|
|
||||||
container.appendChild(handTiles);
|
|
||||||
// Draw tile
|
|
||||||
if (drawTile) {
|
|
||||||
const drawTileDiv = document.createElement('div');
|
const drawTileDiv = document.createElement('div');
|
||||||
drawTileDiv.className = 'hand-tile-item draw-tile';
|
drawTileDiv.className = 'hand-tile-item draw-tile';
|
||||||
drawTileDiv.innerHTML = drawTile.getSVG();
|
drawTileDiv.innerHTML = data.draw.svg;
|
||||||
drawTileDiv.dataset.tileId = drawTile.id;
|
drawTileDiv.dataset.tileId = data.draw.id;
|
||||||
drawTileDiv.style.cursor = 'pointer';
|
drawTileDiv.style.cursor = 'pointer';
|
||||||
drawTileDiv.addEventListener('click', () => discardTile('draw'));
|
drawTileDiv.addEventListener('click', () => discardTile(data.draw.id, drawTileDiv));
|
||||||
|
container.appendChild(drawTileDiv);
|
||||||
|
|
||||||
|
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');
|
||||||
|
tileDiv.className = 'hand-tile-item';
|
||||||
|
tileDiv.innerHTML = tile.svg;
|
||||||
|
tileDiv.dataset.tileId = tile.id;
|
||||||
|
tileDiv.style.cursor = 'pointer';
|
||||||
|
tileDiv.addEventListener('click', () => discardTile(tile.id, tileDiv));
|
||||||
|
handTiles.appendChild(tileDiv);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mettre à jour le draw
|
||||||
|
const container = handTiles.parentElement;
|
||||||
|
const oldDrawTile = container.querySelector('.draw-tile');
|
||||||
|
if (oldDrawTile) {
|
||||||
|
oldDrawTile.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drawData) {
|
||||||
|
const drawTileDiv = document.createElement('div');
|
||||||
|
drawTileDiv.className = 'hand-tile-item draw-tile';
|
||||||
|
drawTileDiv.innerHTML = drawData.svg;
|
||||||
|
drawTileDiv.dataset.tileId = drawData.id;
|
||||||
|
drawTileDiv.style.cursor = 'pointer';
|
||||||
|
drawTileDiv.addEventListener('click', () => discardTile(drawData.id, drawTileDiv));
|
||||||
container.appendChild(drawTileDiv);
|
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,7 +85,6 @@
|
||||||
<div id="mascotte"></div>
|
<div id="mascotte"></div>
|
||||||
|
|
||||||
<script src="/frontend/js/fr/texts.js"></script>
|
<script src="/frontend/js/fr/texts.js"></script>
|
||||||
<script src="/frontend/js/tile.js"></script>
|
|
||||||
<script src="/frontend/js/hand.js"></script>
|
<script src="/frontend/js/hand.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Charger le menu
|
// Charger le menu
|
||||||
|
|
@ -108,18 +107,21 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fonction utilitaire pour afficher les tuiles
|
// Fonction utilitaire pour afficher les tuiles
|
||||||
const allTiles = Tile.generateAll();
|
|
||||||
function displayTiles(displayId, tileIndices) {
|
function displayTiles(displayId, tileIndices) {
|
||||||
const display = document.getElementById(displayId);
|
fetch('/api/tiles')
|
||||||
display.innerHTML = '';
|
.then(r => r.json())
|
||||||
tileIndices.forEach(idx => {
|
.then(data => {
|
||||||
const tile = allTiles[idx];
|
const display = document.getElementById(displayId);
|
||||||
if (tile) {
|
tileIndices.forEach(idx => {
|
||||||
const tileDiv = document.createElement('div');
|
const tile = data.tiles[idx];
|
||||||
tileDiv.innerHTML = tile.getSVG();
|
if (tile) {
|
||||||
display.appendChild(tileDiv);
|
const tileDiv = document.createElement('div');
|
||||||
}
|
tileDiv.innerHTML = tile.svg;
|
||||||
});
|
display.appendChild(tileDiv);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Erreur:', error));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Afficher les combinaisons
|
// Afficher les combinaisons
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,6 @@
|
||||||
<div id="mascotte"></div>
|
<div id="mascotte"></div>
|
||||||
|
|
||||||
<script src="/frontend/js/fr/texts.js"></script>
|
<script src="/frontend/js/fr/texts.js"></script>
|
||||||
<script src="/frontend/js/tile.js"></script>
|
|
||||||
<script src="/frontend/js/hand.js"></script>
|
<script src="/frontend/js/hand.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Charger le menu
|
// Charger le menu
|
||||||
|
|
@ -119,18 +118,21 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fonction utilitaire pour afficher les tuiles
|
// Fonction utilitaire pour afficher les tuiles
|
||||||
const allTiles = Tile.generateAll();
|
|
||||||
function displayTiles(displayId, tileIndices) {
|
function displayTiles(displayId, tileIndices) {
|
||||||
const display = document.getElementById(displayId);
|
fetch('/api/tiles')
|
||||||
display.innerHTML = '';
|
.then(r => r.json())
|
||||||
tileIndices.forEach(idx => {
|
.then(data => {
|
||||||
const tile = allTiles[idx];
|
const display = document.getElementById(displayId);
|
||||||
if (tile) {
|
tileIndices.forEach(idx => {
|
||||||
const tileDiv = document.createElement('div');
|
const tile = data.tiles[idx];
|
||||||
tileDiv.innerHTML = tile.getSVG();
|
if (tile) {
|
||||||
display.appendChild(tileDiv);
|
const tileDiv = document.createElement('div');
|
||||||
}
|
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,7 +94,6 @@
|
||||||
<div id="mascotte"></div>
|
<div id="mascotte"></div>
|
||||||
|
|
||||||
<script src="/frontend/js/fr/texts.js"></script>
|
<script src="/frontend/js/fr/texts.js"></script>
|
||||||
<script src="/frontend/js/tile.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
// Charger le menu
|
// Charger le menu
|
||||||
fetch('/components/menu.html')
|
fetch('/components/menu.html')
|
||||||
|
|
@ -115,19 +114,23 @@
|
||||||
: '<img src="/img/tilineau/idle.png" alt="Tilineau">';
|
: '<img src="/img/tilineau/idle.png" alt="Tilineau">';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Affichage local des tuiles sans backend
|
// Fonction utilitaire pour afficher les tuiles
|
||||||
function displayTiles(displayId, tileIndices) {
|
function displayTiles(displayId, tileIndices) {
|
||||||
const allTiles = Tile.generateAll();
|
fetch('/api/tiles')
|
||||||
const display = document.getElementById(displayId);
|
.then(r => r.json())
|
||||||
tileIndices.forEach(idx => {
|
.then(data => {
|
||||||
const tile = allTiles[idx];
|
const display = document.getElementById(displayId);
|
||||||
if (tile) {
|
tileIndices.forEach(idx => {
|
||||||
const tileDiv = document.createElement('div');
|
const tile = data.tiles[idx];
|
||||||
tileDiv.innerHTML = tile.getSVG();
|
if (tile) {
|
||||||
display.appendChild(tileDiv);
|
const tileDiv = document.createElement('div');
|
||||||
}
|
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]);
|
||||||
|
|
@ -137,7 +140,7 @@
|
||||||
displayTiles('tiles-display-6', [18, 18, 18, 20, 21, 22, 21, 22, 23, 25, 25, 27, 27, 27]);
|
displayTiles('tiles-display-6', [18, 18, 18, 20, 21, 22, 21, 22, 23, 25, 25, 27, 27, 27]);
|
||||||
displayTiles('tiles-display-7', [1, 1, 6, 6, 18, 18, 19, 19, 24, 24, 28, 28, 30, 30]);
|
displayTiles('tiles-display-7', [1, 1, 6, 6, 18, 18, 19, 19, 24, 24, 28, 28, 30, 30]);
|
||||||
displayTiles('tiles-display-8', [2, 2, 2, 7, 7, 7, 20, 20, 25, 25, 25, 29, 29, 29]);
|
displayTiles('tiles-display-8', [2, 2, 2, 7, 7, 7, 20, 20, 25, 25, 25, 29, 29, 29]);
|
||||||
displayTiles('tiles-display-9', [1, 2, 3, 9, 10, 11, 15, 16, 17, 20, 21, 28, 28]);
|
displayTiles('tiles-display-9', [1, 2, 3, 9, 10, 11, 15, 16, 17, 20, 21, 28, 28])
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 MiB |
21
index.html
21
index.html
|
|
@ -73,7 +73,6 @@
|
||||||
<div id="mascotte"></div>
|
<div id="mascotte"></div>
|
||||||
|
|
||||||
<script src="/frontend/js/fr/texts.js"></script>
|
<script src="/frontend/js/fr/texts.js"></script>
|
||||||
<script src="/frontend/js/tile.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
// Charger le menu
|
// Charger le menu
|
||||||
fetch('/components/menu.html')
|
fetch('/components/menu.html')
|
||||||
|
|
@ -96,10 +95,17 @@
|
||||||
: '<img src="/img/tilineau/speaking.png" alt="Tilineau">';
|
: '<img src="/img/tilineau/speaking.png" alt="Tilineau">';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Générer les tuiles localement
|
let allTiles = [];
|
||||||
const allTiles = Tile.generateAll();
|
|
||||||
// Lancer la pluie de tuiles
|
// Récupérer les tuiles
|
||||||
startTileRain();
|
fetch('/api/tiles')
|
||||||
|
.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');
|
||||||
|
|
@ -107,15 +113,18 @@
|
||||||
const randomTile = allTiles[Math.floor(Math.random() * allTiles.length)];
|
const randomTile = allTiles[Math.floor(Math.random() * allTiles.length)];
|
||||||
const tileElem = document.createElement('div');
|
const tileElem = document.createElement('div');
|
||||||
tileElem.className = 'falling-tile';
|
tileElem.className = 'falling-tile';
|
||||||
tileElem.innerHTML = randomTile.getSVG();
|
tileElem.innerHTML = randomTile.svg;
|
||||||
tileElem.style.left = Math.random() * window.innerWidth + 'px';
|
tileElem.style.left = Math.random() * window.innerWidth + 'px';
|
||||||
const duration = 8 + Math.random() * 2;
|
const duration = 8 + Math.random() * 2;
|
||||||
tileElem.style.animationDuration = duration + 's';
|
tileElem.style.animationDuration = duration + 's';
|
||||||
|
|
||||||
// Rotation aléatoire à droite ou à gauche
|
// Rotation aléatoire à droite ou à gauche
|
||||||
const rotation = Math.random() * 720 - 360;
|
const rotation = Math.random() * 720 - 360;
|
||||||
const direction = Math.random() > 0.5 ? 1 : -1;
|
const direction = Math.random() > 0.5 ? 1 : -1;
|
||||||
tileElem.style.setProperty('--rotation', (rotation * direction) + 'deg');
|
tileElem.style.setProperty('--rotation', (rotation * direction) + 'deg');
|
||||||
|
|
||||||
container.appendChild(tileElem);
|
container.appendChild(tileElem);
|
||||||
|
|
||||||
setTimeout(() => tileElem.remove(), duration * 1000);
|
setTimeout(() => tileElem.remove(), duration * 1000);
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,14 @@ http {
|
||||||
add_header Cache-Control "public, immutable";
|
add_header Cache-Control "public, immutable";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Proxy vers le backend Flask
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://riichi:5000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
# Servir les fichiers statiques (à la fin, après les autres locations)
|
# Servir les fichiers statiques (à la fin, après les autres locations)
|
||||||
location / {
|
location / {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue