Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
jonmumm committed Nov 10, 2024
1 parent 5a54daa commit e9fab4f
Show file tree
Hide file tree
Showing 7 changed files with 364 additions and 109 deletions.
6 changes: 3 additions & 3 deletions app/components/host-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ const LobbyControls = ({
const [isStarting, setIsStarting] = useState(false);

const copyGameCode = async () => {
if (gameState.id) {
await navigator.clipboard.writeText(gameState.id);
if (gameState.gameCode) {
await navigator.clipboard.writeText(gameState.gameCode);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
Expand Down Expand Up @@ -174,7 +174,7 @@ const LobbyControls = ({
<div className="absolute inset-0 bg-indigo-500/20 rounded-xl blur-xl group-hover:bg-indigo-500/30 transition-all" />
<div className="relative bg-gray-800/50 backdrop-blur-sm border border-indigo-500/30 rounded-xl p-6 flex items-center justify-center gap-4">
<span className="text-4xl font-mono font-bold tracking-wider bg-clip-text text-transparent bg-gradient-to-r from-indigo-400 to-purple-400">
{gameState.id}
{gameState.gameCode}
</span>
<AnimatePresence mode="wait">
{copied ? (
Expand Down
199 changes: 102 additions & 97 deletions app/components/player-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,109 +145,114 @@ const GameplayDisplay = ({
onBuzzIn: () => void;
lastAnswerResult?: { playerId: string; playerName: string; correct: boolean } | null;
userId: string;
}) => (
<div className="min-h-screen flex flex-col items-center justify-center p-4 relative">
{/* Background Animation */}
<div className="absolute inset-0 overflow-hidden">
<div className="absolute inset-0 opacity-10">
<motion.div
className="absolute inset-0 bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500"
animate={{
rotate: [0, 360],
scale: [1, 1.2, 1],
}}
transition={{
duration: 20,
repeat: Infinity,
ease: "linear",
}}
/>
</div>
</div>
}) => {
// Check if this player has already answered incorrectly
const hasAnsweredIncorrectly = lastAnswerResult?.playerId === userId && !lastAnswerResult.correct;

<div className="relative z-10 w-full max-w-4xl">
{/* Score Display */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-gray-800/30 backdrop-blur-sm rounded-2xl p-6 border border-gray-700/50 mb-8"
>
<div className="text-xl text-indigo-300">Your Score</div>
<div className="text-4xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-indigo-400 to-purple-400">
{playerScore}
return (
<div className="min-h-screen flex flex-col items-center justify-center p-4 relative">
{/* Background Animation */}
<div className="absolute inset-0 overflow-hidden">
<div className="absolute inset-0 opacity-10">
<motion.div
className="absolute inset-0 bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500"
animate={{
rotate: [0, 360],
scale: [1, 1.2, 1],
}}
transition={{
duration: 20,
repeat: Infinity,
ease: "linear",
}}
/>
</div>
</motion.div>
</div>

{/* Answer Result Feedback */}
<AnimatePresence>
{lastAnswerResult && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className={`text-center p-6 rounded-2xl mb-8 backdrop-blur-sm border ${
lastAnswerResult.correct
? 'bg-green-500/20 border-green-500/30'
: 'bg-red-500/20 border-red-500/30'
}`}
>
{lastAnswerResult.playerId === userId ? (
<span className="text-2xl font-bold">
{lastAnswerResult.correct ? "You got it correct! 🎉" : "Sorry, that's incorrect"}
</span>
) : (
<span className="text-2xl">
<span className="font-bold">{lastAnswerResult.playerName}</span>
{lastAnswerResult.correct ? " got it correct! 🎉" : " got it wrong"}
</span>
)}
</motion.div>
)}
</AnimatePresence>
<div className="relative z-10 w-full max-w-4xl space-y-6">
{/* Score Display */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-gray-800/30 backdrop-blur-sm rounded-2xl p-6 border border-gray-700/50"
>
<div className="text-xl text-indigo-300">Your Score</div>
<div className="text-4xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-indigo-400 to-purple-400">
{playerScore}
</div>
</motion.div>

{/* Question & Buzzer Area */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="bg-gray-800/30 backdrop-blur-sm rounded-2xl p-8 border border-gray-700/50"
>
{currentQuestion?.isVisible ? (
<>
<h2 className="text-4xl font-bold text-center mb-8 bg-clip-text text-transparent bg-gradient-to-r from-indigo-400 to-purple-400">
{currentQuestion.text}
</h2>
{isPlayerInQueue ? (
<div className="text-2xl text-center">
{isFirstInQueue ? (
<span className="text-yellow-400 font-bold">Your turn to answer!</span>
) : (
<span className="text-indigo-300">Waiting for your turn...</span>
)}
</div>
) : (
<motion.button
onClick={onBuzzIn}
className="group relative mx-auto block"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<div className="absolute inset-0 bg-red-600 rounded-full blur-xl opacity-50 group-hover:opacity-75 transition-opacity" />
<div className="relative px-12 py-6 bg-gradient-to-br from-red-500 to-pink-500 rounded-full text-2xl font-bold shadow-lg flex items-center gap-3">
<Bell className="w-8 h-8" />
BUZZ!
{/* Answer Result Feedback */}
<AnimatePresence>
{lastAnswerResult && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className={`text-center p-6 rounded-2xl backdrop-blur-sm border ${
lastAnswerResult.correct
? 'bg-green-500/20 border-green-500/30'
: 'bg-red-500/20 border-red-500/30'
}`}
>
{lastAnswerResult.playerId === userId ? (
<span className="text-2xl font-bold">
{lastAnswerResult.correct ? "You got it correct! 🎉" : "Sorry, that's incorrect"}
</span>
) : (
<span className="text-2xl">
<span className="font-bold">{lastAnswerResult.playerName}</span>
{lastAnswerResult.correct ? " got it correct! 🎉" : " got it wrong"}
</span>
)}
</motion.div>
)}
</AnimatePresence>

{/* Question & Buzzer Area */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="bg-gray-800/30 backdrop-blur-sm rounded-2xl p-8 border border-gray-700/50 flex flex-col items-center"
>
{currentQuestion?.isVisible ? (
<>
<h2 className="text-4xl font-bold text-center bg-clip-text text-transparent bg-gradient-to-r from-indigo-400 to-purple-400 mb-12">
{currentQuestion.text}
</h2>
{isPlayerInQueue ? (
<div className="text-2xl text-center">
{isFirstInQueue ? (
<span className="text-yellow-400 font-bold">Your turn to answer!</span>
) : (
<span className="text-indigo-300">Waiting for your turn...</span>
)}
</div>
</motion.button>
)}
</>
) : (
<div className="text-2xl text-center text-indigo-300/60">
Waiting for question...
</div>
)}
</motion.div>
) : !hasAnsweredIncorrectly ? (
<motion.button
onClick={onBuzzIn}
className="group relative"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<div className="absolute inset-0 bg-red-600 rounded-full blur-xl opacity-50 group-hover:opacity-75 transition-opacity" />
<div className="relative px-12 py-6 bg-gradient-to-br from-red-500 to-pink-500 rounded-full text-2xl font-bold shadow-lg flex items-center gap-3">
<Bell className="w-8 h-8" />
BUZZ!
</div>
</motion.button>
) : null}
</>
) : (
<div className="text-2xl text-center text-indigo-300/60">
Waiting for question...
</div>
)}
</motion.div>
</div>
</div>
</div>
);
);
};

const GameFinishedDisplay = ({
players,
Expand Down
71 changes: 63 additions & 8 deletions app/game.machine.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ActorKitStateMachine } from "actor-kit";
import { and, assign, setup } from "xstate";
import { produce } from "immer";
import { and, assign, DoneActorEvent, fromPromise, setup } from "xstate";
import type { GameEvent, GameInput, GameServerContext } from "./game.types";

export const gameMachine = setup({
Expand All @@ -17,7 +18,23 @@ export const gameMachine = setup({
isQuestionVisible: ({ context }) =>
context.public.currentQuestion?.isVisible ?? false,
},
actors: {
generateGameCode: fromPromise(async () => {
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let code = "";
for (let i = 0; i < 6; i++) {
code += characters[Math.floor(Math.random() * characters.length)];
}
return code;
}),
},
actions: {
assignGameCode: assign({
public: ({ context }, params: { gameCode: string }) =>
produce(context.public, (draft) => {
draft.gameCode = params.gameCode;
}),
}),
addPlayer: assign({
public: ({ context, event }) => {
if (event.type !== "JOIN_GAME") return context.public;
Expand Down Expand Up @@ -63,8 +80,10 @@ export const gameMachine = setup({
validateAnswer: assign({
public: ({ context, event }) => {
if (event.type !== "VALIDATE_ANSWER") return context.public;
const player = context.public.players.find(p => p.id === event.playerId);

const player = context.public.players.find(
(p) => p.id === event.playerId
);

return {
...context.public,
players: context.public.players.map((player) =>
Expand All @@ -76,11 +95,13 @@ export const gameMachine = setup({
currentQuestion: event.correct
? null
: context.public.currentQuestion,
lastAnswerResult: player ? {
playerId: player.id,
playerName: player.name,
correct: event.correct
} : null,
lastAnswerResult: player
? {
playerId: player.id,
playerName: player.name,
correct: event.correct,
}
: null,
};
},
}),
Expand All @@ -99,6 +120,20 @@ export const gameMachine = setup({
).id,
}),
}),
assignGeneratedGameCode: assign({
public: ({ context, event }: {
context: GameServerContext;
event: GameEvent | DoneActorEvent<string, "generateGameCode">
}) => {
if (event.type === "xstate.done.actor.generateGameCode") {
return {
...context.public,
gameCode: event.output
};
}
return context.public;
}
}),
},
}).createMachine({
id: "triviaGame",
Expand All @@ -107,6 +142,7 @@ export const gameMachine = setup({
id: input.id,
hostId: input.caller.id,
hostName: input.hostName,
gameCode: undefined,
players: [],
currentQuestion: null,
buzzerQueue: [],
Expand All @@ -132,6 +168,25 @@ export const gameMachine = setup({
actions: "startGame",
},
},
type: "parallel",
states: {
GameCode: {
initial: "Generating",
states: {
Generating: {
invoke: {
id: "generateGameCode",
src: "generateGameCode",
onDone: {
target: "Created",
actions: "assignGeneratedGameCode"
},
},
},
Created: {},
},
},
},
},
active: {
initial: "questionPrep",
Expand Down
2 changes: 2 additions & 0 deletions stories/host-view.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export const StartingGame: Story = {
{ id: "player-2", name: "Player 2", score: 0 },
],
},
value: { lobby: { GameCode: "Created" } },
},
},
},
Expand All @@ -97,6 +98,7 @@ export const StartingGame: Story = {
{ id: "player-2", name: "Player 2", score: 0 },
],
},
value: { lobby: { GameCode: "Created" } },
},
});

Expand Down
Loading

0 comments on commit e9fab4f

Please sign in to comment.