Fix guessing phase

This commit is contained in:
Mathias Wagner 2025-03-01 00:40:01 +01:00
parent 87fc9a2f39
commit a3daab2f84
6 changed files with 285 additions and 44 deletions

View File

@ -19,6 +19,7 @@ export const Game = () => {
const [selectedSong, setSelectedSong] = useState(null);
const [guessResult, setGuessResult] = useState(null);
const [isHost, setIsHost] = useState(false);
const [hasGuessed, setHasGuessed] = useState(false);
const [messages, setMessages] = useState([]);
const [inputValue, setInputValue] = useState("");
@ -48,9 +49,10 @@ export const Game = () => {
setTimeLeft(data.timeRemaining);
},
"guessing-phase-started": (data) => {
console.log("Guessing phase started:", data);
setPhase("guessing");
setTimeLeft(data.timeRemaining);
setSongOptions(data.songOptions);
setSongOptions(data.songOptions || []);
},
"guess-result": (result) => {
setGuessResult(result.isCorrect);
@ -94,6 +96,13 @@ export const Game = () => {
console.log("Received frequency update:", data.frequency);
setFrequency(data.frequency);
}
},
"phase-changed": (data) => {
console.log("Phase changed:", data);
setPhase(data.phase);
if (data.timeRemaining) {
setTimeLeft(data.timeRemaining);
}
}
};
@ -143,8 +152,21 @@ export const Game = () => {
const handleSongSelect = useCallback((song) => {
setSelectedSong(song);
send("submit-guess", { songId: song.id });
}, [send]);
}, []);
const handleSubmitGuess = useCallback(() => {
if (!selectedSong || phase !== 'guessing') return;
console.log("Submitting guess:", selectedSong.id);
send("submit-guess", { songId: selectedSong.id });
setMessages(prev => [...prev, {
system: true,
text: `Du hast "${selectedSong.title}" von ${selectedSong.artist} gewählt.`
}]);
setHasGuessed(true);
}, [selectedSong, send, phase]);
const handleNextRound = useCallback(() => {
send("next-round");
@ -212,23 +234,54 @@ export const Game = () => {
<p>Die Rater versuchen nun, deinen Song zu erraten...</p>
</div>
) : (
<div className="song-options">
<div className="song-selection">
<p className="instruction">Welchen Song hat der Komponist gespielt?</p>
<div className="song-grid">
{songOptions.map(song => (
<div
key={song.id}
className={`song-option ${selectedSong?.id === song.id ? 'selected' : ''}`}
onClick={() => handleSongSelect(song)}
>
<img src={song.coverUrl} alt={song.title} />
<div className="song-details">
<div className="song-title">{song.title}</div>
<div className="song-artist">{song.artist}</div>
</div>
{songOptions.length === 0 ? (
<div className="loading-songs">
<p>Lade Songoptionen...</p>
</div>
) : (
<>
<div className="song-grid">
{songOptions.map(song => (
<div
key={song.id}
className={`song-option ${selectedSong?.id === song.id ? 'selected' : ''} ${hasGuessed ? 'disabled' : ''}`}
onClick={() => !hasGuessed && handleSongSelect(song)}
>
<div className="song-image">
<img src={song.coverUrl} alt={song.title} />
{selectedSong?.id === song.id && (
<div className="selection-indicator">
<FontAwesomeIcon icon="fa-solid fa-check-circle" />
</div>
)}
</div>
<div className="song-details">
<div className="song-title">{song.title}</div>
<div className="song-artist">{song.artist}</div>
</div>
</div>
))}
</div>
))}
</div>
<div className="guess-actions">
{hasGuessed ? (
<p className="guess-submitted">Deine Antwort wurde eingereicht!</p>
) : (
<button
className={`submit-guess-button ${!selectedSong ? 'disabled' : ''}`}
onClick={handleSubmitGuess}
disabled={!selectedSong}
>
<FontAwesomeIcon icon="fa-solid fa-paper-plane" />
Antwort einreichen
</button>
)}
</div>
</>
)}
</div>
)}
</div>

View File

@ -187,6 +187,14 @@ export const MusicSlider = ({ isReadOnly = false, onFrequencyChange, frequency:
};
}, []);
useEffect(() => {
return () => {
if (isPlaying) {
stopAudio();
}
};
}, [isPlaying]);
return (
<div className={`otamatone-container ${isReadOnly ? 'read-only' : ''}`}>
<div

View File

@ -332,34 +332,124 @@
grid-template-columns: repeat(3, 1fr)
gap: 20px
margin-top: 20px
animation: fade-in 0.5s ease-out
@media (max-width: 768px)
grid-template-columns: repeat(2, 1fr)
@media (max-width: 500px)
grid-template-columns: 1fr
.song-option
background: rgba(255, 255, 255, 0.1)
border-radius: 15px
overflow: hidden
cursor: pointer
transition: all 0.3s ease
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)
border: 2px solid transparent
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2)
&:hover
transform: translateY(-5px)
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3)
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3), 0 0 20px rgba(255, 255, 255, 0.1)
background: rgba(255, 255, 255, 0.15)
&.selected
border: 2px solid $yellow
box-shadow: 0 0 20px rgba(255, 255, 0, 0.5)
box-shadow: 0 0 20px rgba(255, 255, 0, 0.5), 0 10px 30px rgba(0, 0, 0, 0.3)
background: rgba(255, 255, 255, 0.2)
img
width: 100%
height: 120px
object-fit: cover
.song-image
position: relative
overflow: hidden
img
width: 100%
height: 150px
object-fit: cover
transition: transform 0.3s ease
.selected &
transform: scale(1.05)
.selection-indicator
position: absolute
top: 10px
right: 10px
background-color: $yellow
color: #000
width: 30px
height: 30px
border-radius: 50%
display: flex
align-items: center
justify-content: center
box-shadow: 0 0 15px rgba(255, 255, 0, 0.7)
animation: pulse 1.5s infinite ease-in-out
.song-details
padding: 10px
padding: 15px
.song-title
font-weight: bold
font-size: 16px
font-size: 18px
color: #fff
margin-bottom: 5px
.song-artist
font-size: 14px
color: rgba(255, 255, 255, 0.8)
.guess-actions
margin-top: 30px
text-align: center
animation: fade-in 0.5s ease-out 0.2s both
.submit-guess-button
background: linear-gradient(135deg, $purple, $blue)
color: #fff
border: none
border-radius: 10px
padding: 12px 25px
font-size: 16px
font-weight: bold
cursor: pointer
transition: all 0.3s ease
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3)
display: inline-flex
align-items: center
justify-content: center
svg
margin-right: 8px
&:hover:not(.disabled)
transform: translateY(-3px)
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4), 0 0 20px rgba(102, 204, 255, 0.4)
&:active:not(.disabled)
transform: translateY(-1px)
&.disabled
opacity: 0.5
cursor: not-allowed
background: linear-gradient(135deg, #999, #666)
.selection-hint
margin-top: 10px
color: rgba(255, 255, 255, 0.7)
font-size: 14px
font-style: italic
.loading-songs
text-align: center
padding: 40px
color: $white
p
font-size: 18px
opacity: 0.8
animation: pulse 1.5s infinite ease-in-out
@keyframes subtle-text-glow
0%, 100%
@ -417,4 +507,20 @@
filter: drop-shadow(0 0 15px rgba(255, 255, 255, 0.7))
50%
transform: translateY(-10px) scale(1.05) rotate(1deg)
filter: drop-shadow(0 0 25px rgba(255, 255, 255, 0.9)) drop-shadow(0 0 50px rgba(102, 204, 255, 0.6))
filter: drop-shadow(0 0 25px rgba(255, 255, 255, 0.9)) drop-shadow(0 0 50px rgba(102, 204, 255, 0.6))
@keyframes fade-in
0%
opacity: 0
transform: translateY(20px)
100%
opacity: 1
transform: translateY(0)
@keyframes pulse
0%, 100%
opacity: 0.8
transform: scale(1)
50%
opacity: 1
transform: scale(1.05)

View File

@ -107,19 +107,31 @@ const getTimeRemaining = (roomId) => {
const advancePhase = (roomId) => {
const gameState = gameStates[roomId];
if (!gameState) return false;
if (gameState.phase === 'composing') {
if (!gameState) {
console.error(`Cannot advance phase: no game state for room ${roomId}`);
return false;
}
const currentPhase = gameState.phase;
console.log(`Advancing phase for room ${roomId} from ${currentPhase}`);
if (currentPhase === 'composing') {
gameState.phase = 'guessing';
gameState.roundStartTime = Date.now();
return true;
} else if (gameState.phase === 'guessing') {
console.log(`Room ${roomId} advanced to guessing phase`);
return { phase: 'guessing' };
}
else if (currentPhase === 'guessing') {
gameState.phase = 'results';
return true;
} else if (gameState.phase === 'results') {
console.log(`Room ${roomId} advanced to results phase`);
return { phase: 'results' };
}
else if (currentPhase === 'results') {
console.log(`Room ${roomId} starting new round from results phase`);
return startNewRound(roomId);
}
console.warn(`Cannot advance from unknown phase "${currentPhase}" in room ${roomId}`);
return false;
};
@ -172,6 +184,12 @@ const cleanupGameState = (roomId) => {
delete gameStates[roomId];
};
const getCurrentComposer = (roomId) => gameStates[roomId]?.currentComposer || null;
const getGameState = (roomId) => {
return gameStates[roomId] || null;
};
module.exports = {
initializeGameState,
startNewRound,
@ -185,5 +203,7 @@ module.exports = {
getUserRole,
getSongOptions,
getSelectedSong,
cleanupGameState
cleanupGameState,
getCurrentComposer,
getGameState
};

View File

@ -121,4 +121,15 @@ module.exports.validateRoomMembers = (io, roomId) => {
return validMembers;
}
module.exports.setCleanupGameState = setCleanupGameState;
module.exports.setCleanupGameState = setCleanupGameState;
module.exports.getUserName = (userId) => {
for (const roomId in rooms) {
const room = rooms[roomId];
const member = room.members.find(m => m.id === userId);
if (member) {
return member.name;
}
}
return null;
};

View File

@ -8,6 +8,7 @@ module.exports = (io) => (socket) => {
const clearRoomTimers = (roomId) => {
if (phaseTimers[roomId]) {
console.log(`Clearing timer for room ${roomId}`);
clearTimeout(phaseTimers[roomId]);
delete phaseTimers[roomId];
}
@ -16,21 +17,33 @@ module.exports = (io) => (socket) => {
const startPhaseTimer = (roomId) => {
clearRoomTimers(roomId);
const gameState = gameController.getGameState(roomId);
if (!gameState) {
console.error(`Cannot start timer: no game state for room ${roomId}`);
return;
}
const timeRemaining = gameController.getTimeRemaining(roomId) * 1000;
console.log(`Starting ${gameState.phase} phase timer for room ${roomId} with ${timeRemaining}ms`);
phaseTimers[roomId] = setTimeout(() => {
console.log(`Timer expired for room ${roomId}, advancing phase from ${gameState.phase}`);
const advanced = gameController.advancePhase(roomId);
if (!advanced) return;
if (!advanced) {
console.log(`Failed to advance phase for room ${roomId}`);
return;
}
if (typeof advanced === 'boolean') {
handleRoundStart(roomId);
} else {
const currentPhase = advanced.phase || 'results';
const newPhase = gameController.getGameState(roomId).phase;
console.log(`Advanced to ${newPhase} phase in room ${roomId}`);
if (currentPhase === 'guessing') {
if (newPhase === 'guessing') {
handleGuessingPhaseStart(roomId);
} else if (currentPhase === 'results') {
} else if (newPhase === 'results') {
handleResultsPhaseStart(roomId);
}
}
@ -59,10 +72,20 @@ module.exports = (io) => (socket) => {
};
const handleGuessingPhaseStart = (roomId) => {
const gameState = gameController.getGameState(roomId);
if (!gameState) return;
const roles = gameController.getRoles(roomId);
const songOptions = gameController.getSongOptions(roomId);
const timeLeft = gameController.getTimeRemaining(roomId);
console.log(`Starting guessing phase for room ${roomId} with ${Object.keys(roles).length} players`);
io.to(roomId).emit('phase-changed', {
phase: 'guessing',
timeRemaining: timeLeft
});
Object.entries(roles).forEach(([userId, role]) => {
if (role === 'guesser') {
io.to(userId).emit('guessing-phase-started', {
@ -71,7 +94,7 @@ module.exports = (io) => (socket) => {
});
}
});
startPhaseTimer(roomId);
};
@ -186,8 +209,28 @@ module.exports = (io) => (socket) => {
const roomId = roomController.getUserRoom(socket.id);
if (!roomId) return;
console.log(`User ${socket.id} submitted guess: Song ID ${songId}`);
const gamePhase = gameController.getGameState(roomId)?.phase;
if (gamePhase !== 'guessing') {
console.log(`Ignoring guess: room ${roomId} is in ${gamePhase} phase, not guessing`);
return;
}
const result = gameController.submitGuess(roomId, socket.id, songId);
if (result) socket.emit("guess-result", result);
if (result) {
console.log(`Guess result for ${socket.id}:`, result);
socket.emit("guess-result", result);
const currentComposer = gameController.getCurrentComposer(roomId);
if (currentComposer) {
const guesserName = roomController.getUserName(socket.id) || "Someone";
io.to(currentComposer).emit("player-guessed", {
guesserName,
isCorrect: result.isCorrect
});
}
}
});
socket.on("next-round", () => {