McCarthyism/front/pages/index.tsx

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 red (heart or diamond) 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 black card or cannot flip over their claimed number of red 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} red 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>
</>
);
}