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.dao.GameView;
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.User;

View File

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

View File

@ -1,82 +1,101 @@
import { usePolling, doPushing } from '../hook/api';
import { gamesInitialState } from '../reducer/games';
import { Color } from '../components/Checkers';
import { useGamesGuideReducer, useGamesStateReducer, gamesGuideTemplate } from '../reducer/games';
export default function useGamesApi(gamesReducer, config) {
const [games, dispatchGames] = gamesReducer;
export default function useGamesApi() {
const [state, dispatchState] = useGamesStateReducer();
const [guide, dispatchGuide] = useGamesGuideReducer();
const usePollingGamesList = () => {
const onSuccess = (gamesList) => {
dispatchGames({ type: 'next', gamesList });
}
const useGamesPolling = (config) => {
const onPolling = (isPolling) => dispatchGuide({ type: 'next', isPolling });
const onSuccess = (games) => dispatchState({ type: 'next', games });
const isPollingGamesList = usePolling('/api/game', onSuccess, config.intervalMode(30));
if (games.isPollingGamesList !== isPollingGamesList) {
dispatchGames({ type: 'next', isPollingGamesList });
}
usePolling('/api/game', { onPolling, onSuccess }, config.intervalMode(30));
}
return games;
const pushNewGame = ({ opponentName, opponentColor, board, message }) => {
doPushing('/api/gameproposal', 'POST', { opponentName, opponentColor, board, message }, {
onPushing: (isPushing) => dispatchGuide({ type: 'nextNewGame', isPushing }),
onSuccess: (game) => {
dispatchState({ type: 'add', game });
dispatchGuide({ type: 'nextNewGame', ...gamesGuideTemplate.newGame });
}
})
}
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 {
pollGamesList: usePollingGamesList,
pushNewGame: ({ opponentName, myColor, board, message }) => ifNot(games.isPushingNewGame) &&
doPushing('/api/gameproposal', 'POST', { opponentName, opponentColor: Color.opposite(myColor), board, message }, {
onPushing: (isPushingNewGame) => dispatchGames({ type: 'next', isPushingNewGame }),
onSuccess: (game) => dispatchGames({ type: 'next', gamesList: [game, ...games.gamesList], newGame: gamesInitialState.newGame })
}),
pushGameProposalCancel: ({ uuid }) => ifNot(games.isPushingGameProposalCancel) &&
doPushing(`/api/gameproposal/${uuid}/cancel`, 'PUT', null, {
onPushing: (isPushingGameProposalCancel) => dispatchGames({ type: 'next', isPushingGameProposalCancel }),
onSuccess: (canceledGame) => dispatchGames({ type: 'next', gamesList: games.nextGame(canceledGame), proposal: gamesInitialState.proposal })
}),
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 })
}),
state,
guide,
dispatchGuide,
api: {
useGamesPolling,
pushNewGame,
pushGameProposalAccept,
pushGameProposalReject,
pushGameProposalCancel,
pushGameSurrender,
pushGameDrawRequest,
pushGameDrawAccept,
pushGameDrawReject,
pushGameMove
}
}
}
function ifNot(expression) {
return !expression;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,28 +1,33 @@
import React, { useContext } from 'react';
import { GamesContext } from '../../context/games';
import React from 'react';
import { Color } from '../../components/Checkers';
import Wobler from '../../components/Wobler';
import './ActionPanel.css'
/*
* NewGame actoins
*/
export function Create({ onClick }) {
const games = useContext(GamesContext);
export function Create({ getGame, onClick }) {
const [game, isPushing] = getGame();
const hasPlayers = (game.opponentName && game.myColor) ? true : '';
const hasPlayers = games.newGame.opponentName && games.newGame.myColor;
const validateNewGame = () => {
const validate = () => {
if (!hasPlayers)
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 (
<button className={'Create' + (hasPlayers ? ' ready' : '')}
onClick={validateNewGame}
>
<Wobler text="Create" dance={games.isPushingNewGame} />
<button className={'Create' + (hasPlayers && ' ready')} onClick={() => validate()} >
<Wobler text="Create" dance={isPushing} />
</button>
)
}
@ -30,149 +35,165 @@ export function Create({ onClick }) {
/*
* 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 games = useContext(GamesContext);
const validate = () => {
if (!hasGame)
return alert('You have to select pending GameProposal');
const selectedGame = games.findGame({ uuid: games.proposal.selectedUUID });
const isReady = selectedGame?.status === 'GAME_PROPOSAL_WAIT_FOR_YOU' ? true : '';
if (isPushing)
return; // busy
if (selectedGame?.status !== 'GAME_PROPOSAL_WAIT_FOR_OPPONENT')
onClick({ uuid: game.uuid });
}
if (game.status !== 'GAME_PROPOSAL_WAIT_FOR_OPPONENT')
return (
<button className={'Accept' + (isReady && ' ready')}
onClick={() => isReady ? onClick(selectedGame.uuid) : alert('You have to select some GameProposal')}
>
<Wobler text="Accept" dance={games.isPushingGameProposalAccept} />
<button className={'Accept' + (hasGame && ' ready')} onClick={() => validate()}>
<Wobler text="Accept" dance={isPushing === 'accept'} />
</button>
)
}
export function Reject({ onClick }) {
const games = useContext(GamesContext);
export function Reject({ getGame, onClick }) {
const [game, isPushing] = getGame();
const hasGame = (game.status === 'GAME_PROPOSAL_WAIT_FOR_YOU') ? true : '';
const selectedGame = games.findGame({ uuid: games.proposal.selectedUUID });
const isReady = selectedGame?.status === 'GAME_PROPOSAL_WAIT_FOR_YOU' ? true : '';
const validate = () => {
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 (
<button className={'Reject' + (isReady && ' ready')}
onClick={() => isReady ? onClick(selectedGame.uuid) : alert('You have to select some GameProposal')}
>
<Wobler text="Reject" dance={games.isPushingGameProposalReject} />
<button className={'Reject' + (hasGame && ' ready')} onClick={() => validate()} >
<Wobler text="Reject" dance={isPushing === 'reject'} />
</button>
)
}
export function Cancel({ onClick }) {
const games = useContext(GamesContext);
export function Cancel({ getGame, onClick }) {
const [game, isPushing] = getGame();
const selectedGame = games.findGame({ uuid: games.proposal.selectedUUID });
const isReady = selectedGame?.status === 'GAME_PROPOSAL_WAIT_FOR_OPPONENT' ? true : '';
if (game.status !== 'GAME_PROPOSAL_WAIT_FOR_OPPONENT')
return;
if (selectedGame?.status === 'GAME_PROPOSAL_WAIT_FOR_OPPONENT')
return (
<button className={'Cancel' + (isReady && ' ready')}
onClick={() => isReady ? onClick(selectedGame.uuid) : alert('You have to select pending GameProposal')}
>
<Wobler text="Cancel" dance={games.isPushingGameProposalCancel} />
</button>
)
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 });
}
return (
<button className={'Cancel' + (hasGame && ' ready')} onClick={() => validate()} >
<Wobler text="Cancel" dance={isPushing === 'cancel'} />
</button>
)
}
/*
* Game actions
*/
export function Surrender({ getGame, onClick }) {
const [game, isPushing] = getGame();
export function Surrender({ onClick }) {
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')
if (game.status === 'DRAW_REQUEST_WAIT_FOR_YOU' || game.status === 'DRAW_REQUEST_WAIT_FOR_OPPONENT')
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 (
<button className={'Surrender' + (isReady && ' ready')}
onClick={() => isReady ? onClick(selectedGame.uuid) : alert('You have to select a game')}
>
<Wobler text="Surrender" dance={games.isPushingGameSurrender} />
</button>
<button className={'Surrender' + (hasGame && ' ready')} onClick={() => validate()} >
<Wobler text="Surrender" dance={isPushing === 'surrender'} />
</ button>
)
}
/*
* Game actions: Draw
*/
export function DrawRequest({ getGame, onClick }) {
const [game, isPushing] = getGame();
export function DrawRequest({ onClick }) {
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')
if (game.status === 'DRAW_REQUEST_WAIT_FOR_YOU')
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 (
<button className={'Draw' + (isReady && ' ready')} onClick={() => checkStatus()} >
<Wobler text="Draw?" dance={games.isPushingGameDrawRequest} />
<button className={'Draw' + (hasGame && ' ready')} onClick={() => validate()} >
<Wobler text="Draw?" dance={isPushing === 'draw_request'} />
</button >
)
}
export function DrawAccept({ onClick }) {
const games = useContext(GamesContext);
export function DrawAccept({ getGame, onClick }) {
const [game, isPushing] = getGame();
const selectedGame = games.findGame({ uuid: games.active.selectedUUID });
const gameStatus = selectedGame?.status;
if (game.status !== 'DRAW_REQUEST_WAIT_FOR_YOU')
return;
if (gameStatus === 'DRAW_REQUEST_WAIT_FOR_YOU')
return (
<button className='Draw accept' onClick={() => onClick(selectedGame.uuid)} >
<Wobler text="Draw accept" dance={games.isPushingGameDrawAccept} />
</button>
)
return (
<button className='Draw accept' onClick={() => onClick({ uuid: game.uuid })} >
<Wobler text="Draw accept" dance={isPushing === 'draw_accept'} />
</button>
)
}
export function DrawReject({ onClick }) {
const games = useContext(GamesContext);
export function DrawReject({ getGame, onClick }) {
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 (
<button className='Draw reject' onClick={() => onClick(selectedGame.uuid)} >
<Wobler text="Reject" dance={games.isPushingGameDrawReject} />
</button>
)
return (
<button className='Draw reject' onClick={() => onClick({ uuid: game.uuid })}>
<Wobler text="Reject" dance={isPushing === 'draw_reject'} />
</button>
)
}
/*
* GameArchive actions
*/
export function Backward() {
return <button className='Backward' disabled>Backward</button>
}
export function Forward() {
return <button className='Forward' disabled>Forward</button>
}

View File

@ -1,12 +1,10 @@
import React, { useContext } from 'react';
import { useLocation, matchPath } from 'react-router-dom';
import { GamesContext } from '../../context/games';
import React from 'react';
import { Color, Player, Board } from '../../components/Checkers';
import './GameBoard.css';
export default function GameBoard({ username, onStoneClick, onStoneMove }) {
const game = useSelectedGame() || {};
export default function GameBoard({ username, getGame, onStoneClick, onStoneMove }) {
const [game, isPushing] = getGame();
const myName = game.opponentName ? username : '';
const opponentColor = Color.opposite(game.myColor);
@ -15,37 +13,26 @@ export default function GameBoard({ username, onStoneClick, onStoneMove }) {
const optionalOnStoneClick = (typeof onStoneClick !== 'function') ? null :
(cellId) => onStoneClick(game.uuid, cellId);
const optionalOnStoneMove = (typeof onStoneMove !== 'function') ? null :
const optionalOnStoneMove = (typeof onStoneMove !== 'function' || isPushing) ? null :
(move) => onStoneMove(game.uuid, move);
return (
<div className='GameBoard'>
<Player color={opponentColor || Color.black} name={game.opponentName} />
<Board board={game.board} flip={flipBoard}
<Player
color={opponentColor || Color.black}
name={game.opponentName}
/>
<Board
board={game.board || []}
flip={flipBoard}
onStoneClick={optionalOnStoneClick}
onStoneMove={optionalOnStoneMove}
/>
<Player color={game.myColor || Color.white} name={myName} />
{game.isPushingGameMove ? <span>Moving...</span> : null /* TODO: isPushing shall be stored per game. curernty it is global indicator */}
<Player
color={game.myColor || Color.white}
name={myName}
/>
{(isPushing === 'move') ? <span>Moving...</span> : null}
</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 { useLocation, matchPath } from 'react-router-dom';
import { GamesContext } from '../../context/games';
import React, { useRef, useState } from 'react';
export default function Message2Opponent({ dispatchGames }) {
const games = useContext(GamesContext);
const { pathname } = useLocation();
export default function Message2Opponent({ getMessage, setMessage }) {
const [value, setValue] = useState('');
const syncTimeoutRef = useRef(null);
if (matchPath('/games/archive', pathname))
return; // Shhh.. no chatting in the archives!
if (matchPath('/games/proposal', pathname))
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 message = getMessage();
if (value !== message && syncTimeoutRef.current === null) {
// Absorb external value ONLY if there is no scheduled sync
setValue(message);
}
const disabled = message === undefined;
/* --- */
const sync = (message) => {
const sync = (nextMessage) => {
syncTimeoutRef.current = null;
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');
setMessage(nextMessage);
}
const update = (value) => {
setValue(value);
if (syncTimeoutRef.current)
clearTimeout(syncTimeoutRef.current); // <<--- Cancel previous sync
clearTimeout(syncTimeoutRef.current); // <<--- Cancel previous sync
syncTimeoutRef.current = setTimeout(() => sync(value), 500);
}
return (
<input className='Message2Opponent'
placeholder='Message'
value={value}
disabled={disabled}
value={value || ''}
maxLength={150}
onChange={e => update(e.target.value)}
onChange={(e) => update(e.target.value)}
/>
)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -2,91 +2,143 @@ import { useReducer } from 'react';
import { nextState } from '../util/StateHelper';
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: {
opponentName: '',
myColor: '',
board: defaultBoard,
message: '',
isPushing: false
},
proposal: {
selectedUUID: null,
message: '',
awaiting: {
proposal: 0,
active: 0
},
active: {
selectedUUID: null,
message: '',
selectedUUID: {
proposal: null,
active: null,
archive: null,
},
archive: {
selectedUUID: null,
UUIDmessage: { // UUIDmessage[uuid]
},
// Network
isPollingGamesList: false,
isPushingNewGame: false,
UUIDpushing: { // UUIDpushing[uuid]
},
isPushingGameProposalCancel: false,
isPushingGameProposalReject: false,
isPushingGameProposalAccept: false,
isPushingGameSurrender: false,
isPushingGameDrawRequest: false,
isPushingGameDrawAccept: false,
isPushingGameDrawReject: false,
isPushingGameMove: false,
findGame,
nextGame,
isPolling: false,
};
function reducer(state, action) {
function gamesGuideReducer(state, action) {
switch (action.type) {
case 'next':
return nextState(state, action);
return nextState(state, action, 'GamesGuide');
case 'sync':
//console.log('sync');
return {
...state,
awaiting: calcAwating(action.gamesState)
}
case 'nextNewGame':
return {
...state,
newGame: nextState(state.newGame, action)
newGame: nextState(state.newGame, action, 'GamesGuide.newGame')
};
case 'nextProposal':
case 'selectedUUID':
return {
...state,
proposal: nextState(state.proposal, action)
selectedUUID: nextState(state.selectedUUID, action, 'GamesGuide.selectedUUID')
};
case 'nextActive':
return {
...state,
active: nextState(state.active, action)
};
case 'UUIDmessage': {
const next = { ...state };
next.UUIDmessage[action.uuid] = action.message;
return next;
}
case 'nextArchive':
return {
...state,
archive: nextState(state.archive, action)
};
case 'UUIDpushing': {
const next = { ...state };
next.UUIDpushing[action.uuid] = action.what
return next;
}
default:
throw Error('GamesReducer: unknown action.type', action.type);
throw Error('GamesGuide: unknown action.type: ' + action.type);
}
}
export default function useGamesReducer() {
return useReducer(reducer, gamesInitialState);
//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}");
export function useGamesGuideReducer() {
return useReducer(gamesGuideReducer, gamesGuideTemplate);
}
function findGame({ uuid }) {
return this.gamesList?.find((game) => game.uuid === uuid);
}
function nextGame(nextGame) {
return this.gamesList?.map((game) => (game.uuid === nextGame?.uuid) ? nextGame : game);
function calcAwating(games) {
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));
}

View File

@ -1,24 +1,48 @@
import { useReducer } from 'react';
import { nextState } from '../util/StateHelper';
const initialState = {
table: null,
// Network
isPollingTable: false
/*
* State
*/
const stateTemplate = {
// name : { rank }
// Bobik: {total: 10, victory 5: draw: 1}
};
function reducer(state, action) {
function stateReducer(state, action) {
switch (action.type) {
case 'next':
return nextState(state, action);
return action.table;
default:
throw Error('LeaderboardReducer: unknown action.type', action.type);
throw Error('LeaderboardState: unknown action.type ' +action.type);
}
}
export default function useLeaderboardReducer() {
return useReducer(reducer, initialState);
export function useLeaderboardStateReducer() {
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 { localeCompare } from '../util/Locale';
const initialState = {
username: '',
/*
* State
*/
const stateTemplate = {
name: '',
isCurrentUser: function (otherUsername) {
return localeCompare(this.username, otherUsername)
isCurrentUser: function (otherName) {
return localeCompare(this.name, otherName) || null; // true -or- null
}
};
function reducer(state, action) {
function stateReducer(state, action) {
switch (action.type) {
case 'parse':
return {
...state,
username: action.userJson.holdingIdentity.name
name: action.userJson.holdingIdentity.name
};
default:
throw Error('UserReducer: unknown action.type', action.type);
throw Error('UserState: unknown action.type ' +action.type);
}
}
export default function useUserReducer() {
return useReducer(reducer, initialState);
export function useUserStateReducer() {
return useReducer(stateReducer, stateTemplate);
}

View File

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