exemples de mains dans chap 2

This commit is contained in:
Didictateur 2026-02-18 19:40:10 +01:00
parent c471c84239
commit d23d6a24f7
10 changed files with 571 additions and 2 deletions

View file

@ -2,6 +2,8 @@ from flask import Flask, jsonify, request
from flask_cors import CORS from flask_cors import CORS
from game.tile import * from game.tile import *
from game.hand import *
from game.game import *
from game.error import * from game.error import *
app = Flask(__name__) app = Flask(__name__)
@ -38,6 +40,46 @@ def get_tile(tile_id):
'svg': svg '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"""
game = Game()
game.draw_tile(game.hand_player)
hand_data = [
{
'id': tile.id,
'family': str(tile.family),
'value': str(tile.value),
'svg': tile.get_img()
}
for tile in game.hand_player.tiles
]
draw_data = {
'id': game.hand_player.draw.id,
'family': str(game.hand_player.draw.family),
'value': str(game.hand_player.draw.value),
'svg': game.hand_player.draw.get_img()
} if game.hand_player.draw else None
return jsonify({'hand': hand_data, 'draw': draw_data})
# ============== POST ================ # ============== POST ================
if __name__ == '__main__': if __name__ == '__main__':

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

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

View file

@ -0,0 +1,38 @@
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
return draw
for i, tile in enumerate(self.tiles):
if tile.id == id:
return self.tiles.pop(i)
return None
@staticmethod
def random() -> Hand:
tiles = Tile.generateAll()
rd.shuffle(tiles)
hand = Hand(tiles[:13])
hand.sort()
return hand

View file

@ -1,6 +1,8 @@
from game.error import * from game.error import *
from game.value import * from game.value import *
from functools import total_ordering
@total_ordering
class Tile: class Tile:
def __init__(self, family, value, id): def __init__(self, family, value, id):
self.family = family self.family = family
@ -74,7 +76,11 @@ class Tile:
return self.value == other.value and self.family == other.family return self.value == other.value and self.family == other.family
def __lt__(self, other: Tile): def __lt__(self, other: Tile):
return (self.family < other.family) or (self.family == other.family and self.value < other.value) 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 @staticmethod
def generateAll(): def generateAll():

View file

@ -0,0 +1,22 @@
from .tile import *
from .hand import *
import random as rd
class Wall:
def __init__(self):
self.tiles = Tile.generateAll()
self.shuffle()
def shuffle(self):
rd.shuffle(self.tiles)
def draw(self) -> Tile:
if len(self.tiles) == 0:
raise Exception("No more tiles in the wall")
return self.tiles.pop()
def draw_hand(self) -> Hand:
hand_tiles = [self.draw() for _ in range(13)]
hand_tiles.sort()
return Hand(hand_tiles)

View file

@ -11,7 +11,7 @@
<a href="#">Riichi</a> <a href="#">Riichi</a>
<ul class="submenu"> <ul class="submenu">
<li><a href="/frontend/pages/chap1.html">Chapitre 1 : Les tuiles</a></li> <li><a href="/frontend/pages/chap1.html">Chapitre 1 : Les tuiles</a></li>
<li><a href="">Chapitre 2 : La main</a></li> <li><a href="/frontend/pages/chap2.html">Chapitre 2 : La main</a></li>
<li><a href="">Chapitre 3 : Les groupes</a></li> <li><a href="">Chapitre 3 : Les groupes</a></li>
</ul> </ul>
</li> </li>

102
frontend/css/hand.css Normal file
View file

@ -0,0 +1,102 @@
/* 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;
}
/* 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;
}

View file

@ -32,6 +32,19 @@ const texts = {
<br> <br>
Les vents et dragons quant à eux n'ont pas de numéro. On les appelle<br> Les vents et dragons quant à eux n'ont pas de numéro. On les appelle<br>
les honneurs, et ne sont caractérisés que par leur nom. les honneurs, et ne sont caractérisés que par leur nom.
`,
chap2:`
Les joueurs ne se contentent pas de regarder les tuiles. Ils<br>
constituent une main composée de 13 tuiles.<br>
<br>
Au début de leur tour, ils commencent par récupérer une<br>
tuile, soit du mur (la pioche), soit de la défausse d'un<br>
adversaire (nous verrons les détails plus tard).<br>
<br>
Après avoir récupéré une tuile, le joueur doit se défausser<br>
d'une tuile de sa main, pour revenir à 13 tuiles. C'est à ce<br>
moment que les autres joueurs peuvent réagir à la tuile<br>
défaussée, en la récupérant pour compléter leur propre main.
` `
} }
}; };

64
frontend/js/hand.js Normal file
View file

@ -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 || 'http://localhost:5000/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);
}
}

263
frontend/pages/chap2.html Normal file
View file

@ -0,0 +1,263 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Tuto Riichi</title>
<link rel="stylesheet" href="../../frontend/css/style.css">
<link rel="stylesheet" href="../../frontend/css/speech-bubble.css">
<link rel="stylesheet" href="../../frontend/css/hand.css">
<style>
#menu {
position: relative;
z-index: 10;
}
#mascotte {
position: fixed;
bottom: 0px;
right: 160px;
z-index: 999;
transform: scaleX(-1);
cursor: pointer;
}
#mascotte img {
width: 250px;
height: auto;
display: block;
}
.speech-bubble {
position: fixed;
bottom: 320px;
right: 170px;
z-index: 998;
display: none;
}
.speech-bubble.visible {
display: flex;
}
#tiles-display {
padding: 20px;
max-width: 900px;
margin: 20px auto;
display: grid;
grid-template-columns: 100px 1fr;
gap: 30px;
align-items: start;
}
.section-title {
grid-column: 1 / -1;
color: #ffffff;
font-size: 1.5rem;
margin: 20px 0 10px 0;
font-weight: bold;
}
.hands-container {
grid-column: 1 / -1;
display: flex;
flex-direction: column;
gap: 30px;
justify-content: flex-start;
}
.hands-container .hand-display {
grid-column: auto;
margin-bottom: 0;
width: 100%;
}
.tiles-family {
margin-bottom: 0;
grid-column: 1 / -1;
display: grid;
grid-template-columns: 100px 1fr;
gap: 30px;
align-items: start;
}
.tiles-family h3 {
color: #ffffff;
margin: 0;
font-size: 1.2rem;
white-space: nowrap;
}
.tiles-grid {
display: grid;
grid-template-columns: repeat(9, minmax(0, 1fr));
gap: 3px;
}
.tile-item {
padding: 0;
text-align: center;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
}
.tiles-numbers {
grid-column: 2;
display: grid;
grid-template-columns: repeat(9, minmax(0, 1fr));
gap: 3px;
margin-bottom: 20px;
}
.tiles-numbers div {
text-align: center;
font-weight: bold;
color: #ffffff;
padding: 5px 0;
min-height: 50px;
display: flex;
align-items: center;
justify-content: center;
}
</style>
</head>
<body>
<div id="menu"></div>
<div id="tile-container"></div>
<div id="tiles-display"></div>
<div class="speech-bubble">
<div class="speech-text" id="speech-text"></div>
</div>
<div id="mascotte"></div>
<script src="../../frontend/js/fr/texts.js"></script>
<script src="../../frontend/js/hand.js"></script>
<script>
// Charger le menu
fetch('../../components/menu.html')
.then(r => r.text())
.then(html => document.getElementById('menu').innerHTML = html);
// Afficher la mascotte
const mascolteEl = document.getElementById('mascotte');
mascolteEl.innerHTML = '<img src="../../img/tilineau/speaking.png" alt="Tilineau">';
// Afficher le texte de bienvenue
document.getElementById('speech-text').innerHTML = texts.riichi.chap2;
// Toggle bulle de dialogue au clic sur la mascotte
const speechBubble = document.querySelector('.speech-bubble');
mascolteEl.addEventListener('click', () => {
speechBubble.classList.toggle('visible');
if (speechBubble.classList.contains('visible')) {
mascolteEl.innerHTML = '<img class="explaining" src="../../img/tilineau/explaining.png" alt="Tilineau">';
} else {
mascolteEl.innerHTML = '<img src="../../img/tilineau/speaking.png" alt="Tilineau">';
}
});
// Créer le titre de section
const tilesDisplay = document.getElementById('tiles-display');
const title = document.createElement('div');
title.className = 'section-title';
title.textContent = 'Exemples de mains';
tilesDisplay.appendChild(title);
// Créer le conteneur des mains
const handsContainer = document.createElement('div');
handsContainer.className = 'hands-container';
tilesDisplay.appendChild(handsContainer);
// Créer et afficher trois mains aléatoires
for (let i = 0; i < 3; i++) {
const hand = new Hand({
label: `Main ${i + 1}`,
position: 'top-label',
tilt: 'none',
containerId: 'tiles-display'
});
// Modifier temporairement le containerId pour ajouter à handsContainer
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
const interactiveTitle = document.createElement('div');
interactiveTitle.className = 'section-title';
interactiveTitle.textContent = 'Main interactive';
tilesDisplay.appendChild(interactiveTitle);
// Créer et afficher la main interactive avec une tuile piochée
fetch('http://localhost:5000/api/hand_with_draw')
.then(r => r.json())
.then(data => {
const handDiv = document.createElement('div');
handDiv.className = 'hand-display top-label tilt-none';
handDiv.id = 'interactive-hand';
const label = document.createElement('div');
label.className = 'hand-label';
label.textContent = 'Votre main';
handDiv.appendChild(label);
// Conteneur qui gère l'affichage avec flex (gère auto le draw si présent)
const container = document.createElement('div');
container.className = 'hand-container';
// Créer le grid des 13 tuiles
const handTiles = document.createElement('div');
handTiles.className = 'hand-tiles';
handTiles.id = 'interactive-tiles';
data.hand.forEach(tile => {
const tileDiv = document.createElement('div');
tileDiv.className = 'hand-tile-item';
tileDiv.innerHTML = tile.svg;
handTiles.appendChild(tileDiv);
});
container.appendChild(handTiles);
const drawTileDiv = document.createElement('div');
drawTileDiv.className = 'hand-tile-item draw-tile';
drawTileDiv.innerHTML = data.draw.svg;
container.appendChild(drawTileDiv);
handDiv.appendChild(container);
tilesDisplay.appendChild(handDiv);
})
.catch(error => console.error('Erreur:', error));
</script>
</body>
</html>