feat: initial Regnum project structure (Vue3 + Phaser3 + Socket.io)
This commit is contained in:
commit
1db221e468
23 files changed
+1265
No files matched your search
@@ -0,0 +1,185 @@
|
||||
const { dealHands, createDeck } = require('./cards')
|
||||
|
||||
const MAX_PLAYERS = 4
|
||||
const MAX_TURNS = 10
|
||||
const HAND_SIZE = 5
|
||||
|
||||
class Room {
|
||||
constructor(id) {
|
||||
this.id = id
|
||||
this.players = [] // { socketId, name, score, hand, protected }
|
||||
this.phase = 'lobby' // lobby | selecting | waiting | resolve | end
|
||||
this.currentTurn = 0
|
||||
this.maxTurns = MAX_TURNS
|
||||
this.pendingPlays = new Map() // socketId -> card
|
||||
this.deck = []
|
||||
}
|
||||
|
||||
get playerCount() { return this.players.length }
|
||||
|
||||
addPlayer(socketId, name) {
|
||||
if (this.players.length >= MAX_PLAYERS) return false
|
||||
if (this.players.find(p => p.socketId === socketId)) return true
|
||||
|
||||
this.players.push({
|
||||
socketId,
|
||||
name,
|
||||
score: 0,
|
||||
hand: [],
|
||||
protected: false
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
removePlayer(socketId) {
|
||||
this.players = this.players.filter(p => p.socketId !== socketId)
|
||||
}
|
||||
|
||||
startGame() {
|
||||
const { hands, remaining } = dealHands(this.players.length, HAND_SIZE)
|
||||
this.deck = remaining
|
||||
this.players.forEach((p, i) => { p.hand = hands[i] })
|
||||
this.currentTurn = 1
|
||||
this.phase = 'selecting'
|
||||
this.pendingPlays.clear()
|
||||
}
|
||||
|
||||
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' }
|
||||
|
||||
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' }
|
||||
|
||||
const card = player.hand.splice(cardIndex, 1)[0]
|
||||
this.pendingPlays.set(socketId, { player, card })
|
||||
|
||||
this.phase = 'waiting'
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
allPlayersPlayed() {
|
||||
return this.pendingPlays.size === this.players.length
|
||||
}
|
||||
|
||||
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 })
|
||||
|
||||
// 1. Appliquer les effets de protection (Enchanteur)
|
||||
plays.forEach(({ player, card }) => {
|
||||
if (card.effect === 'protect') {
|
||||
player.protected = true
|
||||
}
|
||||
})
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const message = messages.length > 0 ? messages.join(' | ') : 'Tour résolu !'
|
||||
|
||||
this.pendingPlays.clear()
|
||||
this.phase = 'resolve'
|
||||
|
||||
return { playedCards, message }
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
isGameOver() {
|
||||
return this.currentTurn >= this.maxTurns
|
||||
}
|
||||
|
||||
getPlayersInfo() {
|
||||
return this.players.map(p => ({
|
||||
name: p.name,
|
||||
score: p.score,
|
||||
handSize: p.hand.length
|
||||
}))
|
||||
}
|
||||
|
||||
getStateFor(socketId) {
|
||||
const player = this.players.find(p => p.socketId === socketId)
|
||||
return {
|
||||
players: this.getPlayersInfo(),
|
||||
currentTurn: this.currentTurn,
|
||||
maxTurns: this.maxTurns,
|
||||
phase: this.phase,
|
||||
hand: player ? player.hand : []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Room }
|
||||
@@ -0,0 +1,24 @@
|
||||
const { Room } = require('./Room')
|
||||
|
||||
class RoomManager {
|
||||
constructor() {
|
||||
this.rooms = new Map()
|
||||
}
|
||||
|
||||
getOrCreate(roomId) {
|
||||
if (!this.rooms.has(roomId)) {
|
||||
this.rooms.set(roomId, new Room(roomId))
|
||||
}
|
||||
return this.rooms.get(roomId)
|
||||
}
|
||||
|
||||
get(roomId) {
|
||||
return this.rooms.get(roomId) || null
|
||||
}
|
||||
|
||||
delete(roomId) {
|
||||
this.rooms.delete(roomId)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { RoomManager }
|
||||
@@ -0,0 +1,51 @@
|
||||
// Deck de base de Regnum
|
||||
// Types: Royaume (points fixes), Armée (attaque), Commerce (multiplicateur), Magie (effets spéciaux)
|
||||
|
||||
const BASE_DECK = [
|
||||
// === ROYAUME ===
|
||||
{ id: 'r1', name: 'Village', type: 'Royaume', points: 2, effect: null },
|
||||
{ id: 'r2', name: 'Château', type: 'Royaume', points: 5, effect: null },
|
||||
{ id: 'r3', name: 'Cathédrale', type: 'Royaume', points: 4, effect: null },
|
||||
{ id: 'r4', name: 'Forteresse', type: 'Royaume', points: 3, effect: null },
|
||||
{ id: 'r5', name: 'Palais Royal', type: 'Royaume', points: 7, effect: null },
|
||||
|
||||
// === ARMÉE ===
|
||||
{ id: 'a1', name: 'Chevaliers', type: 'Armée', points: 3, effect: 'steal:1' },
|
||||
{ id: 'a2', name: 'Archers', type: 'Armée', points: 2, effect: 'steal:1' },
|
||||
{ id: 'a3', name: 'Catapulte', type: 'Armée', points: 1, effect: 'steal:2' },
|
||||
{ id: 'a4', name: 'Légion', type: 'Armée', points: 4, effect: 'steal:2' },
|
||||
|
||||
// === COMMERCE ===
|
||||
{ id: 'c1', name: 'Marché', type: 'Commerce', points: 2, effect: 'bonus:1' },
|
||||
{ id: 'c2', name: 'Port', type: 'Commerce', points: 3, effect: 'bonus:2' },
|
||||
{ id: 'c3', name: 'Route de soie', type: 'Commerce', points: 5, effect: 'bonus:1' },
|
||||
|
||||
// === MAGIE ===
|
||||
{ id: 'm1', name: 'Oracle', type: 'Magie', points: 1, effect: 'draw:1' },
|
||||
{ id: 'm2', name: 'Enchanteur', type: 'Magie', points: 2, effect: 'protect' },
|
||||
{ id: 'm3', name: 'Alchimiste', type: 'Magie', points: 3, effect: 'double' },
|
||||
]
|
||||
|
||||
function shuffle(arr) {
|
||||
const a = [...arr]
|
||||
for (let i = a.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[a[i], a[j]] = [a[j], a[i]]
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
function createDeck() {
|
||||
return shuffle([...BASE_DECK, ...BASE_DECK]) // 2 exemplaires
|
||||
}
|
||||
|
||||
function dealHands(playerCount, handSize = 5) {
|
||||
const deck = createDeck()
|
||||
const hands = []
|
||||
for (let i = 0; i < playerCount; i++) {
|
||||
hands.push(deck.splice(0, handSize))
|
||||
}
|
||||
return { hands, remaining: deck }
|
||||
}
|
||||
|
||||
module.exports = { createDeck, dealHands, BASE_DECK }
|
||||
@@ -0,0 +1,117 @@
|
||||
const express = require('express')
|
||||
const http = require('http')
|
||||
const { Server } = require('socket.io')
|
||||
const cors = require('cors')
|
||||
const { RoomManager } = require('./game/RoomManager')
|
||||
|
||||
const app = express()
|
||||
app.use(cors())
|
||||
app.use(express.json())
|
||||
|
||||
const server = http.createServer(app)
|
||||
const io = new Server(server, {
|
||||
cors: { origin: '*', methods: ['GET', 'POST'] }
|
||||
})
|
||||
|
||||
const rooms = new RoomManager()
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
console.log(`[+] Connexion: ${socket.id}`)
|
||||
|
||||
socket.on('join-room', ({ roomId, playerName }) => {
|
||||
const room = rooms.getOrCreate(roomId)
|
||||
const joined = room.addPlayer(socket.id, playerName)
|
||||
|
||||
if (!joined) {
|
||||
socket.emit('error', { message: 'Salle pleine (4 joueurs max)' })
|
||||
return
|
||||
}
|
||||
|
||||
socket.join(roomId)
|
||||
socket.data.roomId = roomId
|
||||
socket.data.playerName = playerName
|
||||
|
||||
io.to(roomId).emit('room-update', { players: room.getPlayersInfo() })
|
||||
console.log(`[room:${roomId}] ${playerName} rejoint (${room.playerCount}/4)`)
|
||||
})
|
||||
|
||||
socket.on('start-game', ({ roomId }) => {
|
||||
const room = rooms.get(roomId)
|
||||
if (!room || room.playerCount < 2) return
|
||||
|
||||
room.startGame()
|
||||
io.to(roomId).emit('game-start')
|
||||
|
||||
// Envoyer l'état initial à chaque joueur avec sa main
|
||||
room.players.forEach((player) => {
|
||||
io.to(player.socketId).emit('game-state', room.getStateFor(player.socketId))
|
||||
})
|
||||
})
|
||||
|
||||
socket.on('request-state', ({ roomId }) => {
|
||||
const room = rooms.get(roomId)
|
||||
if (!room) return
|
||||
socket.emit('game-state', room.getStateFor(socket.id))
|
||||
})
|
||||
|
||||
socket.on('play-card', ({ roomId, cardId }) => {
|
||||
const room = rooms.get(roomId)
|
||||
if (!room) return
|
||||
|
||||
const result = room.playCard(socket.id, cardId)
|
||||
if (!result.ok) {
|
||||
socket.emit('error', { message: result.error })
|
||||
return
|
||||
}
|
||||
|
||||
// Mise à jour de phase pour tous
|
||||
io.to(roomId).emit('game-state', {
|
||||
players: room.getPlayersInfo(),
|
||||
currentTurn: room.currentTurn,
|
||||
maxTurns: room.maxTurns,
|
||||
phase: room.phase
|
||||
})
|
||||
|
||||
// Tous les joueurs ont joué → résolution
|
||||
if (room.allPlayersPlayed()) {
|
||||
const roundResult = room.resolveRound()
|
||||
io.to(roomId).emit('game-state', {
|
||||
players: room.getPlayersInfo(),
|
||||
currentTurn: room.currentTurn,
|
||||
maxTurns: room.maxTurns,
|
||||
phase: 'resolve',
|
||||
playedCards: roundResult.playedCards
|
||||
})
|
||||
|
||||
io.to(roomId).emit('round-result', { message: roundResult.message })
|
||||
|
||||
setTimeout(() => {
|
||||
if (room.isGameOver()) {
|
||||
io.to(roomId).emit('game-over', room.getPlayersInfo())
|
||||
} else {
|
||||
room.nextTurn()
|
||||
room.players.forEach((player) => {
|
||||
io.to(player.socketId).emit('game-state', room.getStateFor(player.socketId))
|
||||
})
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
const roomId = socket.data.roomId
|
||||
if (roomId) {
|
||||
const room = rooms.get(roomId)
|
||||
if (room) {
|
||||
room.removePlayer(socket.id)
|
||||
io.to(roomId).emit('room-update', { players: room.getPlayersInfo() })
|
||||
}
|
||||
}
|
||||
console.log(`[-] Déconnexion: ${socket.id}`)
|
||||
})
|
||||
})
|
||||
|
||||
const PORT = process.env.PORT || 3000
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Serveur Regnum démarré sur le port ${PORT}`)
|
||||
})
|
||||
Reference in new issue
Block a user