366 lines
10 KiB
TypeScript
366 lines
10 KiB
TypeScript
import {Card, Suit} from 'mccarthyism-core';
|
|
import {useEffect, useState} from 'react';
|
|
import io from 'socket.io-client';
|
|
|
|
import CreateRoomForm from '../components/CreateRoomForm';
|
|
import JoinRoomForm from '../components/JoinRoomForm';
|
|
import LoginForm from '../components/LoginForm';
|
|
|
|
interface GameState {
|
|
cards: Card[],
|
|
players: {username: string, numCards: number, stackSize: number, flipped: Card[], rank: number}[],
|
|
lastPlayed: number,
|
|
lastPlayedPlayer: string | null,
|
|
playerTurn: string
|
|
phase: number
|
|
}
|
|
|
|
const rankStrs = ['', 'A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'];
|
|
const suitChars = ['♦', '♥', '♣', '♠'];
|
|
|
|
const rules = `There are only 5 simple rules!
|
|
|
|
1. Each player will first be dealt the same number of cards.
|
|
|
|
2. At the beginning of each round, you must rearrange the order of your cards and place them face down in a stack.
|
|
|
|
3. During the round, players go around in a circle, claiming increasingly greater numbers. If it is your turn to claim a number, you must either claim a larger number than the previously claimed number or call BS. If you call BS, the previous player must flip over their claimed number of black (clubs or spades) cards from the tops of everyone's stacks.
|
|
|
|
4. When you click flip, it flips over the top card from that person's stack. If the previous player flips over a red card or cannot flip over their claimed number of black cards, they must choose a card from their stack to give up. Otherwise, the person who called BS must choose one of their cards to give up.
|
|
|
|
5. If you give up all your cards, you lose! Last player remaining wins!
|
|
|
|
`;
|
|
|
|
function useForceUpdate(){
|
|
const [value, setValue] = useState(0);
|
|
return () => setValue(value => value + 1);
|
|
}
|
|
|
|
export default function Game() {
|
|
const forceUpdate = useForceUpdate();
|
|
|
|
const [socket, setSocket] = useState<SocketIOClient.Socket | null>(null);
|
|
const [connected, setConnected] = useState(false);
|
|
const [loggedIn, setLoggedIn] = useState(false);
|
|
const [username, setUsername] = useState<string | null>(null);
|
|
|
|
const [room, setRoom] = useState<string | null>(null);
|
|
const [roomUsers, setRoomUsers] = useState<string[]>([]);
|
|
const [roomHost, setRoomHost] = useState('');
|
|
|
|
const [gameState, setGameState] = useState<GameState | null>(null);
|
|
|
|
const [num, setNum] = useState(0);
|
|
|
|
useEffect(() => {
|
|
const socket = io(process.env.NEXT_PUBLIC_BACK_HOST!);
|
|
setSocket(socket);
|
|
socket.on('connect', () => setConnected(true));
|
|
socket.on('disconnect', () => {
|
|
setConnected(false);
|
|
setLoggedIn(false);
|
|
setRoom(null);
|
|
setGameState(null);
|
|
});
|
|
|
|
socket.on('joinRoom', (data: {name: string}) => setRoom(data.name));
|
|
socket.on('leaveRoom', () => {
|
|
setRoom(null);
|
|
setGameState(null);
|
|
});
|
|
socket.on('roomUpdate', (data: {users: string[], host: string}) => {
|
|
setRoomUsers(data.users);
|
|
setRoomHost(data.host);
|
|
});
|
|
|
|
socket.on('gameState', (data: GameState) => {
|
|
setGameState(data);
|
|
});
|
|
socket.on('endGame', () => setGameState(null));
|
|
|
|
return () => {socket.close()};
|
|
}, []);
|
|
|
|
if (!socket) return null;
|
|
if (!loggedIn) {
|
|
return (
|
|
<>
|
|
<h2>
|
|
Welcome to McCarthyism!
|
|
</h2>
|
|
<div>
|
|
{rules}
|
|
</div>
|
|
<LoginForm
|
|
socket={socket}
|
|
finish={(s) => {
|
|
setLoggedIn(true);
|
|
setUsername(s);
|
|
}}
|
|
username={username}
|
|
/>
|
|
This game is licensed under the AGPL. <a href="https://git.exozy.me/Ta180m/McCarthyism">Source code here!</a>
|
|
</>
|
|
);
|
|
}
|
|
if (!room) {
|
|
return (
|
|
<>
|
|
<p>Logged in as {username}</p>
|
|
<hr />
|
|
<JoinRoomForm socket={socket} />
|
|
<hr />
|
|
<CreateRoomForm socket={socket} />
|
|
</>
|
|
);
|
|
}
|
|
if (!gameState) {
|
|
return (
|
|
<>
|
|
<p>Room {room}</p>
|
|
<p>Users:</p>
|
|
<ul>
|
|
{roomUsers.map(user => <li key={user}>{user + (user === roomHost ? ' (Host)':'')}</li>)}
|
|
</ul>
|
|
<button onClick={() => socket.emit('leaveRoom')}>Leave</button>
|
|
{username === roomHost &&
|
|
<button
|
|
onClick={() => socket.emit('startGame')}
|
|
disabled={roomUsers.length < 2}
|
|
>
|
|
Start
|
|
</button>
|
|
}
|
|
</>
|
|
);
|
|
}
|
|
if (gameState.phase === 0) {
|
|
return (
|
|
<>
|
|
<button
|
|
onClick={() => {
|
|
document.getElementById('Rules')!.style.display = (document.getElementById('Rules')!.style.display === 'none' ? 'block' : 'none');
|
|
}}
|
|
>
|
|
Show/hide rules
|
|
</button>
|
|
<div id='Rules' style={{display: 'none'}}>
|
|
{rules}
|
|
</div>
|
|
<ul>
|
|
{gameState.players.map(p => (
|
|
<li key={p.username}>
|
|
{p.username + (p.rank ? ` (Rank ${p.rank})` : '') + (p.numCards ? ` (${p.numCards} cards)` : '')}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<div>
|
|
{gameState.playerTurn+` will go first this round!`}
|
|
</div>
|
|
{`Rearrange your card stack from top to bottom!`}
|
|
<div>
|
|
<p>Your cards stack:</p>
|
|
{gameState.cards.map((card, i) => (
|
|
<label key={card.rank+' '+card.suit}>
|
|
<div>
|
|
<button
|
|
onClick={() => {
|
|
const tmp = gameState.cards[i];
|
|
gameState.cards[i] = gameState.cards[i-1];
|
|
gameState.cards[i-1] = tmp;
|
|
forceUpdate();
|
|
}}
|
|
disabled={i === 0}
|
|
>
|
|
Move up
|
|
</button>
|
|
<span style={{color: (card.suit === Suit.Hearts || card.suit === Suit.Diamonds ? 'red' : 'black')}}>
|
|
{' '+rankStrs[card.rank]+' '+suitChars[card.suit]}
|
|
</span>
|
|
</div>
|
|
</label>
|
|
))}
|
|
<button
|
|
onClick={() => socket.emit('prepare', gameState.cards)}
|
|
//disabled={username !== gameState.playerTurn || !canPlay(gameState.lastPlayed, selectedCards)}
|
|
>
|
|
I'm ready!
|
|
</button>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
if (gameState.phase === 1) {
|
|
return (
|
|
<>
|
|
<button
|
|
onClick={() => {
|
|
document.getElementById('Rules')!.style.display = (document.getElementById('Rules')!.style.display === 'none' ? 'block' : 'none');
|
|
}}
|
|
>
|
|
Show/hide rules
|
|
</button>
|
|
<div id='Rules' style={{display: 'none'}}>
|
|
{rules}
|
|
</div>
|
|
<ul>
|
|
{gameState.players.map(p => (
|
|
<li key={p.username}>
|
|
{p.username + (p.rank ? ` (Rank ${p.rank})` : '') + (p.numCards ? ` (${p.numCards} cards)` : '')}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<div>
|
|
<p>Last played:</p>
|
|
{gameState.lastPlayed ? (
|
|
<>
|
|
{gameState.lastPlayed}
|
|
{` by ${gameState.lastPlayedPlayer}`}
|
|
</>
|
|
) : '(Nothing)'}
|
|
</div>
|
|
{`It's ${gameState.playerTurn}'s turn!`}
|
|
<div>
|
|
<input
|
|
type="text"
|
|
placeholder="Claim a number greater than the last claimed number..."
|
|
value={num}
|
|
onChange={(e) => setNum(+e.target.value)}
|
|
/>
|
|
<button
|
|
onClick={() => socket.emit('turn', num)}
|
|
disabled={username !== gameState.playerTurn || num <= gameState.lastPlayed}
|
|
>
|
|
Claim!
|
|
</button>
|
|
<button
|
|
onClick={() => socket.emit('turn', -1)}
|
|
disabled={username !== gameState.playerTurn || gameState.lastPlayed === 0}
|
|
>
|
|
BS!
|
|
</button>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
if (gameState.phase === 2) {
|
|
return (
|
|
<>
|
|
<button
|
|
onClick={() => {
|
|
document.getElementById('Rules')!.style.display = (document.getElementById('Rules')!.style.display === 'none' ? 'block' : 'none');
|
|
}}
|
|
>
|
|
Show/hide rules
|
|
</button>
|
|
<div id='Rules' style={{display: 'none'}}>
|
|
{rules}
|
|
</div>
|
|
<ul>
|
|
{gameState.players.map(p => (
|
|
<li key={p.username}>
|
|
{p.username + (p.rank ? ` (Rank ${p.rank})` : '') + (p.numCards ? ` (${p.numCards} cards)` : '')}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
{`${gameState.playerTurn} has called BS! ${gameState.lastPlayedPlayer} must flip over ${gameState.lastPlayed} black cards!`}
|
|
<div>
|
|
<p>Stacks:</p>
|
|
{gameState.players.map((player, i) => (
|
|
<label key={player.username+': '+player.stackSize}>
|
|
<div>
|
|
<button
|
|
onClick={() => socket.emit('flip', i)}
|
|
disabled={username !== gameState.lastPlayedPlayer || player.stackSize === 0}
|
|
>
|
|
Flip!
|
|
</button>
|
|
{' '+player.username+': '+player.stackSize+' cards '}
|
|
{player.flipped.map((card, i) => (
|
|
<label key={card.rank+' '+card.suit}>
|
|
<span style={{color: (card.suit === Suit.Hearts || card.suit === Suit.Diamonds ? 'red' : 'black')}}>
|
|
{' '+rankStrs[card.rank]+' '+suitChars[card.suit]}
|
|
</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
if (gameState.phase === 3) {
|
|
return (
|
|
<>
|
|
<button
|
|
onClick={() => {
|
|
document.getElementById('Rules')!.style.display = (document.getElementById('Rules')!.style.display === 'none' ? 'block' : 'none');
|
|
}}
|
|
>
|
|
Show/hide rules
|
|
</button>
|
|
<div id='Rules' style={{display: 'none'}}>
|
|
{rules}
|
|
</div>
|
|
<ul>
|
|
{gameState.players.map(p => (
|
|
<li key={p.username}>
|
|
{p.username + (p.rank ? ` (Rank ${p.rank})` : '') + (p.numCards ? ` (${p.numCards} cards)` : '')}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
{(gameState.lastPlayed > 0 ? `${gameState.lastPlayedPlayer}` : `${gameState.playerTurn}`) + ` lost! Now they must give up one of their cards!`}
|
|
<div>
|
|
<p>Stacks:</p>
|
|
{gameState.players.map((player, i) => (
|
|
<label key={player.username+': '+player.stackSize}>
|
|
<div>
|
|
<button
|
|
onClick={() => socket.emit('flip', i)}
|
|
disabled={true}
|
|
>
|
|
Flip!
|
|
</button>
|
|
{' '+player.username+': '+player.stackSize+' cards '}
|
|
{player.flipped.map((card, i) => (
|
|
<label key={card.rank+' '+card.suit}>
|
|
<span style={{color: (card.suit === Suit.Hearts || card.suit === Suit.Diamonds ? 'red' : 'black')}}>
|
|
{' '+rankStrs[card.rank]+' '+suitChars[card.suit]}
|
|
</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</label>
|
|
))}
|
|
</div>
|
|
<div>
|
|
<p>Your cards:</p>
|
|
{gameState.cards.map((card, i) => (
|
|
<label key={card.rank+' '+card.suit}>
|
|
<div>
|
|
<button
|
|
onClick={() => socket.emit('giveup', i)}
|
|
disabled={username !== (gameState.lastPlayed > 0 ? gameState.lastPlayedPlayer : gameState.playerTurn)}
|
|
>
|
|
Give up this card!
|
|
</button>
|
|
<span style={{color: (card.suit === Suit.Hearts || card.suit === Suit.Diamonds ? 'red' : 'black')}}>
|
|
{' '+rankStrs[card.rank]+' '+suitChars[card.suit]}
|
|
</span>
|
|
</div>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
return (
|
|
<>
|
|
<div>
|
|
<p>Something went wrong! :/</p>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|