feat: 12 character cards with 4 color-coded attributes (food/war/health/diplo)
This commit is contained in:
1 parent
1db221e468
commit
38814668e7
7 files changed
+2190
-317
No files matched your search
+109
-99
@@ -1,22 +1,32 @@
|
||||
const { dealHands, createDeck } = require('./cards')
|
||||
const { dealHands } = require('./cards')
|
||||
|
||||
const MAX_PLAYERS = 4
|
||||
const MAX_TURNS = 10
|
||||
const HAND_SIZE = 5
|
||||
const MAX_TURNS = 12 // 12 tours (1 par personnage potentiellement)
|
||||
const HAND_SIZE = 4 // 4 cartes en main au départ
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
class Room {
|
||||
constructor(id) {
|
||||
this.id = id
|
||||
this.players = [] // { socketId, name, score, hand, protected }
|
||||
this.phase = 'lobby' // lobby | selecting | waiting | resolve | end
|
||||
this.id = id
|
||||
this.players = [] // { socketId, name, teamIndex, score, hand }
|
||||
this.phase = 'lobby' // lobby | selecting | waiting | resolve | end
|
||||
this.currentTurn = 0
|
||||
this.maxTurns = MAX_TURNS
|
||||
this.maxTurns = MAX_TURNS
|
||||
this.pendingPlays = new Map() // socketId -> card
|
||||
this.deck = []
|
||||
}
|
||||
|
||||
get playerCount() { return this.players.length }
|
||||
|
||||
// ─── Gestion des joueurs ────────────────────────────────────────────────────
|
||||
|
||||
addPlayer(socketId, name) {
|
||||
if (this.players.length >= MAX_PLAYERS) return false
|
||||
if (this.players.find(p => p.socketId === socketId)) return true
|
||||
@@ -24,9 +34,9 @@ class Room {
|
||||
this.players.push({
|
||||
socketId,
|
||||
name,
|
||||
teamIndex: this.players.length, // 0=Écarlate 1=Azur 2=Émeraude 3=Or
|
||||
score: 0,
|
||||
hand: [],
|
||||
protected: false
|
||||
hand: []
|
||||
})
|
||||
return true
|
||||
}
|
||||
@@ -35,29 +45,38 @@ class Room {
|
||||
this.players = this.players.filter(p => p.socketId !== socketId)
|
||||
}
|
||||
|
||||
// ─── Démarrage ──────────────────────────────────────────────────────────────
|
||||
|
||||
startGame() {
|
||||
const { hands, remaining } = dealHands(this.players.length, HAND_SIZE)
|
||||
this.deck = remaining
|
||||
const hands = dealHands(this.players.length, HAND_SIZE)
|
||||
this.players.forEach((p, i) => { p.hand = hands[i] })
|
||||
this.currentTurn = 1
|
||||
this.phase = 'selecting'
|
||||
this.pendingPlays.clear()
|
||||
}
|
||||
|
||||
// ─── Phase de jeu ───────────────────────────────────────────────────────────
|
||||
|
||||
playCard(socketId, cardId) {
|
||||
if (this.phase !== 'selecting') return { ok: false, error: 'Ce n\'est pas la phase de jeu' }
|
||||
if (this.pendingPlays.has(socketId)) return { ok: false, error: 'Tu as déjà joué ce tour' }
|
||||
if (this.phase !== 'selecting')
|
||||
return { ok: false, error: 'Ce n\'est pas la phase de sélection' }
|
||||
if (this.pendingPlays.has(socketId))
|
||||
return { ok: false, error: 'Tu as déjà joué une carte ce tour' }
|
||||
|
||||
const player = this.players.find(p => p.socketId === socketId)
|
||||
if (!player) return { ok: false, error: 'Joueur introuvable' }
|
||||
|
||||
const cardIndex = player.hand.findIndex(c => c.id === cardId)
|
||||
if (cardIndex === -1) return { ok: false, error: 'Carte non trouvée dans ta main' }
|
||||
if (cardIndex === -1) return { ok: false, error: 'Carte absente de ta main' }
|
||||
|
||||
const card = player.hand.splice(cardIndex, 1)[0]
|
||||
const [card] = player.hand.splice(cardIndex, 1)
|
||||
this.pendingPlays.set(socketId, { player, card })
|
||||
|
||||
this.phase = 'waiting'
|
||||
// Si tous ont joué → passer en "waiting" visible côté client
|
||||
if (this.pendingPlays.size === this.players.length) {
|
||||
this.phase = 'waiting'
|
||||
}
|
||||
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
@@ -65,119 +84,110 @@ class Room {
|
||||
return this.pendingPlays.size === this.players.length
|
||||
}
|
||||
|
||||
// ─── Résolution du tour ─────────────────────────────────────────────────────
|
||||
|
||||
resolveRound() {
|
||||
const plays = [...this.pendingPlays.values()]
|
||||
const playedCards = plays.map(p => ({ name: p.player.name, cardName: p.card.name }))
|
||||
|
||||
// Réinitialiser protection
|
||||
this.players.forEach(p => { p.protected = false })
|
||||
// Cartes jouées (pour 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
|
||||
}))
|
||||
|
||||
// 1. Appliquer les effets de protection (Enchanteur)
|
||||
plays.forEach(({ player, card }) => {
|
||||
if (card.effect === 'protect') {
|
||||
player.protected = true
|
||||
// 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 }
|
||||
})
|
||||
|
||||
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) }
|
||||
}
|
||||
})
|
||||
|
||||
// 2. Calculer les points de base et effets
|
||||
const messages = []
|
||||
plays.forEach(({ player, card }) => {
|
||||
// Points de base
|
||||
player.score += card.points
|
||||
|
||||
if (!card.effect) return
|
||||
|
||||
const [effectType, valueStr] = card.effect.split(':')
|
||||
const value = parseInt(valueStr) || 0
|
||||
|
||||
switch (effectType) {
|
||||
case 'steal': {
|
||||
// Voler des points au joueur le plus riche non protégé
|
||||
const targets = this.players
|
||||
.filter(p => p.socketId !== player.socketId && !p.protected && p.score > 0)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
|
||||
if (targets.length > 0) {
|
||||
const stolen = Math.min(value, targets[0].score)
|
||||
targets[0].score -= stolen
|
||||
player.score += stolen
|
||||
messages.push(`${player.name} vole ${stolen} pts à ${targets[0].name}`)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'bonus': {
|
||||
// Bonus si le joueur a le plus de points de Royaume ce tour
|
||||
const myRoyaume = plays
|
||||
.filter(p => p.player.socketId === player.socketId && p.card.type === 'Royaume')
|
||||
.reduce((sum, p) => sum + p.card.points, 0)
|
||||
if (myRoyaume > 0) {
|
||||
player.score += value
|
||||
messages.push(`${player.name} gagne +${value} pts bonus (Commerce)`)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'double': {
|
||||
// Double les points gagnés CE tour (uniquement la carte jouée)
|
||||
player.score += card.points
|
||||
messages.push(`${player.name} double ses gains (Alchimiste)`)
|
||||
break
|
||||
}
|
||||
|
||||
case 'draw': {
|
||||
// Piocher une carte supplémentaire
|
||||
if (this.deck.length > 0) {
|
||||
player.hand.push(this.deck.pop())
|
||||
messages.push(`${player.name} pioche une carte bonus`)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
// 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`
|
||||
})
|
||||
|
||||
const message = messages.length > 0 ? messages.join(' | ') : 'Tour résolu !'
|
||||
|
||||
this.pendingPlays.clear()
|
||||
this.phase = 'resolve'
|
||||
|
||||
return { playedCards, message }
|
||||
return {
|
||||
playedCards,
|
||||
attrResults,
|
||||
pointsGained,
|
||||
message: lines.join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Tour suivant ────────────────────────────────────────────────────────────
|
||||
|
||||
nextTurn() {
|
||||
this.currentTurn++
|
||||
this.phase = 'selecting'
|
||||
|
||||
// Distribuer une nouvelle carte si la main est vide
|
||||
this.players.forEach(p => {
|
||||
if (p.hand.length === 0 && this.deck.length >= HAND_SIZE) {
|
||||
p.hand = this.deck.splice(0, HAND_SIZE)
|
||||
} else if (p.hand.length < 2 && this.deck.length > 0) {
|
||||
p.hand.push(this.deck.pop())
|
||||
}
|
||||
})
|
||||
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() {
|
||||
return this.currentTurn >= this.maxTurns
|
||||
// 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
|
||||
}
|
||||
|
||||
// ─── Données publiques ───────────────────────────────────────────────────────
|
||||
|
||||
getPlayersInfo() {
|
||||
return this.players.map(p => ({
|
||||
name: p.name,
|
||||
score: p.score,
|
||||
handSize: p.hand.length
|
||||
name: p.name,
|
||||
teamIndex: p.teamIndex,
|
||||
score: p.score,
|
||||
handSize: p.hand.length
|
||||
}))
|
||||
}
|
||||
|
||||
/** État envoyé à UN joueur spécifique (inclut sa main) */
|
||||
getStateFor(socketId) {
|
||||
const player = this.players.find(p => p.socketId === socketId)
|
||||
return {
|
||||
players: this.getPlayersInfo(),
|
||||
players: this.getPlayersInfo(),
|
||||
currentTurn: this.currentTurn,
|
||||
maxTurns: this.maxTurns,
|
||||
phase: this.phase,
|
||||
hand: player ? player.hand : []
|
||||
maxTurns: this.maxTurns,
|
||||
phase: this.phase,
|
||||
hand: player ? player.hand : [],
|
||||
myTeamIndex: player ? player.teamIndex : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user