Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Client - game play (making moves) logic #20

Merged
merged 10 commits into from
Jul 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,12 @@ Fields:
- [x] [Authentication](https://github.com/alan-hadyk/side-stacker-game/pull/18)
- [x] [Client - game creation and game board](https://github.com/alan-hadyk/side-stacker-game/pull/19)
- [x] [Client - game joining logic](https://github.com/alan-hadyk/side-stacker-game/pull/19)
- [ ] Client - game play (making moves) logic
- [ ] Client - game end (win/draw) logic
- [x] [Client - game play (making moves) logic](https://github.com/alan-hadyk/side-stacker-game/pull/20)
- [x] [Client - game end (win/draw) logic](https://github.com/alan-hadyk/side-stacker-game/pull/20)
- [ ] Client - 404 page
- [ ] Server - abandoned games
- [ ] Remove obsolete code
- [ ] Testing of all routes, controllers, and user interfaces
- [ ] Missing documentation

### Potential additional features

Expand Down Expand Up @@ -259,5 +260,3 @@ TODO
4. **Cheating**: Players might try to cheat by modifying the client code or sending fake requests to the server. This can be addressed by validating all moves on the server and checking that they come from the player whose turn it is.

5. **User experience**: Creating a user interface that is intuitive and responsive can be challenging. This can be addressed by using a modern front-end framework, and by testing the user interface with real users and iterating based on their feedback.

6. **Abandoned games**: If a given user participates in a game, and then closes the browser or loses connection while the game is still in progress, and never comes back, there might a "ghost game" with that player assigned, and without the possibility to finish the game. To prevent this, there might be a CRON job that frequently checks all games in progress, and removes players that weren't active for a given period of time.
2 changes: 2 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"prettier": "^3.0.0",
"tailwindcss": "^3.3.3",
"tailwindcss-3d": "^0.2.6",
"ts-prune": "^0.10.3",
"typescript": "^5.0.2",
"vite": "^4.4.0",
"vite-tsconfig-paths": "^4.2.0"
Expand All @@ -49,6 +50,7 @@
"private": true,
"scripts": {
"build": "tsc && vite build",
"dead-code": "ts-prune",
"dev": "vite",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
Expand Down
36 changes: 36 additions & 0 deletions client/src/api/mutations/useCreateMove.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { axiosPost } from "@client/helpers/api/axiosPost"
import { getAxiosError } from "@client/helpers/api/getAxiosError"
import { useToast } from "@client/hooks/useToast"
import { CreateMovePostBody, MoveResponse } from "@server/@types/api"
import { MutateOptions, useMutation } from "@tanstack/react-query"
import { Path } from "@server/routes/paths"

export const useCreateMove = () => {
const { mutate, ...createMoveMutation } = useMutation({
mutationFn: (body: CreateMovePostBody) =>
axiosPost<MoveResponse>(Path.Moves, body),
})
const { errorToast } = useToast()

const createMove = (
body: CreateMovePostBody,
options?: MutateOptions<MoveResponse, unknown, CreateMovePostBody, unknown>,
) =>
mutate(body, {
onError: (error) => {
const apiError = getAxiosError(error)

if (apiError) {
apiError.errors.forEach((message) => {
errorToast(message)
})
}
},
...options,
})

return {
...createMoveMutation,
createMove,
}
}
9 changes: 5 additions & 4 deletions client/src/api/queries/useGetGames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { queryKeys } from "@client/api/queryKeys"
import { axiosGet } from "@client/helpers/api/axiosGet"
import { useQuery, UseQueryOptions } from "@tanstack/react-query"
import { AxiosError } from "axios"
import { GameResponse, GamesGetAllQueryParams } from "@server/@types/api"
import { GamesGetAllQueryParams, GamesResponse } from "@server/@types/api"
import { useToast } from "@client/hooks/useToast"
import { getAxiosError } from "@client/helpers/api/getAxiosError"
import { Path } from "@server/routes/paths"

export const useGetGames = (
params?: GamesGetAllQueryParams,
options?: UseQueryOptions<GameResponse[], AxiosError, GameResponse[]>,
options?: UseQueryOptions<GamesResponse, AxiosError, GamesResponse>,
) => {
const { errorToast } = useToast()

Expand All @@ -23,15 +23,16 @@ export const useGetGames = (
})
}
},
queryFn: () => axiosGet<GameResponse[]>(Path.Games, { params }),
queryFn: () => axiosGet<GamesResponse>(Path.Games, { params }),
queryKey: queryKeys.games.list(params),
...options,
})

const games = getGamesQuery.data
const { games, total } = getGamesQuery.data || {}

return {
...getGamesQuery,
games,
total,
}
}
9 changes: 5 additions & 4 deletions client/src/api/queries/useGetPlayers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { queryKeys } from "@client/api/queryKeys"
import { axiosGet } from "@client/helpers/api/axiosGet"
import { useQuery, UseQueryOptions } from "@tanstack/react-query"
import { AxiosError } from "axios"
import { PlayerResponse, PlayersGetAllQueryParams } from "@server/@types/api"
import { PlayersGetAllQueryParams, PlayersResponse } from "@server/@types/api"
import { getAxiosError } from "@client/helpers/api/getAxiosError"
import { useToast } from "@client/hooks/useToast"
import { Path } from "@server/routes/paths"

export const useGetPlayers = (
params?: PlayersGetAllQueryParams,
options?: UseQueryOptions<PlayerResponse[], AxiosError, PlayerResponse[]>,
options?: UseQueryOptions<PlayersResponse, AxiosError, PlayersResponse>,
) => {
const { errorToast } = useToast()

Expand All @@ -23,15 +23,16 @@ export const useGetPlayers = (
})
}
},
queryFn: () => axiosGet<PlayerResponse[]>(Path.Players, { params }),
queryFn: () => axiosGet<PlayersResponse>(Path.Players, { params }),
queryKey: queryKeys.players.list(params),
...options,
})

const players = getPlayersQuery.data
const { players, total } = getPlayersQuery.data || {}

return {
...getPlayersQuery,
players,
total,
}
}
1 change: 1 addition & 0 deletions client/src/components/atoms/Button/@types/Button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export enum ButtonFill {
}

export enum ButtonVariant {
Default = "",
Neutral = "btn-neutral",
Primary = "btn-primary",
Secondary = "btn-secondary",
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/atoms/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const Button: React.FC<ButtonProps> = ({
<button
disabled={disabled || isLoading}
className={`btn
${size} ${shape} ${fill} ${variant}
${size} ${shape} ${fill} ${disabled ? "btn-disabled" : variant}
${className}
`}
onClick={onClick}
Expand Down
12 changes: 12 additions & 0 deletions client/src/components/atoms/FlexRow/@types/FlexRow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ReactNode } from "react"

export enum FlexRowGap {
Gap2 = "gap-2",
Gap4 = "gap-4",
Gap8 = "gap-8",
}

export interface FlexRowProps {
children: ReactNode | ReactNode[]
gap?: FlexRowGap
}
9 changes: 9 additions & 0 deletions client/src/components/atoms/FlexRow/FlexRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {
FlexRowGap,
FlexRowProps,
} from "@client/components/atoms/FlexRow/@types/FlexRow"

export const FlexRow: React.FC<FlexRowProps> = ({
children,
gap = FlexRowGap.Gap2,
}) => <div className={`flex items-center justify-start ${gap}`}>{children}</div>
71 changes: 40 additions & 31 deletions client/src/components/atoms/GameBoardCell/GameBoardCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,61 +20,70 @@ export const GameBoardCell: React.FC<GameBoardCellProps> = ({
flex items-center justify-center
border-r-2 last:border-r-0 border-base-300
h-0 flex-[0_0_calc(100%/7)] pb-[calc(100%/7)]
relative
relative group

${
isLoading
? "animate-bg-gradient-slow bg-gradient-to-r bg-400% from-primary to-secondary"
: ""
}
${onClick ? "cursor-pointer transition-all" : "cursor-not-allowed"}
${onClick ? "cursor-pointer transition-all" : ""}
${
!onClick && cell === "empty" && !isNextPossibleMove
? "diagonal-lines"
: ""
}
${onClick && nextMoveType === "X" ? "hover:shadow-inner-primary" : ""}
${
onClick && nextMoveType === "O"
? "hover:from-secondary hover:to-primary/50"
: ""
}

${isWinningCell ? "winning-cell" : ""}
${
isWinningCell && winDirection === WinDirection.Horizontal
? "winning-cell-horizontal"
: ""
}
${
isWinningCell && winDirection === WinDirection.Vertical
? "winning-cell-vertical"
: ""
}
${
isWinningCell && winDirection === WinDirection.Diagonal
? "winning-cell-diagonal"
: ""
}
${
isWinningCell && winDirection === WinDirection.ReverseDiagonal
? "winning-cell-reverse-diagonal"
: ""
}
${onClick && nextMoveType === "O" ? "hover:shadow-inner-secondary" : ""}

`}
onClick={onClick}
>
{isWinningCell && (
<div
className={`
absolute left-0 top-0 z-50
w-full h-full
winning-cell
${
isWinningCell && winDirection === WinDirection.Horizontal
? "winning-cell-horizontal"
: ""
}
${
isWinningCell && winDirection === WinDirection.Vertical
? "winning-cell-vertical"
: ""
}
${
isWinningCell && winDirection === WinDirection.Diagonal
? "winning-cell-diagonal"
: ""
}
${
isWinningCell && winDirection === WinDirection.ReverseDiagonal
? "winning-cell-reverse-diagonal"
: ""
}
`}
/>
)}
{cell === "X" && (
<XIcon
className={`${commonCellClassNames} w-1/2 h-1/2 text-primary`}
className={`
${commonCellClassNames}
text-primary
`}
stroke="currentColor"
fill="currentColor"
/>
)}
{cell === "O" && (
<OIcon
className={`${commonCellClassNames} w-1/2 h-1/2 text-secondary`}
className={`
${commonCellClassNames}
text-secondary
`}
stroke="currentColor"
fill="currentColor"
/>
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/atoms/GameBoardCell/styles.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export const commonCellClassNames =
"block absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%]"
"block absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] w-1/2 h-1/2 z-30 animate-fade-in"

This file was deleted.

17 changes: 0 additions & 17 deletions client/src/components/atoms/GamePreviewCell/GamePreviewCell.tsx

This file was deleted.

This file was deleted.

7 changes: 0 additions & 7 deletions client/src/components/atoms/GamePreviewRow/GamePreviewRow.tsx

This file was deleted.

6 changes: 6 additions & 0 deletions client/src/components/molecules/Card/@types/Card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@ export enum CardVariant {
Secondary = "bg-base-100",
}

export enum CardPosition {
Default = "",
Sticky = "sticky",
}

export interface CardProps {
children: ReactNode | ReactNode[]
className?: string
contentBottom?: ReactNode | ReactNode[]
contentTop?: ReactNode | ReactNode[]
isLoading?: boolean
position?: CardPosition
title?: string
type?: CardType
variant?: CardVariant
Expand Down
5 changes: 4 additions & 1 deletion client/src/components/molecules/Card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from "@client/components/atoms/Typography/@types/Typography"
import { Typography } from "@client/components/atoms/Typography/Typography"
import {
CardPosition,
CardProps,
CardType,
CardVariant,
Expand All @@ -17,13 +18,15 @@ export const Card: React.FC<CardProps> = ({
contentBottom,
contentTop,
isLoading = false,
position = CardPosition.Default,
title,
type = CardType.Normal,
variant = CardVariant.Primary,
}) => (
<div
className={`
card z-[1] compact shadow ${variant} rounded-box
card z-[1] compact shadow ${variant} rounded-box ${position}
${position === CardPosition.Sticky ? "top-0" : ""}
${
type === CardType.Link
? "transition-all ease-in-out duration-150 hover:shadow-xl active:shadow cursor-pointer"
Expand Down
Loading