Implement the rest of gameplay, no game status yet

This commit is contained in:
snen 2021-09-24 22:51:26 -04:00
parent 7e0ba5431a
commit 23285b89b0
11 changed files with 314 additions and 104 deletions

View File

@ -8,19 +8,31 @@ const EMPTY_SPACE = '-'
interface CardTokenProps { interface CardTokenProps {
card: Card | null card: Card | null
onSelect: () => void onSelect: () => void
isSelected: boolean
disabled: boolean
} }
export default function BoardSlot(props: CardTokenProps): JSX.Element { export default function BoardSlot(props: CardTokenProps): JSX.Element {
const {onSelect, card} = props const {onSelect, card, isSelected, disabled} = props
if (card == null) { if (card === null || card.type === -1) {
return ( return (
<button> <button
type="button"
className={`board-slot${isSelected ? ' selected' : ''}`}
onClick={onSelect}
disabled={disabled}
>
<span>{EMPTY_SPACE}</span> <span>{EMPTY_SPACE}</span>
</button> </button>
) )
} }
return ( return (
<CardToken onSelect={onSelect} cardKey={card.id} /> <CardToken
onSelect={onSelect}
cardKey={card.type}
isSelected={isSelected}
disabled={disabled}
/>
) )
} }

View File

@ -3,11 +3,19 @@ import React from 'react'
import type { GameHandle, Selection } from './types.ts' import type { GameHandle, Selection } from './types.ts'
import BoardSlot from './BoardSlot.tsx' import BoardSlot from './BoardSlot.tsx'
import CardToken from './cards/Card.tsx' import CardToken from './cards/Card.tsx'
import {isAttackSelection, isMoveSelection, isPlayCardSelection} from './selection.tsx' import {
isAttackSelection,
isMoveSelection,
isPlayCardSelection,
isAlly,
isBoard,
isHand,
} from './selection.tsx'
type GameProps = GameHandle type GameProps = GameHandle
export default function Game({ export default function Game(props: GameProps): JSX.Element {
const {
team, team,
board, board,
player, player,
@ -19,6 +27,7 @@ export default function Game({
canDraw, canDraw,
hasDrawn, hasDrawn,
gameStatus, gameStatus,
drawChoices,
startTurn, startTurn,
endTurn, endTurn,
startDraw, startDraw,
@ -26,11 +35,12 @@ export default function Game({
attackCard, attackCard,
moveCard, moveCard,
playCard, playCard,
}: GameProps): JSX.Element { getView,
} = props
const [selection, setSelection] = React.useState<Selection | null>(null) const [selection, setSelection] = React.useState<Selection | null>(null)
function selectCard(nextSelection: Selection): void { function selectCard(nextSelection: Selection): void {
if (!selection) { if (selection === null) {
setSelection(nextSelection) setSelection(nextSelection)
} else { } else {
if (isAttackSelection(selection, nextSelection)) { if (isAttackSelection(selection, nextSelection)) {
@ -44,11 +54,27 @@ export default function Game({
} }
} }
function selectTurnButton(): void { React.useEffect(() => {
if (currentTurn === team) {
startTurn()
} }
}, [currentTurn, startTurn])
React.useEffect(() => {
const intervalId = setInterval(
() => {
getView()
},
1000,
)
return () => {
clearInterval(intervalId)
}
}, [getView])
const enemyBoard = team === 1 ? board.scourge : board.sentinal const enemyBoard = team === 1 ? board.scourge : board.sentinal
const allyBoard = team === 1 ? board.sentinal : board.scourge const allyBoard = team === 1 ? board.sentinal : board.scourge
const isMyTurn = currentTurn === team
return ( return (
<div className="game-container"> <div className="game-container">
@ -58,19 +84,20 @@ export default function Game({
<p>Life: {enemyLife}</p> <p>Life: {enemyLife}</p>
<p>Deck: {enemyDeckSize}</p> <p>Deck: {enemyDeckSize}</p>
</div> </div>
<p>Turn: {currentTurn}</p> <div>
{canDraw && <p>Drawing phase...</p>} <p>{isMyTurn ? 'My' : 'Enemy'} Turn</p>
{hasDrawn && <p>Action phase...</p>} {isMyTurn && canDraw && <button type="button" onClick={startDraw}>Start Draw</button>}
<button onClick={selectTurnButton}>End/Start Turn</button> {isMyTurn && hasDrawn && <button type="button" onClick={endTurn}>End Turn</button>}
</div>
<div className="player-info"> <div className="player-info">
<p>Life: {player.life}</p> <p>Life: {player.life}</p>
<p>Deck: {deck.length}</p> <p>Deck: {deck.cards.length}</p>
</div> </div>
</div> </div>
<div className="game-board"> <div className="game-board">
<div className="hand"> <div className="hand">
{Array(enemyHandSize).fill(null).map(() => ( {Array(enemyHandSize).fill(null).map(() => (
<CardToken cardKey={null} /> <CardToken cardKey={null} isSelected={false} disabled />
))} ))}
</div> </div>
<div className="fighter-area enemy"> <div className="fighter-area enemy">
@ -78,6 +105,12 @@ export default function Game({
<BoardSlot <BoardSlot
card={card ?? null} card={card ?? null}
onSelect={() => selectCard({target: 'opponent', type: 'board', index})} onSelect={() => selectCard({target: 'opponent', type: 'board', index})}
isSelected={
selection
? !isAlly(selection) && isBoard(selection) && selection.index === index
: false
}
disabled={!(currentTurn === team && hasDrawn)}
/> />
))} ))}
</div> </div>
@ -86,14 +119,55 @@ export default function Game({
<BoardSlot <BoardSlot
card={card ?? null} card={card ?? null}
onSelect={() => selectCard({target: 'ally', type: 'board', index})} onSelect={() => selectCard({target: 'ally', type: 'board', index})}
isSelected={
selection
? isAlly(selection) && isBoard(selection) && selection.index === index
: false
}
disabled={!(currentTurn === team && hasDrawn)}
/> />
))} ))}
</div> </div>
{drawChoices.length > 0 && (
<React.Fragment>
<div className="draw-choices">
{drawChoices.map((card, index) => (
<CardToken
cardKey={card.type === -1 ? null : card.type}
onSelect={() => selectCard({target: 'ally', type: 'draws', index})}
isSelected={
selection
? selection.type === 'draws' && selection.index === index
: false
}
disabled={false}
/>
))}
</div>
<div>
<button
type="button"
onClick={
() => selection && selection.type === 'draws' && commitDraw(selection.index)
}
disabled={selection?.type !== 'draws'}
>
Draw Card
</button>
</div>
</React.Fragment>
)}
<div className="hand"> <div className="hand">
{player.hand.map((card, index) => ( {player.hand.map((card, index) => (
<CardToken <CardToken
cardKey={card.id} cardKey={card.type === -1 ? null : card.type}
onSelect={() => selectCard({target: 'ally', type: 'hand', index})} onSelect={() => selectCard({target: 'ally', type: 'hand', index})}
isSelected={
selection
? isAlly(selection) && isHand(selection) && selection.index === index
: false
}
disabled={!(isMyTurn && hasDrawn)}
/> />
))} ))}
</div> </div>

View File

@ -6,6 +6,7 @@ import Loading from '../components/Loading.tsx'
import { import {
AsyncHandle, AsyncHandle,
Card,
FighterArea, FighterArea,
GameState, GameState,
GameAction, GameAction,
@ -14,19 +15,69 @@ import {
import useServerSocket from './useServerSocket.ts' import useServerSocket from './useServerSocket.ts'
import Game from './Game.tsx' import Game from './Game.tsx'
function reducer(state: GameState, action: GameAction): GameState { interface GameClientState extends GameState {
isDrawing: boolean
}
type GameClientAction = GameAction
function reducer(state: GameClientState, action: GameClientAction): GameClientState {
switch (action.type) { switch (action.type) {
case 'update-state': { case 'update-state': {
return {...action.state, team: state.team} return {...state, ...action.state}
} }
case 'set-player': { case 'set-player-team': {
return {...state, team: action.team} return {...state, team: action.team}
} }
case 'play-card': {
const nextHand = [
...state.player.hand.slice(0, action.handIndex),
...state.player.hand.slice(action.handIndex + 1),
]
return {
...state,
player: { ...state.player, hand: nextHand },
}
}
case 'receive-cards': {
// first, i can draw and i am not yet drawing
// scry N cards from the deck
if (state.canDraw && !state.isDrawing) {
return {
...state,
isDrawing: true,
drawChoices: action.cards,
}
}
// then, i am drawing, i can still draw, and I haven't yet drawn.
// i draw a card into my hand
if (state.canDraw && state.isDrawing && !state.hasDrawn) {
return {
...state,
drawChoices: [],
player: {
...state.player,
hand: action.cards
},
isDrawing: false,
hasDrawn: true,
}
}
// then, i am no longer drawing and cannot draw. this is the board.
const team = state.team === 1 ? 'sentinal' : 'scourge' as const
return {
...state,
board: {
...state.board,
[team]: action.cards,
},
}
}
default: return state default: return state
} }
} }
const initialState: GameState = { const initialState: GameClientState = {
board: { board: {
sentinal: Array(4).fill(undefined) as FighterArea, sentinal: Array(4).fill(undefined) as FighterArea,
scourge: Array(4).fill(undefined) as FighterArea, scourge: Array(4).fill(undefined) as FighterArea,
@ -38,7 +89,9 @@ const initialState: GameState = {
life: 0, life: 0,
ready: false, ready: false,
}, },
deck: [], deck: {
cards: []
},
team: 1, team: 1,
enemyLife: 0, enemyLife: 0,
enemyDeckSize: 0, enemyDeckSize: 0,
@ -47,17 +100,20 @@ const initialState: GameState = {
canDraw: false, canDraw: false,
hasDrawn: false, hasDrawn: false,
gameStatus: 0, gameStatus: 0,
isDrawing: false,
drawChoices: [],
} }
export default function GameClient(): JSX.Element { export default function GameClient(): JSX.Element {
const [state, dispatch] = React.useReducer(reducer, initialState) const [state, dispatch] = React.useReducer(reducer, initialState)
const handleGameUpdate = React.useCallback((data: Record<string, unknown>) => { const handleGameUpdate = React.useCallback((action: GameAction) => {
console.log(data) dispatch(action)
}, []) }, [])
const socketHandle = useServerSocket(handleGameUpdate) const socketHandle = useServerSocket(handleGameUpdate)
const {team} = state
const gameHandle = React.useMemo<AsyncHandle<GameCommandAPI>>( const gameHandle = React.useMemo<AsyncHandle<GameCommandAPI>>(
() => { () => {
if (socketHandle.status !== 'connected') return socketHandle if (socketHandle.status !== 'connected') return socketHandle
@ -69,6 +125,7 @@ export default function GameClient(): JSX.Element {
socketHandle.handle.sendGameCommand({ socketHandle.handle.sendGameCommand({
type: 's', type: 's',
cmd: 'b', cmd: 'b',
player_id: team,
}) })
}, },
startTurn: () => { startTurn: () => {
@ -76,6 +133,7 @@ export default function GameClient(): JSX.Element {
socketHandle.handle.sendGameCommand({ socketHandle.handle.sendGameCommand({
type: 's', type: 's',
cmd: 's', cmd: 's',
player_id: team,
}) })
}, },
endTurn: () => { endTurn: () => {
@ -83,6 +141,7 @@ export default function GameClient(): JSX.Element {
socketHandle.handle.sendGameCommand({ socketHandle.handle.sendGameCommand({
type: 's', type: 's',
cmd: 'e', cmd: 'e',
player_id: team,
}) })
}, },
getView: () => { getView: () => {
@ -90,6 +149,7 @@ export default function GameClient(): JSX.Element {
socketHandle.handle.sendGameCommand({ socketHandle.handle.sendGameCommand({
type: 's', type: 's',
cmd: 'g', cmd: 'g',
player_id: team,
}) })
}, },
startDraw: () => { startDraw: () => {
@ -97,20 +157,25 @@ export default function GameClient(): JSX.Element {
socketHandle.handle.sendGameCommand({ socketHandle.handle.sendGameCommand({
type: 'a', type: 'a',
cmd: 's', cmd: 's',
player_id: team,
}) })
}, },
commitDraw: (cardIndex: number) => { commitDraw: (cardIndex: number) => {
if (socketHandle.status !== 'connected') return if (socketHandle.status !== 'connected') return
// dispatch({type})
socketHandle.handle.sendGameCommand({ socketHandle.handle.sendGameCommand({
type: 'a', type: 'a',
cmd: `d ${cardIndex}`, cmd: `d ${cardIndex}`,
player_id: team,
}) })
}, },
playCard: (handIndex: number, positionIndex: number) => { playCard: (handIndex: number, positionIndex: number) => {
if (socketHandle.status !== 'connected') return if (socketHandle.status !== 'connected') return
dispatch({ type: 'play-card', handIndex })
socketHandle.handle.sendGameCommand({ socketHandle.handle.sendGameCommand({
type: 'a', type: 'a',
cmd: `p ${handIndex} ${positionIndex}`, cmd: `p ${handIndex} ${positionIndex}`,
player_id: team,
}) })
}, },
moveCard: (positionFrom: number, positionTo: number) => { moveCard: (positionFrom: number, positionTo: number) => {
@ -118,6 +183,7 @@ export default function GameClient(): JSX.Element {
socketHandle.handle.sendGameCommand({ socketHandle.handle.sendGameCommand({
type: 'a', type: 'a',
cmd: `m ${positionFrom} ${positionTo}`, cmd: `m ${positionFrom} ${positionTo}`,
player_id: team,
}) })
}, },
attackCard: (positionFrom: number, positionTo: number) => { attackCard: (positionFrom: number, positionTo: number) => {
@ -125,23 +191,22 @@ export default function GameClient(): JSX.Element {
socketHandle.handle.sendGameCommand({ socketHandle.handle.sendGameCommand({
type: 'a', type: 'a',
cmd: `a ${positionFrom} ${positionTo}`, cmd: `a ${positionFrom} ${positionTo}`,
player_id: team,
}) })
}, },
}, },
} }
}, },
[socketHandle, state], [socketHandle, team],
) )
React.useEffect(() => { React.useEffect(() => {
if (gameHandle.status !== 'connected') return if (gameHandle.status !== 'connected') return
gameHandle.handle.readyPlayer() gameHandle.handle.readyPlayer()
if (state?.team === 1) gameHandle.handle.startTurn() }, [gameHandle, team])
}, [gameHandle, state?.team])
switch (gameHandle.status) { switch (gameHandle.status) {
case 'connected': { case 'connected': {
if (!state) return <Loading />
return <Game {...state} {...gameHandle.handle} /> return <Game {...state} {...gameHandle.handle} />
} }
case 'not-connected': case 'not-connected':

View File

@ -9,21 +9,30 @@ const EMPTY_SPACE = '-'
interface CardTokenProps { interface CardTokenProps {
cardKey: CardKey | null cardKey: CardKey | null
onSelect?: () => void onSelect?: () => void
isSelected: boolean
disabled: boolean
} }
export default function CardToken(props: CardTokenProps): JSX.Element { export default function CardToken(props: CardTokenProps): JSX.Element {
const {onSelect, cardKey} = props const {onSelect, cardKey, isSelected, disabled} = props
if (cardKey == null) { if (cardKey == null) {
return ( return (
<div className={`card-button card-back${isSelected ? ' selected' : ''}`}>
<div> <div>
<span>{EMPTY_SPACE}</span> <span>{EMPTY_SPACE}</span>
</div> </div>
</div>
) )
} }
return ( return (
<button onClick={onSelect}> <button
type="button"
className={`card-button${isSelected ? ' selected' : ''}`}
onClick={onSelect}
disabled={disabled}
>
<img <img
className="card-image"
src={getCardSrc(cardKey)} src={getCardSrc(cardKey)}
alt={getCardAlt(cardKey)} alt={getCardAlt(cardKey)}
/> />

View File

@ -2,8 +2,8 @@ import type {CardKey} from '../types.ts'
const cardPaths: Record<CardKey, string> = { const cardPaths: Record<CardKey, string> = {
0: 'joker.png', 0: 'joker.png',
1: 'sp_2.png', 1: 'sp_14.png',
2: 'sp_14.png', 2: 'sp_2.png',
3: 'sp_3.png', 3: 'sp_3.png',
4: 'sp_4.png', 4: 'sp_4.png',
5: 'sp_5.png', 5: 'sp_5.png',

View File

@ -1,17 +1,21 @@
import type {Selection} from './types.ts' import type {Selection} from './types.ts'
function isAlly(selection: Selection): boolean { export function isAlly(selection: Selection): boolean {
return selection.target === 'ally' return selection.target === 'ally'
} }
function isHand(selection: Selection): boolean { export function isHand(selection: Selection): boolean {
return selection.type === 'hand' return selection.type === 'hand'
} }
function isBoard(selection: Selection): boolean { export function isBoard(selection: Selection): boolean {
return selection.type === 'board' return selection.type === 'board'
} }
export function isDrawSelection(selection: Selection): boolean {
return selection.type === 'draws'
}
export function isPlayCardSelection(first: Selection, second: Selection): boolean { export function isPlayCardSelection(first: Selection, second: Selection): boolean {
const isMyHand = isAlly(first) && isHand(first) const isMyHand = isAlly(first) && isHand(first)
const targetsMyBoard = isAlly(second) && isBoard(second) const targetsMyBoard = isAlly(second) && isBoard(second)

View File

@ -11,10 +11,10 @@ export type AsyncHandle<T> =
export type CardKey = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 export type CardKey = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8
export interface Card { export interface Card {
type: number type: CardKey | -1
basePower: number basePower: number
power: number power: number
id: CardKey id: string
counters: number counters: number
owner: number owner: number
position: number position: number
@ -36,6 +36,7 @@ export interface Player {
life: number life: number
ready: boolean ready: boolean
} }
export type Team = 1 | 2
interface TeamEnumMap { interface TeamEnumMap {
1: 'sentinal' 1: 'sentinal'
@ -55,8 +56,10 @@ interface GameStatusMap {
export interface GameState { export interface GameState {
board: Board board: Board
player: Player player: Player
deck: Card[] deck: {
team: 1 | 2 cards: Card[]
}
team: Team
enemyLife: number enemyLife: number
enemyDeckSize: number enemyDeckSize: number
enemyHandSize: number enemyHandSize: number
@ -64,11 +67,14 @@ export interface GameState {
canDraw: boolean canDraw: boolean
hasDrawn: boolean hasDrawn: boolean
gameStatus: keyof GameStatusMap gameStatus: keyof GameStatusMap
drawChoices: Card[]
} }
export type GameAction = export type GameAction =
| { type: 'set-player'; team: 1 | 2} | { type: 'set-player-team'; team: Team}
| { type: 'receive-cards'; cards: Card[]}
| { type: 'update-state'; state: Omit<GameState, 'team'> } | { type: 'update-state'; state: Omit<GameState, 'team'> }
| { type: 'play-card', handIndex: number }
export interface GameCommandAPI { export interface GameCommandAPI {
readyPlayer: () => void readyPlayer: () => void
@ -106,13 +112,13 @@ export interface CommandVariantParamMap {
} }
export interface GameCommand { export interface GameCommand {
playerId: string player_id: Team
type: GameCommandEnum, type: GameCommandEnum
cmd: string // "<Variant> <VariantParam1> <VariantParam2> ..." cmd: string // "<Variant> <VariantParam1> <VariantParam2> ..."
} }
type SelectionTarget = 'ally' | 'opponent' type SelectionTarget = 'ally' | 'opponent'
type SelectionType = 'hand' | 'board' type SelectionType = 'hand' | 'board' | 'draws'
export interface Selection { export interface Selection {
target: SelectionTarget target: SelectionTarget

View File

@ -75,7 +75,7 @@ function reducer(state: GameSocketSessionState, action: Action): GameSocketSessi
const initialState: GameSocketSessionState = {status: 'connecting'} const initialState: GameSocketSessionState = {status: 'connecting'}
interface SocketHandle { interface SocketHandle {
sendGameCommand: (command: Omit<GameCommand, 'playerId'>) => void sendGameCommand: (command: GameCommand) => void
} }
export default function useServerSocket( export default function useServerSocket(
@ -98,18 +98,49 @@ export default function useServerSocket(
return return
} }
case 'joined p1': { case 'joined p1': {
onUpdate({ type: 'set-player', team: 1 }) onUpdate({ type: 'set-player-team', team: 1 })
dispatch ({ type: 'join-game' }) dispatch ({ type: 'join-game' })
return return
} }
case 'joined p2': { case 'joined p2': {
onUpdate({ type: 'set-player', team: 2 }) onUpdate({ type: 'set-player-team', team: 2 })
dispatch ({ type: 'join-game' }) dispatch ({ type: 'join-game' })
return return
} }
default: break; case 'played': {
switch (data.game_result.result_type) {
case 'a': {
const result = data.game_result.action_result
if (result) onUpdate({
type: 'receive-cards',
cards: result.cards,
})
return
}
case 's': {
const result = data.game_result.state_result
if (result) onUpdate({
type: 'update-state',
state: {
board: result.board,
player: result.player,
deck: result.deck,
enemyLife: result.enemy_life,
enemyDeckSize: result.enemy_deck_size,
enemyHandSize: result.enemy_hand_size,
currentTurn: result.current_turn,
canDraw: result.can_draw,
hasDrawn: result.has_drawn,
gameStatus: result.game_status,
},
})
return
}
default: return
}
}
default: return;
} }
onUpdate(data)
}, [onUpdate]) }, [onUpdate])
const onOpen = React.useCallback(() => { const onOpen = React.useCallback(() => {
@ -141,16 +172,13 @@ export default function useServerSocket(
sendJsonMessage(message) sendJsonMessage(message)
}, [sendJsonMessage]) }, [sendJsonMessage])
const sendGameCommand = React.useCallback((gameCommand: Omit<GameCommand, 'playerId'>) => { const sendGameCommand = React.useCallback((gameCommand: GameCommand) => {
if (state.status !== 'in-game') return if (state.status !== 'in-game') return
sendJson({ sendJson({
player_id: MY_ID, player_id: MY_ID,
match_id: state.matchId, match_id: state.matchId,
command: 'play', command: 'play',
game_command: { game_command: gameCommand,
...gameCommand,
playerId: state.playerId,
},
}) })
}, [state, sendJson]) }, [state, sendJson])

View File

@ -1,5 +1,6 @@
* { * {
font-family: sans-serif; font-family: sans-serif;
box-sizing: border-box;
} }
h1 { h1 {
@ -151,6 +152,7 @@ h2 {
.game-sidebar { .game-sidebar {
flex: 0 0 auto; flex: 0 0 auto;
width: 10em; width: 10em;
padding: 1em;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -162,7 +164,6 @@ h2 {
} }
.player-info { .player-info {
margin: 1em;
padding: 1em; padding: 1em;
background-color: #c4c4c4; background-color: #c4c4c4;
color: #000000; color: #000000;
@ -187,3 +188,40 @@ h2 {
justify-content: center; justify-content: center;
text-align: center; text-align: center;
} }
.board-slot {
width: 5em;
height: 7em;
margin: 0 1em;
}
.card-button {
width: 10em;
height: 14em;
margin: 0 1em;
background: transparent;
border: none;
}
.card-button:disabled {
cursor: not-allowed;
}
.card-back {
background: grey;
display: flex;
justify-content: center;
align-items: center;
font-size: 0.7em;
}
.selected {
box-shadow: 0px 0px 8px 4px rgba(245, 194, 10, 0.5);
}
.card-image {
width: 100%;
height: 100%;
}

View File

@ -1,19 +0,0 @@
import { Router } from 'oak'
function handleSocket(socket: WebSocket) {
console.log(socket)
}
const apiRouter = new Router()
apiRouter
.get('/api/ws', async (context) => {
if (!context.isUpgradable) throw new Error('Context not upgradable.')
const ws = await context.upgrade()
handleSocket(ws)
})
.get('/api/hello', (context) => {
context.response.body = "Hello world!";
})
export default apiRouter

View File

@ -1,9 +1,5 @@
import { Application, Status } from 'oak' import { Application, Status } from 'oak'
//// import any necessary wasm file here
// import init from '~/common/wasm.js'
import apiRouter from './routes/api.ts'
import staticRouter from './routes/static.tsx' import staticRouter from './routes/static.tsx'
const app = new Application() const app = new Application()
@ -28,9 +24,6 @@ app.use((ctx, next) => {
return next() return next()
}) })
app.use(apiRouter.routes())
app.use(apiRouter.allowedMethods())
app.use(staticRouter.routes()) app.use(staticRouter.routes())
app.use(staticRouter.allowedMethods()) app.use(staticRouter.allowedMethods())