364 lines
10 KiB
TypeScript
364 lines
10 KiB
TypeScript
import {Card, Suit} from 'bsx-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 = `Welcome to BSX!
|
|
|
|
There are only 5 simple rules!
|
|
|
|
1. You will first be dealt 5 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 you 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. If the previous player manages to flip over their claimed number of black cards, you must choose a card from your stack to give up. Otherwise, the previous player 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 (
|
|
<>
|
|
<div>
|
|
{rules}
|
|
</div>
|
|
<LoginForm
|
|
socket={socket}
|
|
finish={(s) => {
|
|
setLoggedIn(true);
|
|
setUsername(s);
|
|
}}
|
|
username={username}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
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:</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>
|
|
</>
|
|
);
|
|
}
|