Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions app/api/game/[room]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import type { Card, Discussion, Player, State } from "@/types/game";

import { NextResponse } from "next/server";

const rooms = new Map<string, State & { openDiscussions: Discussion[] }>();

export async function GET(
_request: Request,
{ params }: { params: { room: string } },
) {
const state = rooms.get(params.room);

if (!state) {
return NextResponse.json({ error: "Room not found" }, { status: 404 });
}

return NextResponse.json(state);
}

export async function POST(
request: Request,
{ params }: { params: { room: string } },
) {
const body = await request.json();
const room = params.room;
let state = rooms.get(room);

switch (body.type) {
case "createRoom": {
if (!state) {
state = {
deck: body.game.deck as Card[],
throws: body.game.throws as Card[],
players: body.game.players as Player[],
isGameStarted: false,
turn: body.game.players[0] as Player,
openDiscussions: [],
};
rooms.set(room, state);
}
break;
}
case "updatePlayerName": {
if (!state) break;
const idx = state.players.findIndex((p) => p.id === body.player.id);

if (idx === -1) {
state.players.push(body.player as Player);
} else {
state.players[idx] = { ...state.players[idx], ...body.player };
}
break;
}
case "startGame": {
if (!state) break;
state.isGameStarted = true;
state.players.forEach((p) => {
p.coins = state.players.length === 2 ? 1 : 2;
p.cards = state.deck.splice(0, 2);
});
break;
}
case "playTurn": {
if (!state) break;
const player = state.players.find((p) => p.id === body.player.id);

if (!player) break;
// Very basic actions implementation
switch (body.action) {
case "Income":
player.coins += 1;
break;
case "Foreign aid":
player.coins += 2;
break;
case "Tax":
player.coins += 3;
break;
case "Coup": {
const target = state.players.find(
(p: Player) => p.id === body.target?.id,
);

if (player.coins >= 7 && target && target.cards.length > 0) {
player.coins -= 7;
target.cards.pop();
}
break;
}
case "Assassinate": {
const target = state.players.find(
(p: Player) => p.id === body.target?.id,
);

if (player.coins >= 3 && target && target.cards.length > 0) {
player.coins -= 3;
target.cards.pop();
}
break;
}
case "Steal": {
const target = state.players.find(
(p: Player) => p.id === body.target?.id,
);

if (target) {
const stolen = Math.min(2, target.coins);

target.coins -= stolen;
player.coins += stolen;
}
break;
}
case "Exchange": {
const newCards = state.deck.splice(0, 2);

state.deck.push(...player.cards.splice(0, player.cards.length));
player.cards.push(...newCards.slice(0, 2));
break;
}
case "Lose card": {
if (typeof body.cardToThrow === "number") {
player.cards.splice(body.cardToThrow, 1);
state.throws.push({});
}
break;
}
}
// advance turn
const currentIndex = state.players.findIndex((p) => p.id === player.id);

state.turn = state.players[(currentIndex + 1) % state.players.length];
break;
}
}

if (state) rooms.set(room, state);

return NextResponse.json(state ?? {});
}
12 changes: 7 additions & 5 deletions app/game/[...slug]/components/RunningGame/RunningGame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import GameOver from "./components/GameOver";
import { cardsImg, discussionMeta, icons } from "./RunningGame.utils";
import CounterActionModal from "./components/CounteractionModal/CounterActionModal";

import { useWebSocket } from "@/app/providers";
import { Action, Card, Discussion, Player } from "@/types/game";
import { subtitle } from "@/shared/primitives";

Expand Down Expand Up @@ -79,7 +78,6 @@ const RunningGame: React.FC<RunningGameProps> = ({
);
const [globalActiveDiscussion, setGlobalActiveDiscussion] =
useState<Discussion | null>(null);
const { socket } = useWebSocket();
const handleActionsModal = () => {
onOpenActionMenu();
};
Expand All @@ -106,8 +104,8 @@ const RunningGame: React.FC<RunningGameProps> = ({
return players.find((p) => p.id == selectedTargetId);
};

const sendAction = (action: Action, extraData: any = {}) => {
let currentStep = globalActiveDiscussion?.step || 0;
const sendAction = async (action: Action, extraData: any = {}) => {
const currentStep = globalActiveDiscussion?.step || 0;
const payload = {
type: "playTurn",
room,
Expand All @@ -124,7 +122,11 @@ const RunningGame: React.FC<RunningGameProps> = ({
...extraData,
};

socket?.send(JSON.stringify(payload));
await fetch(`/api/game/${room}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
};
const sendSelectedAction = (
expicit_action: Action = "None",
Expand Down
182 changes: 103 additions & 79 deletions app/game/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import { createDeck, createThrows } from "./page.utils";
import PlayersComponent from "./components/AllPlayers";
import RunningGame from "./components/RunningGame";

import { useWebSocket } from "@/app/providers";
import WebSocketComponent from "@/shared/websocket";
import { Card, Discussion, Player } from "@/types/game";
import { title, subtitle } from "@/shared/primitives";

Expand All @@ -28,22 +26,28 @@ export default function Game() {
const [room, setRoom] = useState<string>("none");
const [deck, setDeck] = useState<Card[]>([]);
const [throws, setThrows] = useState<Card[]>([]);
const { socket } = useWebSocket();

const handleNamingButton = () => {
const apiCall = async (payload: any) => {
if (room === "none") return;
await fetch(`/api/game/${room}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
};

const handleNamingButton = async () => {
if (!player) return;
player.name = name;
setPlayer(player);
localStorage.setItem("player", JSON.stringify(player));
socket?.send(
JSON.stringify({ type: "updatePlayerName", player: player, room: room }),
);
await apiCall({ type: "updatePlayerName", player });
};
const gameStarts = () => {
setIsGameStarted(true);
};
const sendStartGameSignal = () => {
socket?.send(JSON.stringify({ type: "startGame", room: room }));
const sendStartGameSignal = async () => {
await apiCall({ type: "startGame" });
};

useEffect(() => {
Expand Down Expand Up @@ -75,81 +79,101 @@ export default function Game() {

setRoom(routeUuid);
setRouteUuid(routeUuid);

const createRoom = async () => {
await fetch(`/api/game/${routeUuid}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type: "createRoom",
game: { deck, throws, players },
}),
});
};

createRoom();

const fetchStatus = async () => {
const res = await fetch(`/api/game/${routeUuid}`);

if (!res.ok) return;
const data = await res.json();

setDeck(data.deck);
setThrows(data.throws);
setPlayers(data.players);
setPlayer(
data.players.find((p: Player) => p.id === newPlayer.id) as Player,
);
setTurn(data.turn);
setDiscussions(data.openDiscussions || []);
if (data.isGameStarted) gameStarts();
};

fetchStatus();
const interval = setInterval(fetchStatus, 1000);

return () => clearInterval(interval);
}, []);

if (room !== "none")
return (
<WebSocketComponent
deck={deck}
gameStarts={gameStarts}
player={player as Player}
players={players}
room={room}
setDeck={setDeck}
setDiscussions={setDiscussions}
setPlayer={setPlayer}
setPlayers={setPlayers}
setThrows={setThrows}
setTurn={setTurn}
throws={throws}
>
<div className="w-full">
{player && !isGameStarted && (
<div className="flex flex-col gap-3">
<h1 className={title()}>Coup room</h1>
<p className={subtitle()}>
Invite your friends to this URL link and play together.
</p>

{players.length > 1 && player?.isHost && (
<Button
color="danger"
variant="flat"
onPress={sendStartGameSignal}
>
Start Game
</Button>
)}

<h2 className={"text-left"}>Insert your nickname:</h2>
<form
className="flex gap-2"
onSubmit={(e) => {
e.preventDefault();
handleNamingButton();
}}
<div className="w-full">
{player && !isGameStarted && (
<div className="flex flex-col gap-3">
<h1 className={title()}>Coup room</h1>
<p className={subtitle()}>
Invite your friends to this URL link and play together.
</p>

{players.length > 1 && player?.isHost && (
<Button
color="danger"
variant="flat"
onPress={sendStartGameSignal}
>
<Input
className="mb-3"
color="default"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<Button
color="primary"
variant="flat"
onPress={handleNamingButton}
>
Update
</Button>
</form>

<h2 className={"text-left"}>Players in the room:</h2>
<PlayersComponent playerId={player.id} players={players} />
</div>
)}
{isGameStarted && (
<RunningGame
deck={deck}
discussions={discussions}
player={player}
players={players}
room={room}
turn={turn}
/>
)}
</div>
</WebSocketComponent>
Start Game
</Button>
)}

<h2 className={"text-left"}>Insert your nickname:</h2>
<form
className="flex gap-2"
onSubmit={(e) => {
e.preventDefault();
handleNamingButton();
}}
>
<Input
className="mb-3"
color="default"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<Button
color="primary"
variant="flat"
onPress={handleNamingButton}
>
Update
</Button>
</form>

<h2 className={"text-left"}>Players in the room:</h2>
<PlayersComponent playerId={player.id} players={players} />
</div>
)}
{isGameStarted && (
<RunningGame
deck={deck}
discussions={discussions}
player={player}
players={players}
room={room}
turn={turn}
/>
)}
</div>
);
else return null;
}
Loading