From bd21116e83c2f50f095587ba13e492424595b389 Mon Sep 17 00:00:00 2001 From: Neel Kapse Date: Wed, 17 May 2023 21:49:31 -0700 Subject: [PATCH 1/5] WIP --- backend/src/extractors/auth.rs | 2 +- .../components/game-online/GameOnline.tsx | 10 +- js/frontend/pages/game/[gameid].tsx | 9 +- js/frontend/state/game-online/game.ts | 4 +- js/frontend/state/game-online/selectors.ts | 27 ++-- js/frontend/state/game-online/slice.ts | 19 +++ js/hive-db/src/PlayerProvider.tsx | 46 ++++-- js/hive-db/src/api.ts | 44 +++-- js/hive-db/src/db/playGameMoves.ts | 14 +- js/hive-db/src/game/game.ts | 153 +++++++++++++++++- 10 files changed, 271 insertions(+), 57 deletions(-) diff --git a/backend/src/extractors/auth.rs b/backend/src/extractors/auth.rs index a797342..e7a102c 100644 --- a/backend/src/extractors/auth.rs +++ b/backend/src/extractors/auth.rs @@ -57,7 +57,7 @@ impl FromRequest for AuthenticatedUser { .expect("couldn't retrieve server config"); let auth_token = req_clone .headers() - .get("x-authentication") + .get("x-authorization") .ok_or(AuthenticationError::MissingToken)? .to_str() .map_err(|err| { diff --git a/js/frontend/components/game-online/GameOnline.tsx b/js/frontend/components/game-online/GameOnline.tsx index 7ac7395..f89d241 100644 --- a/js/frontend/components/game-online/GameOnline.tsx +++ b/js/frontend/components/game-online/GameOnline.tsx @@ -1,6 +1,6 @@ import { Game } from 'hive-db'; import { HexCoordinate, TileId } from 'hive-lib'; -import { useCallback } from 'react'; +import { useCallback, useEffect } from 'react'; import { useGameDispatch, useGameSelector @@ -14,9 +14,11 @@ import { import { ghostClicked, tableClicked, - tableStackClicked + tableStackClicked, + authTokenAdded } from '../../state/game-online/slice'; import { Table } from '../game-common/Table'; +import { usePlayer } from '../../../hive-db/src/PlayerProvider'; const GameOnline = ({ uid, game }: { uid: string | null; game: Game }) => { const hexSize = 50; @@ -26,6 +28,10 @@ const GameOnline = ({ uid, game }: { uid: string | null; game: Game }) => { const ghosts = useGameSelector(selectValidMovesForTile); const selectedTileId = useGameSelector(selectSelectedTileId); const boardCentered = useGameSelector(selectBoardCentered); + const { authToken } = usePlayer(); + + // TODO: Neel: probably not the right place to do this + useEffect(() => dispatch(authTokenAdded(authToken))); const onClickTable = useCallback(() => dispatch(tableClicked()), [dispatch]); const onClickTableStack = useCallback( diff --git a/js/frontend/pages/game/[gameid].tsx b/js/frontend/pages/game/[gameid].tsx index 34295cf..d54d07e 100644 --- a/js/frontend/pages/game/[gameid].tsx +++ b/js/frontend/pages/game/[gameid].tsx @@ -55,11 +55,18 @@ const GameView = ({ uid, game }: { uid: string | null; game: Game }) => { const Game = () => { const router = useRouter(); - const { user } = usePlayer(); + const { user, activeGames } = usePlayer(); const { gameid } = router.query; const [game, setGame] = useState(); const title = useTitle(); + useEffect(() => { + // TODO: should this be a strict type check while enforcing that gameid is a string? + setGame(activeGames.find((g) => g.gid == gameid)); + }, [activeGames, gameid]); + + console.log(game); + return ( <> diff --git a/js/frontend/state/game-online/game.ts b/js/frontend/state/game-online/game.ts index 71df927..a32069e 100644 --- a/js/frontend/state/game-online/game.ts +++ b/js/frontend/state/game-online/game.ts @@ -5,7 +5,7 @@ export interface GameProps { // the uid of the player viewing the game uid: string | null; // the JWT authentication token - authToken: string | null, + authToken: string | null; // a string indicating the last time the user requested the board to be centered boardCentered: string; // the game data @@ -13,7 +13,7 @@ export interface GameProps { // a flag to indicate that the game data has been updated but updates have not been viewed newMovesToView: boolean; // a list of valid next moves - validNextMoves: Move[], + validNextMoves: Move[]; // the proposed move that the player is currently viewing proposedMove: Move | null; // the coordinate of the proposed move diff --git a/js/frontend/state/game-online/selectors.ts b/js/frontend/state/game-online/selectors.ts index 8ad6405..454a7d9 100644 --- a/js/frontend/state/game-online/selectors.ts +++ b/js/frontend/state/game-online/selectors.ts @@ -39,7 +39,8 @@ export const selectGame = (state: GameState): Game | null => state.game; /** * Get the set of valid next moves */ -export const selectValidMoves = (state: GameState): Move[] | null => state.validNextMoves; +export const selectValidMoves = (state: GameState): Move[] | null => + state.validNextMoves; /** * Get the id of the currently selected tile. @@ -206,23 +207,23 @@ export const selectValidMovesForTile = createSelector( selectGameBoard, selectIsViewingHistory, selectColorTurn, - selectSelectedTileId, + selectSelectedTileId ], - ( - moves, - game, - board, - isHistory, - player, - selected, - ): HexCoordinate[] => { + (moves, game, board, isHistory, player, selected): HexCoordinate[] => { if (!game || isHistory || !selected || !player) return []; - return moves.filter(move => move.tileId === selected) - .map(move => { + if (game.state.moveCount === 0 && player === 'w') { + return [{ r: 0, q: 0 }]; + } + return moves + .filter((move) => move.tileId === selected) + .map((move) => { if (move.refId === 'x') { return { r: 0, q: 0 }; } else { - return relativeHexCoordinate(findTileCoordinate(board, move.refId), move.dir) + return relativeHexCoordinate( + findTileCoordinate(board, move.refId), + move.dir + ); } }); } diff --git a/js/frontend/state/game-online/slice.ts b/js/frontend/state/game-online/slice.ts index b03a94b..709804a 100644 --- a/js/frontend/state/game-online/slice.ts +++ b/js/frontend/state/game-online/slice.ts @@ -18,6 +18,9 @@ const slice = createSlice({ name: 'game', initialState, reducers: { + authTokenAdded(state, action: PayloadAction) { + state.authToken = action.payload; + }, boardCentered(state) { state.boardCentered = new Date().toJSON(); }, @@ -52,6 +55,21 @@ const slice = createSlice({ state.proposedMoveCoordinate = coordinate; state.upTo = -1; } + + // TODO: Neel: this should probably be a separate dispatch action, and probably shouldn't be spawned by a ghost click + if (game.state.moveCount === 0) { + // The first move is being played + state.selectedTileId = null; + console.log(state.authToken); + playGameMove(game, state.proposedMove, state.authToken) + .then(({ game, validNextMoves }) => { + state.game = game; + state.validNextMoves = validNextMoves; + }) + .catch((error) => { + console.error(error); + }); + } }, firstMoveClicked(state) { state.upTo = 0; @@ -144,6 +162,7 @@ const slice = createSlice({ }); export const { + authTokenAdded, boardCentered, firstMoveClicked, gameChanged, diff --git a/js/hive-db/src/PlayerProvider.tsx b/js/hive-db/src/PlayerProvider.tsx index e589a5f..9924581 100644 --- a/js/hive-db/src/PlayerProvider.tsx +++ b/js/hive-db/src/PlayerProvider.tsx @@ -1,4 +1,8 @@ -import { getAuth, User as FirebaseUser, onAuthStateChanged } from '@firebase/auth'; +import { + getAuth, + User as FirebaseUser, + onAuthStateChanged +} from '@firebase/auth'; import app from './db/app'; import { createContext, @@ -14,7 +18,14 @@ import { signInAnonymously, signOut } from 'firebase/auth'; -import { Game, getGameIsEnded, getGameIsStarted, getUserGames } from './game/game'; +import { + BackendGame, + Game, + getGameIsEnded, + getGameIsStarted, + getUserGames, + newGameFromBackendGame +} from './game/game'; import { createGuestUser, createUser, getUser } from '..'; export interface PlayerContextProps { @@ -63,15 +74,18 @@ function usePlayerState(): PlayerContextProps { */ useEffect(() => { if (user === null) return; - getUserGames(user) - .then((games: Game[]) => { - const activeGames = games.filter( - (game) => getGameIsStarted(game) && !getGameIsEnded(game) - ); - const completedGames = games.filter((game) => getGameIsEnded(game)); - setActiveGames(activeGames); - setCompletedGames(completedGames); - }); + getUserGames(user).then((backendGames: BackendGame[]) => { + const games = backendGames.map((backendGame) => + newGameFromBackendGame(backendGame) + ); + + const activeGames = games.filter( + (game) => getGameIsStarted(game) && !getGameIsEnded(game) + ); + const completedGames = games.filter((game) => getGameIsEnded(game)); + setActiveGames(activeGames); + setCompletedGames(completedGames); + }); }, [user]); async function usernameChanged(username: string) { @@ -110,11 +124,11 @@ function usePlayerState(): PlayerContextProps { useEffect(() => { return onAuthStateChanged(auth, setFirebaseUser); - }, []) + }, []); useEffect(() => { handleFirebaseUserChanged(); - }, [firebaseUser]) + }, [firebaseUser]); /** * Sign in using Google. @@ -132,7 +146,7 @@ function usePlayerState(): PlayerContextProps { */ const signInAsGuest = async () => { await signInAnonymously(auth); - } + }; /** * Sign out the current user and optionally redirect to a page. @@ -146,7 +160,9 @@ function usePlayerState(): PlayerContextProps { setIncompleteProfile(false); setActiveGames([]); setCompletedGames([]); - if (redirect) { /* router.push(redirect) */ } + if (redirect) { + /* router.push(redirect) */ + } }) .catch((error) => { console.error(error); diff --git a/js/hive-db/src/api.ts b/js/hive-db/src/api.ts index db8246d..1cff340 100644 --- a/js/hive-db/src/api.ts +++ b/js/hive-db/src/api.ts @@ -1,20 +1,35 @@ -export async function postJSON(uri: string, body: any, authToken: string | null = null): Promise { - const res = await jsonReq(uri, { method: 'POST', body: JSON.stringify(body) }, authToken); +export async function postJSON( + uri: string, + body: any, + authToken: string | null = null +): Promise { + const res = await jsonReq( + uri, + { method: 'POST', body: JSON.stringify(body) }, + authToken + ); if (res.ok) { - return await res.json() as T; + return (await res.json()) as T; } else { - throw new Error(`non-successful status code for POST ${uri}: ${res.statusText}`); + throw new Error( + `non-successful status code for POST ${uri}: ${res.statusText}` + ); } } -export async function getJSON(uri: string, authToken: string | null = null): Promise { +export async function getJSON( + uri: string, + authToken: string | null = null +): Promise { const res = await jsonReq(uri, { method: 'GET' }, authToken); if (res.ok) { - return await res.json() as T; + return (await res.json()) as T; } else if (res.status === 404) { return null; } else { - throw new Error(`non-successful status code for GET ${uri}: ${res.statusText}`); + throw new Error( + `non-successful status code for GET ${uri}: ${res.statusText}` + ); } } @@ -22,12 +37,16 @@ function setAuthHeader(options: any, authToken: string) { if (!options.headers) { options.headers = {}; } - options.headers['X-Authentication'] = authToken; + options.headers['X-Authorization'] = authToken; } -async function jsonReq(uri: string, options: any, authToken: string | null): Promise { +async function jsonReq( + uri: string, + options: any, + authToken: string | null +): Promise { options.headers = { - 'Content-Type': 'application/json', + 'Content-Type': 'application/json' }; if (authToken) { setAuthHeader(options, authToken); @@ -35,7 +54,10 @@ async function jsonReq(uri: string, options: any, authToken: string | null): Pro return fetch(uri, options); } -export async function deleteReq(uri: string, authToken: string): Promise { +export async function deleteReq( + uri: string, + authToken: string +): Promise { const options = { method: 'DELETE' }; setAuthHeader(options, authToken); return fetch(uri, options); diff --git a/js/hive-db/src/db/playGameMoves.ts b/js/hive-db/src/db/playGameMoves.ts index 6e88122..9100d2c 100644 --- a/js/hive-db/src/db/playGameMoves.ts +++ b/js/hive-db/src/db/playGameMoves.ts @@ -3,8 +3,8 @@ import { Move } from 'hive-lib'; import { postJSON } from '../api'; interface GameMoveResponse { - game: Game, - validNextMoves: Move[], + game: Game; + validNextMoves: Move[]; } /** @@ -19,7 +19,11 @@ interface GameMoveResponse { * @param game The game to update. * @param moves An ordered list of moves to play. */ -export function playGameMove(game: Game, move: Move, authToken: string): Promise { - const uri = `/api/board/${game.gid}/move/${move.notation}`; - return postJSON(uri, {}, authToken); +export function playGameMove( + game: Game, + move: Move, + authToken: string +): Promise { + const uri = `/api/game/${game.gid}/play`; + return postJSON(uri, { Turn: ['.', '${move.notation}'] }, authToken); } diff --git a/js/hive-db/src/game/game.ts b/js/hive-db/src/game/game.ts index 62ebc3f..6288bd7 100644 --- a/js/hive-db/src/game/game.ts +++ b/js/hive-db/src/game/game.ts @@ -11,6 +11,20 @@ import { newGameMeta, newGameMetaWithFieldValues } from './meta'; import { newGameState } from './state'; import { getJSON } from '../api'; +// TODO: move this to the right place +export interface BackendGame { + black_uid: string; + white_uid: string; + game_control_history: string; + game_status: string; + game_type: string; + history: string; + id: string; + ranked: boolean; + tournament_queen_rule: boolean; + turn: number; +} + export interface Game { gid: string; meta: GameMeta; @@ -42,6 +56,132 @@ export function newGame( }; } +function getOptionsFromBackendGame(backendGame: BackendGame): GameOptions { + let pillbug = false; + let ladybug = false; + let mosquito = false; + switch (backendGame.game_type) { + case 'Base+M': + mosquito = true; + break; + case 'Base+L': + ladybug = true; + break; + case 'Base+P': + pillbug = true; + break; + case 'Base+ML': + mosquito = true; + ladybug = true; + break; + case 'Base+MP': + mosquito = true; + pillbug = true; + break; + case 'Base+LP': + ladybug = true; + pillbug = true; + break; + case 'Base+MLP': + mosquito = true; + ladybug = true; + pillbug = true; + break; + } + + return { + tournament: backendGame.tournament_queen_rule, + pillbug, + ladybug, + mosquito + }; +} + +function getPlayersFromBackendGame(backendGame: BackendGame): GamePlayers { + return { + uids: [backendGame.black_uid, backendGame.white_uid], + // TODO: Neel: fix this + black: { + uid: backendGame.black_uid, + username: "black player's username", + is_guest: false + }, + white: { + uid: backendGame.white_uid, + username: "white player's username", + is_guest: false + } + }; +} + +function getMetaFromBackendGame(backendGame: BackendGame): GameMeta { + let isStarted = false; + let isEnded = false; + let result = ''; + switch (backendGame.game_status) { + case 'NotStarted': + // TODO: Neel: make consistent with backend + isStarted = true; + break; + case 'InProgress': + isStarted = true; + break; + case 'Finished(Winner(b))': + result = backendGame.black_uid; + case 'Finished(Winner(w))': + result = backendGame.white_uid; + case 'Finished(Draw)': + result = 'draw'; + case 'Finished(Unknown)': + // TODO: Neel: what is this? should it (or something else) map to tie? + isEnded = true; + break; + default: + throw new Error(`unknown game status: ${backendGame.game_status}`); + } + + // TODO: Neel: fix this + return { + public: true, + creator: backendGame.black_uid, + isStarted, + isEnded, + result, + createdDate: '', + acceptedDate: '', + playedDate: '', + endedDate: '' + }; +} + +function getStateFromBackendGame(backendGame: BackendGame): GameState { + return newGameState(backendGame.history); +} + +/** + * Create a new game object from the backend's representation of a game. + * + * @param creatorUid The UID of the player creating the game. + * @param players A GamePlayers object. + * @param options A GameOptions object. + * @param isPublic A boolean indicating game visibility. + */ +export function newGameFromBackendGame(backendGame: BackendGame): Game { + console.log(backendGame); + const options = getOptionsFromBackendGame(backendGame); + const players = getPlayersFromBackendGame(backendGame); + const meta = getMetaFromBackendGame(backendGame); + const state = getStateFromBackendGame(backendGame); + + return { + gid: backendGame.id, + options, + players, + meta, + state: newGameState() + }; +} + /** * Create a new game object using FieldValues in place of timestamp strings. * This allows for FieldValue objects to be used while maintaining type safety. @@ -68,13 +208,12 @@ export function newGameWithFieldValues( } export function getUserGames(user: UserData): Promise { - return getJSON(`/api/user/${user.uid}/games`) - .then(maybeGames => { - if (!maybeGames) { - throw new Error(`no games found for that user`) - } - return maybeGames; - }); + return getJSON(`/api/user/${user.uid}/games`).then((maybeGames) => { + if (!maybeGames) { + throw new Error(`no games found for that user`); + } + return maybeGames; + }); } /** From 6daf1766a6efa8a1e17c768f907329bc0ce9a125 Mon Sep 17 00:00:00 2001 From: Neel Kapse Date: Wed, 17 May 2023 21:50:15 -0700 Subject: [PATCH 2/5] WIP --- js/hive-db/src/db/playGameMoves.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/js/hive-db/src/db/playGameMoves.ts b/js/hive-db/src/db/playGameMoves.ts index 9100d2c..a5b2a5d 100644 --- a/js/hive-db/src/db/playGameMoves.ts +++ b/js/hive-db/src/db/playGameMoves.ts @@ -25,5 +25,6 @@ export function playGameMove( authToken: string ): Promise { const uri = `/api/game/${game.gid}/play`; + // TODO: Neel: need to play around with this more and figure out any conversion return postJSON(uri, { Turn: ['.', '${move.notation}'] }, authToken); } From b3e5e5aa186b67838cd7727ad86f0801cd5b7476 Mon Sep 17 00:00:00 2001 From: Neel Kapse Date: Sat, 20 May 2023 09:02:47 -0700 Subject: [PATCH 3/5] Messing around with the move parser --- js/hive-db/src/db/playGameMoves.ts | 2 +- js/hive-db/src/game/game.ts | 2 +- js/hive-db/src/game/state.ts | 1 + js/hive-lib/src/board.ts | 2 + js/hive-lib/src/notation.test.ts | 118 ++++++++++++++--------------- js/hive-lib/src/notation.ts | 30 +++++++- 6 files changed, 90 insertions(+), 65 deletions(-) diff --git a/js/hive-db/src/db/playGameMoves.ts b/js/hive-db/src/db/playGameMoves.ts index a5b2a5d..e8e3a6f 100644 --- a/js/hive-db/src/db/playGameMoves.ts +++ b/js/hive-db/src/db/playGameMoves.ts @@ -26,5 +26,5 @@ export function playGameMove( ): Promise { const uri = `/api/game/${game.gid}/play`; // TODO: Neel: need to play around with this more and figure out any conversion - return postJSON(uri, { Turn: ['.', '${move.notation}'] }, authToken); + return postJSON(uri, { Turn: [move.notation, '.'] }, authToken); } diff --git a/js/hive-db/src/game/game.ts b/js/hive-db/src/game/game.ts index 6288bd7..bfc087b 100644 --- a/js/hive-db/src/game/game.ts +++ b/js/hive-db/src/game/game.ts @@ -178,7 +178,7 @@ export function newGameFromBackendGame(backendGame: BackendGame): Game { options, players, meta, - state: newGameState() + state }; } diff --git a/js/hive-db/src/game/state.ts b/js/hive-db/src/game/state.ts index c65d8ef..7c1f0e6 100644 --- a/js/hive-db/src/game/state.ts +++ b/js/hive-db/src/game/state.ts @@ -18,6 +18,7 @@ export interface GameState { export const newGameState = (notation?: string): GameState => { if (!notation) notation = ''; const moves = getGameMoves(notation); + console.log(moves); return { notation: notation, turn: moves.length % 2 === 0 ? 'w' : 'b', diff --git a/js/hive-lib/src/board.ts b/js/hive-lib/src/board.ts index a58cb54..c7836d0 100644 --- a/js/hive-lib/src/board.ts +++ b/js/hive-lib/src/board.ts @@ -40,6 +40,8 @@ export function buildBoard(moves: Move[], upTo?: number): GameBoard { // Extract the data we need from the move object const { tileId, refId, dir } = move; + console.log(draft); + if (isEmpty(draft)) { // The first tile placed on the board is always at (0, 0) _placeTile(draft, tileId, { q: 0, r: 0 }); diff --git a/js/hive-lib/src/notation.test.ts b/js/hive-lib/src/notation.test.ts index f4bcb23..70c507e 100644 --- a/js/hive-lib/src/notation.test.ts +++ b/js/hive-lib/src/notation.test.ts @@ -87,65 +87,65 @@ describe('notation parsing', () => { })); }); - describe('_parseTurnNotation', () => { - test('turn 3: white pass, black has not moved', () => - expect(_parseTurnNotation('3. x')).toEqual({ - notation: '3. x', - index: 3, - white: { - notation: 'x', - tileId: 'x', - refId: 'x', - dir: -1 - } - })); - test('turn 150: white ant 2 to top right of black queen, game end', () => - expect(_parseTurnNotation('150. wA2 bQ/#')).toEqual({ - notation: '150. wA2 bQ/#', - index: 150, - white: { - notation: 'wA2 bQ/#', - tileId: 'wA2', - refId: 'bQ', - dir: 1, - end: true - } - })); - test('turn 10: white mosquito on top of black beetle 2, black pass', () => - expect(_parseTurnNotation('10. wM bB2, x')).toEqual({ - notation: '10. wM bB2, x', - index: 10, - white: { - notation: 'wM bB2', - tileId: 'wM', - refId: 'bB2', - dir: 0 - }, - black: { - notation: 'x', - tileId: 'x', - refId: 'x', - dir: -1 - } - })); - test('turn 73: white grasshopper 1 to bottom right of black queen, black ladybug to left of white pillbug', () => - expect(_parseTurnNotation('73. wG1 bQ\\, bL -wP')).toEqual({ - notation: '73. wG1 bQ\\, bL -wP', - index: 73, - white: { - notation: 'wG1 bQ\\', - tileId: 'wG1', - refId: 'bQ', - dir: 3 - }, - black: { - notation: 'bL -wP', - tileId: 'bL', - refId: 'wP', - dir: 5 - } - })); - }); + // describe('_parseTurnNotation', () => { + // test('turn 3: white pass, black has not moved', () => + // expect(_parseTurnNotation('3. x')).toEqual({ + // notation: '3. x', + // index: 3, + // white: { + // notation: 'x', + // tileId: 'x', + // refId: 'x', + // dir: -1 + // } + // })); + // test('turn 150: white ant 2 to top right of black queen, game end', () => + // expect(_parseTurnNotation('150. wA2 bQ/#')).toEqual({ + // notation: '150. wA2 bQ/#', + // index: 150, + // white: { + // notation: 'wA2 bQ/#', + // tileId: 'wA2', + // refId: 'bQ', + // dir: 1, + // end: true + // } + // })); + // test('turn 10: white mosquito on top of black beetle 2, black pass', () => + // expect(_parseTurnNotation('10. wM bB2, x')).toEqual({ + // notation: '10. wM bB2, x', + // index: 10, + // white: { + // notation: 'wM bB2', + // tileId: 'wM', + // refId: 'bB2', + // dir: 0 + // }, + // black: { + // notation: 'x', + // tileId: 'x', + // refId: 'x', + // dir: -1 + // } + // })); + // test('turn 73: white grasshopper 1 to bottom right of black queen, black ladybug to left of white pillbug', () => + // expect(_parseTurnNotation('73. wG1 bQ\\, bL -wP')).toEqual({ + // notation: '73. wG1 bQ\\, bL -wP', + // index: 73, + // white: { + // notation: 'wG1 bQ\\', + // tileId: 'wG1', + // refId: 'bQ', + // dir: 3 + // }, + // black: { + // notation: 'bL -wP', + // tileId: 'bL', + // refId: 'wP', + // dir: 5 + // } + // })); + // }); describe('_parseGameNotation', () => { test('a game with two completed turns (game0)', () => diff --git a/js/hive-lib/src/notation.ts b/js/hive-lib/src/notation.ts index 0e16c9e..902d4dd 100644 --- a/js/hive-lib/src/notation.ts +++ b/js/hive-lib/src/notation.ts @@ -126,7 +126,9 @@ export function _buildTurnNotation( * @return An array of *Turn* objects. */ export function _parseGameNotation(notation: string): Turn[] { - return notation.split(/\s(?=\d+\.)/g).map(_parseTurnNotation); + return notation + .split(';') + .map((turnNotation) => _parseTurnNotation(turnNotation)); } /** @@ -135,11 +137,20 @@ export function _parseGameNotation(notation: string): Turn[] { * @param notation A turn notation string. * @return A *Turn* object. */ +// export function _parseTurnNotation(notation: string, index: number): Turn { +// const moves = notation.split(' '); +// return { +// notation, +// index: index + 1, +// white: _parseMoveNotation(moves[0]), +// black: _parseMoveNotation(moves[1]) +// }; +// } export function _parseTurnNotation(notation: string): Turn { const sepLocation = notation.indexOf('.'); const indexString = notation.slice(0, sepLocation); const placementsString = notation.slice(sepLocation + 1); - const placements = placementsString.split(','); + const placements = placementsString.split(' '); return { notation, index: parseInt(indexString), @@ -159,7 +170,10 @@ export function _parseMoveNotation(notation?: string): Move | undefined { // Split notation into moving tile and reference tile portions notation = notation.trim(); - const [tileId, refNotation] = notation.split(/\s/g); + let [tileId, refNotation] = notation.split(/\s/g); + console.log(notation); + console.log(tileId); + console.log(refNotation); // Check for and return a passing move if (tileId === 'x') @@ -171,11 +185,19 @@ export function _parseMoveNotation(notation?: string): Move | undefined { }; // Parse the reference notation to get the reference tile and direction - const { refId, dir, end } = + let { refId, dir, end } = refNotation !== undefined ? _parseReferenceNotation(refNotation) // only when there is one : { refId: tileId, dir: 0, end: false }; // first move notation + // // TODO: is this the right way to do this? + // // handle first move notation + // if (tileId === '.') { + // refId = tileId; + // dir = 0; + // end = false; + // } + // Return a playing move return { notation, From 0fa5e0236948999375dadf10796e2e502056430e Mon Sep 17 00:00:00 2001 From: Neel Kapse Date: Thu, 15 Jun 2023 07:16:02 -0400 Subject: [PATCH 4/5] WIP --- .../components/game-online/GameOnline.tsx | 2 + js/frontend/pages/game/[gameid].tsx | 19 ++-- js/frontend/state/game-online/selectors.ts | 6 +- js/frontend/state/game-online/slice.ts | 10 ++- js/hive-db/src/game/game.ts | 52 ++++++++++- js/hive-db/src/game/state.ts | 1 - js/hive-lib/src/board.ts | 2 - js/hive-lib/src/notation.test.ts | 2 +- js/hive-lib/src/notation.ts | 89 ++++++++++--------- js/hive-lib/src/types.ts | 6 ++ 10 files changed, 129 insertions(+), 60 deletions(-) diff --git a/js/frontend/components/game-online/GameOnline.tsx b/js/frontend/components/game-online/GameOnline.tsx index f89d241..e4de9ce 100644 --- a/js/frontend/components/game-online/GameOnline.tsx +++ b/js/frontend/components/game-online/GameOnline.tsx @@ -30,6 +30,8 @@ const GameOnline = ({ uid, game }: { uid: string | null; game: Game }) => { const boardCentered = useGameSelector(selectBoardCentered); const { authToken } = usePlayer(); + console.log(ghosts); + // TODO: Neel: probably not the right place to do this useEffect(() => dispatch(authTokenAdded(authToken))); diff --git a/js/frontend/pages/game/[gameid].tsx b/js/frontend/pages/game/[gameid].tsx index d54d07e..9c59e80 100644 --- a/js/frontend/pages/game/[gameid].tsx +++ b/js/frontend/pages/game/[gameid].tsx @@ -1,7 +1,7 @@ import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; import { Provider } from 'react-redux'; -import { Game, usePlayer } from 'hive-db'; +import { Game, usePlayer, getGame, newGameFromBackendGame } from 'hive-db'; import { GameOnline } from '../../components/game-online/GameOnline'; import { GameOnlineSidebar } from '../../components/game-online/GameOnlineSidebar'; import { NavBar } from '../../components/nav/NavBar'; @@ -62,10 +62,13 @@ const Game = () => { useEffect(() => { // TODO: should this be a strict type check while enforcing that gameid is a string? - setGame(activeGames.find((g) => g.gid == gameid)); - }, [activeGames, gameid]); - - console.log(game); + if (gameid) { + getGame(gameid).then((game) => { + console.log(game); + setGame(newGameFromBackendGame(game)); + }); + } + }, [gameid]); return ( <> @@ -75,7 +78,11 @@ const Game = () => {
- {game ? : 'Loading...'} + {game && user ? ( + + ) : ( + 'Loading...' + )}
diff --git a/js/frontend/state/game-online/selectors.ts b/js/frontend/state/game-online/selectors.ts index 454a7d9..f5a78b6 100644 --- a/js/frontend/state/game-online/selectors.ts +++ b/js/frontend/state/game-online/selectors.ts @@ -39,7 +39,7 @@ export const selectGame = (state: GameState): Game | null => state.game; /** * Get the set of valid next moves */ -export const selectValidMoves = (state: GameState): Move[] | null => +export const selectValidMoves = (state: GameState): PossibleMove[] | null => state.validNextMoves; /** @@ -143,7 +143,9 @@ export const selectGameBoard = createSelector( */ export const selectDisplayGameBoard = createSelector( [selectDisplayMoves, selectDisplayUpTo], - (moves, upTo): GameBoard => buildBoard(moves, upTo) + (moves, upTo): GameBoard => { + return buildBoard(moves, upTo); + } ); /** diff --git a/js/frontend/state/game-online/slice.ts b/js/frontend/state/game-online/slice.ts index 709804a..65da003 100644 --- a/js/frontend/state/game-online/slice.ts +++ b/js/frontend/state/game-online/slice.ts @@ -39,6 +39,15 @@ const slice = createSlice({ if (oldNotation !== newNotation && state.upTo !== -1) { state.newMovesToView = true; } + // TODO: Neel: populate this from game.validMoves and game.validSpawns + let validNextMoves = []; + for (const move of game.validMoves) { + validNextMoves.push(move); + } + for (const spawn of game.validSpawns) { + validNextMoves.push(spawn); + } + state.validNextMoves = validNextMoves; } state.game = game; state.proposedMove = null; @@ -60,7 +69,6 @@ const slice = createSlice({ if (game.state.moveCount === 0) { // The first move is being played state.selectedTileId = null; - console.log(state.authToken); playGameMove(game, state.proposedMove, state.authToken) .then(({ game, validNextMoves }) => { state.game = game; diff --git a/js/hive-db/src/game/game.ts b/js/hive-db/src/game/game.ts index bfc087b..2b6344b 100644 --- a/js/hive-db/src/game/game.ts +++ b/js/hive-db/src/game/game.ts @@ -1,4 +1,4 @@ -import type { ColorKey, GameOptions } from 'hive-lib'; +import type { ColorKey, GameOptions, HexCoordinate } from 'hive-lib'; import type { GameMeta } from './meta'; import type { GamePlayers } from './players'; import type { GameState } from './state'; @@ -23,9 +23,11 @@ export interface BackendGame { ranked: boolean; tournament_queen_rule: boolean; turn: number; + moves: string; + spawns: HexCoordinate[]; } -export interface Game { +export interface GameOverview { gid: string; meta: GameMeta; options: GameOptions; @@ -33,6 +35,11 @@ export interface Game { state: GameState; } +export type Game = GameOverview & { + validMoves: string[]; + validSpawns: string[]; +}; + /** * Create a new game object. * @@ -46,7 +53,7 @@ export function newGame( players: GamePlayers, options: GameOptions, isPublic: boolean -): Game { +): OverviewGame { return { gid: '', options, @@ -158,6 +165,35 @@ function getStateFromBackendGame(backendGame: BackendGame): GameState { return newGameState(backendGame.history); } +function getValidMovesFromBackendGame( + backendGame: BackendGame +): PossibleMove[] { + console.log(backendGame.moves); + return backendGame.moves; +}wekj + +function convertReserveToPieceSymbol(colorSymbol: string, pieceName: string, reserveSize: number): string { + const pieceNumber = reserveSize + return colorSymbol + pieceName[0].toUpperCase() + ; + +function getValidSpawnsFromBackendGame( + backendGame: BackendGame +): PossibleMove[] { + const reserve = backendGame.turn % 2 == 0 ? backendGame.reserve_white : backendGame.reserve_black; + const colorSymbol = backendGame.turn % 2 == 0 ? 'w' : 'b'; + const spawnablePieces = []; + for (let [key, value] of reserve) { + if (value === 0) + continue; + spawnablePieces.push(convertReserveToPieceSymbol(colorSymbol, key, value)) + console.log(key + ' = ' + value); + } + for (const spawn of backendGame.spawns) { + console.log(spawn); + } + return backendGame.spawns; +} + /** * Create a new game object from the backend's representation of a game. * @@ -172,13 +208,17 @@ export function newGameFromBackendGame(backendGame: BackendGame): Game { const players = getPlayersFromBackendGame(backendGame); const meta = getMetaFromBackendGame(backendGame); const state = getStateFromBackendGame(backendGame); + const validMoves = getValidMovesFromBackendGame(backendGame); + const validSpawns = getValidSpawnsFromBackendGame(backendGame); return { gid: backendGame.id, options, players, meta, - state + state, + validMoves, + validSpawns }; } @@ -216,6 +256,10 @@ export function getUserGames(user: UserData): Promise { }); } +export function getGame(uid: string): Promise { + return getJSON(`/api/game/${uid}`); +} + /** * Create a new partial game object, allowing for FieldValue objects to be used * as field values. Objects created using this method can be used in Firestore diff --git a/js/hive-db/src/game/state.ts b/js/hive-db/src/game/state.ts index 7c1f0e6..c65d8ef 100644 --- a/js/hive-db/src/game/state.ts +++ b/js/hive-db/src/game/state.ts @@ -18,7 +18,6 @@ export interface GameState { export const newGameState = (notation?: string): GameState => { if (!notation) notation = ''; const moves = getGameMoves(notation); - console.log(moves); return { notation: notation, turn: moves.length % 2 === 0 ? 'w' : 'b', diff --git a/js/hive-lib/src/board.ts b/js/hive-lib/src/board.ts index c7836d0..a58cb54 100644 --- a/js/hive-lib/src/board.ts +++ b/js/hive-lib/src/board.ts @@ -40,8 +40,6 @@ export function buildBoard(moves: Move[], upTo?: number): GameBoard { // Extract the data we need from the move object const { tileId, refId, dir } = move; - console.log(draft); - if (isEmpty(draft)) { // The first tile placed on the board is always at (0, 0) _placeTile(draft, tileId, { q: 0, r: 0 }); diff --git a/js/hive-lib/src/notation.test.ts b/js/hive-lib/src/notation.test.ts index 70c507e..303502d 100644 --- a/js/hive-lib/src/notation.test.ts +++ b/js/hive-lib/src/notation.test.ts @@ -56,7 +56,7 @@ describe('notation parsing', () => { }); describe('_parseMoveNotation', () => { - test('undefined move', () => expect(_parseMoveNotation()).toBeUndefined()); + // test('undefined move', () => expect(_parseMoveNotation()).toBeUndefined()); test('passing move', () => expect(_parseMoveNotation('x')).toEqual({ notation: 'x', diff --git a/js/hive-lib/src/notation.ts b/js/hive-lib/src/notation.ts index 902d4dd..e046e1a 100644 --- a/js/hive-lib/src/notation.ts +++ b/js/hive-lib/src/notation.ts @@ -53,12 +53,8 @@ export function buildMoveNotation( * @return An array of *Move* objects. */ export function getGameMoves(notation: string): Move[] { - const turns = _parseGameNotation(notation); - return flatten( - turns.map((turn) => - turn.white ? (turn.black ? [turn.white, turn.black] : [turn.white]) : [] - ) - ); + const turns = _parseMoves(notation); + return turns; } /** @@ -126,9 +122,27 @@ export function _buildTurnNotation( * @return An array of *Turn* objects. */ export function _parseGameNotation(notation: string): Turn[] { - return notation + return []; + // return notation + // .split(';') + // .filter((s) => s) + // .map((turnNotation) => _parseMoveNotation(turnNotation)); +} + +/** + * Create an ordered array of *Move* objects by parsing a game notation string. + * + * @param notation A game notation string. + * @return An array of *Move* objects. + */ +export function _parseMoves(notation: string): Move[] { + if (!notation) { + return []; + } + return (notation + '') .split(';') - .map((turnNotation) => _parseTurnNotation(turnNotation)); + .filter((s) => s) + .map((moveNotation) => _parseMoveNotation(moveNotation)); } /** @@ -137,27 +151,27 @@ export function _parseGameNotation(notation: string): Turn[] { * @param notation A turn notation string. * @return A *Turn* object. */ -// export function _parseTurnNotation(notation: string, index: number): Turn { -// const moves = notation.split(' '); -// return { -// notation, -// index: index + 1, -// white: _parseMoveNotation(moves[0]), -// black: _parseMoveNotation(moves[1]) -// }; -// } -export function _parseTurnNotation(notation: string): Turn { - const sepLocation = notation.indexOf('.'); - const indexString = notation.slice(0, sepLocation); - const placementsString = notation.slice(sepLocation + 1); - const placements = placementsString.split(' '); +export function _parseTurnNotation(notation: string, index: number): Turn { + const moves = notation.split(' '); return { notation, - index: parseInt(indexString), - white: _parseMoveNotation(placements[0]), - black: _parseMoveNotation(placements[1]) + index: index + 1, + white: _parseMoveNotation(moves[0]), + black: _parseMoveNotation(moves[1]) }; } +// export function _parseTurnNotation(notation: string): Turn { +// const sepLocation = notation.indexOf('.'); +// const indexString = notation.slice(0, sepLocation); +// const placementsString = notation.slice(sepLocation + 1); +// const placements = placementsString.split(' '); +// return { +// notation, +// index: parseInt(indexString), +// white: _parseMoveNotation(placements[0]), +// black: _parseMoveNotation(placements[1]) +// }; +// } /** * Create a *Move* object by parsing a move notation string. @@ -165,15 +179,12 @@ export function _parseTurnNotation(notation: string): Turn { * @param notation A move notation string. * @return A *Move* object. */ -export function _parseMoveNotation(notation?: string): Move | undefined { - if (!notation) return undefined; - +export function _parseMoveNotation(notation: string): Move { // Split notation into moving tile and reference tile portions notation = notation.trim(); - let [tileId, refNotation] = notation.split(/\s/g); - console.log(notation); - console.log(tileId); - console.log(refNotation); + + // TODO: Neel: figure out why this is getting called twice, once with commas as separators and once with spaces + let [tileId, refNotation] = notation.split(/\s|,/g); // Check for and return a passing move if (tileId === 'x') @@ -186,17 +197,9 @@ export function _parseMoveNotation(notation?: string): Move | undefined { // Parse the reference notation to get the reference tile and direction let { refId, dir, end } = - refNotation !== undefined - ? _parseReferenceNotation(refNotation) // only when there is one - : { refId: tileId, dir: 0, end: false }; // first move notation - - // // TODO: is this the right way to do this? - // // handle first move notation - // if (tileId === '.') { - // refId = tileId; - // dir = 0; - // end = false; - // } + refNotation === '.' + ? { refId: tileId, dir: 0, end: false } // first move notation + : _parseReferenceNotation(refNotation); // only when there is one // Return a playing move return { diff --git a/js/hive-lib/src/types.ts b/js/hive-lib/src/types.ts index 697e531..61b3807 100644 --- a/js/hive-lib/src/types.ts +++ b/js/hive-lib/src/types.ts @@ -70,6 +70,12 @@ export type GameOptions = { mosquito: boolean; }; +export type PossibleMove = { + qCoordinate: number; + rCoordinate: number; + tileId: TileId; +}; + /** * An object describing a player's move, which can either be a pass or move or * place a tile. From 80a3fa5d70d1f5436359ad79954c88a8c085e7b6 Mon Sep 17 00:00:00 2001 From: Neel Kapse Date: Sun, 23 Jul 2023 14:14:56 -0700 Subject: [PATCH 5/5] Switch to 'rated' --- backend/src/db/schema.rs | 1 - .../components/lists/ListLobbyGames.tsx | 10 +- .../components/lists/ListPlayerChallenges.tsx | 60 ++++++++---- js/frontend/pages/challenge/[challengeId].tsx | 95 +++++++++++++------ js/hive-db/src/game/challenge.ts | 59 +++++++----- js/hive-db/src/game/game.ts | 19 ++-- 6 files changed, 159 insertions(+), 85 deletions(-) diff --git a/backend/src/db/schema.rs b/backend/src/db/schema.rs index 587e427..a032fe8 100644 --- a/backend/src/db/schema.rs +++ b/backend/src/db/schema.rs @@ -56,7 +56,6 @@ diesel::table! { diesel::table! { users (uid) { uid -> Text, - #[max_length = 40] username -> Varchar, is_guest -> Bool, } diff --git a/js/frontend/components/lists/ListLobbyGames.tsx b/js/frontend/components/lists/ListLobbyGames.tsx index b840659..90737e3 100644 --- a/js/frontend/components/lists/ListLobbyGames.tsx +++ b/js/frontend/components/lists/ListLobbyGames.tsx @@ -3,7 +3,7 @@ import { acceptGameChallenge, GameChallenge, useLobbyChallenges, - usePlayer, + usePlayer } from 'hive-db'; import { Button } from '@chakra-ui/react'; import { Header, HeaderItem } from './Header'; @@ -26,15 +26,15 @@ const LobbyChallengeRow = ({ challenge }: LobbyChallengeRowProps) => { return ( {challenge.challenger.username} - {isRated ? 'Rated' : 'Unrated'} + {isRated ? 'Rated' : 'Not rated'} {tournament ? 'Tournament' : 'Normal'} {challenge.createdAt.toDateString()} + @@ -28,7 +38,7 @@ const ShareLinkButton = ({ text }: { text: string }) => { Send this link to a friend to invite them! { - ) -} + ); +}; -const DeleteButton = ({ id, onDelete }: { id: string, onDelete: () => void }) => { +const DeleteButton = ({ + id, + onDelete +}: { + id: string; + onDelete: () => void; +}) => { const { authToken } = usePlayer(); return ( - } + )} ); -} +}; const Challenge = () => { const router = useRouter(); @@ -115,8 +148,9 @@ const Challenge = () => { useEffect(() => { if (!challengeId) return; - getGameChallenge(challengeId as string) - .then((challenge) => setChallenge(challenge)); + getGameChallenge(challengeId as string).then((challenge) => + setChallenge(challenge) + ); }, [challengeId]); return ( @@ -128,7 +162,14 @@ const Challenge = () => {
- { !challenge ? 'Loading...' : } + {!challenge ? ( + 'Loading...' + ) : ( + + )}
diff --git a/js/hive-db/src/game/challenge.ts b/js/hive-db/src/game/challenge.ts index acab9b0..ec76b18 100644 --- a/js/hive-db/src/game/challenge.ts +++ b/js/hive-db/src/game/challenge.ts @@ -8,14 +8,14 @@ import useSWR, { Fetcher } from 'swr'; import { usePlayer } from '../PlayerProvider'; export interface GameChallengeResponse { - id: string, - gameType: string, - rated: boolean, - public: boolean, - tournamentQueenRule: boolean, - colorChoice: string, - createdAt: Date, - challenger: UserData, + id: string; + gameType: string; + rated: boolean; + public: boolean; + tournamentQueenRule: boolean; + colorChoice: string; + createdAt: Date; + challenger: UserData; } export class GameChallenge { @@ -55,38 +55,46 @@ export async function createGameChallenge( visibility: VisibilityChoice, colorChoice: ColorChoice, expansions: ExpansionsChoice, - authToken: string, + authToken: string ): Promise { const isPublic = visibility === 'Public'; - const gameType = gameOptionsToString(newGameOptions( - expansions.ladybug, - expansions.mosquito, - expansions.pillbug - )); + const gameType = gameOptionsToString( + newGameOptions(expansions.ladybug, expansions.mosquito, expansions.pillbug) + ); const reqBody = { public: isPublic, rated: false, // not implemented yet tournamentQueenRule: true, // always on for now gameType, - colorChoice, + colorChoice }; - let res = await postJSON('/api/game/challenge', reqBody, authToken); + let res = await postJSON( + '/api/game/challenge', + reqBody, + authToken + ); return new GameChallenge(res); } export async function getGameChallenge(id: string): Promise { let res = await getJSON(`/api/game/challenge/${id}`); if (!res) { - throw new Error(`No such challenge found`); + throw new Error(`No such challenge found`); } return new GameChallenge(res); } -export async function acceptGameChallenge(id: string, authToken: string): Promise { +export async function acceptGameChallenge( + id: string, + authToken: string +): Promise { return postJSON(`/api/game/challenge/${id}/accept`, {}, authToken); } -export async function deleteGameChallenge(id: string, authToken: string): Promise { +export async function deleteGameChallenge( + id: string, + authToken: string +): Promise { await deleteReq(`/api/game/challenge/${id}`, authToken); } @@ -108,7 +116,9 @@ function gameOptionsFromString(gameType: string): GameOptions { return newGameOptions(l, m, p); } -async function gameChallengesFetcher([uri, authToken]): Promise { +async function gameChallengesFetcher([uri, authToken]): Promise< + GameChallenge[] | null +> { if (!uri) { return null; } @@ -127,7 +137,12 @@ export function useLobbyChallenges() { export function usePlayerChallenges() { const { user, authToken } = usePlayer(); const uri = user ? `/api/user/${user.uid}/challenges` : null; - let { data: challenges, error, isLoading, mutate } = useSWR([uri, authToken], gameChallengesFetcher); + let { + data: challenges, + error, + isLoading, + mutate + } = useSWR([uri, authToken], gameChallengesFetcher); if (!error && !isLoading && !challenges) { error = new Error(`No challenges found for user ${user}`); } @@ -136,6 +151,6 @@ export function usePlayerChallenges() { challenges, error, isLoading, - mutate, + mutate }; } diff --git a/js/hive-db/src/game/game.ts b/js/hive-db/src/game/game.ts index 2b6344b..214351b 100644 --- a/js/hive-db/src/game/game.ts +++ b/js/hive-db/src/game/game.ts @@ -20,7 +20,7 @@ export interface BackendGame { game_type: string; history: string; id: string; - ranked: boolean; + rated: boolean; tournament_queen_rule: boolean; turn: number; moves: string; @@ -170,22 +170,21 @@ function getValidMovesFromBackendGame( ): PossibleMove[] { console.log(backendGame.moves); return backendGame.moves; -}wekj - -function convertReserveToPieceSymbol(colorSymbol: string, pieceName: string, reserveSize: number): string { - const pieceNumber = reserveSize - return colorSymbol + pieceName[0].toUpperCase() + ; +} function getValidSpawnsFromBackendGame( backendGame: BackendGame ): PossibleMove[] { - const reserve = backendGame.turn % 2 == 0 ? backendGame.reserve_white : backendGame.reserve_black; + console.log(backendGame); + const reserve = + backendGame.turn % 2 == 0 + ? backendGame.reserve_white + : backendGame.reserve_black; const colorSymbol = backendGame.turn % 2 == 0 ? 'w' : 'b'; const spawnablePieces = []; for (let [key, value] of reserve) { - if (value === 0) - continue; - spawnablePieces.push(convertReserveToPieceSymbol(colorSymbol, key, value)) + if (value === 0) continue; + spawnablePieces.push(value); console.log(key + ' = ' + value); } for (const spawn of backendGame.spawns) {