From ae6ba413a2d71de5ce85cd2fb421b2fd2416a3ff Mon Sep 17 00:00:00 2001 From: djmil Date: Sun, 19 Nov 2023 12:10:26 +0100 Subject: [PATCH] movable stones! --- webapp/src/api/games.js | 22 +-- webapp/src/components/Checkers.css | 5 + webapp/src/components/Checkers.jsx | 167 ++++++++++++++--------- webapp/src/container/Games.jsx | 28 +++- webapp/src/container/games/GameBoard.css | 5 +- webapp/src/container/games/GameBoard.jsx | 49 ++----- webapp/src/reducer/games.js | 9 +- webapp/src/util/StateHelper.js | 10 +- 8 files changed, 169 insertions(+), 126 deletions(-) diff --git a/webapp/src/api/games.js b/webapp/src/api/games.js index dc8ee8c..ff93fa7 100644 --- a/webapp/src/api/games.js +++ b/webapp/src/api/games.js @@ -30,43 +30,49 @@ export default function useGamesApi(gamesReducer, config) { 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.nextGameList(canceledGame), proposal: gamesInitialState.proposal }) + 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.nextGameList(rejectedGame), proposal: gamesInitialState.proposal }) + 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.nextGameList(acceptedGame), proposal: gamesInitialState.proposal }) + 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.nextGameList(finishedGame), proposal: gamesInitialState.active }) + 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.nextGameList(drawReqGame), proposal: gamesInitialState.active }) + 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.nextGameList(drawAccGame), proposal: gamesInitialState.active }) + 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.nextGameList(drawRejGame), proposal: gamesInitialState.active }) + 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', null, { + onPushing: (isPushingGameMove) => dispatchGames({ type: 'next', isPushingGameMove }), + onSuccess: (game) => dispatchGames({ type: 'next', gamesList: games.nextGame(game), active: gamesInitialState.active }) }), } } diff --git a/webapp/src/components/Checkers.css b/webapp/src/components/Checkers.css index 99a8455..aca0459 100644 --- a/webapp/src/components/Checkers.css +++ b/webapp/src/components/Checkers.css @@ -1,6 +1,7 @@ .Stone { cursor: default; /* disable 'I beam' cursor change */ user-select: none; + pointer-events: none; } .Board { @@ -26,4 +27,8 @@ .Tile.white { background: white; +} + +.Tile.selected { + color: grey; } \ No newline at end of file diff --git a/webapp/src/components/Checkers.jsx b/webapp/src/components/Checkers.jsx index d7dddf3..25c73d4 100644 --- a/webapp/src/components/Checkers.jsx +++ b/webapp/src/components/Checkers.jsx @@ -1,5 +1,5 @@ import './Checkers.css' -import React from 'react' +import React, { useState } from 'react' export const Color = { white: "WHITE", @@ -18,13 +18,19 @@ export const Color = { /* * Stone */ -export function Stone({ color, type }) { +export function Stone({ color, type, move }) { + const style = !move ? null : { + position: 'absolute', + left: move[0], + top: move[1], + } + switch (color) { case Color.white: - return ; + return ; case Color.black: - return ; + return ; case '': case undefined: @@ -32,22 +38,22 @@ export function Stone({ color, type }) { return; // no stone :) default: - console.warn("Unknown color: ", color) + console.warn('Unknown color', color) } } -export function WhiteStone({ type }) { +export function WhiteStone({ type, style }) { if (type === 'KING') - return + return else - return + return } -export function BlackStone({ type }) { +export function BlackStone({ type, style }) { if (type === 'KING') - return + return else - return + return } /* @@ -65,75 +71,102 @@ export function Player({ color, name }) { /* * Board */ -export function Board({ game, onClick }) { - const board = (game && game.board && typeof game.board === 'object') ? game.board : defaultBoard; +export function Board({ game, onStoneClick, onStoneMove }) { + const [[moveId, moveX, moveY], setMove] = useState([0, 0, 0]); - const BlackTile = () => { - return
- } + const board = (game && game.board && typeof game.board === 'object') ? game.board : defaultBoard; + const isInteractive = (typeof onStoneClick === 'function' || typeof onStoneMove === 'function') ? ' interactive' : ''; const WhiteTile = ({ id }) => { const stone = board[id]; - const isInteractive = (typeof onClick === 'function') ? ' interactive' : ''; return ( -
isInteractive && onClick(id)}> +
onStoneClick(game.uuid, id) + } + + onMouseDown={!onStoneMove || !stone ? null : + (e) => setMove([id, e.clientX, e.clientY]) + } + + onMouseUp={!onStoneMove || !moveId ? null : + () => { onStoneMove(game.uuid, [moveId, id]); setMove([0, 0, 0]) } + } + >
); } - return
-
- - - - -
-
- - - - -
-
- - - - -
-
- - - - -
+ const BlackTile = () => { + return
+ } -
- - - - + const movingStone = board[moveId]; + + return ( +
e.buttons ? setMove([moveId, e.clientX, e.clientY]) : setMove([0, 0, 0]) + } + > + + {!movingStone ? null : + + } + +
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+ +
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
-
- - - - -
-
- - - - -
-
- - - - -
-
+ ) } const blackMan = { color: Color.black, type: 'MAN' }; diff --git a/webapp/src/container/Games.jsx b/webapp/src/container/Games.jsx index 5ec55f0..472e172 100644 --- a/webapp/src/container/Games.jsx +++ b/webapp/src/container/Games.jsx @@ -6,6 +6,7 @@ import NewGame from './games/view/NewGame'; import { GameProposalSelector, ActiveGameSelector, GameArchiveSelector } from './games/view/GameSelector'; import { Create, Accept, Reject, Cancel, DrawRequest, DrawAccept, DrawReject, Surrender, Backward, Forward } from './games/ActionPanel'; import GameBoard from './games/GameBoard'; +import { nextStone } from '../components/Checkers'; import Message2Opponent from './games/Message2Opponent'; import Counter from '../components/Counter'; @@ -26,11 +27,7 @@ export default function Games({ context: { gamesReducer, gamesApi }, players })
{/* */} - dispatchGames({ type: 'nextNewGame', board })} - onActiveGameMove={(uuid, from, to) => console.log(uuid, 'move', from, '->', to)} - /> +
@@ -113,6 +110,27 @@ function ActionPanel({ gamesApi }) { ) } +function GameBoardRoutes({ gamesReducer, gamesApi, username }) { + const [games, dispatchGames] = gamesReducer; + + const onStoneClick = (uuid, cellId) => { + let board = { ...games.newGame.board }; + board[cellId] = nextStone(board[cellId]); + dispatchGames({ type: 'nextNewGame', board }); + } + + const onStoneMove = (uuid, move) => console.log(uuid, 'move', move); + + return ( + + } /> + } /> + } /> + } /> + + ) +} + function countGames(gamesList) { var awaiting = { diff --git a/webapp/src/container/games/GameBoard.css b/webapp/src/container/games/GameBoard.css index 2c90743..85bd13c 100644 --- a/webapp/src/container/games/GameBoard.css +++ b/webapp/src/container/games/GameBoard.css @@ -6,7 +6,6 @@ } .GameBoard .Tile { - font-size: 200%; line-height: 34px; height: 34px; width: 34px; @@ -14,6 +13,10 @@ margin-top: -1px; } +.GameBoard .Board .Stone { + font-size: 200%; +} + .GameBoard .Tile.white.interactive:hover { background-color:azure; } diff --git a/webapp/src/container/games/GameBoard.jsx b/webapp/src/container/games/GameBoard.jsx index bd3ecb7..e1e1ffd 100644 --- a/webapp/src/container/games/GameBoard.jsx +++ b/webapp/src/container/games/GameBoard.jsx @@ -1,58 +1,37 @@ -import React, { useContext, useState } from 'react'; +import React, { useContext } from 'react'; import { useLocation, matchPath } from 'react-router-dom'; import { GamesContext } from '../../context/games'; -import { Color, Player, Board, nextStone } from '../../components/Checkers'; +import { Color, Player, Board } from '../../components/Checkers'; import './GameBoard.css'; -export default function GameBoard({ username, onNewGameBoardStone, onActiveGameMove }) { +export default function GameBoard({ username, onStoneClick, onStoneMove }) { const games = useContext(GamesContext); const { pathname } = useLocation(); - const [startId, setStartId] = useState(null); - const onClick_NewGame = (cellId) => { - let nextBoard = { ...games.newGame.board }; - nextBoard[cellId] = nextStone(nextBoard[cellId]); - - onNewGameBoardStone(nextBoard); - } - - const onClick_ActiveGame = (cellId) => { - if (startId === cellId) - return setStartId(null); - - if (startId === null) - return setStartId(cellId); - - if (game?.uuid) - onActiveGameMove(game.uuid, startId, cellId); - - setStartId(null); - }; - - const [game, onClick] = (() => { + const game = (() => { if (matchPath('/games/new', pathname)) - return [games.newGame, onClick_NewGame]; + return games.newGame; if (matchPath('/games/proposal', pathname)) - return [games.findGame({ uuid: games.proposal.selectedUUID }), null]; - else if (matchPath('/games/active', pathname)) - return [games.findGame({ uuid: games.active.selectedUUID }), onClick_ActiveGame]; - else if (matchPath('/games/archive', pathname)) - return [games.findGame({ uuid: games.archive.selectedUUID }), null]; + return games.findGame({ uuid: games.proposal.selectedUUID }); - return [{}, null]; + 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 {}; })(); // <<-- Execute const opponentColor = Color.opposite(game?.myColor); const [opponentName, myName] = game?.opponentName ? [game.opponentName, username] : ['', '']; - const optionalOnClick = (onClick && game?.board) ? (id) => onClick(id) : null; - return (
- +
) diff --git a/webapp/src/reducer/games.js b/webapp/src/reducer/games.js index 4fb7eca..6d79c0a 100644 --- a/webapp/src/reducer/games.js +++ b/webapp/src/reducer/games.js @@ -38,9 +38,10 @@ export const gamesInitialState = { isPushingGameDrawRequest: false, isPushingGameDrawAccept: false, isPushingGameDrawReject: false, + isPushingGameMove: false, findGame, - nextGameList, + nextGame, }; function reducer(state, action) { @@ -86,8 +87,6 @@ function findGame({ uuid }) { return this.gamesList?.find((game) => game.uuid === uuid); } -function nextGameList(nextGame) { - return this.gamesList?.map( - (game) => (nextGame?.uuid === game.uuid) ? nextGame : game - ); +function nextGame(nextGame) { + return this.gamesList?.map((game) => (game.uuid === nextGame?.uuid) ? nextGame : game); } \ No newline at end of file diff --git a/webapp/src/util/StateHelper.js b/webapp/src/util/StateHelper.js index 1cda2df..4262349 100644 --- a/webapp/src/util/StateHelper.js +++ b/webapp/src/util/StateHelper.js @@ -1,14 +1,14 @@ -export function nextState(state, action) { +export function nextState(state, delta) { const nextState = { ...state }; - Object.keys(action) + Object.keys(delta) .slice(1) // skip first property i.e. 'next' .forEach(key => { if (Object.hasOwn(nextState, key)) { - console.log("next [", key, "] = ", action[key]); - nextState[key] = action[key]; + console.log("next [", key, "] = ", delta[key]); + nextState[key] = delta[key]; } else { - console.warn("nextState: bad action property\n", key + ":", action[key]); + console.warn("nextState: bad action property\n", key + ":", delta[key]); } })