user states improvements and fixes

This commit is contained in:
aronmal 2023-09-08 19:55:33 +02:00
parent 252f6f6028
commit efcb61b1ed
Signed by: aronmal
GPG key ID: 816B7707426FC612
19 changed files with 309 additions and 273 deletions

View file

@ -30,6 +30,7 @@
"drizzle-orm": "^0.28.6", "drizzle-orm": "^0.28.6",
"drizzle-zod": "^0.5.1", "drizzle-zod": "^0.5.1",
"http-status": "^1.7.0", "http-status": "^1.7.0",
"json-stable-stringify": "^1.0.2",
"nodemailer": "^6.9.5", "nodemailer": "^6.9.5",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"postgres": "^3.3.5", "postgres": "^3.3.5",
@ -44,6 +45,7 @@
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.37.1", "@playwright/test": "^1.37.1",
"@total-typescript/ts-reset": "^0.4.2", "@total-typescript/ts-reset": "^0.4.2",
"@types/json-stable-stringify": "^1.0.34",
"@types/node": "^20.5.9", "@types/node": "^20.5.9",
"@types/nodemailer": "^6.4.9", "@types/nodemailer": "^6.4.9",
"@types/object-hash": "^3.0.4", "@types/object-hash": "^3.0.4",

View file

@ -66,6 +66,9 @@ dependencies:
http-status: http-status:
specifier: ^1.7.0 specifier: ^1.7.0
version: 1.7.0 version: 1.7.0
json-stable-stringify:
specifier: ^1.0.2
version: 1.0.2
nodemailer: nodemailer:
specifier: ^6.9.5 specifier: ^6.9.5
version: 6.9.5 version: 6.9.5
@ -101,6 +104,9 @@ devDependencies:
'@total-typescript/ts-reset': '@total-typescript/ts-reset':
specifier: ^0.4.2 specifier: ^0.4.2
version: 0.4.2 version: 0.4.2
'@types/json-stable-stringify':
specifier: ^1.0.34
version: 1.0.34
'@types/node': '@types/node':
specifier: ^20.5.9 specifier: ^20.5.9
version: 20.5.9 version: 20.5.9
@ -2050,6 +2056,10 @@ packages:
resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
dev: true dev: true
/@types/json-stable-stringify@1.0.34:
resolution: {integrity: sha512-s2cfwagOQAS8o06TcwKfr9Wx11dNGbH2E9vJz1cqV+a/LOyhWNLUNd6JSRYNzvB4d29UuJX2M0Dj9vE1T8fRXw==}
dev: true
/@types/ms@0.7.31: /@types/ms@0.7.31:
resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
@ -4008,6 +4018,12 @@ packages:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
dev: true dev: true
/json-stable-stringify@1.0.2:
resolution: {integrity: sha512-eunSSaEnxV12z+Z73y/j5N37/In40GK4GmsSy+tEHJMxknvqnA7/djeYtAgW0GsWHUfg+847WJjKaEylk2y09g==}
dependencies:
jsonify: 0.0.1
dev: false
/json5@2.2.3: /json5@2.2.3:
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -4020,6 +4036,10 @@ packages:
optionalDependencies: optionalDependencies:
graceful-fs: 4.2.11 graceful-fs: 4.2.11
/jsonify@0.0.1:
resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==}
dev: false
/jsx-ast-utils@3.3.5: /jsx-ast-utils@3.3.5:
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}

View file

@ -31,43 +31,104 @@ type TilesType = {
y: number y: number
} }
function BorderTiles() { function settingTarget(
const { selfIndex, ships } = useSession() isGameTile: boolean,
x: number,
const settingTarget = (isGameTile: boolean, x: number, y: number) => { y: number,
const sIndex = selfIndex() index: 0 | 1,
if (sIndex === -1) return {
if (gameProps.gameState === "running") { activeIndex,
const list = targetList(targetPreview(), gameProps.mode) ships,
if ( }: Pick<ReturnType<typeof useSession>, "activeIndex" | "ships">,
!isGameTile || ) {
!list.filter( if (gameProps.gameState === "running") {
({ x, y }) => !isAlreadyHit(x, y, compiledHits(sIndex === 0 ? 1 : 0)), const list = targetList(targetPreview(), gameProps.mode)
).length if (
) !isGameTile ||
return !list.filter(
if (!overlapsWithAnyBorder(targetPreview(), gameProps.mode)) ({ x, y }) => !isAlreadyHit(x, y, compiledHits(activeIndex())),
setTarget({ ).length
show: true, )
x, return
y, if (!overlapsWithAnyBorder(targetPreview(), gameProps.mode))
orientation: targetPreview().orientation, setTarget({
}) show: true,
} else if ( x,
gameProps.gameState === "starting" && y,
targetPreview().show && orientation: targetPreview().orientation,
!intersectingShip( })
ships(), } else if (
shipProps(ships(), gameProps.mode, targetPreview()), gameProps.gameState === "starting" &&
).score targetPreview().show &&
) { !intersectingShip(
setMouseCursor((e) => ({ ...e, shouldShow: false })) ships(),
setShips( shipProps(ships(), gameProps.mode, targetPreview()),
[...ships(), shipProps(ships(), gameProps.mode, targetPreview())], ).score
sIndex, ) {
) setMouseCursor((e) => ({ ...e, shouldShow: false }))
setShips(
[...ships(), shipProps(ships(), gameProps.mode, targetPreview())],
index,
)
}
}
function onClick(
props: TilesType,
{ selfIndex, activeIndex, ships }: ReturnType<typeof useSession>,
) {
const sIndex = selfIndex()
if (!sIndex) return
if (gameProps.gameState === "running") {
settingTarget(props.isGameTile, props.x, props.y, sIndex.i, {
activeIndex,
ships,
})
} else if (gameProps.gameState === "starting") {
const { index } = intersectingShip(ships(), {
...mouseCursor(),
size: 1,
variant: 0,
orientation: "h",
})
if (typeof index === "undefined")
settingTarget(props.isGameTile, props.x, props.y, sIndex.i, {
activeIndex,
ships,
})
else {
const ship = ships()[index]
setGameProps("mode", ship.size - 2)
removeShip(ship, sIndex.i)
setMouseCursor((e) => ({ ...e, shouldShow: true }))
} }
} }
}
function onMouseEnter(
props: TilesType,
{ ships }: ReturnType<typeof useSession>,
) {
setMouseCursor({
x: props.x,
y: props.y,
shouldShow:
props.isGameTile &&
(gameProps.gameState === "starting"
? intersectingShip(
ships(),
shipProps(ships(), gameProps.mode, {
x: props.x,
y: props.y,
orientation: targetPreview().orientation,
}),
true,
).score < 2
: true),
})
}
function BorderTiles() {
const sessionProps = useSession()
const tilesProperties: TilesType[] = [] const tilesProperties: TilesType[] = []
@ -97,47 +158,8 @@ function BorderTiles() {
<div <div
class={props.className} class={props.className}
style={{ "--x": props.x, "--y": props.y }} style={{ "--x": props.x, "--y": props.y }}
onClick={() => { onClick={() => onClick(props, sessionProps)}
const sIndex = selfIndex() onMouseEnter={() => onMouseEnter(props, sessionProps)}
if (sIndex === -1) return
if (gameProps.gameState === "running") {
settingTarget(props.isGameTile, props.x, props.y)
} else if (gameProps.gameState === "starting") {
const { index } = intersectingShip(ships(), {
...mouseCursor(),
size: 1,
variant: 0,
orientation: "h",
})
if (typeof index === "undefined")
settingTarget(props.isGameTile, props.x, props.y)
else {
const ship = ships()[index]
setGameProps("mode", ship.size - 2)
removeShip(ship, sIndex)
setMouseCursor((e) => ({ ...e, shouldShow: true }))
}
}
}}
onMouseEnter={() =>
setMouseCursor({
x: props.x,
y: props.y,
shouldShow:
props.isGameTile &&
(gameProps.gameState === "starting"
? intersectingShip(
ships(),
shipProps(ships(), gameProps.mode, {
x: props.x,
y: props.y,
orientation: targetPreview().orientation,
}),
true,
).score < 2
: true),
})
}
/> />
)} )}
</For> </For>

View file

@ -44,7 +44,7 @@ import Item from "./Item"
function EventBar(props: { clear: () => void }) { function EventBar(props: { clear: () => void }) {
const { shouldHide, setShouldHide, setEnable, color } = useDrawProps const { shouldHide, setShouldHide, setEnable, color } = useDrawProps
const { selfIndex, isActiveIndex, selfUser, ships } = useSession() const { selfIndex, selfIsActiveIndex, selfUser, ships } = useSession()
const navigator = useNavigate() const navigator = useNavigate()
const items = (): EventBarModes => ({ const items = (): EventBarModes => ({
@ -260,7 +260,7 @@ function EventBar(props: { clear: () => void }) {
// if (gameProps.gameState !== "running") return // if (gameProps.gameState !== "running") return
// const toastId = "otherPlayer" // const toastId = "otherPlayer"
// if (isActiveIndex) toast.dismiss(toastId) // if (selfIsActiveIndex) toast.dismiss(toastId)
// else // else
// toast.info("Waiting for other player...", { // toast.info("Waiting for other player...", {
// toastId, // toastId,
@ -299,7 +299,7 @@ function EventBar(props: { clear: () => void }) {
<For each={items()[gameProps.menu]}> <For each={items()[gameProps.menu]}>
{(e, i) => ( {(e, i) => (
<Show <Show
when={isActiveIndex() || gameProps.menu !== "main" || i() !== 1} when={selfIsActiveIndex() || gameProps.menu !== "main" || i() !== 1}
> >
<Item {...e} /> <Item {...e} />
</Show> </Show>
@ -319,12 +319,12 @@ function EventBar(props: { clear: () => void }) {
gameProps.mode >= 0 && gameProps.mode >= 0 &&
target().show, target().show,
callback: () => { callback: () => {
const i = selfIndex() const sIndex = selfIndex()
if (i === -1) return if (!sIndex) return
switch (gameProps.gameState) { switch (gameProps.gameState) {
case "starting": case "starting":
const isReady = !users[i]?.isReady const isReady = !users[sIndex.i]?.isReady
setIsReadyFor({ isReady, i }) setIsReadyFor({ isReady, i: sIndex.i })
socket.emit("isReady", isReady) socket.emit("isReady", isReady)
break break

View file

@ -2,15 +2,16 @@ import { faBurst, faXmark } from "@fortawesome/pro-solid-svg-icons"
import { For } from "solid-js" import { For } from "solid-js"
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon" import { FontAwesomeIcon } from "~/components/FontAwesomeIcon"
import { useSession } from "~/hooks/useSession" import { useSession } from "~/hooks/useSession"
import { compiledHits } from "~/lib/utils/helpers"
import { Hit } from "../../interfaces/frontend" import { Hit } from "../../interfaces/frontend"
function HitElems(props: { hits?: Hit[]; colorOverride?: string }) { function HitElems(props: { hits?: Hit[]; colorOverride?: string }) {
const { activeUser } = useSession() const { activeIndex } = useSession()
const hits = () => props?.hits const hits = () => props?.hits
const colorOverride = () => props?.colorOverride const colorOverride = () => props?.colorOverride
return ( return (
<For each={hits() ?? activeUser()?.hits()}> <For each={hits() ?? compiledHits(activeIndex())}>
{(props) => ( {(props) => (
<div class="hit-svg" style={{ "--x": props.x, "--y": props.y }}> <div class="hit-svg" style={{ "--x": props.x, "--y": props.y }}>
<FontAwesomeIcon <FontAwesomeIcon

View file

@ -18,7 +18,7 @@ function Ship(
) { ) {
const { selfIndex } = useSession() const { selfIndex } = useSession()
const filename = () => const filename = () =>
`ship_${selfIndex() > 0 ? "red" : "blue"}_${props.size}x_${ `ship_${selfIndex()?.i === 1 ? "red" : "blue"}_${props.size}x_${
props.variant props.variant
}.gif` }.gif`
let canvasRef: HTMLCanvasElement let canvasRef: HTMLCanvasElement

View file

@ -4,10 +4,10 @@ import { useSession } from "~/hooks/useSession"
import Ship from "./Ship" import Ship from "./Ship"
function Ships() { function Ships() {
const { isActiveIndex, selfUser } = useSession() const { selfIsActiveIndex, selfUser } = useSession()
return ( return (
<Show when={gameProps.gameState !== "running" || !isActiveIndex()}> <Show when={gameProps.gameState !== "running" || !selfIsActiveIndex()}>
<For each={selfUser()?.ships}>{(props) => <Ship {...props} />}</For> <For each={selfUser()?.ships}>{(props) => <Ship {...props} />}</For>
</Show> </Show>
) )

View file

@ -24,7 +24,7 @@ function Targets() {
each={composeTargetTiles( each={composeTargetTiles(
target(), target(),
gameProps.mode, gameProps.mode,
compiledHits(activeIndex() === 0 ? 1 : 0), compiledHits(activeIndex()),
)} )}
> >
{(props) => <GamefieldPointer {...props} />} {(props) => <GamefieldPointer {...props} />}
@ -33,7 +33,7 @@ function Targets() {
each={composeTargetTiles( each={composeTargetTiles(
targetPreview(), targetPreview(),
gameProps.mode, gameProps.mode,
compiledHits(activeIndex() === 0 ? 1 : 0), compiledHits(activeIndex()),
)} )}
> >
{(props) => <GamefieldPointer {...props} preview />} {(props) => <GamefieldPointer {...props} preview />}

View file

@ -103,17 +103,18 @@ function LobbyFrame(props: { openSettings: () => void }) {
</p> </p>
} }
> >
<> <Player src="player_blue.png" i={0} userId={session()?.user?.id} />
<Player src="player_blue.png" i={0} userId={session()?.user?.id} /> <p class="font-farro m-4 text-6xl font-semibold">VS</p>
<p class="font-farro m-4 text-6xl font-semibold">VS</p> <Show
{users[1] ? ( when={users[1]}
<Player src="player_red.png" i={1} userId={session()?.user?.id} /> fallback={
) : (
<p class="font-farro w-96 text-center text-4xl font-medium"> <p class="font-farro w-96 text-center text-4xl font-medium">
<WithDots>Warte auf Spieler 2</WithDots> <WithDots>Warte auf Spieler 2</WithDots>
</p> </p>
)} }
</> >
<Player src="player_red.png" i={1} userId={session()?.user?.id} />
</Show>
</Show> </Show>
</div> </div>
<div class="flex items-center justify-around border-t-2 border-slate-900 p-4"> <div class="flex items-center justify-around border-t-2 border-slate-900 p-4">

View file

@ -74,25 +74,25 @@ function Player(props: { src: string; i: 0 | 1; userId?: string }) {
<Button <Button
type={ type={
player()?.isConnected player()?.isConnected
? player()?.isReady ? users[props.i]?.isReady
? "green" ? "green"
: "orange" : "orange"
: "gray" : "gray"
} }
latching latching
isLatched={player()?.isReady} isLatched={users[props.i]?.isReady}
onClick={() => { onClick={() => {
if (!player()) return if (!player()) return
socket.emit("isReady", !player()?.isReady) socket.emit("isReady", !users[props.i]?.isReady)
setIsReadyFor({ setIsReadyFor({
i: props.i, i: props.i,
isReady: !player()?.isReady, isReady: !users[props.i]?.isReady,
}) })
}} }}
disabled={!primary()} disabled={!primary()}
> >
Ready Ready
{player()?.isReady && player()?.isConnected ? ( {users[props.i]?.isReady && player()?.isConnected ? (
<FontAwesomeIcon icon={faCheck} class="ml-4 w-12" /> <FontAwesomeIcon icon={faCheck} class="ml-4 w-12" />
) : primary() ? ( ) : primary() ? (
<FontAwesomeIcon <FontAwesomeIcon

View file

@ -5,25 +5,11 @@ import {
import classNames from "classnames" import classNames from "classnames"
import { JSX } from "solid-js" import { JSX } from "solid-js"
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon" import { FontAwesomeIcon } from "~/components/FontAwesomeIcon"
import { import { gameProps, setGameSetting } from "~/hooks/useGameProps"
allowChat,
allowMarkDraw,
allowSpecials,
allowSpectators,
setGameSetting,
} from "~/hooks/useGameProps"
import { GameSettingKeys } from "../../../interfaces/frontend" import { GameSettingKeys } from "../../../interfaces/frontend"
function Setting(props: { children: JSX.Element; key: GameSettingKeys }) { function Setting(props: { children: JSX.Element; key: GameSettingKeys }) {
const state = () => { const state = () => gameProps[props.key]
const gameProps = {
allowChat,
allowMarkDraw,
allowSpecials,
allowSpectators,
}
return gameProps[props.key]()
}
return ( return (
<label class="flex items-center justify-between" for={props.key}> <label class="flex items-center justify-between" for={props.key}>

View file

@ -3,7 +3,7 @@ import { socket } from "~/lib/socket"
import { GamePropsSchema, GameState } from "~/lib/zodSchemas" import { GamePropsSchema, GameState } from "~/lib/zodSchemas"
// import { toast } from "react-toastify" // import { toast } from "react-toastify"
import { createSignal } from "solid-js" import { createSignal } from "solid-js"
import { createStore } from "solid-js/store" import { createStore, produce } from "solid-js/store"
import { getPayloadFromProps } from "~/lib/getPayloadFromProps" import { getPayloadFromProps } from "~/lib/getPayloadFromProps"
import { getPayloadwithChecksum } from "~/lib/getPayloadwithChecksum" import { getPayloadwithChecksum } from "~/lib/getPayloadwithChecksum"
import { import {
@ -16,6 +16,7 @@ import {
GameSettings, GameSettings,
MouseCursor, MouseCursor,
MoveDispatchProps, MoveDispatchProps,
Players,
ShipProps, ShipProps,
Target, Target,
TargetPreview, TargetPreview,
@ -53,7 +54,43 @@ export const [gameProps, setGameProps] =
createStore<GameProps>(initialGameProps) createStore<GameProps>(initialGameProps)
const initialUsers = { 0: null, 1: null } const initialUsers = { 0: null, 1: null }
export const [users, setUsers] = createStore<Users>(initialUsers) export const [users, setUsersStore] = createStore<Users>(initialUsers)
export function setUsers(
i: 0 | 1,
userInput: Players | Partial<NonNullable<User>>,
): void {
setUsersStore(i, (state) => {
if (0 in userInput) {
const user = userInput[i]
if (!user && state) return null
else if (!state)
return {
...user,
isReady: false,
isConnected: false,
}
else
return produce<NonNullable<typeof state>>((u) => {
Object.assign(u, user)
})(state)
} else {
const user = userInput
if (!state) {
console.error(
"Everything is fine! Still, this was unexpected and should probably be fixed. 😁",
userInput,
state,
)
console.trace(userInput)
return null
}
return produce<NonNullable<typeof state>>((u) => {
Object.assign(u, user)
})(state)
}
})
}
export const [target, setTarget] = createSignal<Target>(initlialTarget) export const [target, setTarget] = createSignal<Target>(initlialTarget)
export const [targetPreview, setTargetPreview] = createSignal<TargetPreview>( export const [targetPreview, setTargetPreview] = createSignal<TargetPreview>(
@ -63,28 +100,34 @@ export const [mouseCursor, setMouseCursor] =
createSignal<MouseCursor>(initlialMouseCursor) createSignal<MouseCursor>(initlialMouseCursor)
export function DispatchMove(move: MoveDispatchProps, index: 0 | 1) { export function DispatchMove(move: MoveDispatchProps, index: 0 | 1) {
setUsers(index, "moves", (e) => [...e, move]) setUsers(index, {
} moves: [...(users[index]?.moves ?? []), move],
export function setShips(ships: ShipProps[], index: 0 | 1) { isReady: false,
setUsers(index, "ships", ships)
}
export function removeShip({ size, variant, x, y }: ShipProps, index: 0 | 1) {
setUsers(index, "ships", (ships) => {
const indexToRemove = ships.findIndex(
(ship) =>
ship.size === size &&
ship.variant === variant &&
ship.x === x &&
ship.y === y,
)
return ships.filter((_, i) => i !== indexToRemove)
}) })
} }
export function setPlayer(newUsers: Users): string | null { export function setShips(ships: ShipProps[], index: 0 | 1) {
setUsers(index, { ships })
}
export function removeShip({ size, variant, x, y }: ShipProps, index: 0 | 1) {
const ships = users[index]?.ships ?? []
const indexToRemove = users[index]?.ships.findIndex(
(ship) =>
ship.size === size &&
ship.variant === variant &&
ship.x === x &&
ship.y === y,
)
setUsers(index, {
ships: ships.filter((_, i) => i !== indexToRemove),
})
}
export function setPlayer(newUsers: Players): string | null {
let hash: string | null = null let hash: string | null = null
setUsers(newUsers) setUsers(0, newUsers)
setUsers(1, newUsers)
const body = getPayloadwithChecksum(getPayloadFromProps()) const body = getPayloadwithChecksum(getPayloadFromProps())
if (!body.hash) { if (!body.hash) {
console.log("Something is wrong... ") console.log("Something is wrong... ")
@ -98,6 +141,7 @@ export function setPlayer(newUsers: Users): string | null {
setGameProps("hash", hash) setGameProps("hash", hash)
return hash return hash
} }
export function setSetting(newSettings: GameSettings): string | null { export function setSetting(newSettings: GameSettings): string | null {
let hash: string | null = null let hash: string | null = null
setGameProps("allowChat", (e) => newSettings.allowChat ?? e) setGameProps("allowChat", (e) => newSettings.allowChat ?? e)
@ -152,21 +196,11 @@ export function full(newProps: GamePropsSchema) {
allowSpecials: newProps.payload.game?.allowSpecials ?? false, allowSpecials: newProps.payload.game?.allowSpecials ?? false,
allowSpectators: newProps.payload.game?.allowSpectators ?? false, allowSpectators: newProps.payload.game?.allowSpectators ?? false,
}) })
const compiledUsers = [ setUsers(0, newProps.payload.users)
newProps.payload.users[0], setUsers(1, newProps.payload.users)
newProps.payload.users[1],
].map((user) =>
user
? {
...user,
isReady: false,
isConnected: false,
}
: null,
) as [User, User]
setUsers({ 0: compiledUsers[0], 1: compiledUsers[1] })
} }
} }
export function leave(cb: () => void) { export function leave(cb: () => void) {
socket.emit("leave", (ack) => { socket.emit("leave", (ack) => {
if (!ack) { if (!ack) {
@ -176,24 +210,28 @@ export function leave(cb: () => void) {
cb() cb()
}) })
} }
export function setIsReadyFor({ i, isReady }: { i: 0 | 1; isReady: boolean }) { export function setIsReadyFor({ i, isReady }: { i: 0 | 1; isReady: boolean }) {
setUsers(i, (e) => ({ ...e, isReady, isConnected: true })) setUsers(i, {
isReady: isReady,
isConnected: true,
})
} }
export function newGameState(newState: GameState) { export function newGameState(newState: GameState) {
setGameProps("gameState", newState) setGameProps("gameState", newState)
setUsers(0, (e) => (e && e.isReady ? { isReady: false } : e)) setUsers(0, { isReady: false })
setUsers(1, (e) => (e && e.isReady ? { isReady: false } : e)) setUsers(1, { isReady: false })
} }
export function setIsConnectedFor({
i, export function setIsConnectedFor(props: { i: 0 | 1; isConnected: boolean }) {
isConnected, setUsers(props.i, {
}: { isConnected: props.isConnected,
i: 0 | 1 })
isConnected: boolean if (props.isConnected) return
}) { setUsers(props.i, {
setUsers(i, "isConnected", isConnected) isReady: false,
if (isConnected) return })
setUsers(i, "isReady", false)
} }
export function reset() { export function reset() {
@ -201,5 +239,5 @@ export function reset() {
setTarget(initlialTarget) setTarget(initlialTarget)
setTargetPreview(initlialTargetPreview) setTargetPreview(initlialTargetPreview)
setMouseCursor(initlialMouseCursor) setMouseCursor(initlialMouseCursor)
setUsers(initialUsers) setUsersStore(initialUsers)
} }

View file

@ -1,5 +1,5 @@
import { Session } from "@auth/core/types" import { Session } from "@auth/core/types"
import objectHash from "object-hash" import stringify from "json-stable-stringify"
import { import {
JSX, JSX,
createContext, createContext,
@ -13,14 +13,14 @@ import { gameProps, setGameProps, users } from "./useGameProps"
const [state, setState] = createSignal<Session | null | undefined>(undefined) const [state, setState] = createSignal<Session | null | undefined>(undefined)
const selfIndex = () => { function selfIndex(): { i: 0 | 1 } | null {
switch (state()?.user?.id) { switch (state()?.user?.id) {
case users[0]?.id: case users[0]?.id:
return 0 return { i: 0 }
case users[1]?.id: case users[1]?.id:
return 1 return { i: 1 }
default: default:
return -1 return null
} }
} }
@ -31,23 +31,22 @@ const activeIndex = () => {
return l1 > l2 ? 1 : 0 return l1 > l2 ? 1 : 0
} }
const isActiveIndex = () => { const selfIsActiveIndex = () => {
const sI = selfIndex() const sIndex = selfIndex()
return sI >= 0 && activeIndex() === sI return !!sIndex && activeIndex() === sIndex.i
} }
const selfUser = () => { const selfUser = () => {
const i = selfIndex() const sIndex = selfIndex()
if (i === -1) return null return sIndex ? users[sIndex.i] : null
return users[i]
} }
/** /**
* It should be the opposite of `activeIndex`. * It should be the opposite of `activeIndex`.
* *
* This is because `activeIndex` is attacking the `activeUser`. * This is because `activeIndex` is attacking the `enemyUser`.
*/ */
const activeUser = () => users[activeIndex() === 0 ? 1 : 0] const enemyUser = () => users[activeIndex() === 0 ? 1 : 0]
const ships = () => selfUser()?.ships ?? [] const ships = () => selfUser()?.ships ?? []
@ -55,9 +54,9 @@ const contextValue = {
session: state, session: state,
selfIndex, selfIndex,
activeIndex, activeIndex,
isActiveIndex, selfIsActiveIndex,
selfUser, selfUser,
activeUser, activeUser: enemyUser,
ships, ships,
} }
export const SessionCtx = createContext(contextValue) export const SessionCtx = createContext(contextValue)
@ -77,7 +76,7 @@ export function SessionProvider(props: { children: JSX.Element }) {
createEffect(() => { createEffect(() => {
const session = data() const session = data()
const hashDiff = objectHash(session ?? null) !== objectHash(state() ?? null) const hashDiff = stringify(session) !== stringify(state())
if (session === undefined || data.loading || !hashDiff) return if (session === undefined || data.loading || !hashDiff) return
console.log("Session updated.") console.log("Session updated.")
setState(session) setState(session)
@ -85,7 +84,9 @@ export function SessionProvider(props: { children: JSX.Element }) {
createEffect(() => { createEffect(() => {
if (gameProps.gameState !== "running") return if (gameProps.gameState !== "running") return
if (activeIndex() === selfIndex()) {
const sIndex = selfIndex()
if (activeIndex() === sIndex?.i) {
setGameProps("menu", "moves") setGameProps("menu", "moves")
setGameProps("mode", 0) setGameProps("mode", 0)
} else { } else {

View file

@ -3,7 +3,6 @@ import status from "http-status"
import { createEffect, createSignal, onCleanup } from "solid-js" import { createEffect, createSignal, onCleanup } from "solid-js"
import { useNavigate } from "solid-start" import { useNavigate } from "solid-start"
import { socket } from "~/lib/socket" import { socket } from "~/lib/socket"
import { frontendUsers } from "~/lib/utils/helpers"
import { GamePropsSchema, GameState } from "~/lib/zodSchemas" import { GamePropsSchema, GameState } from "~/lib/zodSchemas"
import { isAuthenticated } from "~/routes/start" import { isAuthenticated } from "~/routes/start"
import { GameSettings, PlayerEvent } from "../interfaces/frontend" import { GameSettings, PlayerEvent } from "../interfaces/frontend"
@ -28,17 +27,18 @@ function useSocket() {
const navigator = useNavigate() const navigator = useNavigate()
const isConnected = () => { const isConnected = () => {
const i = selfIndex() const sIndex = selfIndex()
return i !== -1 return sIndex
? users[i]?.isConnected && isConnectedState() ? users[sIndex.i]?.isConnected && isConnectedState()
: isConnectedState() : isConnectedState()
} }
createEffect(() => { createEffect(() => {
const i = selfIndex() const sIndex = selfIndex()
if (i === -1) return if (!sIndex) return
if (!users[sIndex.i]) return
setIsConnectedFor({ setIsConnectedFor({
i, i: sIndex.i,
isConnected: isConnectedState(), isConnected: isConnectedState(),
}) })
}) })
@ -68,7 +68,17 @@ function useSocket() {
const playerEvent = (event: PlayerEvent) => { const playerEvent = (event: PlayerEvent) => {
const { type, i } = event const { type, i } = event
let message: string let message: string
console.log("playerEvent", type) if (type !== "disconnect") {
const { hash } = event
const newHash = setPlayer(event.users)
if (newHash && newHash !== hash) {
console.log("hash", hash, newHash)
socket.emit("update", (body) => {
console.log("Update is needed after", type)
full(body)
})
}
}
switch (type) { switch (type) {
case "disconnect": case "disconnect":
setIsConnectedFor({ setIsConnectedFor({
@ -87,9 +97,8 @@ function useSocket() {
i, i,
isConnected: true, isConnected: true,
}) })
const index = selfIndex() const sIndex = selfIndex()
if (index !== -1) if (sIndex) socket.emit("isReady", users[sIndex.i]?.isReady ?? false)
socket.emit("isReady", users[index]?.isReady ?? false)
message = "Player has joined the lobby." message = "Player has joined the lobby."
break break
@ -99,19 +108,13 @@ function useSocket() {
} }
// toast.info(message, { toastId: message }) // toast.info(message, { toastId: message })
console.log(message) console.log(message)
if (type === "disconnect") return
const { hash } = event
const newHash = setPlayer(frontendUsers(event.users))
if (!newHash || newHash === hash) return
console.log("hash", hash, newHash)
socket.emit("update", (body) => {
console.log("Update is needed after ", type)
full(body)
})
} }
const setGameState = (state: GameState) => setGameProps("gameState", state) const setGameState = (state: GameState) => {
setGameProps("gameState", state)
setIsReadyFor({ i: 0, isReady: false })
setIsReadyFor({ i: 1, isReady: false })
}
const gameSetting = (newSettings: GameSettings, hash: string) => { const gameSetting = (newSettings: GameSettings, hash: string) => {
const newHash = setSetting(newSettings) const newHash = setSetting(newSettings)

View file

@ -55,14 +55,9 @@ interface InterServerEvents {
} }
interface SocketData { interface SocketData {
props: {
userId: string
gameId: string
index: 0 | 1
}
user: Session["user"] user: Session["user"]
gameId: string gameId: string
index: 0 | 1 index: { i: 0 | 1 } | null
} }
export type sServer = Server< export type sServer = Server<

View file

@ -1,6 +1,7 @@
import hash from "object-hash" import stringify from "json-stable-stringify"
import objectHash from "object-hash"
import { GamePropsSchema } from "./zodSchemas" import { GamePropsSchema } from "./zodSchemas"
export const getPayloadwithChecksum = ( export const getPayloadwithChecksum = (
payload: GamePropsSchema["payload"], payload: GamePropsSchema["payload"],
): GamePropsSchema => ({ payload, hash: hash(payload) }) ): GamePropsSchema => ({ payload, hash: objectHash(stringify(payload)) })

View file

@ -4,14 +4,12 @@ import type {
Hit, Hit,
IndexedPosition, IndexedPosition,
Mode, Mode,
Players,
PointerProps, PointerProps,
Position, Position,
ShipProps, ShipProps,
Target, Target,
TargetList, TargetList,
TargetPreview, TargetPreview,
User,
} from "../../interfaces/frontend" } from "../../interfaces/frontend"
import { MoveType, Orientation } from "../zodSchemas" import { MoveType, Orientation } from "../zodSchemas"
@ -227,14 +225,14 @@ export function intersectingShip(
export function compiledHits(i: 0 | 1) { export function compiledHits(i: 0 | 1) {
return ( return (
users[i === 0 ? 1 : 0]?.moves.reduce((hits, move) => { users[i]?.moves.reduce((hits, move) => {
const list = targetList(move, move.type) const list = targetList(move, move.type)
return move.type === MoveType.Enum.radar return move.type === MoveType.Enum.radar
? hits ? hits
: [ : [
...hits, ...hits,
...list.map(({ x, y }) => ({ ...list.map(({ x, y }) => ({
hit: !!intersectingShip(users[i]?.ships ?? [], { hit: !!intersectingShip(users[i === 0 ? 1 : 0]?.ships ?? [], {
...move, ...move,
size: 1, size: 1,
variant: 0, variant: 0,
@ -246,16 +244,3 @@ export function compiledHits(i: 0 | 1) {
}, [] as Hit[]) ?? [] }, [] as Hit[]) ?? []
) )
} }
export const frontendUsers = (users: Players) => {
const compiledUsers = [users[0], users[1]].map((user) =>
user
? {
...user,
isReady: false,
isConnected: false,
}
: null,
) as [User, User]
return { 0: compiledUsers[0], 1: compiledUsers[1] }
}

View file

@ -104,22 +104,9 @@ export function composeBody(
...props, ...props,
...user, ...user,
})) }))
const emptyUser = {
id: "",
name: "",
chats: [],
moves: [],
ships: [],
}
const composedUsers = { const composedUsers = {
0: mappedUsers.find((e) => e.index === 0) ?? { 0: mappedUsers.find((e) => e.index === 0) ?? null,
index: 0, 1: mappedUsers.find((e) => e.index === 1) ?? null,
...emptyUser,
},
1: mappedUsers.find((e) => e.index === 1) ?? {
index: 1,
...emptyUser,
},
} }
const payload = { const payload = {
game: game, game: game,

View file

@ -65,12 +65,12 @@ export async function GET({
const { payload, hash } = composeBody(game) const { payload, hash } = composeBody(game)
const index = payload.users[0]?.id === socket.data.user?.id ? 0 : 1 const index = payload.users[0]?.id === socket.data.user?.id ? 0 : 1
if (index !== 0 && index !== 1) return next(new Error(status["401"])) if (index !== 0 && index !== 1) return next(new Error(status["401"]))
socket.data.index = index socket.data.index = { i: index }
socket.data.gameId = game.id socket.data.gameId = game.id
socket.join(game.id) socket.join(game.id)
socket.to(game.id).emit("playerEvent", { socket.to(game.id).emit("playerEvent", {
type: "connect", type: "connect",
i: socket.data.index, i: socket.data.index.i,
users: payload.users, users: payload.users,
hash, hash,
}) })
@ -93,9 +93,9 @@ export async function GET({
socket.on("update", async (cb) => { socket.on("update", async (cb) => {
const game = await getGameById(socket.data.gameId ?? "") const game = await getGameById(socket.data.gameId ?? "")
if (!game) return if (!game || !socket.data.index) return
if (socket.data.index === 1 && game.users.length === 1) if (socket.data.index.i === 1 && game.users.length === 1)
socket.data.index = 0 socket.data.index.i = 0
const body = composeBody(game) const body = composeBody(game)
cb(body) cb(body)
}) })
@ -122,6 +122,7 @@ export async function GET({
socket.on("ping", (callback) => callback()) socket.on("ping", (callback) => callback())
socket.on("leave", async (cb) => { socket.on("leave", async (cb) => {
if (!socket.data.index) return
const user_Game = ( const user_Game = (
await db await db
.delete(user_games) .delete(user_games)
@ -170,7 +171,7 @@ export async function GET({
const { payload, hash } = body const { payload, hash } = body
socket.to(socket.data.gameId).emit("playerEvent", { socket.to(socket.data.gameId).emit("playerEvent", {
type: "leave", type: "leave",
i: socket.data.index, i: socket.data.index.i,
users: payload.users, users: payload.users,
hash, hash,
}) })
@ -183,13 +184,10 @@ export async function GET({
}) })
socket.on("isReady", async (isReady) => { socket.on("isReady", async (isReady) => {
if (socket.data.index === undefined || !socket.data.gameId) return if (!socket.data.index || !socket.data.gameId) return
socket socket
.to(socket.data.gameId) .to(socket.data.gameId)
.emit("isReady", { i: socket.data.index, isReady }) .emit("isReady", { i: socket.data.index.i, isReady })
socket
.to(socket.data.gameId)
.emit("isConnected", { i: socket.data.index, isConnected: true })
}) })
socket.on("canvas-state", (state) => { socket.on("canvas-state", (state) => {
@ -197,7 +195,7 @@ export async function GET({
console.log("received canvas state") console.log("received canvas state")
socket socket
.to(socket.data.gameId) .to(socket.data.gameId)
.emit("canvas-state-from-server", state, socket.data.index) .emit("canvas-state-from-server", state, socket.data.index.i)
}) })
socket.on("draw-line", ({ prevPoint, currentPoint, color }) => { socket.on("draw-line", ({ prevPoint, currentPoint, color }) => {
@ -207,7 +205,7 @@ export async function GET({
.emit( .emit(
"draw-line", "draw-line",
{ prevPoint, currentPoint, color }, { prevPoint, currentPoint, color },
socket.data.index, socket.data.index.i,
) )
}) })
@ -217,7 +215,7 @@ export async function GET({
}) })
socket.on("gameState", async (newState) => { socket.on("gameState", async (newState) => {
if (socket.data.index !== 0 || !socket.data.gameId) return if (socket.data.index?.i !== 0 || !socket.data.gameId) return
await db await db
.update(games) .update(games)
.set({ .set({
@ -228,11 +226,7 @@ export async function GET({
}) })
socket.on("ships", async (shipsData) => { socket.on("ships", async (shipsData) => {
if ( if (!socket.data.gameId || !socket.data.user?.id || !socket.data.index)
!socket.data.gameId ||
!socket.data.user?.id ||
typeof socket.data.index === "undefined"
)
return return
const user_Game = await db.query.user_games.findFirst({ const user_Game = await db.query.user_games.findFirst({
@ -254,15 +248,11 @@ export async function GET({
socket socket
.to(socket.data.gameId) .to(socket.data.gameId)
.emit("ships", shipsData, socket.data.index) .emit("ships", shipsData, socket.data.index.i)
}) })
socket.on("dispatchMove", async (props) => { socket.on("dispatchMove", async (props) => {
if ( if (!socket.data.gameId || !socket.data.user?.id || !socket.data.index)
!socket.data.gameId ||
!socket.data.user?.id ||
typeof socket.data.index === "undefined"
)
return return
const user_Game = await db.query.user_games.findFirst({ const user_Game = await db.query.user_games.findFirst({
@ -280,7 +270,11 @@ export async function GET({
.values({ ...props, id: createId(), user_game_id: user_Game.id }) .values({ ...props, id: createId(), user_game_id: user_Game.id })
.returning() .returning()
io.to(socket.data.gameId).emit("dispatchMove", props, socket.data.index) io.to(socket.data.gameId).emit(
"dispatchMove",
props,
socket.data.index.i,
)
}) })
socket.on("disconnecting", async () => { socket.on("disconnecting", async () => {
@ -289,10 +283,10 @@ export async function GET({
["debug"], ["debug"],
request, request,
) )
if (!socket.data.gameId) return if (!socket.data.gameId || !socket.data.index) return
socket.to(socket.data.gameId).emit("playerEvent", { socket.to(socket.data.gameId).emit("playerEvent", {
type: "disconnect", type: "disconnect",
i: socket.data.index, i: socket.data.index.i,
}) })
}) })