diff --git a/.gitignore b/.gitignore
index 7d245c9..2fa3ae9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
node_modules/
package-lock.json
-build/
\ No newline at end of file
+build/
+__pycache__
\ No newline at end of file
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..cff7006
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,17 @@
+{
+ "semi": true,
+ "singleQuote": true,
+ "tabWidth": 2,
+ "useTabs": false,
+ "trailingComma": "all",
+ "printWidth": 100,
+ "bracketSpacing": true,
+ "arrowParens": "avoid",
+ "endOfLine": "lf",
+ "quoteProps": "as-needed",
+ "jsxSingleQuote": false,
+ "bracketSameLine": false,
+ "proseWrap": "preserve",
+ "htmlWhitespaceSensitivity": "css",
+ "embeddedLanguageFormatting": "auto"
+}
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..edd85b4
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,11 @@
+{
+ "recommendations": [
+ "esbenp.prettier-vscode",
+ "dbaeumer.vscode-eslint",
+ "bradlc.vscode-tailwindcss",
+ "formulahendry.auto-rename-tag",
+ "streetsidesoftware.code-spell-checker",
+ "streetsidesoftware.code-spell-checker-french",
+ "ms-vscode.vscode-typescript-next"
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..f3af47d
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,37 @@
+{
+ "typescript.tsdk": "node_modules/typescript/lib",
+ "editor.formatOnSave": true,
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "editor.codeActionsOnSave": {
+ "source.fixAll.eslint": "explicit",
+ "source.organizeImports": "explicit"
+ },
+ "[typescript]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[javascript]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[json]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[css]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[html]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "files.exclude": {
+ "**/node_modules": true,
+ "**/build": false,
+ "**/.git": true
+ },
+ "search.exclude": {
+ "**/node_modules": true,
+ "**/build": true,
+ "**/*.js.map": true
+ },
+ "emmet.includeLanguages": {
+ "typescript": "html"
+ }
+}
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..d284401
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,26 @@
+FROM python:3.11-slim
+
+WORKDIR /app
+
+# Installer les dépendances Python
+COPY backend/src/requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+# Copier le code backend
+COPY backend/src/ /app/
+
+# Copier le frontend
+COPY frontend /app/frontend
+COPY components /app/components
+COPY img /app/img
+COPY index.html /app/
+COPY *.json /app/
+COPY vite.config.ts /app/
+
+# Port Flask
+EXPOSE 5000
+
+# Définir PYTHONPATH pour que les imports relatifs fonctionnent
+ENV PYTHONPATH=/app:$PYTHONPATH
+
+CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "api:app"]
diff --git a/Makefile b/Makefile
index 494cd77..b49fc9a 100644
--- a/Makefile
+++ b/Makefile
@@ -1,27 +1,107 @@
-TESTS = $(wildcard build/test*)
+# ==================================
+# Riichi Mahjong Tutorial - Makefile
+# ==================================
-all:
- npx webpack --mode production
+.PHONY: all dev build preview test lint format clean hard-clean zip help install
+# Default target
+all: build
+
+# Install dependencies
+install:
+ @echo "Installing dependencies..."
+ npm install
+
+# Development server with hot reload
dev:
- npx webpack --mode development
+ @echo "Starting development server..."
+ npm run dev
-zip:
- npx webpack --mode production
+# Production build
+build:
+ @echo "Building for production..."
+ npm run build
+
+# Preview production build
+preview:
+ @echo "Previewing production build..."
+ npm run preview
+
+# Run tests
+test:
+ @echo "Running tests..."
+ npm run test
+
+# Run tests in watch mode
+test-watch:
+ @echo "Running tests in watch mode..."
+ npm run test:watch
+
+# Lint code
+lint:
+ @echo "Linting code..."
+ npm run lint
+
+# Fix linting issues
+lint-fix:
+ @echo "Fixing linting issues..."
+ npm run lint:fix
+
+# Format code
+format:
+ @echo "Formatting code..."
+ npm run format
+
+# Type check
+typecheck:
+ @echo "Type checking..."
+ npm run typecheck
+
+# Create distribution zip
+zip: build
+ @echo "Creating distribution archive..."
@rm -rf riichi
- mkdir riichi
- cp -r build riichi
- cp index.html riichi
- cp -r img riichi
- zip -r riichi.zip riichi
- @rm -r riichi
-
+ @mkdir riichi
+ @cp -r build riichi/
+ @cp index.html riichi/
+ @cp -r img riichi/
+ @cp -r src/styles riichi/src/ 2>/dev/null || mkdir -p riichi/src && cp -r src/styles riichi/src/
+ @zip -r riichi.zip riichi
+ @rm -rf riichi
+ @echo "Created riichi.zip"
+# Clean build artifacts
clean:
- rm -rf build/
- rm -f riichi.zip
+ @echo "Cleaning build artifacts..."
+ @rm -rf build/
+ @rm -rf dist/
+ @rm -f riichi.zip
+ @rm -rf riichi/
-hard-clean:
- rm -rf build/
- rm -rf node_modules/
- rm -f package-lock.json
- rm -f richi.zip
+# Deep clean (including dependencies)
+hard-clean: clean
+ @echo "Deep cleaning (including dependencies)..."
+ @rm -rf node_modules/
+ @rm -f package-lock.json
+ @rm -rf .cache/
+ @rm -f *.tsbuildinfo
+
+# Show help
+help:
+ @echo ""
+ @echo "Riichi Mahjong Tutorial - Available Commands"
+ @echo "================================================"
+ @echo ""
+ @echo " make install - Install npm dependencies"
+ @echo " make dev - Start development server with hot reload"
+ @echo " make build - Build for production"
+ @echo " make preview - Preview production build"
+ @echo " make test - Run tests"
+ @echo " make test-watch - Run tests in watch mode"
+ @echo " make lint - Check code for linting errors"
+ @echo " make lint-fix - Fix linting errors automatically"
+ @echo " make format - Format code with Prettier"
+ @echo " make typecheck - Run TypeScript type checking"
+ @echo " make zip - Create distribution archive"
+ @echo " make clean - Remove build artifacts"
+ @echo " make hard-clean - Remove all generated files and dependencies"
+ @echo ""
diff --git a/README.md b/README.md
index 1ecff3e..c9f166b 100644
--- a/README.md
+++ b/README.md
@@ -1,83 +1,127 @@
-# 🀄 Tutoriel interactif de Mahjong 🀄
-Un tutoriel complet et interactif pour apprendre à jouer au mahjong, développé en TypeScript. Ce projet vise à rendre l'apprentissage du mahjong accessible à tous.
+# Tutoriel interactif de Mahjong Riichi
+
+
+
+
+
+Un tutoriel complet et interactif pour apprendre à jouer au Mahjong Riichi, développé en TypeScript moderne avec une interface élégante.
-## 🔗 Démonstration
+## Démonstration
Ce tutoriel est accessible directement sur [ce site](https://perso.eleves.ens-rennes.fr/people/adrien.decosse/riichi/).
+## Fonctionnalités
+- **Guide étape par étape** : Apprentissage progressif découpé en chapitres
+- **Interface interactive** : Manipulez les tuiles et expérimentez les dynamiques
+- **Exemples de jeu** : Scénarios dirigés pour s'entraîner aux différentes situations
+- **Design moderne** : Interface élégante avec animations fluides
+- **Performances optimisées** : Rendu canvas optimisé avec double buffering
-## ✨ Fonctionnalités
+## Stack Technique
-- Guide étape par étape: Apprentissage progressif découpé en chapitre
-- Interface interactive: Manipulez les tuiles et expérimenter les dynamiques
-- Exemples de jeu: Scénarios dirigés pour s'entrainer aux différentes situations
-- Non compatible pour le format téléphone ou tablette
+| Technologie | Version | Usage |
+|------------|---------|-------|
+| TypeScript | 5.6+ | Langage principal |
+| Vite | 6.0+ | Build tool & dev server |
+| Vitest | 2.1+ | Tests unitaires |
+| Canvas API | - | Rendu graphique |
+## Démarrage Rapide
+### Prérequis
-## 🛠️ Configuration Typescript
+- Node.js 18.0 ou supérieur
+- npm ou yarn
-- Cible ES2015
-- Mode strict activé
-- Module de type "esnext"
+### Installation
+```bash
+# Cloner le dépôt
+git clone https://github.com/Didictateur/Riichi.git
+cd Riichi
+# Installer les dépendances
+npm install
-## 🛠️ Prérequis
-
-- ts-loader ^9.5.2
-- webpack ^5.98.0
-- webpack-cli ^6.0.1
-
-
-
-## 🚀 Installation
-
-- Clonage du dépot: ```git clone https://github.com/Didictateur/Riichi.git && cd Riichi```
-- Installation des dépendances: ```npm install```
-- Compilation: ```make```
-
-
-
-## Ⓜ️ Makefile
-
-Un makefile est à disposition pour exécuter plusieurs commandes:
-
-- `make` compile le projet dans `build/` en mode `production`
-- `make dev` compile le projet dans `build/` en mode `development`
-- `make zip` compresse le contenu du `build` et les ressource nécessaires dans `riichi.zip`
-- `make clean` supprime `riichi.zip` et le contenu de `build/`
-- `make hard-clean` en plus de clean, supprime toutes les dépendances installées
-
-
-
-## 📁 Structure du projet
-
-```
-Riichi/
-├── src/
-│ ├── img/ # Dossier contenant les images
-│ ├── display/ # Composant gérant l'affichage de chaque section
-│ ├── text/ # Textes des tutoriels
-│ ├── yakus/ # Implémentation des yakus
-│ └── [autres fichiers sources]
-├── img/ # Images et ressources
-├── tests/ # Tests unitaires et d'intégration
-├── build/ # Ensemble du projet compilé en JavaScript
-└── [autres fichiers de configuration]
+# Lancer le serveur de développement
+npm run dev
```
+## Scripts Disponibles
+| Commande | Description |
+|----------|-------------|
+| `npm run dev` | Lance le serveur de développement avec hot reload |
+| `npm run build` | Compile le projet pour la production |
+| `npm run preview` | Prévisualise le build de production |
+| `npm run typecheck` | Vérifie les types TypeScript |
+| `npm run test` | Lance les tests |
+| `npm run test:watch` | Lance les tests en mode watch |
-## 🤝 Source
+## Makefile
-Les images ont été récupérées depuis le github de [FluffyStuff](https://github.com/FluffyStuff/riichi-mahjong-tiles?tab=readme-ov-file).
+Un Makefile est également disponible :
+```bash
+make dev # Serveur de développement
+make build # Build production
+make preview # Prévisualiser le build
+make test # Lancer les tests
+make clean # Nettoyer les fichiers générés
+make hard-clean # Nettoyage complet (incluant node_modules)
+make zip # Créer une archive de distribution
+make help # Afficher l'aide
+```
+## Structure du Projet
-## 📝 Licence
+```
+Riichi/
+├── src/
+│ ├── constants/ # Constantes partagées
+│ ├── display/ # Composants d'affichage (dp*.ts)
+│ ├── styles/ # Feuilles de style CSS
+│ ├── text/ # Textes des tutoriels
+│ ├── types/ # Définitions de types TypeScript
+│ ├── utils/ # Fonctions utilitaires
+│ └── yakus/ # Implémentation des yakus
+├── img/ # Images et ressources
+├── tests/ # Tests unitaires
+├── build/ # Fichiers compilés
+├── index.html # Point d'entrée HTML
+├── vite.config.ts # Configuration Vite
+└── tsconfig.json # Configuration TypeScript
+```
-Tout le projet est dans le [domaine public](https://creativecommons.org/publicdomain/zero/1.0/).
+## Design System
+
+Le projet utilise un système de variables CSS moderne :
+
+```css
+:root {
+ --color-primary: #1a7f4b;
+ --color-accent: #ffd700;
+ --bg-main: linear-gradient(...);
+ /* ... */
+}
+```
+
+## Contribution
+
+Les contributions sont les bienvenues ! N'hésitez pas à :
+1. Fork le projet
+2. Créer une branche (`git checkout -b feature/amelioration`)
+3. Commit vos changements (`git commit -am 'Ajout de fonctionnalité'`)
+4. Push la branche (`git push origin feature/amelioration`)
+5. Ouvrir une Pull Request
+
+## Sources
+
+Les images des tuiles proviennent du dépôt de [FluffyStuff](https://github.com/FluffyStuff/riichi-mahjong-tiles).
+
+## Licence
+
+Ce projet est dans le [domaine public](https://creativecommons.org/publicdomain/zero/1.0/) (CC0 1.0).
diff --git a/backend/src/api.py b/backend/src/api.py
new file mode 100644
index 0000000..3c8a19b
--- /dev/null
+++ b/backend/src/api.py
@@ -0,0 +1,124 @@
+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__)
+CORS(app)
+
+tiles = Tile.generateAll()
+current_game = None # Stockage de la partie actuelle
+
+# ============= 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/', 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''
+ 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
+ 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 current_game.hand_player.draw 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
+ 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)
+
+ # Retourner la main mise à jour
+ 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 current_game.hand_player.draw else None
+
+ return jsonify({'hand': hand_data, 'draw': draw_data})
+
+# ============== POST ================
+
+if __name__ == '__main__':
+ app.run(debug=True, port=5000)
diff --git a/backend/src/game/__init__.py b/backend/src/game/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/src/game/discard.py b/backend/src/game/discard.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/src/game/error.py b/backend/src/game/error.py
new file mode 100644
index 0000000..3ee77e3
--- /dev/null
+++ b/backend/src/game/error.py
@@ -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
\ No newline at end of file
diff --git a/backend/src/game/game.py b/backend/src/game/game.py
new file mode 100644
index 0000000..121d54e
--- /dev/null
+++ b/backend/src/game/game.py
@@ -0,0 +1,20 @@
+from __future__ import annotations
+from .hand import *
+from .wall import *
+
+import random as rd
+
+class Game:
+ def __init__(self):
+ self.wall = Wall()
+
+ self.hand_player = self.wall.draw_hand()
+ self.bot_hands = [self.wall.draw_hand() for _ in range(3)]
+
+ 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()
\ No newline at end of file
diff --git a/backend/src/game/hand.py b/backend/src/game/hand.py
new file mode 100644
index 0000000..3ee6a5f
--- /dev/null
+++ b/backend/src/game/hand.py
@@ -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
\ No newline at end of file
diff --git a/backend/src/game/tile.py b/backend/src/game/tile.py
new file mode 100644
index 0000000..4749262
--- /dev/null
+++ b/backend/src/game/tile.py
@@ -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""""""
+ 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'{svg_content}'
+ 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
\ No newline at end of file
diff --git a/backend/src/game/value.py b/backend/src/game/value.py
new file mode 100644
index 0000000..b5a607b
--- /dev/null
+++ b/backend/src/game/value.py
@@ -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
\ No newline at end of file
diff --git a/backend/src/game/wall.py b/backend/src/game/wall.py
new file mode 100644
index 0000000..b353bdd
--- /dev/null
+++ b/backend/src/game/wall.py
@@ -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)
\ No newline at end of file
diff --git a/backend/src/requirements.txt b/backend/src/requirements.txt
new file mode 100644
index 0000000..6119aea
--- /dev/null
+++ b/backend/src/requirements.txt
@@ -0,0 +1,3 @@
+Flask==3.0.0
+flask-cors==4.0.0
+gunicorn==21.2.0
diff --git a/components/menu.html b/components/menu.html
new file mode 100644
index 0000000..694de32
--- /dev/null
+++ b/components/menu.html
@@ -0,0 +1,142 @@
+
+
+
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..0ae4d9b
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,25 @@
+services:
+ riichi:
+ build: .
+ ports:
+ - "5000:5000"
+ environment:
+ - FLASK_ENV=production
+ volumes:
+ - ./frontend:/app/frontend
+ - ./backend/src:/app/backend/src
+ restart: unless-stopped
+
+ nginx:
+ image: nginx:latest
+ ports:
+ - "127.0.0.1:3002:80"
+ volumes:
+ - ./nginx.conf:/etc/nginx/nginx.conf:ro
+ - ${PWD}/frontend:/usr/share/nginx/html/frontend
+ - ${PWD}/index.html:/usr/share/nginx/html/index.html:ro
+ - ${PWD}/components:/usr/share/nginx/html/components
+ - ${PWD}/img:/usr/share/nginx/html/img
+ depends_on:
+ - riichi
+ restart: unless-stopped
diff --git a/frontend/css/hand.css b/frontend/css/hand.css
new file mode 100644
index 0000000..b52b828
--- /dev/null
+++ b/frontend/css/hand.css
@@ -0,0 +1,108 @@
+/* Hand Display Styles */
+
+.hand-display {
+ margin-bottom: 30px;
+ grid-column: 1 / -1;
+ display: grid;
+ grid-template-columns: 100px 1fr;
+ gap: 30px;
+ align-items: start;
+}
+
+.hand-label {
+ color: #ffffff;
+ margin: 0;
+ font-size: 1.2rem;
+ white-space: nowrap;
+}
+
+.hand-tiles {
+ display: grid;
+ grid-template-columns: repeat(13, minmax(0, 1fr));
+ gap: 3px;
+ flex: 0 0 auto;
+}
+
+.hand-tile-item {
+ padding: 0;
+ text-align: center;
+ border-radius: 4px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ transition: transform 0.2s ease;
+}
+
+/* Effet hover seulement sur la main interactive */
+#interactive-hand .hand-tile-item:hover {
+ transform: translateY(-10px);
+}
+
+/* Hand container - gère l'affichage des tuiles et du draw */
+.hand-container {
+ display: flex;
+ gap: 20px;
+ align-items: flex-start;
+}
+
+.hand-tile-item.draw-tile {
+ /* Le draw s'affiche simplement à droite grâce à flex */
+ flex: 0 0 auto;
+}
+
+/* Orientation variations */
+.hand-display.vertical {
+ grid-template-columns: 100px 1fr;
+}
+
+.hand-display.horizontal {
+ grid-template-columns: 1fr;
+}
+
+.hand-display.horizontal .hand-label {
+ grid-column: 1;
+ margin-bottom: 10px;
+}
+
+.hand-display.horizontal .hand-tiles {
+ grid-column: 1;
+}
+
+/* Position variations */
+.hand-display.left-label {
+ grid-template-columns: 100px 1fr;
+}
+
+.hand-display.top-label {
+ grid-template-columns: 1fr;
+}
+
+.hand-display.top-label .hand-label {
+ grid-column: 1;
+ margin-bottom: 10px;
+}
+
+.hand-display.top-label .hand-tiles {
+ grid-column: 1;
+}
+
+/* Tilt variations */
+.hand-display.tilt-left {
+ transform: rotate(90deg);
+ transform-origin: center;
+}
+
+.hand-display.tilt-right {
+ transform: rotate(-90deg);
+ transform-origin: center;
+}
+
+.hand-display.tilt-upside-down {
+ transform: rotate(180deg);
+ transform-origin: center;
+}
+
+.hand-display.tilt-none {
+ transform: rotate(0deg);
+ transform-origin: center;
+}
diff --git a/frontend/css/speech-bubble.css b/frontend/css/speech-bubble.css
new file mode 100644
index 0000000..0fa29e1
--- /dev/null
+++ b/frontend/css/speech-bubble.css
@@ -0,0 +1,66 @@
+.speech-bubble {
+ background: white;
+ border: 2px solid #333;
+ border-radius: 10px;
+ padding: 12px 16px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.speech-bubble::after {
+ content: '';
+ position: absolute;
+ bottom: -12px;
+ right: 20px;
+ width: 0;
+ height: 0;
+ border-left: 10px solid transparent;
+ border-right: 0 solid transparent;
+ border-top: 12px solid #333;
+}
+
+.speech-bubble::before {
+ content: '';
+ position: absolute;
+ bottom: -8px;
+ right: 22px;
+ width: 0;
+ height: 0;
+ border-left: 8px solid transparent;
+ border-right: 0 solid transparent;
+ border-top: 10px solid white;
+}
+
+.speech-text {
+ flex: 1;
+ white-space: nowrap;
+ font-size: 1.5rem;
+}
+
+.dialog-buttons {
+ display: flex;
+ gap: 5px;
+ margin-left: 10px;
+}
+
+.dialog-btn {
+ padding: 4px 8px;
+ border: 1px solid #333;
+ background: #f0f0f0;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 0.8rem;
+ transition: all 0.2s;
+}
+
+.dialog-btn:hover:not(:disabled) {
+ background: #333;
+ color: white;
+}
+
+.dialog-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
diff --git a/frontend/css/style.css b/frontend/css/style.css
new file mode 100644
index 0000000..f346cf9
--- /dev/null
+++ b/frontend/css/style.css
@@ -0,0 +1,26 @@
+/* Style global */
+html {
+ overflow-y: auto;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ background: radial-gradient(circle at center, #1a5c1a 0%, #0d3d0d 100%);
+ background-attachment: fixed;
+ font-family: Arial, sans-serif;
+ overscroll-behavior: none;
+}
+
+/* Brillance subtile */
+body::after {
+ content: '';
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: radial-gradient(ellipse at center, transparent 0%, rgba(0,0,0,0.15) 100%);
+ pointer-events: none;
+ z-index: -1;
+}
diff --git a/frontend/js/fr/texts.js b/frontend/js/fr/texts.js
new file mode 100644
index 0000000..535c6ca
--- /dev/null
+++ b/frontend/js/fr/texts.js
@@ -0,0 +1,238 @@
+const texts = {
+ homePage: {
+ welcome: `
+ Bienvenue dans le monde du Riichi Mahjong !
+ Installe toi confortablement, et prépare toi à découvrir ce vaste univers.
+ Tu as fais un très bon choix en venant ici, et je suis sûr que tu vas adorer ce jeu !
+ Le Riichi Mahjong est un jeu de société d'origine chinoise, qui a été adapté au Japon et qui est
+ maintenant joué dans le monde entier. C'est un jeu de stratégie, de chance et de psychologie, où
+ les joueurs s'affrontent pour former des combinaisons de tuiles et remporter la partie.
+ Que tu sois complètement nouveau, ou légèrement familier, ce site est fait pour toi. Nous allons
+ te guider à travers les règles du jeu pour que tu puisses profiter d'une partie.
+ Alors prépare tes tuiles, rassemble tes amis, et plonge dans l'aventure du Riichi Mahjong !
+ Ah, j'ai oublié de me présenter ! Je suis Tilineau, ton guide à partir de maintenant. Ne t'inquiète
+ pas, tu es entre de bonnes mains.
+ Pour obtenir mon aide et ouvrir et fermer ce dialogue, il suffit de cliquer sur moi, je ne vais
+ pas te mordre !
+ `
+ },
+
+ riichi: {
+ chap1: `
+ Voici l'ensemble des tuiles au Riichi. Chacune des tuiles présentent ici
+ existe en exactement 4 exemplaires, formant un jeu de 136 tuiles.
+
+ Il y a 3 types de tuiles:
+ - Les familles : Caractères, Ronds et Bambous
+ - Les vents : Est, Sud, Ouest et Nord
+ - Les dragons : Rouge, Vert et Blanc
+
+ Les familles sont numérotées de 1 à 9, exactement comme dans un jeu
+ de cartes.
+
+ Les vents et dragons quant à eux n'ont pas de numéro. On les appelle
+ les honneurs, et ne sont caractérisés que par leur nom.
+ `,
+
+ chap2:`
+ Les joueurs ne se contentent pas de regarder les tuiles. Ils
+ constituent une main composée de 13 tuiles.
+
+ Au début de leur tour, ils commencent par récupérer une
+ tuile, soit du mur (la pioche), soit de la défausse d'un
+ adversaire (nous verrons les détails plus tard).
+
+ Après avoir récupéré une tuile, le joueur doit se défausser
+ d'une tuile de sa main, pour revenir à 13 tuiles. C'est à ce
+ moment que les autres joueurs peuvent réagir à la tuile
+ défaussée, en la récupérant pour compléter leur propre main.
+ `,
+
+ chap3: `
+ Avec ces tuiles, les joueurs peuvent s'amuser à former des
+ combinaisons.
+
+ En soit, c'en est même la mécanique principale. On en distingue
+ deux types:
+ - Les brelans : 3 tuiles identiques
+ - Les suites : 3 tuiles consécutives de la même famille
+
+ Attention, on ne peux pas non plus faire n'importe quoi avec !
+ - Ces combinaisons nécessitent toujours 3 tuiles de la même famille
+ - Les vents et les dragons ne peuvent pas former de suite,
+ seulement des brelans
+
+ Point vocabulaire ! Au riichi, nous utilisons les termes:
+ - Chii pour une suite
+ - Pon pour un brelan
+ `,
+
+ chap4:`
+ Pour gagner, il faut avoir nécessairement un main bien formée.
+ cela a évidemment un rapport avec les groupes vus précédément.
+
+ Pour être exacte, une main est bien formée si elle possède:
+ - 4 groupes de 3 (pon ou chii)
+ - 1 paire
+ Je précise qu'une paire doit être formée par deux tuiles identique.
+
+ Si tu as bien suivie, tu remarquera que cela correspond à un total
+ de 14 tuiles dans sa main. Ainsi, il n'est possible d'avoir une main
+ bien formée que lorsque l'on pioche ou vole une tuile.
+
+ Plus tard, nous verrons qu'il existe deux exceptions à cette
+ forme bien précise.
+ `,
+
+ chap5:`
+ Maintenant, passons aux choses sérieuses: le vole !
+ Enfin... Le vole de tuile en tout cas. Lorsque quelqu'un
+ défausse une tuile, tu as la possibilité de la récupérer
+ si elle t'arrange.
+
+ Mais attention, même voler doit se faire dans les règles
+ de l'art:
+ - seule la dernière tuile défaussée peut être volée
+ - tu peux voler n'importe qui pour former un pon
+ - tu ne peux voler que la personne précédente pour un chii
+ - le pon a la priorité sur le chii
+ - on ne peut pas voler pour former une paire
+
+ Tu as tout retenu ? Ne t'inquiètes pas, ça va vite devenir
+ clair si tu t'y entraines.
+ `,
+
+ chap6:`
+ Avant d'attaquer le point clef du Riichi, nous allons
+ nous pencher un peu plus sur la déclaration de la victoire.
+
+ Pour commencer, lorsqu'une personne est à une seule tuile
+ de la victoire (4 groupes de prêts, et presque pour le dernier),
+ nous disons qu'elle est en Tenpai. C'est l'équivalent du "Uno"
+ version MahJong.
+
+ À ce moment là, si une personne défausse une tuile qui complète
+ sa main - que ça complète un pon, un chii ou même une pair - elle
+ gagne immédiatement.
+
+ Pour être précis, il y a deux situations:
+ - la tuile gagnante est piochée, c'est un "Tsumo"
+ - la tuile gagnante est volée, c'est un "Ron"
+
+ Ces terme sont importants, car ils doivent être pronnoncés pour
+ signaler la victoire !
+
+
+ Au passage, s'il n'y a plus de pioche, alors il y a égalité.
+ `,
+
+ chap7:`
+ Si vous avez déjà joué au tarot, vous savez très bien que le
+ placement des joueurs est important. Il l'est encore plus
+ au Riichi.
+
+ Pour se distinguer, chaque joueur se voit attribuer un vent
+ nommé par les directions cardinales (Est, Sud, Ouest, Nord).
+
+ Sachant que:
+ - l'Est est celui qui commence à jouer (c'est le Dealer)
+ - le vent tourne à la fin de chaque manche
+
+ Il y a cependant une seule exception: si c'est l'Est qui gagne,
+ alors il y a une répétition, et les vents ne tournent pas. La
+ partie s'arrête quand tout le monde a été le joueur Est.
+
+
+ Point culture: Tu pourras également remarquer que l'Est et
+ l'Ouest sont inversés. C'est tout à fait normal ! En effet,
+ les chinois étaient à cette époque très familiers avec l'univers
+ marin. Or, en mer, nous utilisons des cartes célestes où les
+ axes sont inversés.
+ `,
+
+ chap8:`
+ Jettons-nous maintenant dans le feu de l'action: les Yakus.
+
+ Pour déclarer victoire, avoir une main formée n'est pas suffisant. Il faut
+ également avoir ce que l'on appelle un "Yaku". C'est une organisation particulière
+ de sa main qui la rend rare et unique. Plus cette organisation est rare, et plus
+ la main va valoir beaucoup de point en cas de victoire.
+
+ La plupart du temps, il suffit de connaitre quelques yakus pour pouvoir jouer et
+ s'amuser. Par exemple:
+ - brelan de dragon
+ - brelan d'Est, ou de son propre vent
+ - main (semie) pure: toutes les tuiles (plus des vents/dragons) de la même famille
+ - tout ordinaire: pas de dragon, ni de vent, ni de 1, ni de 9
+
+ Il y a également deux Yakus qui sortent du lot:
+ - 7 paires: explicite et assez courant. Attention ! Toutes les paires sont distinctes
+ - 13 orphelins: réalisable une fois dans sa vie, mais très beau yaku
+
+ Une liste exhaustive des yakus les plus courants au moins courants sont présentés dans
+ l'onglet dédié.
+ `,
+
+ chap9:`
+ Le Riichi est le yaku le plus important du jeu ! Ce n'est pas pour rien
+ que ça s'appelle ainsi.
+
+ Le principe est très simple. Lorsqu'une personne est en Tenpai (à une tuile
+ de la victoire), et que sa main est FERMÉE, alors il peut déclarer le riichi.
+ À ce moment là, elle ne peut plus changer sa main: soit elle gagne par Tsumo
+ ou Ron, soit elle défausse une tuile qui fait gagner quelqu'un d'autre.
+
+ À partir de maintenant, il va donc falloir bien réfléchir aux appels
+ (pon et chii), puisque cela ouvre la main et fixe des groupes, et va donc
+ restreindre les yakus qui seront réalisables.
+ `,
+
+ chap10:`
+ À ce stade de l'histoire, vous conaissez toutes les règles principales pour jouer
+ au Riichi. Mais on va s'attarder sur le cas du Furiten. Pour faire court, c'est
+ l'inderdiction pour une personne de faire Ron si elle aurait dû déjà avoir gagné.
+
+ Il y a deux cas:
+ - Une des tuiles de ta défausse pourrait compléter ta main
+ - Tu refuses de Ron quelqu'un
+
+ Dans le premier cas, tu es foutu. Tu as l'interdiction de faire un Ron, et tu ne
+ peux gagner qu'avec un Tsumo. Dans le second cas, c'est un furiten temporaire,
+ et il faut attendre ton tour pour pouvoir Ron de nouveau.
+
+ Précisions:
+ - Même si une tuile ne te donne pas de yaku, du momentqu'elle te permet de
+ rendre ta main valide, il y a la question du furiten
+ - Une tuile t'empêche de faire ron sur N'IMPORTE QU'ELLE tuile
+
+ L'objectif derrière cette règle est d'empêcher de viser quelqu'un spécifiquement.
+
+ Cela permet également de savoir quelles tuiles peuvent être défausser pour ne pas
+ faire gagner quelqu'un d'autre.
+ `,
+
+ chap11:`
+ Ça y est, tu as fait le plus gros travail ! Félicitation
+ d'être arrivé jusqu'ici !
+
+ Après t'être entrainé à jouer des parties, je teconseil
+ très vivement de lire la section Scoring, a minima les
+ deux premiers chapitres
+ `,
+ },
+
+ liste_yakus:{
+ explanation:`
+ Voici la liste exhaustive des yakus avec leur fréquence d'apparition
+ selon le site Tenhou pour janvier 2026.
+
+ Je précise qu'une main peut avoir plusieurs yakus en même temps,
+ donc la somme des fréquences dépasse 100%.
+
+ De surcroît, ce classement est très dépendante des joueurs. En effet
+ les 7 paires est un yaku accessible, mais peu de joueurs confirmés le
+ choisissent, car sa forme particulière permet très difficilement de
+ manipuler sa main pour en avoir des plus intéressantes.
+ `,
+ },
+};
\ No newline at end of file
diff --git a/frontend/js/hand.js b/frontend/js/hand.js
new file mode 100644
index 0000000..f10170c
--- /dev/null
+++ b/frontend/js/hand.js
@@ -0,0 +1,64 @@
+/**
+ * Hand Display Object
+ * Affiche une main de Riichi avec options de position et orientation
+ */
+
+class Hand {
+ constructor(options = {}) {
+ this.label = options.label || 'Main';
+ this.position = options.position || 'left-label'; // 'left-label' or 'top-label'
+ this.tilt = options.tilt || 'none'; // 'none', 'left' (90deg), 'right' (-90deg), 'upside-down' (180deg)
+ this.containerId = options.containerId || 'tiles-display';
+ this.apiUrl = options.apiUrl || '/api/random_hand';
+ }
+
+ /**
+ * Récupère une main aléatoire et l'affiche
+ */
+ async render() {
+ try {
+ const response = await fetch(this.apiUrl);
+ const data = await response.json();
+ this.displayHand(data.hand);
+ } catch (error) {
+ console.error('Erreur lors de la récupération de la main:', error);
+ }
+ }
+
+ /**
+ * Affiche la main dans le DOM
+ * @param {Array} tiles - Tableau des tuiles à afficher
+ */
+ displayHand(tiles) {
+ const container = document.getElementById(this.containerId);
+
+ // Créer la section de la main
+ const handDiv = document.createElement('div');
+ const classNames = `hand-display ${this.position}`;
+ if (this.tilt !== 'none') {
+ handDiv.className = `${classNames} tilt-${this.tilt}`;
+ } else {
+ handDiv.className = classNames;
+ }
+
+ // Créer le label
+ const label = document.createElement('div');
+ label.className = 'hand-label';
+ label.textContent = this.label;
+ handDiv.appendChild(label);
+
+ // Créer la grille de tuiles
+ const handTiles = document.createElement('div');
+ handTiles.className = 'hand-tiles';
+
+ tiles.forEach(tile => {
+ const tileDiv = document.createElement('div');
+ tileDiv.className = 'hand-tile-item';
+ tileDiv.innerHTML = tile.svg;
+ handTiles.appendChild(tileDiv);
+ });
+
+ handDiv.appendChild(handTiles);
+ container.appendChild(handDiv);
+ }
+}
diff --git a/frontend/pages/liste_yakus.html b/frontend/pages/liste_yakus.html
new file mode 100644
index 0000000..9d280b0
--- /dev/null
+++ b/frontend/pages/liste_yakus.html
@@ -0,0 +1,411 @@
+
+
+
+
+ Liste des Yakus - Tuto Riichi
+
+
+
+
+
+
+