diff --git a/server/index.js b/server/index.js
index e743316..38e2853 100644
--- a/server/index.js
+++ b/server/index.js
@@ -135,10 +135,12 @@ function createInitialGridState() {
// Helper function to create initial game state
function createInitialGameState() {
return {
- phase: 'waiting', // waiting, rolling, color-selection, active
+ phase: 'waiting', // waiting, rolling, color-selection, turn-rolling, active
diceRolls: {},
winner: null,
- loser: null
+ loser: null,
+ currentTurn: null, // Player index (0 or 1) whose turn it is
+ turnDiceRolls: {} // Dice rolls for turn determination
};
}
@@ -368,6 +370,129 @@ function determineDiceWinner(lobbyName) {
});
}
+// Start dice roll for turn determination
+function startTurnDiceRoll(lobbyName) {
+ const lobby = lobbies.get(lobbyName);
+ if (!lobby || lobby.players.length !== MAX_PLAYERS) {
+ return;
+ }
+
+ lobby.gameState.phase = 'turn-rolling';
+ lobby.gameState.turnDiceRolls = {};
+
+ // Generate dice rolls for both players
+ lobby.players.forEach((player, index) => {
+ const roll = Math.floor(Math.random() * 6) + 1;
+ lobby.gameState.turnDiceRolls[index] = roll;
+ });
+
+ // Broadcast turn dice roll start to both players
+ lobby.players.forEach((player, index) => {
+ if (player.ws.readyState === WebSocket.OPEN) {
+ player.ws.send(JSON.stringify({
+ type: 'turnDiceRollStart',
+ playerIndex: index,
+ roll: lobby.gameState.turnDiceRolls[index]
+ }));
+ }
+ });
+
+ // After animation delay, determine winner
+ setTimeout(() => {
+ determineTurnDiceWinner(lobbyName);
+ }, 3000); // 3 second delay for animation
+}
+
+// Determine who goes first from turn dice rolls
+function determineTurnDiceWinner(lobbyName) {
+ const lobby = lobbies.get(lobbyName);
+ if (!lobby) {
+ return;
+ }
+
+ const roll1 = lobby.gameState.turnDiceRolls[0];
+ const roll2 = lobby.gameState.turnDiceRolls[1];
+
+ // Check for tie
+ if (roll1 === roll2) {
+ // Broadcast tie, then restart roll
+ lobby.players.forEach((player, index) => {
+ if (player.ws.readyState === WebSocket.OPEN) {
+ player.ws.send(JSON.stringify({
+ type: 'turnDiceRollTie',
+ message: 'It\'s a tie! Rolling again...'
+ }));
+ }
+ });
+
+ // Restart roll after delay
+ setTimeout(() => {
+ startTurnDiceRoll(lobbyName);
+ }, 2000);
+ return;
+ }
+
+ // Determine who goes first
+ const firstPlayerIndex = roll1 > roll2 ? 0 : 1;
+
+ lobby.gameState.currentTurn = firstPlayerIndex;
+ lobby.gameState.phase = 'active';
+
+ // Notify both players of result and start the game
+ lobby.players.forEach((player, index) => {
+ if (player.ws.readyState === WebSocket.OPEN) {
+ player.ws.send(JSON.stringify({
+ type: 'turnDiceRollWinner',
+ isFirst: index === firstPlayerIndex,
+ firstPlayerIndex: firstPlayerIndex,
+ roll1: roll1,
+ roll2: roll2,
+ playerIndex: index
+ }));
+ }
+ });
+
+ // Broadcast game status as active
+ broadcastGameStatus(lobbyName);
+
+ // Send turn notification to the first player
+ setTimeout(() => {
+ sendTurnNotification(lobbyName, firstPlayerIndex);
+ }, 2000);
+}
+
+// Send turn notification to current player
+function sendTurnNotification(lobbyName, playerIndex) {
+ const lobby = lobbies.get(lobbyName);
+ if (!lobby) {
+ return;
+ }
+
+ lobby.players.forEach((player, index) => {
+ if (player.ws.readyState === WebSocket.OPEN) {
+ player.ws.send(JSON.stringify({
+ type: 'turnChanged',
+ currentTurn: playerIndex,
+ isYourTurn: index === playerIndex
+ }));
+ }
+ });
+}
+
+// Switch turn to the other player
+function switchTurn(lobbyName) {
+ const lobby = lobbies.get(lobbyName);
+ if (!lobby || lobby.gameState.phase !== 'active') {
+ return;
+ }
+
+ // Switch to the other player
+ lobby.gameState.currentTurn = lobby.gameState.currentTurn === 0 ? 1 : 0;
+
+ // Notify both players
+ sendTurnNotification(lobbyName, lobby.gameState.currentTurn);
+}
+
// Handle color selection from winner
function handleColorSelection(lobbyName, color) {
const lobby = lobbies.get(lobbyName);
@@ -383,9 +508,6 @@ function handleColorSelection(lobbyName, color) {
winnerPlayer.color = color;
loserPlayer.color = loserColor;
- // Update game state to active
- lobby.gameState.phase = 'active';
-
// Notify both players of their final color assignments
if (winnerPlayer.ws.readyState === WebSocket.OPEN) {
winnerPlayer.ws.send(JSON.stringify({
@@ -403,19 +525,10 @@ function handleColorSelection(lobbyName, color) {
}));
}
- // Broadcast game status as active
- broadcastGameStatus(lobbyName);
-
- // Send game start to both players
- lobby.players.forEach((player) => {
- if (player.ws.readyState === WebSocket.OPEN) {
- player.ws.send(JSON.stringify({
- type: 'gameStart',
- message: 'Welcome to the game!',
- yourColor: player.color
- }));
- }
- });
+ // Start turn determination dice roll after a delay
+ setTimeout(() => {
+ startTurnDiceRoll(lobbyName);
+ }, 1000);
}
// Broadcast chat message to lobby
@@ -689,6 +802,21 @@ wss.on('connection', (ws, req) => {
// Handle grid toggle
if (message.type === 'toggleSquare') {
+ // Validate it's the player's turn
+ if (lobby.gameState.phase !== 'active') {
+ console.log('Game is not in active phase');
+ return;
+ }
+
+ if (lobby.gameState.currentTurn !== playerIndex) {
+ console.log(`Not player ${playerIndex}'s turn (current turn: ${lobby.gameState.currentTurn})`);
+ ws.send(JSON.stringify({
+ type: 'error',
+ message: 'It\'s not your turn!'
+ }));
+ return;
+ }
+
// Get player data
const playerData = lobby.players.find(p => p.ws === ws);
if (!playerData || !playerData.color) {
@@ -721,6 +849,9 @@ wss.on('connection', (ws, req) => {
}));
}
});
+
+ // Switch turn to the other player
+ switchTurn(currentLobbyName);
}
// Handle chat messages
diff --git a/src/App.vue b/src/App.vue
index 8a0183f..ba675e8 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -57,6 +57,48 @@
+
+
+
+
🎲 Rolling for First Turn 🎲
+
It's a Tie!
+
{{ turnDiceResultTitle }}
+
+
+
+
+
Opponent
+
+
{{ opponentTurnDiceRoll }}
+
+
+
+
+
+ Both rolled {{ myTurnDiceRoll }}! Rolling again...
+
+
+
+ {{ turnDiceResultMessage }}
+
+
+
+
+
+
+
+
🐧
+
Your Turn!
+
It's your turn to play! Make your move.
+
+
+
+
@@ -247,6 +289,16 @@ export default {
opponentDiceRoll: 1,
isRolling: false,
isWinner: false,
+ // Turn dice roll state
+ showTurnDiceModal: false,
+ turnDiceState: 'rolling', // rolling, tie, result
+ myTurnDiceRoll: 1,
+ opponentTurnDiceRoll: 1,
+ isTurnDiceRolling: false,
+ isFirstPlayer: false,
+ // Turn indicator state
+ showTurnModal: false,
+ currentTurn: null, // Player index whose turn it is
// Color selection state
showColorSelection: false,
showWaitingForColor: false,
@@ -269,13 +321,19 @@ export default {
return this.currentLobby !== null;
},
isBoardLocked() {
- return !this.connected || !this.isActivePlayer || this.gameStatus !== 'active';
+ return !this.connected || !this.isActivePlayer || this.gameStatus !== 'active' || this.currentTurn !== this.playerIndex;
},
gameStatusMessage() {
if (this.gameStatus === 'waiting') {
return 'Waiting for players...';
} else if (this.gameStatus === 'active') {
- return 'Game Active - Play!';
+ if (this.currentTurn === null) {
+ return 'Game Active - Play!';
+ } else if (this.currentTurn === this.playerIndex) {
+ return '🐧 Your Turn! 🐧';
+ } else {
+ return 'Opponent\'s Turn - Please Wait';
+ }
}
return '';
},
@@ -294,6 +352,16 @@ export default {
} else {
return `You rolled ${this.myDiceRoll} and your opponent rolled ${this.opponentDiceRoll}. Waiting for winner to choose...`;
}
+ },
+ turnDiceResultTitle() {
+ return this.isFirstPlayer ? '🎉 You Go First! 🎉' : 'Opponent Goes First';
+ },
+ turnDiceResultMessage() {
+ if (this.isFirstPlayer) {
+ return `You rolled ${this.myTurnDiceRoll} and your opponent rolled ${this.opponentTurnDiceRoll}. You get the first turn!`;
+ } else {
+ return `You rolled ${this.myTurnDiceRoll} and your opponent rolled ${this.opponentTurnDiceRoll}. Your opponent goes first.`;
+ }
}
},
methods: {
@@ -346,6 +414,18 @@ export default {
this.showDiceModal = false;
this.showColorSelection = false;
this.showWaitingForColor = false;
+ } else if (data.type === 'turnDiceRollStart') {
+ // Start turn determination dice roll animation
+ this.handleTurnDiceRollStart(data);
+ } else if (data.type === 'turnDiceRollTie') {
+ // Handle turn dice roll tie
+ this.handleTurnDiceRollTie(data);
+ } else if (data.type === 'turnDiceRollWinner') {
+ // Show turn determination result
+ this.handleTurnDiceRollWinner(data);
+ } else if (data.type === 'turnChanged') {
+ // Update current turn
+ this.handleTurnChanged(data);
} else if (data.type === 'gameStart') {
// Show welcome modal when game starts
this.showWelcomeModal(data.yourColor);
@@ -354,7 +434,10 @@ export default {
this.showDiceModal = false;
this.showColorSelection = false;
this.showWaitingForColor = false;
+ this.showTurnDiceModal = false;
+ this.showTurnModal = false;
this.playerColor = null;
+ this.currentTurn = null;
this.modalTitle = 'Player Disconnected';
this.modalMessage = data.message;
this.showModal = true;
@@ -384,6 +467,8 @@ export default {
this.playerAssignmentReceived = false;
this.playerIndex = null;
this.gameStatus = 'waiting';
+ this.currentTurn = null;
+ this.showTurnModal = false;
this.chatMessages = [];
this.grid = Array(10).fill(null).map(() => Array(10).fill('lime'));
} else if (data.type === 'error') {
@@ -528,6 +613,74 @@ export default {
this.showColorSelection = false;
},
+ handleTurnDiceRollStart(data) {
+ this.showTurnDiceModal = true;
+ this.turnDiceState = 'rolling';
+ this.isTurnDiceRolling = true;
+
+ // Set initial roll values (will animate)
+ this.myTurnDiceRoll = 1;
+ this.opponentTurnDiceRoll = 1;
+
+ // Animate dice rolling
+ let rollCount = 0;
+ const rollInterval = setInterval(() => {
+ this.myTurnDiceRoll = Math.floor(Math.random() * 6) + 1;
+ this.opponentTurnDiceRoll = Math.floor(Math.random() * 6) + 1;
+ rollCount++;
+
+ // Stop after about 2.5 seconds and show actual result
+ if (rollCount > 15) {
+ clearInterval(rollInterval);
+ this.isTurnDiceRolling = false;
+ // Set the actual roll from server
+ if (data.playerIndex === this.playerIndex) {
+ this.myTurnDiceRoll = data.roll;
+ }
+ // We'll get opponent's roll from the turnDiceRollWinner message
+ }
+ }, 150);
+ },
+ handleTurnDiceRollTie(data) {
+ this.turnDiceState = 'tie';
+ this.isTurnDiceRolling = false;
+ // After delay, it will restart automatically from server
+ },
+ handleTurnDiceRollWinner(data) {
+ this.isFirstPlayer = data.isFirst;
+ this.currentTurn = data.firstPlayerIndex;
+
+ // Set the actual dice roll values from server
+ if (data.playerIndex === 0) {
+ this.myTurnDiceRoll = data.roll1;
+ this.opponentTurnDiceRoll = data.roll2;
+ } else {
+ this.myTurnDiceRoll = data.roll2;
+ this.opponentTurnDiceRoll = data.roll1;
+ }
+
+ this.turnDiceState = 'result';
+ this.isTurnDiceRolling = false;
+
+ // After showing result for 2 seconds, close the modal and show turn modal if first
+ setTimeout(() => {
+ this.showTurnDiceModal = false;
+ if (this.isFirstPlayer) {
+ this.showTurnModal = true;
+ }
+ }, 2000);
+ },
+ handleTurnChanged(data) {
+ this.currentTurn = data.currentTurn;
+
+ // Show turn modal if it's your turn
+ if (data.isYourTurn) {
+ this.showTurnModal = true;
+ }
+ },
+ closeTurnModal() {
+ this.showTurnModal = false;
+ },
createLobby() {
if (!this.connected || !this.newLobbyName.trim()) {
return;
@@ -1104,6 +1257,59 @@ h1 {
}
}
+/* Turn Modal Styles */
+.turn-modal {
+ min-width: 400px;
+ padding: 40px;
+ animation: bounceIn 0.5s ease;
+}
+
+@keyframes bounceIn {
+ 0% {
+ transform: scale(0.3);
+ opacity: 0;
+ }
+ 50% {
+ transform: scale(1.05);
+ }
+ 70% {
+ transform: scale(0.9);
+ }
+ 100% {
+ transform: scale(1);
+ opacity: 1;
+ }
+}
+
+.penguin-emoji {
+ font-size: 80px;
+ margin-bottom: 20px;
+ animation: wiggle 1s ease-in-out infinite;
+}
+
+@keyframes wiggle {
+ 0%, 100% {
+ transform: rotate(0deg);
+ }
+ 25% {
+ transform: rotate(-10deg);
+ }
+ 75% {
+ transform: rotate(10deg);
+ }
+}
+
+.penguin-button {
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+ font-size: 18px;
+ padding: 15px 40px;
+}
+
+.penguin-button:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
+}
+
/* Lobby Browser Styles */
.lobby-browser {
padding: 20px;