UUIDprops

- guide.selectedUUID is used to determina current uuid
- all the game related midifications are stored independantly per UUID
- no global indicators
This commit is contained in:
djmil 2023-11-24 10:38:52 +01:00
parent e017787441
commit df431eb4f1
19 changed files with 670 additions and 494 deletions

View File

@ -16,7 +16,6 @@ import org.springframework.web.bind.annotation.RestController;
import djmil.cordacheckers.cordaclient.CordaClient; import djmil.cordacheckers.cordaclient.CordaClient;
import djmil.cordacheckers.cordaclient.dao.GameView; import djmil.cordacheckers.cordaclient.dao.GameView;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.ReqGameBoardMove; import djmil.cordacheckers.cordaclient.dao.flow.arguments.ReqGameBoardMove;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.ReqGameProposalCreate;
import djmil.cordacheckers.user.HoldingIdentityResolver; import djmil.cordacheckers.user.HoldingIdentityResolver;
import djmil.cordacheckers.user.User; import djmil.cordacheckers.user.User;

View File

@ -10,9 +10,6 @@ import Games from './container/Games';
import Leaderboard from './container/Leaderboard'; import Leaderboard from './container/Leaderboard';
import useConfigReducer from './reducer/config'; import useConfigReducer from './reducer/config';
import useUserReducer from './reducer/user';
import useLeaderboardReducer from './reducer/leaderboard';
import useGamesReducer from './reducer/games';
import useUserApi from './api/user'; import useUserApi from './api/user';
import useLeaderboardApi from './api/leaderboard'; import useLeaderboardApi from './api/leaderboard';
@ -21,22 +18,23 @@ import useGamesApi from './api/games';
export default function App() { export default function App() {
const [config, dispatcConfig] = useConfigReducer(); const [config, dispatcConfig] = useConfigReducer();
const user = useUserApi(useUserReducer()).getUser(); const user = useUserApi();
const leaderboard = useLeaderboardApi(useLeaderboardReducer(), config).pollTable(); const leaderboard = useLeaderboardApi();
const games = useGamesApi();
user.api.useGetUser();
leaderboard.api.useTablePolling(config);
games.api.useGamesPolling(config);
const players = { const players = {
leaderboard, user: user.state,
currentUser: user.username, leaderboard: leaderboard.state,
isCurrentUser: (playerName) => user?.isCurrentUser(playerName) === true ? true : null
}; };
const gamesReducer = useGamesReducer();
const gamesApi = useGamesApi(gamesReducer, config);
const games = gamesApi.pollGamesList();
const isPolling = { const isPolling = {
games: games.isPollingGamesList, games: games.guide.isPolling,
leaderboard: leaderboard.isPollingTable leaderboard: leaderboard.guide.isPolling
} }
return ( return (
@ -45,7 +43,7 @@ export default function App() {
<Routes> <Routes>
<Route path='/' element={<About />} /> <Route path='/' element={<About />} />
<Route path='/about' element={<About />} /> <Route path='/about' element={<About />} />
<Route path='/games/*' element={<Games context={{ gamesReducer, gamesApi }} players={players} />} /> <Route path='/games/*' element={<Games games={games} players={players} />} />
<Route path='/leaderboard' element={<Leaderboard players={players} />} /> <Route path='/leaderboard' element={<Leaderboard players={players} />} />
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>

View File

@ -1,82 +1,101 @@
import { usePolling, doPushing } from '../hook/api'; import { usePolling, doPushing } from '../hook/api';
import { gamesInitialState } from '../reducer/games'; import { useGamesGuideReducer, useGamesStateReducer, gamesGuideTemplate } from '../reducer/games';
import { Color } from '../components/Checkers';
export default function useGamesApi(gamesReducer, config) { export default function useGamesApi() {
const [games, dispatchGames] = gamesReducer; const [state, dispatchState] = useGamesStateReducer();
const [guide, dispatchGuide] = useGamesGuideReducer();
const usePollingGamesList = () => { const useGamesPolling = (config) => {
const onSuccess = (gamesList) => { const onPolling = (isPolling) => dispatchGuide({ type: 'next', isPolling });
dispatchGames({ type: 'next', gamesList }); const onSuccess = (games) => dispatchState({ type: 'next', games });
usePolling('/api/game', { onPolling, onSuccess }, config.intervalMode(30));
} }
const isPollingGamesList = usePolling('/api/game', onSuccess, config.intervalMode(30)); const pushNewGame = ({ opponentName, opponentColor, board, message }) => {
if (games.isPollingGamesList !== isPollingGamesList) { doPushing('/api/gameproposal', 'POST', { opponentName, opponentColor, board, message }, {
dispatchGames({ type: 'next', isPollingGamesList }); onPushing: (isPushing) => dispatchGuide({ type: 'nextNewGame', isPushing }),
onSuccess: (game) => {
dispatchState({ type: 'add', game });
dispatchGuide({ type: 'nextNewGame', ...gamesGuideTemplate.newGame });
}
})
} }
return games; const pushGameProposalAccept = ({ uuid }) => {
doPushing(`/api/gameproposal/${uuid}/accept`, 'PUT', null, {
onPushing: (isPushing) => dispatchGuide({ type: 'UUIDpushing', uuid, what: isPushing && 'accept' }),
onSuccess: (game) => dispatchState({ type: 'update', game })
})
}
const pushGameProposalReject = ({ uuid }) => {
doPushing(`/api/gameproposal/${uuid}/reject`, 'PUT', null, {
onPushing: (isPushing) => dispatchGuide({ type: 'UUIDpushing', uuid, what: isPushing && 'reject' }),
onSuccess: (game) => dispatchState({ type: 'update', game })
})
}
const pushGameProposalCancel = ({ uuid }) => {
doPushing(`/api/gameproposal/${uuid}/cancel`, 'PUT', null, {
onPushing: (isPushing) => dispatchGuide({ type: 'UUIDpushing', uuid, what: isPushing && 'cancel' }),
onSuccess: (game) => dispatchState({ type: 'update', game })
})
}
const pushGameSurrender = ({ uuid }) => {
doPushing(`/api/game/${uuid}/surrender`, 'PUT', null, {
onPushing: (isPushing) => dispatchGuide({ type: 'UUIDpushing', uuid, what: isPushing && 'surrender' }),
onSuccess: (game) => dispatchState({ type: 'update', game })
})
}
const pushGameDrawRequest = ({ uuid }) => {
doPushing(`/api/game/${uuid}/drawreq`, 'PUT', null, {
onPushing: (isPushing) => dispatchGuide({ type: 'UUIDpushing', uuid, what: isPushing && 'draw_request' }),
onSuccess: (game) => dispatchState({ type: 'update', game })
})
}
const pushGameDrawAccept = ({ uuid }) => {
doPushing(`/api/game/${uuid}/drawacc`, 'PUT', null, {
onPushing: (isPushing) => dispatchGuide({ type: 'UUIDpushing', uuid, what: isPushing && 'draw_accept' }),
onSuccess: (game) => dispatchState({ type: 'update', game })
})
}
const pushGameDrawReject = ({ uuid }) => {
doPushing(`/api/game/${uuid}/drawrej`, 'PUT', null, {
onPushing: (isPushing) => dispatchGuide({ type: 'UUIDpushing', uuid, what: isPushing && 'draw_reject' }),
onSuccess: (game) => dispatchState({ type: 'update', game })
})
}
const pushGameMove = ({ uuid, move, message }) => {
doPushing(`/api/game/${uuid}/move`, 'PUT', { move, message }, {
onPushing: (isPushing) => dispatchGuide({ type: 'UUIDpushing', uuid, what: isPushing && 'move' }),
onSuccess: (game) => {
dispatchState({ type: 'update', game });
dispatchGuide({ type: 'UUIDmessage', uuid, message: ''});
}
})
} }
return { return {
pollGamesList: usePollingGamesList, state,
guide,
pushNewGame: ({ opponentName, myColor, board, message }) => ifNot(games.isPushingNewGame) && dispatchGuide,
doPushing('/api/gameproposal', 'POST', { opponentName, opponentColor: Color.opposite(myColor), board, message }, { api: {
onPushing: (isPushingNewGame) => dispatchGames({ type: 'next', isPushingNewGame }), useGamesPolling,
onSuccess: (game) => dispatchGames({ type: 'next', gamesList: [game, ...games.gamesList], newGame: gamesInitialState.newGame }) pushNewGame,
}), pushGameProposalAccept,
pushGameProposalReject,
pushGameProposalCancel: ({ uuid }) => ifNot(games.isPushingGameProposalCancel) && pushGameProposalCancel,
doPushing(`/api/gameproposal/${uuid}/cancel`, 'PUT', null, { pushGameSurrender,
onPushing: (isPushingGameProposalCancel) => dispatchGames({ type: 'next', isPushingGameProposalCancel }), pushGameDrawRequest,
onSuccess: (canceledGame) => dispatchGames({ type: 'next', gamesList: games.nextGame(canceledGame), proposal: gamesInitialState.proposal }) pushGameDrawAccept,
}), pushGameDrawReject,
pushGameMove
pushGameProposalReject: ({ uuid }) => ifNot(games.isPushingGameProposalReject) &&
doPushing(`/api/gameproposal/${uuid}/reject`, 'PUT', null, {
onPushing: (isPushingGameProposalReject) => dispatchGames({ type: 'next', isPushingGameProposalReject }),
onSuccess: (rejectedGame) => dispatchGames({ type: 'next', gamesList: games.nextGame(rejectedGame), proposal: gamesInitialState.proposal })
}),
pushGameProposalAccept: ({ uuid }) => ifNot(games.isPushingGameProposalAccept) &&
doPushing(`/api/gameproposal/${uuid}/accept`, 'PUT', null, {
onPushing: (isPushingGameProposalAccept) => dispatchGames({ type: 'next', isPushingGameProposalAccept }),
onSuccess: (acceptedGame) => dispatchGames({ type: 'next', gamesList: games.nextGame(acceptedGame), proposal: gamesInitialState.proposal })
}),
pushGameSurrender: ({ uuid }) => ifNot(games.isPushingGameSurrender) &&
doPushing(`/api/game/${uuid}/surrender`, 'PUT', null, {
onPushing: (isPushingGameSurrender) => dispatchGames({ type: 'next', isPushingGameSurrender }),
onSuccess: (finishedGame) => dispatchGames({ type: 'next', gamesList: games.nextGame(finishedGame), active: gamesInitialState.active })
}),
pushGameDrawRequest: ({ uuid }) => ifNot(games.isPushingGameDrawRequest) &&
doPushing(`/api/game/${uuid}/drawreq`, 'PUT', null, {
onPushing: (isPushingGameDrawRequest) => dispatchGames({ type: 'next', isPushingGameDrawRequest }),
onSuccess: (drawReqGame) => dispatchGames({ type: 'next', gamesList: games.nextGame(drawReqGame), active: gamesInitialState.active })
}),
pushGameDrawAccept: ({ uuid }) => ifNot(games.isPushingGameDrawAccept) &&
doPushing(`/api/game/${uuid}/drawacc`, 'PUT', null, {
onPushing: (isPushingGameDrawAccept) => dispatchGames({ type: 'next', isPushingGameDrawAccept }),
onSuccess: (drawAccGame) => dispatchGames({ type: 'next', gamesList: games.nextGame(drawAccGame), active: gamesInitialState.active })
}),
pushGameDrawReject: ({ uuid }) => ifNot(games.isPushingGameDrawReject) &&
doPushing(`/api/game/${uuid}/drawrej`, 'PUT', null, {
onPushing: (isPushingGameDrawReject) => dispatchGames({ type: 'next', isPushingGameDrawReject }),
onSuccess: (drawRejGame) => dispatchGames({ type: 'next', gamesList: games.nextGame(drawRejGame), active: gamesInitialState.active })
}),
pushGameMove: ({ uuid, move, message }) => ifNot(games.isPushingGameMove) &&
doPushing(`/api/game/${uuid}/move`, 'PUT', { move, message }, {
onPushing: (isPushingGameMove) => dispatchGames({ type: 'next', isPushingGameMove }),
onSuccess: (game) => dispatchGames({ type: 'next', gamesList: games.nextGame(game), active: gamesInitialState.active })
}),
} }
} }
function ifNot(expression) {
return !expression;
} }

View File

@ -1,22 +1,23 @@
import { usePolling } from "../hook/api"; import { usePolling } from "../hook/api";
import { useLeaderboardStateReducer, useLeaderboardGuideReducer } from '../reducer/leaderboard';
export default function useLeaderboardApi(leaderboardReducer, config) { export default function useLeaderboardApi() {
const [leaderboard, dispatchLeaderboard] = leaderboardReducer; const [state, dispatchState] = useLeaderboardStateReducer();
const [guide, dispatchGuide] = useLeaderboardGuideReducer();
const usePollingTable = () => { const useTablePolling = (config) => {
const onSuccess = (table) => { const onPolling = (isPolling) => dispatchGuide({ type: 'next', isPolling });
dispatchLeaderboard({ type: 'next', table }); const onSuccess = (table) => dispatchState({type: 'next', table });
}
const isPollingTable = usePolling('/api/leaderboard', onSuccess, config.intervalMode(300)); usePolling('/api/leaderboard', { onSuccess, onPolling }, config.intervalMode(300));
if (leaderboard.isPollingTable !== isPollingTable) {
dispatchLeaderboard({ type: 'next', isPollingTable });
}
return leaderboard;
} }
return { return {
pollTable: usePollingTable state,
guide,
dispatchGuide,
api: {
useTablePolling
}
} }
} }

View File

@ -1,19 +1,21 @@
import { usePolling } from "../hook/api"; import { usePolling } from "../hook/api";
import { useUserStateReducer } from "../reducer/user";
export default function useUserApi(userReducer) { export default function useUserApi() {
const [user, dispatchUser] = userReducer; const [state, dispatchState] = useUserStateReducer();
const useGetUser = () => { const useGetUser = () => {
const onSuccess = (userJson) => { const onSuccess = (userJson) => {
dispatchUser({ type: "parse", userJson }); dispatchState({ type: "parse", userJson });
} }
usePolling('/api/user', onSuccess); // <<-- fetch once usePolling('/api/user', { onSuccess }); // <<-- fetch once
return user;
} }
return { return {
getUser: useGetUser state,
api : {
useGetUser
}
} }
} }

View File

@ -74,10 +74,7 @@ export function Player({ color, name }) {
export function Board({ board, flip, onStoneClick, onStoneMove }) { export function Board({ board, flip, onStoneClick, onStoneMove }) {
const [[moveId, moveX, moveY], setMove] = useState([0, 0, 0]); const [[moveId, moveX, moveY], setMove] = useState([0, 0, 0]);
if (!board) const isInteractive = (board && (typeof onStoneClick === 'function' || typeof onStoneMove === 'function')) ? ' interactive' : '';
board = [];
const isInteractive = (typeof onStoneClick === 'function' || typeof onStoneMove === 'function') ? ' interactive' : '';
const WhiteTile = ({ id }) => { const WhiteTile = ({ id }) => {
const stone = board[id]; const stone = board[id];

View File

@ -1,5 +1,5 @@
import React from 'react'; import React, { useContext, useEffect } from 'react';
import { GamesContext } from '../context/games'; import { GamesStateContext, GamesGuideContext } from '../context/games';
import { NavLink, Routes, Route } from 'react-router-dom'; import { NavLink, Routes, Route } from 'react-router-dom';
import NewGame from './games/view/NewGame'; import NewGame from './games/view/NewGame';
@ -12,67 +12,75 @@ import Counter from '../components/Counter';
import './Games.css'; import './Games.css';
export default function Games({ context: { gamesReducer, gamesApi }, players }) { export default function Games({ games, players }) {
const [games, dispatchGames] = gamesReducer; const gamesState = games.state;
const gamesDispatchGuide = games.dispatchGuide;
useEffect(() => {
gamesDispatchGuide({ type: 'sync', gamesState });
}, [gamesState, gamesDispatchGuide]);
return ( return (
<GamesContext.Provider value={games} > <GamesStateContext.Provider value={gamesState} >
<GamesGuideContext.Provider value={games.guide} >
<div className='Games'> <div className='Games'>
<div className='left-side'> <div className='left-side'>
<ViewSelector games={games} /> <ViewSelector />
<ViewProvider dispatchGames={dispatchGames} players={players} /> <ViewProvider dispatchGuide={gamesDispatchGuide} players={players} />
</div> </div>
<div className='right-side'> <div className='right-side'>
<ActionPanel gamesApi={gamesApi} /> <ActionPanel gamesApi={games.api} />
<GameBoardRoutes dispatchGuide={gamesDispatchGuide} gamesApi={games.api} username={players.user.name} />
<Message2OpponentRoutes dispatchGuide={gamesDispatchGuide} />
{/* <GameMessage /> */} {/* <GameMessage /> */}
<GameBoardRoutes gamesReducer={gamesReducer} gamesApi={gamesApi} username={players.currentUser} />
<Message2Opponent dispatchGames={dispatchGames} />
</div> </div>
</div > </div >
</GamesContext.Provider> </GamesGuideContext.Provider>
</GamesStateContext.Provider>
) )
}; };
function ViewSelector({ games }) { function ViewSelector() {
const awaiting = countGames(games.gamesList); const guide = useContext(GamesGuideContext);
return ( return (
<nav className='ViewSelector' > <nav className='ViewSelector' >
<div className='Container' > <div className='Container' >
<NavLink to='new'>New</NavLink> <NavLink to='new'>New</NavLink>
<NavLink to='proposal'>Proposal<Counter number={awaiting.proposals} /></NavLink> <NavLink to='proposal'>Proposal<Counter number={guide.awaiting.proposal} /></NavLink>
<NavLink to='active' >Active<Counter number={awaiting.active} /></NavLink> <NavLink to='active' >Active<Counter number={guide.awaiting.active} /></NavLink>
<NavLink to='archive' >Archive</NavLink> <NavLink to='archive' >Archive</NavLink>
</div> </div>
</nav> </nav>
) )
} }
function ViewProvider({ dispatchGames, players }) { function ViewProvider({ dispatchGuide, players }) {
return ( return (
<div className='ViewProvider'> <div className='ViewProvider'>
<Routes> <Routes>
<Route path='new' element={ <Route path='new' element={
<NewGame setPlayers={(opponentName, myColor) => dispatchGames({ type: 'nextNewGame', opponentName, myColor })} <NewGame
players={players} players={players}
setPlayers={(opponentName, myColor) => dispatchGuide({ type: 'nextNewGame', opponentName, myColor })}
/> />
} /> } />
<Route path='proposal' element={ <Route path='proposal' element={
<GameProposalSelector onSelect={(selectedUUID) => dispatchGames({ type: 'nextProposal', selectedUUID })} /> <GameProposalSelector onSelect={(uuid) => dispatchGuide({ type: 'selectedUUID', proposal: uuid })} />
} /> } />
<Route path='active' element={ <Route path='active' element={
<ActiveGameSelector onSelect={(selectedUUID) => dispatchGames({ type: 'nextActive', selectedUUID })} /> <ActiveGameSelector onSelect={(uuid) => dispatchGuide({ type: 'selectedUUID', active: uuid })} />
} /> } />
<Route path='archive' element={ <Route path='archive' element={
<GameArchiveSelector onSelect={(selectedUUID) => dispatchGames({ type: 'nextArchive', selectedUUID })} /> <GameArchiveSelector onSelect={(uuid) => dispatchGuide({ type: 'selectedUUID', archive: uuid })} />
} /> } />
</Routes> </Routes>
@ -81,25 +89,57 @@ function ViewProvider({ dispatchGames, players }) {
} }
function ActionPanel({ gamesApi }) { function ActionPanel({ gamesApi }) {
const games = useContext(GamesStateContext);
const guide = useContext(GamesGuideContext);
const fromUUID = (uuid) => (!uuid) ? [{}, null] :
[
games.find((game) => game.uuid === uuid) || {}, // game
guide.UUIDpushing[uuid] // pushing
];
return ( return (
<div className='ActionPanel'> <div className='ActionPanel'>
<Routes> <Routes>
<Route path='new' element={ <Route path='new' element={
<Create onClick={(reqParams) => gamesApi.pushNewGame(reqParams)} /> <Create
getGame={() => [guide.newGame, guide.newGame.isPushing]}
onClick={(req) => gamesApi.pushNewGame(req)}
/>
} /> } />
<Route path='proposal' element={[ <Route path='proposal' element={[
<Accept key={1} onClick={(uuid) => gamesApi.pushGameProposalAccept({ uuid })} />, <Accept key={1}
<Reject key={2} onClick={(uuid) => gamesApi.pushGameProposalReject({ uuid })} />, getGame={() => fromUUID(guide.selectedUUID.proposal)}
<Cancel key={3} onClick={(uuid) => gamesApi.pushGameProposalCancel({ uuid })} /> onClick={(req) => gamesApi.pushGameProposalAccept(req)}
/>,
<Reject key={2}
getGame={() => fromUUID(guide.selectedUUID.proposal)}
onClick={(req) => gamesApi.pushGameProposalReject(req)}
/>,
<Cancel key={3}
getGame={() => fromUUID(guide.selectedUUID.proposal)}
onClick={(req) => gamesApi.pushGameProposalCancel(req)}
/>
]} /> ]} />
<Route path='active' element={[ <Route path='active' element={[
<DrawRequest key={1} onClick={(uuid) => gamesApi.pushGameDrawRequest({ uuid })} />, <DrawRequest key={1}
<DrawAccept key={2} onClick={(uuid) => gamesApi.pushGameDrawAccept({ uuid })} />, getGame={() => fromUUID(guide.selectedUUID.active)}
<DrawReject key={3} onClick={(uuid) => gamesApi.pushGameDrawReject({ uuid })} />, onClick={(req) => gamesApi.pushGameDrawRequest(req)}
<Surrender key={4} onClick={(uuid) => gamesApi.pushGameSurrender({ uuid })} /> />,
<DrawAccept key={2}
getGame={() => fromUUID(guide.selectedUUID.active)}
onClick={(req) => gamesApi.pushGameDrawAccept(req)}
/>,
<DrawReject key={3}
getGame={() => fromUUID(guide.selectedUUID.active)}
onClick={(req) => gamesApi.pushGameDrawReject(req)}
/>,
<Surrender key={4}
getGame={() => fromUUID(guide.selectedUUID.active)}
onClick={(req) => gamesApi.pushGameSurrender(req)} />
]} /> ]} />
<Route path='archive' element={[ <Route path='archive' element={[
@ -112,50 +152,80 @@ function ActionPanel({ gamesApi }) {
) )
} }
function GameBoardRoutes({ gamesReducer, gamesApi, username }) { function GameBoardRoutes({ dispatchGuide, gamesApi, username }) {
const [games, dispatchGames] = gamesReducer; const games = useContext(GamesStateContext);
const guide = useContext(GamesGuideContext);
const fromUUID = (uuid) => (!uuid) ? [{}, null] :
[
games.find((game) => game.uuid === uuid) || {}, // game
guide.UUIDpushing[uuid] // pushing
];
const onStoneClick = (uuid, cellId) => { const onStoneClick = (uuid, cellId) => {
let board = { ...games.newGame.board }; let board = { ...guide.newGame.board };
board[cellId] = nextStone(board[cellId]); board[cellId] = nextStone(board[cellId]);
dispatchGames({ type: 'nextNewGame', board }); dispatchGuide({ type: 'nextNewGame', board });
} }
const onStoneMove = (uuid, move) => gamesApi.pushGameMove({ uuid, move, message: games.active.message });
return ( return (
<Routes> <Routes>
<Route path='new' element={<GameBoard username={username} onStoneClick={onStoneClick} />} />
<Route path='proposal' element={<GameBoard username={username} />} /> <Route path='new' element={
<Route path='active' element={<GameBoard username={username} onStoneMove={onStoneMove} />} /> <GameBoard
<Route path='archive' element={<GameBoard username={username} />} /> username={username}
getGame={() => [guide.newGame, null]}
onStoneClick={onStoneClick}
/>
} />
<Route path='proposal' element={
<GameBoard
username={username}
getGame={() => fromUUID(guide.selectedUUID.proposal)}
/>
} />
<Route path='active' element={
<GameBoard
username={username}
getGame={() => fromUUID(guide.selectedUUID.active)}
onStoneMove={(uuid, move) => gamesApi.pushGameMove({ uuid, move, message: guide.UUIDmessage[uuid] })}
/>
} />
<Route path='archive' element={
<GameBoard
username={username}
getGame={() => fromUUID(guide.selectedUUID.archive)}
/>
} />
</Routes> </Routes>
) )
} }
function countGames(gamesList) { function Message2OpponentRoutes({ dispatchGuide }) {
const guide = useContext(GamesGuideContext);
var awaiting = { const getMessage = (uuid) => !uuid ? undefined : // <<-- appears as inactive message field
proposals: 0, guide.UUIDmessage[uuid] || '';
active: 0
};
if (!gamesList) return (
return awaiting; <Routes>
for (const game of gamesList) { <Route path='new' element={
switch (game.status) { <Message2Opponent
case 'GAME_PROPOSAL_WAIT_FOR_YOU': getMessage={() => guide.newGame.message}
awaiting.proposals++; setMessage={(message) => dispatchGuide({ type: 'nextNewGame', message })} />
break; } />
case 'GAME_BOARD_WAIT_FOR_YOU':
case 'DRAW_REQUEST_WAIT_FOR_YOU': <Route path='active' element={
awaiting.active++; <Message2Opponent
break; getMessage={() => getMessage(guide.selectedUUID.active)}
default: setMessage={(message) => dispatchGuide({ type: 'UUIDmessage', message, uuid: guide.selectedUUID.active })} />
break; } />
}
} </Routes>
)
return awaiting;
} }

View File

@ -1,26 +1,22 @@
import './Leaderboard.css'; import './Leaderboard.css';
import React from "react" import React from 'react';
import Loading from '../components/Loading';
export default function Leaderboard({ players }) { export default function Leaderboard({ players }) {
const tableRows = Object.keys(players.leaderboard).map(name => {
var rank = players.leaderboard[name];
const table = players.leaderboard.table; return (
if (table == null) <tr key={name} className={players.user.isCurrentUser(name) && 'currentuser'}>
return <Loading /> <td>{name}</td>
const tableRows = Object.keys(table).map(playerName => {
var rank = table[playerName];
return <tr key={playerName} className={players.isCurrentUser(playerName) && 'currentuser'}>
<td>{playerName}</td>
<td>{rank.total}</td> <td>{rank.total}</td>
<td>{rank.victory}</td> <td>{rank.victory}</td>
<td>{rank.draw}</td> <td>{rank.draw}</td>
</tr> </tr>
)
}); });
return ( return (
<div className="Leaderboard"> <div className='Leaderboard'>
<table> <table>
<thead> <thead>
<tr> <tr>

View File

@ -1,28 +1,33 @@
import React, { useContext } from 'react'; import React from 'react';
import { GamesContext } from '../../context/games'; import { Color } from '../../components/Checkers';
import Wobler from '../../components/Wobler'; import Wobler from '../../components/Wobler';
import './ActionPanel.css' import './ActionPanel.css'
/* /*
* NewGame actoins * NewGame actoins
*/ */
export function Create({ onClick }) { export function Create({ getGame, onClick }) {
const games = useContext(GamesContext); const [game, isPushing] = getGame();
const hasPlayers = (game.opponentName && game.myColor) ? true : '';
const hasPlayers = games.newGame.opponentName && games.newGame.myColor; const validate = () => {
const validateNewGame = () => {
if (!hasPlayers) if (!hasPlayers)
return alert("You have to select an opponent"); return alert("You have to select an opponent");
onClick(games.newGame); if (isPushing)
return; // busy
onClick({
opponentName: game.opponentName,
opponentColor: Color.opposite(game.myColor), // TODO: by fixing this i can simply return GAME
board: game.board,
message: game.message
});
} }
return ( return (
<button className={'Create' + (hasPlayers ? ' ready' : '')} <button className={'Create' + (hasPlayers && ' ready')} onClick={() => validate()} >
onClick={validateNewGame} <Wobler text="Create" dance={isPushing} />
>
<Wobler text="Create" dance={games.isPushingNewGame} />
</button> </button>
) )
} }
@ -30,51 +35,71 @@ export function Create({ onClick }) {
/* /*
* GameProposal actions * GameProposal actions
*/ */
export function Accept({ getGame, onClick }) {
const [game, isPushing] = getGame();
const hasGame = (game.status === 'GAME_PROPOSAL_WAIT_FOR_YOU') ? true : '';
export function Accept({ onClick }) { const validate = () => {
const games = useContext(GamesContext); if (!hasGame)
return alert('You have to select pending GameProposal');
const selectedGame = games.findGame({ uuid: games.proposal.selectedUUID }); if (isPushing)
const isReady = selectedGame?.status === 'GAME_PROPOSAL_WAIT_FOR_YOU' ? true : ''; return; // busy
if (selectedGame?.status !== 'GAME_PROPOSAL_WAIT_FOR_OPPONENT') onClick({ uuid: game.uuid });
}
if (game.status !== 'GAME_PROPOSAL_WAIT_FOR_OPPONENT')
return ( return (
<button className={'Accept' + (isReady && ' ready')} <button className={'Accept' + (hasGame && ' ready')} onClick={() => validate()}>
onClick={() => isReady ? onClick(selectedGame.uuid) : alert('You have to select some GameProposal')} <Wobler text="Accept" dance={isPushing === 'accept'} />
>
<Wobler text="Accept" dance={games.isPushingGameProposalAccept} />
</button> </button>
) )
} }
export function Reject({ onClick }) { export function Reject({ getGame, onClick }) {
const games = useContext(GamesContext); const [game, isPushing] = getGame();
const hasGame = (game.status === 'GAME_PROPOSAL_WAIT_FOR_YOU') ? true : '';
const selectedGame = games.findGame({ uuid: games.proposal.selectedUUID }); const validate = () => {
const isReady = selectedGame?.status === 'GAME_PROPOSAL_WAIT_FOR_YOU' ? true : ''; if (!hasGame)
return alert('You have to select some GameProposal');
if (selectedGame?.status !== 'GAME_PROPOSAL_WAIT_FOR_OPPONENT') if (isPushing)
return; // busy
onClick({ uuid: game.uuid });
}
if (game.status !== 'GAME_PROPOSAL_WAIT_FOR_OPPONENT')
return ( return (
<button className={'Reject' + (isReady && ' ready')} <button className={'Reject' + (hasGame && ' ready')} onClick={() => validate()} >
onClick={() => isReady ? onClick(selectedGame.uuid) : alert('You have to select some GameProposal')} <Wobler text="Reject" dance={isPushing === 'reject'} />
>
<Wobler text="Reject" dance={games.isPushingGameProposalReject} />
</button> </button>
) )
} }
export function Cancel({ onClick }) { export function Cancel({ getGame, onClick }) {
const games = useContext(GamesContext); const [game, isPushing] = getGame();
const selectedGame = games.findGame({ uuid: games.proposal.selectedUUID }); if (game.status !== 'GAME_PROPOSAL_WAIT_FOR_OPPONENT')
const isReady = selectedGame?.status === 'GAME_PROPOSAL_WAIT_FOR_OPPONENT' ? true : ''; return;
const hasGame = (game.status === 'GAME_PROPOSAL_WAIT_FOR_OPPONENT') ? true : '';
const validate = () => {
if (!hasGame)
return alert('You have to select pending GameProposal');
if (isPushing)
return; // busy
onClick({ uuid: game.uuid });
}
if (selectedGame?.status === 'GAME_PROPOSAL_WAIT_FOR_OPPONENT')
return ( return (
<button className={'Cancel' + (isReady && ' ready')} <button className={'Cancel' + (hasGame && ' ready')} onClick={() => validate()} >
onClick={() => isReady ? onClick(selectedGame.uuid) : alert('You have to select pending GameProposal')} <Wobler text="Cancel" dance={isPushing === 'cancel'} />
>
<Wobler text="Cancel" dance={games.isPushingGameProposalCancel} />
</button> </button>
) )
} }
@ -82,22 +107,27 @@ export function Cancel({ onClick }) {
/* /*
* Game actions * Game actions
*/ */
export function Surrender({ getGame, onClick }) {
const [game, isPushing] = getGame();
export function Surrender({ onClick }) { if (game.status === 'DRAW_REQUEST_WAIT_FOR_YOU' || game.status === 'DRAW_REQUEST_WAIT_FOR_OPPONENT')
const games = useContext(GamesContext);
const selectedGame = games.findGame({ uuid: games.active.selectedUUID });
const gameStatus = selectedGame?.status;
const isReady = (gameStatus === 'GAME_BOARD_WAIT_FOR_OPPONENT' || gameStatus === 'GAME_BOARD_WAIT_FOR_YOU') ? true : '';
if (gameStatus === 'DRAW_REQUEST_WAIT_FOR_YOU' || gameStatus === 'DRAW_REQUEST_WAIT_FOR_OPPONENT')
return; // You shall not surrender if there is an active tie negotiations return; // You shall not surrender if there is an active tie negotiations
const hasGame = (game.status === 'GAME_BOARD_WAIT_FOR_OPPONENT' || game.status === 'GAME_BOARD_WAIT_FOR_YOU') ? true : '';
const validate = () => {
if (!hasGame)
return alert('You have to select a Game');
if (isPushing)
return; // busy
onClick({ uuid: game.uuid });
}
return ( return (
<button className={'Surrender' + (isReady && ' ready')} <button className={'Surrender' + (hasGame && ' ready')} onClick={() => validate()} >
onClick={() => isReady ? onClick(selectedGame.uuid) : alert('You have to select a game')} <Wobler text="Surrender" dance={isPushing === 'surrender'} />
>
<Wobler text="Surrender" dance={games.isPushingGameSurrender} />
</ button> </ button>
) )
} }
@ -105,60 +135,54 @@ export function Surrender({ onClick }) {
/* /*
* Game actions: Draw * Game actions: Draw
*/ */
export function DrawRequest({ getGame, onClick }) {
const [game, isPushing] = getGame();
export function DrawRequest({ onClick }) { if (game.status === 'DRAW_REQUEST_WAIT_FOR_YOU')
const games = useContext(GamesContext);
const selectedGame = games.findGame({ uuid: games.active.selectedUUID });
const gameStatus = selectedGame?.status;
const isReady = gameStatus === 'GAME_BOARD_WAIT_FOR_YOU' ? true : '';
const checkStatus = () => {
if (!selectedGame)
return alert('You have to select a game');
if (gameStatus === 'DRAW_REQUEST_WAIT_FOR_OPPONENT')
return alert('A draw was alredy offered to the opponent');
if (!isReady)
return alert('You can ask for a draw only during your turn');
onClick(selectedGame.uuid);
}
if (gameStatus === 'DRAW_REQUEST_WAIT_FOR_YOU')
return; // You can not send counter draw request return; // You can not send counter draw request
const hasGame = (game.status === 'GAME_BOARD_WAIT_FOR_YOU') ? true : '';
const validate = () => {
if (!hasGame)
return alert('You can ask for a draw only during your turn in a Game');
if (game.status === 'DRAW_REQUEST_WAIT_FOR_OPPONENT')
return alert('A draw was alredy offered to the opponent');
onClick({ uuid: game.uuid });
}
return ( return (
<button className={'Draw' + (isReady && ' ready')} onClick={() => checkStatus()} > <button className={'Draw' + (hasGame && ' ready')} onClick={() => validate()} >
<Wobler text="Draw?" dance={games.isPushingGameDrawRequest} /> <Wobler text="Draw?" dance={isPushing === 'draw_request'} />
</button > </button >
) )
} }
export function DrawAccept({ onClick }) { export function DrawAccept({ getGame, onClick }) {
const games = useContext(GamesContext); const [game, isPushing] = getGame();
const selectedGame = games.findGame({ uuid: games.active.selectedUUID }); if (game.status !== 'DRAW_REQUEST_WAIT_FOR_YOU')
const gameStatus = selectedGame?.status; return;
if (gameStatus === 'DRAW_REQUEST_WAIT_FOR_YOU')
return ( return (
<button className='Draw accept' onClick={() => onClick(selectedGame.uuid)} > <button className='Draw accept' onClick={() => onClick({ uuid: game.uuid })} >
<Wobler text="Draw accept" dance={games.isPushingGameDrawAccept} /> <Wobler text="Draw accept" dance={isPushing === 'draw_accept'} />
</button> </button>
) )
} }
export function DrawReject({ onClick }) { export function DrawReject({ getGame, onClick }) {
const games = useContext(GamesContext); const [game, isPushing] = getGame();
const selectedGame = games.findGame({ uuid: games.active.selectedUUID }); if (game.status !== 'DRAW_REQUEST_WAIT_FOR_YOU')
return;
if (selectedGame?.status === 'DRAW_REQUEST_WAIT_FOR_YOU')
return ( return (
<button className='Draw reject' onClick={() => onClick(selectedGame.uuid)} > <button className='Draw reject' onClick={() => onClick({ uuid: game.uuid })}>
<Wobler text="Reject" dance={games.isPushingGameDrawReject} /> <Wobler text="Reject" dance={isPushing === 'draw_reject'} />
</button> </button>
) )
} }
@ -166,13 +190,10 @@ export function DrawReject({ onClick }) {
/* /*
* GameArchive actions * GameArchive actions
*/ */
export function Backward() { export function Backward() {
return <button className='Backward' disabled>Backward</button> return <button className='Backward' disabled>Backward</button>
} }
export function Forward() { export function Forward() {
return <button className='Forward' disabled>Forward</button> return <button className='Forward' disabled>Forward</button>
} }

View File

@ -1,12 +1,10 @@
import React, { useContext } from 'react'; import React from 'react';
import { useLocation, matchPath } from 'react-router-dom';
import { GamesContext } from '../../context/games';
import { Color, Player, Board } from '../../components/Checkers'; import { Color, Player, Board } from '../../components/Checkers';
import './GameBoard.css'; import './GameBoard.css';
export default function GameBoard({ username, onStoneClick, onStoneMove }) { export default function GameBoard({ username, getGame, onStoneClick, onStoneMove }) {
const game = useSelectedGame() || {}; const [game, isPushing] = getGame();
const myName = game.opponentName ? username : ''; const myName = game.opponentName ? username : '';
const opponentColor = Color.opposite(game.myColor); const opponentColor = Color.opposite(game.myColor);
@ -15,37 +13,26 @@ export default function GameBoard({ username, onStoneClick, onStoneMove }) {
const optionalOnStoneClick = (typeof onStoneClick !== 'function') ? null : const optionalOnStoneClick = (typeof onStoneClick !== 'function') ? null :
(cellId) => onStoneClick(game.uuid, cellId); (cellId) => onStoneClick(game.uuid, cellId);
const optionalOnStoneMove = (typeof onStoneMove !== 'function') ? null : const optionalOnStoneMove = (typeof onStoneMove !== 'function' || isPushing) ? null :
(move) => onStoneMove(game.uuid, move); (move) => onStoneMove(game.uuid, move);
return ( return (
<div className='GameBoard'> <div className='GameBoard'>
<Player color={opponentColor || Color.black} name={game.opponentName} /> <Player
<Board board={game.board} flip={flipBoard} color={opponentColor || Color.black}
name={game.opponentName}
/>
<Board
board={game.board || []}
flip={flipBoard}
onStoneClick={optionalOnStoneClick} onStoneClick={optionalOnStoneClick}
onStoneMove={optionalOnStoneMove} onStoneMove={optionalOnStoneMove}
/> />
<Player color={game.myColor || Color.white} name={myName} /> <Player
{game.isPushingGameMove ? <span>Moving...</span> : null /* TODO: isPushing shall be stored per game. curernty it is global indicator */} color={game.myColor || Color.white}
name={myName}
/>
{(isPushing === 'move') ? <span>Moving...</span> : null}
</div> </div>
) )
} }
export function useSelectedGame() {
const games = useContext(GamesContext);
const { pathname } = useLocation();
if (matchPath('/games/new', pathname))
return games.newGame;
if (matchPath('/games/proposal', pathname))
return games.findGame({ uuid: games.proposal.selectedUUID });
if (matchPath('/games/active', pathname))
return games.findGame({ uuid: games.active.selectedUUID });
if (matchPath('/games/archive', pathname))
return games.findGame({ uuid: games.archive.selectedUUID });
return undefined;
}

View File

@ -1,65 +1,38 @@
import React, { useContext, useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import { useLocation, matchPath } from 'react-router-dom';
import { GamesContext } from '../../context/games';
export default function Message2Opponent({ dispatchGames }) { export default function Message2Opponent({ getMessage, setMessage }) {
const games = useContext(GamesContext);
const { pathname } = useLocation();
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const syncTimeoutRef = useRef(null); const syncTimeoutRef = useRef(null);
if (matchPath('/games/archive', pathname)) const message = getMessage();
return; // Shhh.. no chatting in the archives! if (value !== message && syncTimeoutRef.current === null) {
// Absorb external value ONLY if there is no scheduled sync
if (matchPath('/games/proposal', pathname)) setValue(message);
return; // TODO: Enable GameProposal messages, as soon as it has been supported by the server side
if (syncTimeoutRef.current === null) { // <<--- Absorb external value ONLY if there is no scheduled sync
var externalValue = '';
if (matchPath('/games/new', pathname))
externalValue = games.newGame.message;
else if (matchPath('/games/proposal', pathname))
externalValue = games.proposal.message;
else if (matchPath('/games/active', pathname))
externalValue = games.active.message;
if (value !== externalValue)
setValue(externalValue);
} }
const disabled = message === undefined;
/* --- */ /* --- */
const sync = (message) => { const sync = (nextMessage) => {
syncTimeoutRef.current = null; syncTimeoutRef.current = null;
setMessage(nextMessage);
if (matchPath('/games/new', pathname))
return dispatchGames({ type: 'nextNewGame', message });
if (matchPath('/games/proposal', pathname))
return dispatchGames({ type: 'nextProposal', message });
if (matchPath('/games/active', pathname))
return dispatchGames({ type: 'nextActive', message });
console.warn('unknown path');
} }
const update = (value) => { const update = (value) => {
setValue(value); setValue(value);
if (syncTimeoutRef.current)
clearTimeout(syncTimeoutRef.current); // <<--- Cancel previous sync clearTimeout(syncTimeoutRef.current); // <<--- Cancel previous sync
syncTimeoutRef.current = setTimeout(() => sync(value), 500); syncTimeoutRef.current = setTimeout(() => sync(value), 500);
} }
return ( return (
<input className='Message2Opponent' <input className='Message2Opponent'
placeholder='Message' placeholder='Message'
value={value} disabled={disabled}
value={value || ''}
maxLength={150} maxLength={150}
onChange={e => update(e.target.value)} onChange={(e) => update(e.target.value)}
/> />
) )
} }

View File

@ -1,17 +1,16 @@
import './GameSelector.css'; import './GameSelector.css';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { GamesContext } from '../../../context/games'; import { GamesStateContext, GamesGuideContext } from '../../../context/games';
import { Board, Color, Player } from '../../../components/Checkers'; import { Board, Color, Player } from '../../../components/Checkers';
import Loading from '../../../components/Loading'; import Loading from '../../../components/Loading';
import Counter from '../../../components/Counter'; import Counter from '../../../components/Counter';
export function GameProposalSelector({ onSelect }) { export function GameProposalSelector({ onSelect }) {
const games = useContext(GamesContext); const games = useContext(GamesStateContext);
if (games.gamesList === null) const guide = useContext(GamesGuideContext);
return <Loading />
const isSelected = (uuid) => uuid === games.proposal.selectedUUID; const isSelected = (uuid) => uuid === guide.selectedUUID.proposal;
const onClick = (uuid) => { const onClick = (uuid) => {
if (isSelected(uuid)) if (isSelected(uuid))
@ -20,12 +19,19 @@ export function GameProposalSelector({ onSelect }) {
onSelect(uuid); onSelect(uuid);
} }
const yoursList = games.gamesList.filter(game => game.status === 'GAME_PROPOSAL_WAIT_FOR_YOU') const yoursList = games.filter(game => game.status === 'GAME_PROPOSAL_WAIT_FOR_YOU')
.map(game => <Selectable game={game} key={game.uuid} selected={isSelected(game.uuid)} onClick={onClick} />) .map(game => <Selectable game={game} key={game.uuid} selected={isSelected(game.uuid)} onClick={onClick} />)
const opponentsList = games.gamesList.filter(game => game.status === 'GAME_PROPOSAL_WAIT_FOR_OPPONENT') const opponentsList = games.filter(game => game.status === 'GAME_PROPOSAL_WAIT_FOR_OPPONENT')
.map(game => <Selectable game={game} key={game.uuid} selected={isSelected(game.uuid)} onClick={onClick} />) .map(game => <Selectable game={game} key={game.uuid} selected={isSelected(game.uuid)} onClick={onClick} />)
if (yoursList.length === 0 && opponentsList.length === 0) {
if (guide.isPolling)
return <Loading />
else
return <>There are no pending GameProposals..</>
}
return ( return (
<div className='GameSelector'> <div className='GameSelector'>
{yoursList} {yoursList}
@ -36,11 +42,10 @@ export function GameProposalSelector({ onSelect }) {
} }
export function ActiveGameSelector({ onSelect }) { export function ActiveGameSelector({ onSelect }) {
const games = useContext(GamesContext); const games = useContext(GamesStateContext);
if (games.gamesList === null) const guide = useContext(GamesGuideContext);
return <Loading />
const isSelected = (uuid) => uuid === games.active.selectedUUID; const isSelected = (uuid) => uuid === guide.selectedUUID.active;
const onClick = (uuid) => { const onClick = (uuid) => {
if (isSelected(uuid)) if (isSelected(uuid))
@ -49,12 +54,19 @@ export function ActiveGameSelector({ onSelect }) {
onSelect(uuid); onSelect(uuid);
} }
const yoursList = games.gamesList.filter(game => (game.status === 'GAME_BOARD_WAIT_FOR_YOU' || game.status === 'DRAW_REQUEST_WAIT_FOR_YOU')) const yoursList = games.filter(game => (game.status === 'GAME_BOARD_WAIT_FOR_YOU' || game.status === 'DRAW_REQUEST_WAIT_FOR_YOU'))
.map(game => <Selectable game={game} key={game.uuid} selected={isSelected(game.uuid)} onClick={onClick} />) .map(game => <Selectable game={game} key={game.uuid} selected={isSelected(game.uuid)} onClick={onClick} />)
const opponentsList = games.gamesList.filter(game => (game.status === 'GAME_BOARD_WAIT_FOR_OPPONENT' || game.status === 'DRAW_REQUEST_WAIT_FOR_OPPONENT')) const opponentsList = games.filter(game => (game.status === 'GAME_BOARD_WAIT_FOR_OPPONENT' || game.status === 'DRAW_REQUEST_WAIT_FOR_OPPONENT'))
.map(game => <Selectable game={game} key={game.uuid} selected={isSelected(game.uuid)} onClick={onClick} />) .map(game => <Selectable game={game} key={game.uuid} selected={isSelected(game.uuid)} onClick={onClick} />)
if (yoursList.length === 0 && opponentsList.length === 0) {
if (guide.isPolling)
return <Loading />
else
return <>There are no pending Games..</>
}
return ( return (
<div className='GameSelector'> <div className='GameSelector'>
{yoursList} {yoursList}
@ -65,11 +77,10 @@ export function ActiveGameSelector({ onSelect }) {
} }
export function GameArchiveSelector({ onSelect }) { export function GameArchiveSelector({ onSelect }) {
const games = useContext(GamesContext); const games = useContext(GamesStateContext);
if (games.gamesList === null) const guide = useContext(GamesGuideContext);
return <Loading />
const isSelected = (uuid) => uuid === games.archive.selectedUUID; const isSelected = (uuid) => uuid === guide.selectedUUID.archive;
const onClick = (uuid) => { const onClick = (uuid) => {
if (isSelected(uuid)) if (isSelected(uuid))
@ -78,21 +89,28 @@ export function GameArchiveSelector({ onSelect }) {
onSelect(uuid); onSelect(uuid);
} }
const rejectedList = games.gamesList.filter(game => game.status === 'GAME_PROPOSAL_REJECTED') const rejectedList = games.filter(game => game.status === 'GAME_PROPOSAL_REJECTED')
.map(game => <Selectable game={game} key={game.uuid} selected={isSelected(game.uuid)} onClick={onClick} />) .map(game => <Selectable game={game} key={game.uuid} selected={isSelected(game.uuid)} onClick={onClick} />)
const canceledList = games.gamesList.filter(game => game.status === 'GAME_PROPOSAL_CANCELED') const canceledList = games.filter(game => game.status === 'GAME_PROPOSAL_CANCELED')
.map(game => <Selectable game={game} key={game.uuid} selected={isSelected(game.uuid)} onClick={onClick} />) .map(game => <Selectable game={game} key={game.uuid} selected={isSelected(game.uuid)} onClick={onClick} />)
const victoryList = games.gamesList.filter(game => game.status === 'GAME_RESULT_YOU_WON') const victoryList = games.filter(game => game.status === 'GAME_RESULT_YOU_WON')
.map(game => <Selectable game={game} key={game.uuid} selected={isSelected(game.uuid)} onClick={onClick} />) .map(game => <Selectable game={game} key={game.uuid} selected={isSelected(game.uuid)} onClick={onClick} />)
const defeatList = games.gamesList.filter(game => game.status === 'GAME_RESULT_YOU_LOOSE') const defeatList = games.filter(game => game.status === 'GAME_RESULT_YOU_LOOSE')
.map(game => <Selectable game={game} key={game.uuid} selected={isSelected(game.uuid)} onClick={onClick} />) .map(game => <Selectable game={game} key={game.uuid} selected={isSelected(game.uuid)} onClick={onClick} />)
const drawList = games.gamesList.filter(game => game.status === 'GAME_RESULT_DRAW') const drawList = games.filter(game => game.status === 'GAME_RESULT_DRAW')
.map(game => <Selectable game={game} key={game.uuid} selected={isSelected(game.uuid)} onClick={onClick} />) .map(game => <Selectable game={game} key={game.uuid} selected={isSelected(game.uuid)} onClick={onClick} />)
if (rejectedList.length === 0 && canceledList.length === 0 && victoryList.length === 0 && defeatList.length === 0 && drawList.length === 0) {
if (guide.isPolling)
return <Loading />
else
return <>Finished Games will be shown here..</>
}
return ( return (
<div className='GameSelector'> <div className='GameSelector'>
{rejectedList.length > 0 && <Separator counter={rejectedList.length}>rejected proposals</Separator>} {rejectedList.length > 0 && <Separator counter={rejectedList.length}>rejected proposals</Separator>}
@ -119,9 +137,15 @@ function Selectable({ game, selected, onClick }) {
<div className={'Selectable' + (selected ? ' selected' : '')} <div className={'Selectable' + (selected ? ' selected' : '')}
onClick={() => onClick(game.uuid)} onClick={() => onClick(game.uuid)}
> >
<Board board={game.board} flip={flipBoard} /> <Board
board={game.board || []}
flip={flipBoard}
/>
<div className='Message'> <div className='Message'>
<Player color={opponentColor} name={opponentName} /> <Player
color={opponentColor}
name={opponentName}
/>
<q>{game.message}</q> <q>{game.message}</q>
</div> </div>
</div> </div>

View File

@ -1,19 +1,19 @@
import './NewGame.css' import './NewGame.css'
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { GamesContext } from '../../../context/games'; import { GamesGuideContext } from '../../../context/games';
import DropdownList from '../../../components/DropdownList'; import DropdownList from '../../../components/DropdownList';
import { Color, WhiteStone, BlackStone } from '../../../components/Checkers'; import { Color, WhiteStone, BlackStone } from '../../../components/Checkers';
export default function NewGame({ players, setPlayers }) { export default function NewGame({ players, setPlayers }) {
const games = useContext(GamesContext); const newGame = useContext(GamesGuideContext).newGame;
const [whitePlayer, blackPlayer] = (() => { const [whitePlayer, blackPlayer] = (() => {
if (games.newGame.myColor === Color.white) if (newGame.myColor === Color.white)
return [players.currentUser, games.newGame.opponentName]; return [players.user.name, newGame.opponentName];
if (games.newGame.myColor === Color.black) if (newGame.myColor === Color.black)
return [games.newGame.opponentName, players.currentUser]; return [newGame.opponentName, players.user.name];
return ['', '']; return ['', ''];
})(); // <<-- Execute! })(); // <<-- Execute!
@ -21,25 +21,24 @@ export default function NewGame({ players, setPlayers }) {
/* /*
* Name options * Name options
*/ */
const nameOptions = !players.leaderboard.table const nameOptions = !players.leaderboard ? [<option key='loading' value='…'>loading</option>] :
? [<option key='loading' value='…'>loading</option>] Object.keys(players.leaderboard).map(name =>
: Object.keys(players.leaderboard.table).map(playerName => <option key={name} value={name}>
<option key={playerName} value={playerName}> {players.user.isCurrentUser(name) ? 'You' : name}
{players.isCurrentUser(playerName) ? 'You' : playerName}
</option>) </option>)
const whiteOptions = Array(nameOptions) const whiteOptions = Array(nameOptions);
whiteOptions.push(<option key='default' value=''>{'white player …'}</option>) whiteOptions.push(<option key='default' value=''>{'white player …'}</option>);
const blackOptions = Array(nameOptions) const blackOptions = Array(nameOptions);
blackOptions.push(<option key='default' value=''>{'black player …'}</option>) blackOptions.push(<option key='default' value=''>{'black player …'}</option>);
/* /*
* The Component * The Component
*/ */
const onSelect = (name, myColor) => { const onSelect = (name, myColor) => {
if (players.isCurrentUser(name)) if (players.user.isCurrentUser(name))
setPlayers(games.newGame.opponentName, myColor); setPlayers(newGame.opponentName, myColor);
else else
setPlayers(name, Color.opposite(myColor)); setPlayers(name, Color.opposite(myColor));
} }

View File

@ -1,6 +1,7 @@
import { createContext } from 'react'; import { createContext } from 'react';
export const GamesContext = createContext(null); export const GamesStateContext = createContext(null);
export const GamesGuideContext = createContext(null);
// export const Games = React.createContext({ // export const Games = React.createContext({
// state: initialState, // state: initialState,

View File

@ -8,7 +8,7 @@ import { useState, useRef, useCallback, useEffect, } from "react"
- interval_stop - interval_stop
*/ */
export function usePolling(uri, onSuccess, mode = null) { export function usePolling(uri, { onSuccess, onPolling }, mode = null) {
const [isPolling, setPolling] = useState(false); const [isPolling, setPolling] = useState(false);
const initialPollRef = useRef(true); const initialPollRef = useRef(true);
@ -19,11 +19,16 @@ export function usePolling(uri, onSuccess, mode = null) {
const pollData = useCallback(() => { const pollData = useCallback(() => {
setPolling(true); setPolling(true);
if (onPolling)
onPolling(true);
initialPollRef.current = false; initialPollRef.current = false;
fetch(uri) fetch(uri)
.then((response) => { .then((response) => {
setPolling(false); setPolling(false);
if (onPolling)
onPolling(false);
if (typeof mode?.interval_sec === 'number') { if (typeof mode?.interval_sec === 'number') {
console.log("Schedule", uri, "fetch in", mode.interval_sec, "sec"); console.log("Schedule", uri, "fetch in", mode.interval_sec, "sec");
@ -38,7 +43,7 @@ export function usePolling(uri, onSuccess, mode = null) {
.catch((err) => { .catch((err) => {
console.warn(err.message); console.warn(err.message);
}) })
}, [uri, mode, onSuccess, initialPollRef, intervalTimerIdRef]); }, [uri, mode, onSuccess, onPolling, initialPollRef, intervalTimerIdRef]);
const stopPollInterval = useCallback(() => { const stopPollInterval = useCallback(() => {
console.log("Cancel scheduled fetch for", uri); console.log("Cancel scheduled fetch for", uri);
@ -55,8 +60,6 @@ export function usePolling(uri, onSuccess, mode = null) {
stopPollInterval(); stopPollInterval();
} }
}, [initialPoll, mode, intervalTimerId, isPolling, pollData, stopPollInterval]); }, [initialPoll, mode, intervalTimerId, isPolling, pollData, stopPollInterval]);
return isPolling;
} }
export async function doPushing(uri, method, data, { onSuccess, onPushing }) { export async function doPushing(uri, method, data, { onSuccess, onPushing }) {

View File

@ -2,91 +2,143 @@ import { useReducer } from 'react';
import { nextState } from '../util/StateHelper'; import { nextState } from '../util/StateHelper';
import { defaultBoard } from '../components/Checkers'; import { defaultBoard } from '../components/Checkers';
export const gamesInitialState = { /*
gamesList: null, * State
*/
const gameTemplate = {
status: '',
myColor: '',
opponentName: '',
board: null,
moveNumber: 0,
previousMove: [],
message: '',
uuid: ''
}
const gamesStateTemplate = [/* gameTemplate */];
function gamesStateReducer(state, action) {
switch (action.type) {
case 'next':
return action.games;
case 'add':
return [
...state,
nextState(gameTemplate, action.game, 'Game.create')
];
case 'update':
return state.map((game) =>
game.uuid !== action.game.uuid ? game :
nextState(gameTemplate, action.game, 'Game.update')
);
default:
throw Error('GamesState: unknown action.type', action.type);
}
}
export function useGamesStateReducer() {
return useReducer(gamesStateReducer, gamesStateTemplate);
}
/*
* Guide
*/
export const gamesGuideTemplate = {
newGame: { newGame: {
opponentName: '', opponentName: '',
myColor: '', myColor: '',
board: defaultBoard, board: defaultBoard,
message: '', message: '',
isPushing: false
}, },
proposal: { awaiting: {
selectedUUID: null, proposal: 0,
message: '', active: 0
}, },
active: { selectedUUID: {
selectedUUID: null, proposal: null,
message: '', active: null,
archive: null,
}, },
archive: { UUIDmessage: { // UUIDmessage[uuid]
selectedUUID: null,
}, },
// Network UUIDpushing: { // UUIDpushing[uuid]
isPollingGamesList: false, },
isPushingNewGame: false,
isPushingGameProposalCancel: false, isPolling: false,
isPushingGameProposalReject: false,
isPushingGameProposalAccept: false,
isPushingGameSurrender: false,
isPushingGameDrawRequest: false,
isPushingGameDrawAccept: false,
isPushingGameDrawReject: false,
isPushingGameMove: false,
findGame,
nextGame,
}; };
function reducer(state, action) { function gamesGuideReducer(state, action) {
switch (action.type) { switch (action.type) {
case 'next': case 'next':
return nextState(state, action); return nextState(state, action, 'GamesGuide');
case 'sync':
//console.log('sync');
return {
...state,
awaiting: calcAwating(action.gamesState)
}
case 'nextNewGame': case 'nextNewGame':
return { return {
...state, ...state,
newGame: nextState(state.newGame, action) newGame: nextState(state.newGame, action, 'GamesGuide.newGame')
}; };
case 'nextProposal': case 'selectedUUID':
return { return {
...state, ...state,
proposal: nextState(state.proposal, action) selectedUUID: nextState(state.selectedUUID, action, 'GamesGuide.selectedUUID')
}; };
case 'nextActive': case 'UUIDmessage': {
return { const next = { ...state };
...state, next.UUIDmessage[action.uuid] = action.message;
active: nextState(state.active, action) return next;
}; }
case 'nextArchive': case 'UUIDpushing': {
return { const next = { ...state };
...state, next.UUIDpushing[action.uuid] = action.what
archive: nextState(state.archive, action) return next;
}; }
default: default:
throw Error('GamesReducer: unknown action.type', action.type); throw Error('GamesGuide: unknown action.type: ' + action.type);
} }
} }
export default function useGamesReducer() { //const uuidRegex = new RegExp("^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}");
return useReducer(reducer, gamesInitialState);
export function useGamesGuideReducer() {
return useReducer(gamesGuideReducer, gamesGuideTemplate);
} }
function findGame({ uuid }) { function calcAwating(games) {
return this.gamesList?.find((game) => game.uuid === uuid); return games.reduce((awaiting, game) => {
switch (game.status) {
case 'GAME_PROPOSAL_WAIT_FOR_YOU':
awaiting.proposal++;
return awaiting;
case 'GAME_BOARD_WAIT_FOR_YOU':
case 'DRAW_REQUEST_WAIT_FOR_YOU':
awaiting.active++;
return awaiting;
default:
return awaiting;
} }
}, structuredClone(gamesGuideTemplate.awaiting));
function nextGame(nextGame) {
return this.gamesList?.map((game) => (game.uuid === nextGame?.uuid) ? nextGame : game);
} }

View File

@ -1,24 +1,48 @@
import { useReducer } from 'react'; import { useReducer } from 'react';
import { nextState } from '../util/StateHelper'; import { nextState } from '../util/StateHelper';
const initialState = { /*
table: null, * State
*/
// Network const stateTemplate = {
isPollingTable: false // name : { rank }
// Bobik: {total: 10, victory 5: draw: 1}
}; };
function reducer(state, action) { function stateReducer(state, action) {
switch (action.type) { switch (action.type) {
case 'next': case 'next':
return nextState(state, action); return action.table;
default: default:
throw Error('LeaderboardReducer: unknown action.type', action.type); throw Error('LeaderboardState: unknown action.type ' +action.type);
} }
} }
export default function useLeaderboardReducer() { export function useLeaderboardStateReducer() {
return useReducer(reducer, initialState); return useReducer(stateReducer, stateTemplate);
}
/*
* Guide
*/
const guideTemplate = {
isPolling: false
}
function guideReducer(state, action) {
switch (action.type) {
case 'next':
return nextState(state, action, 'LeaderboardGuide');
default:
throw Error('LeaderboardGuide: unknown action.type ' +action.type);
}
}
export function useLeaderboardGuideReducer() {
return useReducer(guideReducer, guideTemplate);
} }

View File

@ -1,28 +1,31 @@
import { useReducer } from 'react'; import { useReducer } from 'react';
import { localeCompare } from '../util/Locale'; import { localeCompare } from '../util/Locale';
const initialState = { /*
username: '', * State
*/
const stateTemplate = {
name: '',
isCurrentUser: function (otherUsername) { isCurrentUser: function (otherName) {
return localeCompare(this.username, otherUsername) return localeCompare(this.name, otherName) || null; // true -or- null
} }
}; };
function reducer(state, action) { function stateReducer(state, action) {
switch (action.type) { switch (action.type) {
case 'parse': case 'parse':
return { return {
...state, ...state,
username: action.userJson.holdingIdentity.name name: action.userJson.holdingIdentity.name
}; };
default: default:
throw Error('UserReducer: unknown action.type', action.type); throw Error('UserState: unknown action.type ' +action.type);
} }
} }
export default function useUserReducer() { export function useUserStateReducer() {
return useReducer(reducer, initialState); return useReducer(stateReducer, stateTemplate);
} }

View File

@ -1,17 +1,24 @@
export function nextState(state, delta) { export function nextState(state, delta, coment) {
const nextState = { ...state }; const nextState = { ...state };
Object.keys(delta) let logMsg = '';
.slice(1) // skip first property i.e. 'next' Object.keys(delta).forEach(key => {
.forEach(key => { if (key === 'type')
if (Object.hasOwn(nextState, key)) { return;
console.log("next [", key, "] = ", delta[key]);
if (nextState.hasOwnProperty(key)) {
if (coment)
logMsg += '\n ' + key + ': ' + delta[key];
nextState[key] = delta[key]; nextState[key] = delta[key];
} else { } else {
console.warn("nextState: bad action property\n", key + ":", delta[key]); console.warn("nextState: bad action property\n", key + ":", delta[key]);
} }
}) })
if (coment)
console.log('Next', coment, logMsg);
return nextState; return nextState;
} }