feat: gauge scoring system (subtract left neighbor per attribute)

This commit is contained in:
kevin.lhereec committed 2026-08-17 11:54:23 +02:00
1 parent 25ab76c4ac
commit 91a1a9f4f0
5 files changed
+297 -203

No files matched your search

+60 -69
View File
@@ -1,26 +1,27 @@
const { dealHands } = require('./cards')
const MAX_PLAYERS = 4
const MAX_TURNS = 12 // 12 tours (1 par personnage potentiellement)
const HAND_SIZE = 4 // 4 cartes en main au départ
const MAX_TURNS = 12
const HAND_SIZE = 4
const ATTRS = ['food', 'war', 'health', 'diplo']
/**
* Logique de résolution par tour :
* - Chaque joueur joue une carte simultanément
* - On compare les 4 attributs (food, war, health, diplo) entre tous les joueurs
* - Pour chaque attribut, le joueur avec la valeur la plus élevée marque des points
* - En cas d'égalité, personne ne marque sur cet attribut
* - Les points marqués = valeur de l'attribut gagnant
* Système de score :
* Chaque joueur possède 4 jauges (food, war, health, diplo), toutes à 0 au départ.
* À chaque tour, on compare chaque joueur à son VOISIN DE GAUCHE (sens horaire) :
* gain = max(0, ma_valeur_attr - valeur_attr_voisin_gauche)
* Ce gain s'ajoute à la jauge correspondante du joueur.
* Le score total est la somme des 4 jauges.
*/
class Room {
constructor(id) {
this.id = id
this.players = [] // { socketId, name, teamIndex, score, hand }
this.phase = 'lobby' // lobby | selecting | waiting | resolve | end
this.id = id
this.players = [] // { socketId, name, teamIndex, gauges:{food,war,health,diplo}, hand }
this.phase = 'lobby' // lobby | selecting | waiting | resolve | end
this.currentTurn = 0
this.maxTurns = MAX_TURNS
this.pendingPlays = new Map() // socketId -> card
this.maxTurns = MAX_TURNS
this.pendingPlays = new Map() // socketId -> { player, card }
}
get playerCount() { return this.players.length }
@@ -34,8 +35,8 @@ class Room {
this.players.push({
socketId,
name,
teamIndex: this.players.length, // 0=Écarlate 1=Azur 2=Émeraude 3=Or
score: 0,
teamIndex: this.players.length,
gauges: { food: 0, war: 0, health: 0, diplo: 0 },
hand: []
})
return true
@@ -49,7 +50,10 @@ class Room {
startGame() {
const hands = dealHands(this.players.length, HAND_SIZE)
this.players.forEach((p, i) => { p.hand = hands[i] })
this.players.forEach((p, i) => {
p.hand = hands[i]
p.gauges = { food: 0, war: 0, health: 0, diplo: 0 }
})
this.currentTurn = 1
this.phase = 'selecting'
this.pendingPlays.clear()
@@ -72,8 +76,6 @@ class Room {
const [card] = player.hand.splice(cardIndex, 1)
this.pendingPlays.set(socketId, { player, card })
// La phase globale reste 'selecting' tant que tout le monde n'a pas joué
// (seul le joueur qui vient de jouer passe en 'waiting' côté client via son état perso)
return { ok: true }
}
@@ -84,82 +86,73 @@ class Room {
// ─── Résolution du tour ─────────────────────────────────────────────────────
resolveRound() {
const plays = [...this.pendingPlays.values()]
const n = this.players.length
// Construire un tableau ordonné des cartes jouées (même ordre que this.players)
const plays = this.players.map(p => ({
player: p,
card: this.pendingPlays.get(p.socketId)?.card ?? null
}))
// Cartes jouées (pour affichage)
// Cartes jouées pour l'affichage
const playedCards = plays.map(({ player, card }) => ({
playerName: player.name,
teamIndex: player.teamIndex,
cardId: card.id,
cardName: card.name,
food: card.food,
war: card.war,
health: card.health,
diplo: card.diplo
cardId: card?.id,
cardName: card?.name,
food: card?.food ?? 0,
war: card?.war ?? 0,
health: card?.health ?? 0,
diplo: card?.diplo ?? 0
}))
// Résolution attribut par attribut
const attrs = ['food', 'war', 'health', 'diplo']
const pointsGained = {} // socketId -> { food, war, health, diplo, total }
plays.forEach(({ player }) => {
pointsGained[player.socketId] = { food: 0, war: 0, health: 0, diplo: 0, total: 0 }
// Gains par joueur et par attribut
// Règle : je compare mon attribut à celui du VOISIN DE GAUCHE (index - 1, modulo n)
// gain = max(0, moi - voisin_gauche)
const gains = plays.map((play, i) => {
const leftIndex = (i - 1 + n) % n
const leftCard = plays[leftIndex].card
const g = {}
ATTRS.forEach(attr => {
g[attr] = Math.max(0, (play.card?.[attr] ?? 0) - (leftCard?.[attr] ?? 0))
})
return g
})
const attrResults = {} // attr -> { winner: name | 'tie', value }
attrs.forEach(attr => {
const max = Math.max(...plays.map(p => p.card[attr]))
const winners = plays.filter(p => p.card[attr] === max)
if (winners.length === 1) {
// Un seul gagnant
const { player } = winners[0]
pointsGained[player.socketId][attr] = max
pointsGained[player.socketId].total += max
player.score += max
attrResults[attr] = { winner: player.name, value: max }
} else {
// Égalité → personne ne marque
attrResults[attr] = { winner: 'tie', value: max, tiedPlayers: winners.map(w => w.player.name) }
}
// Appliquer les gains aux jauges
plays.forEach(({ player }, i) => {
ATTRS.forEach(attr => {
player.gauges[attr] += gains[i][attr]
})
})
// Message de résumé
const lines = attrs.map(attr => {
const r = attrResults[attr]
const label = { food: 'Nourriture', war: 'Guerre', health: 'Santé', diplo: 'Diplomatie' }[attr]
if (r.winner === 'tie') return `${label} : Égalité (${r.tiedPlayers.join(' & ')}) — personne ne marque`
return `${label} : ${r.winner} gagne ${r.value} pts`
// Résumé textuel
const lines = plays.map(({ player }, i) => {
const g = gains[i]
const parts = ATTRS
.filter(a => g[a] > 0)
.map(a => `+${g[a]} ${a}`)
return `${player.name} : ${parts.length ? parts.join(', ') : 'rien'}`
})
this.pendingPlays.clear()
this.phase = 'resolve'
return {
playedCards,
attrResults,
pointsGained,
message: lines.join('\n')
}
return { playedCards, gains, message: lines.join(' | ') }
}
// ─── Tour suivant ────────────────────────────────────────────────────────────
nextTurn() {
this.currentTurn++
if (this.isGameOver()) {
this.phase = 'end'
return
}
this.phase = 'selecting'
// Si un joueur n'a plus de cartes en main, on lui repioche depuis son deck perso
// (dans cette version simple la main se vide naturellement)
}
isGameOver() {
// Partie terminée quand tous les joueurs n'ont plus de cartes OU après maxTurns
const allEmpty = this.players.every(p => p.hand.length === 0)
return allEmpty || this.currentTurn >= this.maxTurns
}
@@ -170,16 +163,14 @@ class Room {
return this.players.map(p => ({
name: p.name,
teamIndex: p.teamIndex,
score: p.score,
gauges: { ...p.gauges },
score: ATTRS.reduce((sum, a) => sum + p.gauges[a], 0),
handSize: p.hand.length
}))
}
/** État envoyé à UN joueur spécifique (inclut sa main et sa phase personnelle) */
getStateFor(socketId) {
const player = this.players.find(p => p.socketId === socketId)
// Si ce joueur a déjà joué ce tour → sa phase perso est 'waiting'
// Sinon il est encore en 'selecting'
const hasPlayed = this.pendingPlays.has(socketId)
const personalPhase = this.phase === 'selecting' && hasPlayed ? 'waiting' : this.phase
+2 -2
View File
@@ -81,10 +81,10 @@ io.on('connection', (socket) => {
const roundResult = room.resolveRound()
console.log(`[${roomId}] Tour ${room.currentTurn} résolu`)
// Révéler les cartes jouées et les résultats
// Révéler les cartes jouées et les gains par attribut
io.to(roomId).emit('round-result', {
playedCards: roundResult.playedCards,
attrResults: roundResult.attrResults,
gains: roundResult.gains,
message: roundResult.message
})