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 {
card: Card | null
onSelect: () => void
isSelected: boolean
disabled: boolean
}
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 (
<button>
<button
type="button"
className={`board-slot${isSelected ? ' selected' : ''}`}
onClick={onSelect}
disabled={disabled}
>
<span>{EMPTY_SPACE}</span>
</button>
)
}
return (
<CardToken onSelect={onSelect} cardKey={card.id} />
<CardToken
onSelect={onSelect}
cardKey={card.type}
isSelected={isSelected}
disabled={disabled}
/>
)
}

View File

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

View File

@ -6,6 +6,7 @@ import Loading from '../components/Loading.tsx'
import {
AsyncHandle,
Card,
FighterArea,
GameState,
GameAction,
@ -14,19 +15,69 @@ import {
import useServerSocket from './useServerSocket.ts'
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) {
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}
}
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
}
}
const initialState: GameState = {
const initialState: GameClientState = {
board: {
sentinal: Array(4).fill(undefined) as FighterArea,
scourge: Array(4).fill(undefined) as FighterArea,
@ -38,7 +89,9 @@ const initialState: GameState = {
life: 0,
ready: false,
},
deck: [],
deck: {
cards: []
},
team: 1,
enemyLife: 0,
enemyDeckSize: 0,
@ -47,17 +100,20 @@ const initialState: GameState = {
canDraw: false,
hasDrawn: false,
gameStatus: 0,
isDrawing: false,
drawChoices: [],
}
export default function GameClient(): JSX.Element {
const [state, dispatch] = React.useReducer(reducer, initialState)
const handleGameUpdate = React.useCallback((data: Record<string, unknown>) => {
console.log(data)
const handleGameUpdate = React.useCallback((action: GameAction) => {
dispatch(action)
}, [])
const socketHandle = useServerSocket(handleGameUpdate)
const {team} = state
const gameHandle = React.useMemo<AsyncHandle<GameCommandAPI>>(
() => {
if (socketHandle.status !== 'connected') return socketHandle
@ -69,6 +125,7 @@ export default function GameClient(): JSX.Element {
socketHandle.handle.sendGameCommand({
type: 's',
cmd: 'b',
player_id: team,
})
},
startTurn: () => {
@ -76,6 +133,7 @@ export default function GameClient(): JSX.Element {
socketHandle.handle.sendGameCommand({
type: 's',
cmd: 's',
player_id: team,
})
},
endTurn: () => {
@ -83,6 +141,7 @@ export default function GameClient(): JSX.Element {
socketHandle.handle.sendGameCommand({
type: 's',
cmd: 'e',
player_id: team,
})
},
getView: () => {
@ -90,6 +149,7 @@ export default function GameClient(): JSX.Element {
socketHandle.handle.sendGameCommand({
type: 's',
cmd: 'g',
player_id: team,
})
},
startDraw: () => {
@ -97,20 +157,25 @@ export default function GameClient(): JSX.Element {
socketHandle.handle.sendGameCommand({
type: 'a',
cmd: 's',
player_id: team,
})
},
commitDraw: (cardIndex: number) => {
if (socketHandle.status !== 'connected') return
// dispatch({type})
socketHandle.handle.sendGameCommand({
type: 'a',
cmd: `d ${cardIndex}`,
player_id: team,
})
},
playCard: (handIndex: number, positionIndex: number) => {
if (socketHandle.status !== 'connected') return
dispatch({ type: 'play-card', handIndex })
socketHandle.handle.sendGameCommand({
type: 'a',
cmd: `p ${handIndex} ${positionIndex}`,
player_id: team,
})
},
moveCard: (positionFrom: number, positionTo: number) => {
@ -118,6 +183,7 @@ export default function GameClient(): JSX.Element {
socketHandle.handle.sendGameCommand({
type: 'a',
cmd: `m ${positionFrom} ${positionTo}`,
player_id: team,
})
},
attackCard: (positionFrom: number, positionTo: number) => {
@ -125,23 +191,22 @@ export default function GameClient(): JSX.Element {
socketHandle.handle.sendGameCommand({
type: 'a',
cmd: `a ${positionFrom} ${positionTo}`,
player_id: team,
})
},
},
}
},
[socketHandle, state],
[socketHandle, team],
)
React.useEffect(() => {
if (gameHandle.status !== 'connected') return
gameHandle.handle.readyPlayer()
if (state?.team === 1) gameHandle.handle.startTurn()
}, [gameHandle, state?.team])
}, [gameHandle, team])
switch (gameHandle.status) {
case 'connected': {
if (!state) return <Loading />
return <Game {...state} {...gameHandle.handle} />
}
case 'not-connected':

View File

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

View File

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

View File

@ -1,17 +1,21 @@
import type {Selection} from './types.ts'
function isAlly(selection: Selection): boolean {
export function isAlly(selection: Selection): boolean {
return selection.target === 'ally'
}
function isHand(selection: Selection): boolean {
export function isHand(selection: Selection): boolean {
return selection.type === 'hand'
}
function isBoard(selection: Selection): boolean {
export function isBoard(selection: Selection): boolean {
return selection.type === 'board'
}
export function isDrawSelection(selection: Selection): boolean {
return selection.type === 'draws'
}
export function isPlayCardSelection(first: Selection, second: Selection): boolean {
const isMyHand = isAlly(first) && isHand(first)
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 interface Card {
type: number
type: CardKey | -1
basePower: number
power: number
id: CardKey
id: string
counters: number
owner: number
position: number
@ -36,6 +36,7 @@ export interface Player {
life: number
ready: boolean
}
export type Team = 1 | 2
interface TeamEnumMap {
1: 'sentinal'
@ -55,8 +56,10 @@ interface GameStatusMap {
export interface GameState {
board: Board
player: Player
deck: Card[]
team: 1 | 2
deck: {
cards: Card[]
}
team: Team
enemyLife: number
enemyDeckSize: number
enemyHandSize: number
@ -64,11 +67,14 @@ export interface GameState {
canDraw: boolean
hasDrawn: boolean
gameStatus: keyof GameStatusMap
drawChoices: Card[]
}
export type GameAction =
| { type: 'set-player'; team: 1 | 2}
| { type: 'update-state'; state: Omit<GameState, 'team'>}
| { type: 'set-player-team'; team: Team}
| { type: 'receive-cards'; cards: Card[]}
| { type: 'update-state'; state: Omit<GameState, 'team'> }
| { type: 'play-card', handIndex: number }
export interface GameCommandAPI {
readyPlayer: () => void
@ -106,13 +112,13 @@ export interface CommandVariantParamMap {
}
export interface GameCommand {
playerId: string
type: GameCommandEnum,
player_id: Team
type: GameCommandEnum
cmd: string // "<Variant> <VariantParam1> <VariantParam2> ..."
}
type SelectionTarget = 'ally' | 'opponent'
type SelectionType = 'hand' | 'board'
type SelectionType = 'hand' | 'board' | 'draws'
export interface Selection {
target: SelectionTarget

View File

@ -75,7 +75,7 @@ function reducer(state: GameSocketSessionState, action: Action): GameSocketSessi
const initialState: GameSocketSessionState = {status: 'connecting'}
interface SocketHandle {
sendGameCommand: (command: Omit<GameCommand, 'playerId'>) => void
sendGameCommand: (command: GameCommand) => void
}
export default function useServerSocket(
@ -98,18 +98,49 @@ export default function useServerSocket(
return
}
case 'joined p1': {
onUpdate({ type: 'set-player', team: 1 })
dispatch ({type: 'join-game'})
onUpdate({ type: 'set-player-team', team: 1 })
dispatch ({ type: 'join-game' })
return
}
case 'joined p2': {
onUpdate({ type: 'set-player', team: 2 })
dispatch ({type: 'join-game' })
onUpdate({ type: 'set-player-team', team: 2 })
dispatch ({ type: 'join-game' })
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])
const onOpen = React.useCallback(() => {
@ -141,16 +172,13 @@ export default function useServerSocket(
sendJsonMessage(message)
}, [sendJsonMessage])
const sendGameCommand = React.useCallback((gameCommand: Omit<GameCommand, 'playerId'>) => {
const sendGameCommand = React.useCallback((gameCommand: GameCommand) => {
if (state.status !== 'in-game') return
sendJson({
player_id: MY_ID,
match_id: state.matchId,
command: 'play',
game_command: {
...gameCommand,
playerId: state.playerId,
},
game_command: gameCommand,
})
}, [state, sendJson])

View File

@ -1,5 +1,6 @@
* {
font-family: sans-serif;
box-sizing: border-box;
}
h1 {
@ -151,6 +152,7 @@ h2 {
.game-sidebar {
flex: 0 0 auto;
width: 10em;
padding: 1em;
display: flex;
flex-direction: column;
@ -162,7 +164,6 @@ h2 {
}
.player-info {
margin: 1em;
padding: 1em;
background-color: #c4c4c4;
color: #000000;
@ -187,3 +188,40 @@ h2 {
justify-content: 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 any necessary wasm file here
// import init from '~/common/wasm.js'
import apiRouter from './routes/api.ts'
import staticRouter from './routes/static.tsx'
const app = new Application()
@ -28,9 +24,6 @@ app.use((ctx, next) => {
return next()
})
app.use(apiRouter.routes())
app.use(apiRouter.allowedMethods())
app.use(staticRouter.routes())
app.use(staticRouter.allowedMethods())