Fix some game logic

This commit is contained in:
aronmal 2023-09-03 18:48:06 +02:00
parent b4fd992611
commit b067747d48
Signed by: aronmal
GPG key ID: 816B7707426FC612
15 changed files with 90 additions and 98 deletions

View file

@ -29,6 +29,8 @@ export function FontAwesomeIcon(
return ( return (
<svg <svg
aria-hidden="true" aria-hidden="true"
role="img"
xmlns="http://www.w3.org/2000/svg"
aria-labelledby={props.titleId} aria-labelledby={props.titleId}
data-prefix={props.icon.prefix} data-prefix={props.icon.prefix}
data-icon={props.icon.iconName} data-icon={props.icon.iconName}
@ -45,8 +47,6 @@ export function FontAwesomeIcon(
)} )}
color={props.color} color={props.color}
style={props.style} style={props.style}
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox={`0 0 ${props.icon.icon[0]} ${props.icon.icon[1]}`} viewBox={`0 0 ${props.icon.icon[0]} ${props.icon.icon[1]}`}
> >
<Show when={props.title}> <Show when={props.title}>

View file

@ -234,7 +234,7 @@ function EventBar(props: { clear: () => void }) {
iconColor: "green", iconColor: "green",
callback: async () => { callback: async () => {
socket.emit("gameState", "aborted") socket.emit("gameState", "aborted")
await navigator("/") navigator("/")
reset() reset()
}, },
}, },
@ -307,7 +307,7 @@ function EventBar(props: { clear: () => void }) {
</Show> </Show>
<For each={items()[menu()]}> <For each={items()[menu()]}>
{(e, i) => ( {(e, i) => (
<Show when={isActiveIndex() && menu() !== "main" && i() !== 1}> <Show when={isActiveIndex() || menu() !== "main" || i() !== 1}>
<Item {...e} /> <Item {...e} />
</Show> </Show>
)} )}

View file

@ -94,12 +94,6 @@ function Gamefield() {
reset() reset()
}) })
createEffect(() => {
if (gameId()) return
const timeout = setTimeout(() => navigator("/"), 5000)
return () => clearTimeout(timeout)
})
return ( return (
<div id="gamefield"> <div id="gamefield">
{/* <Bluetooth /> */} {/* <Bluetooth /> */}

View file

@ -20,6 +20,7 @@ function GamefieldPointer(
"--y1": props.y - 1, "--y1": props.y - 1,
"--y2": props.y + 2, "--y2": props.y + 2,
} }
return ( return (
<div <div
class={classNames("hit-svg", "target", props.type, ...props.edges, { class={classNames("hit-svg", "target", props.type, ...props.edges, {
@ -29,7 +30,7 @@ function GamefieldPointer(
})} })}
style={style()} style={style()}
> >
<FontAwesomeIcon icon={!isRadar ? faCrosshairs : faRadar} /> <FontAwesomeIcon icon={!isRadar() ? faCrosshairs : faRadar} />
</div> </div>
) )
} }

View file

@ -7,7 +7,7 @@ function Ships() {
const { isActiveIndex, selfUser } = useSession() const { isActiveIndex, selfUser } = useSession()
return ( return (
<Show when={gameState() === "running" && isActiveIndex}> <Show when={gameState() !== "running" || !isActiveIndex()}>
<For each={selfUser()?.ships()}>{(props) => <Ship {...props} />}</For> <For each={selfUser()?.ships()}>{(props) => <Ship {...props} />}</For>
</Show> </Show>
) )

View file

@ -13,8 +13,8 @@ import Ship from "./Ship"
function Targets() { function Targets() {
const { activeUser, ships } = useSession() const { activeUser, ships } = useSession()
const ship = shipProps(ships(), mode(), targetPreview()) const ship = () => shipProps(ships(), mode(), targetPreview())
const { fields, borders, score } = intersectingShip(ships(), ship) const { fields, borders, score } = intersectingShip(ships(), ship())
return ( return (
<Switch> <Switch>
@ -36,7 +36,7 @@ function Targets() {
when={gameState() === "starting" && mode() >= 0 && targetPreview().show} when={gameState() === "starting" && mode() >= 0 && targetPreview().show}
> >
<Ship <Ship
{...ship} {...ship()}
preview preview
warn={score > 0} warn={score > 0}
color={fields.length ? "red" : borders.length ? "orange" : undefined} color={fields.length ? "red" : borders.length ? "orange" : undefined}

View file

@ -25,7 +25,6 @@ import {
} from "../interfaces/frontend" } from "../interfaces/frontend"
export const [hash, setHash] = createSignal<string | null>(null) export const [hash, setHash] = createSignal<string | null>(null)
export const [activeIndex, setActiveIndex] = createSignal<0 | 1>(0)
export const [gamePin, setGamePin] = createSignal<string | null>(null) export const [gamePin, setGamePin] = createSignal<string | null>(null)
export const [gameId, setGameId] = createSignal<string>("") export const [gameId, setGameId] = createSignal<string>("")
export const [gameState, setGameState] = createSignal<GameState>("unknown") export const [gameState, setGameState] = createSignal<GameState>("unknown")
@ -53,17 +52,6 @@ export const users = {
}, },
} }
// export function setActiveIndex(i: number, selfIndex: number) {
// if (!payload()) return
// payload().activeIndex = i
// if (i === selfIndex) {
// setMenu("moves")
// setMode(0)
// } else {
// setMenu("main")
// setMode(-1)
// }
// }
export function DispatchMove(move: MoveDispatchProps, index: number) { export function DispatchMove(move: MoveDispatchProps, index: number) {
const list = targetList(move, move.type) const list = targetList(move, move.type)
users.forEach((user, i) => { users.forEach((user, i) => {
@ -182,7 +170,6 @@ export function full(newProps: GamePropsSchema) {
) )
setHash(newProps.hash) setHash(newProps.hash)
setActiveIndex(newProps.payload.activeIndex)
setGamePin(newProps.payload.gamePin) setGamePin(newProps.payload.gamePin)
setGameId(newProps.payload.game?.id ?? "") setGameId(newProps.payload.game?.id ?? "")
setGameState(newProps.payload.game?.state ?? "unknown") setGameState(newProps.payload.game?.state ?? "unknown")
@ -234,7 +221,6 @@ export function setIsConnectedFor({
export function reset() { export function reset() {
setHash(null) setHash(null)
setActiveIndex(0)
setGamePin(null) setGamePin(null)
setGameId("") setGameId("")
setGameState("unknown") setGameState("unknown")

View file

@ -1,27 +1,15 @@
import { Session } from "@auth/core/types" import { Session } from "@auth/core/types"
import { getSession } from "@auth/solid-start" import { getSession } from "@auth/solid-start"
import { import {
Accessor,
JSX, JSX,
createContext, createContext,
createEffect,
createSignal, createSignal,
useContext, useContext,
} from "solid-js" } from "solid-js"
import { createServerData$ } from "solid-start/server" import { createServerData$ } from "solid-start/server"
import { ShipProps } from "~/interfaces/frontend"
import { initialUser } from "~/lib/utils/helpers"
import { authOptions } from "~/server/auth" import { authOptions } from "~/server/auth"
import { activeIndex, users } from "./useGameProps" import { gameState, setMenu, setMode, users } from "./useGameProps"
interface Concext {
session: Accessor<Session | null>
selfIndex: () => 0 | 1 | -1
activeIndex: Accessor<0 | 1>
isActiveIndex: () => boolean
selfUser: () => ReturnType<typeof initialUser> | null
activeUser: () => ReturnType<typeof initialUser>
ships: () => ShipProps[]
}
const [state, setState] = createSignal<Session | null>(null) const [state, setState] = createSignal<Session | null>(null)
const selfIndex = () => { const selfIndex = () => {
@ -35,15 +23,29 @@ const selfIndex = () => {
} }
} }
const activeIndex = () => {
if (gameState() !== "running") return 0
const l1 = users[0].moves().length
const l2 = users[1].moves().length
return l1 > l2 ? 1 : 0
}
const isActiveIndex = () => { const isActiveIndex = () => {
const sI = selfIndex() const sI = selfIndex()
return sI >= 0 && activeIndex() === sI return sI >= 0 && activeIndex() === sI
} }
const selfUser = () => { const selfUser = () => {
const i = selfIndex() const i = selfIndex()
if (i === -1) return null if (i === -1) return null
return users[i] return users[i]
} }
/**
* It should be the opposite of `activeIndex`.
*
* This is because `activeIndex` is attacking the `activeUser`.
*/
const activeUser = () => users[activeIndex() === 0 ? 1 : 0] const activeUser = () => users[activeIndex() === 0 ? 1 : 0]
const ships = () => selfUser()?.ships() ?? [] const ships = () => selfUser()?.ships() ?? []
@ -56,7 +58,7 @@ const contextValue = {
activeUser, activeUser,
ships, ships,
} }
export const SessionCtx = createContext<Concext>(contextValue) export const SessionCtx = createContext(contextValue)
export function SessionProvider(props: { children: JSX.Element }) { export function SessionProvider(props: { children: JSX.Element }) {
const session = createServerData$( const session = createServerData$(
@ -67,6 +69,17 @@ export function SessionProvider(props: { children: JSX.Element }) {
)() )()
setState(session ?? null) setState(session ?? null)
createEffect(() => {
if (gameState() !== "running") return
if (activeIndex() === selfIndex()) {
setMenu("moves")
setMode(0)
} else {
setMenu("main")
setMode(-1)
}
})
return ( return (
<SessionCtx.Provider value={contextValue}> <SessionCtx.Provider value={contextValue}>
{props.children} {props.children}

View file

@ -10,12 +10,9 @@ import {
DispatchMove, DispatchMove,
full, full,
gameId, gameId,
gameState, setGameState,
setActiveIndex,
setIsConnectedFor, setIsConnectedFor,
setIsReadyFor, setIsReadyFor,
setMenu,
setMode,
setPlayer, setPlayer,
setSetting, setSetting,
setShips, setShips,
@ -27,7 +24,7 @@ import { useSession } from "./useSession"
function useSocket() { function useSocket() {
const [isConnectedState, setIsConnectedState] = createSignal(false) const [isConnectedState, setIsConnectedState] = createSignal(false)
const { selfIndex } = useSession() const { selfIndex } = useSession()
const navigate = useNavigate() const navigator = useNavigate()
const isConnected = () => { const isConnected = () => {
const i = selfIndex() const i = selfIndex()
@ -54,7 +51,7 @@ function useSocket() {
const connectError = (error: Error) => { const connectError = (error: Error) => {
console.log("Connection error:", error.message) console.log("Connection error:", error.message)
if (error.message === status["403"]) navigate("/") if (error.message === status["403"]) navigator("/")
if (error.message !== "xhr poll error") return if (error.message !== "xhr poll error") return
// const toastId = "connect_error" // const toastId = "connect_error"
// const isActive = toast.isActive(toastId) // const isActive = toast.isActive(toastId)
@ -122,17 +119,6 @@ function useSocket() {
}) })
} }
const activeIndex = (i: 0 | 1) => {
setActiveIndex(i)
if (i === selfIndex()) {
setMenu("moves")
setMode(0)
} else {
setMenu("main")
setMode(-1)
}
}
const disconnect = () => { const disconnect = () => {
console.log("disconnect") console.log("disconnect")
setIsConnectedState(false) setIsConnectedState(false)
@ -143,9 +129,8 @@ function useSocket() {
socket.on("gameSetting", gameSetting) socket.on("gameSetting", gameSetting)
socket.on("playerEvent", playerEvent) socket.on("playerEvent", playerEvent)
socket.on("isReady", setIsReadyFor) socket.on("isReady", setIsReadyFor)
socket.on("gameState", gameState) socket.on("gameState", setGameState)
socket.on("dispatchMove", DispatchMove) socket.on("dispatchMove", DispatchMove)
socket.on("activeIndex", activeIndex)
socket.on("ships", setShips) socket.on("ships", setShips)
socket.on("disconnect", disconnect) socket.on("disconnect", disconnect)
@ -155,9 +140,8 @@ function useSocket() {
socket.off("gameSetting", gameSetting) socket.off("gameSetting", gameSetting)
socket.off("playerEvent", playerEvent) socket.off("playerEvent", playerEvent)
socket.off("isReady", setIsReadyFor) socket.off("isReady", setIsReadyFor)
socket.off("gameState", gameState) socket.off("gameState", setGameState)
socket.off("dispatchMove", DispatchMove) socket.off("dispatchMove", DispatchMove)
socket.off("activeIndex", activeIndex)
socket.off("ships", setShips) socket.off("ships", setShips)
socket.off("disconnect", disconnect) socket.off("disconnect", disconnect)
} }
@ -172,7 +156,10 @@ function useSocket() {
.then(isAuthenticated) .then(isAuthenticated)
.then((game) => GamePropsSchema.parse(game)) .then((game) => GamePropsSchema.parse(game))
.then((res) => full(res)) .then((res) => full(res))
.catch((e) => console.log("Failed to get /api/game/running: ", e)) .catch((e) => {
console.log("Failed to get /api/game/running: ", e)
navigator("/")
})
return return
} }
if (isConnected()) return if (isConnected()) return

View file

@ -21,9 +21,6 @@ export interface SocketServer extends http.Server {
} }
export interface ServerToClientEvents { export interface ServerToClientEvents {
// noArg: () => void
// basicEmit: (a: number, b: string, c: Buffer) => void
// withAck: (d: string, ) => void
gameSetting: (payload: GameSettings, hash: string) => void gameSetting: (payload: GameSettings, hash: string) => void
playerEvent: (event: PlayerEvent) => void playerEvent: (event: PlayerEvent) => void
isReady: (payload: { i: 0 | 1; isReady: boolean }) => void isReady: (payload: { i: 0 | 1; isReady: boolean }) => void
@ -34,8 +31,7 @@ export interface ServerToClientEvents {
"canvas-clear": () => void "canvas-clear": () => void
gameState: (newState: GameState) => void gameState: (newState: GameState) => void
ships: (ships: ShipProps[], index: number) => void ships: (ships: ShipProps[], index: number) => void
activeIndex: (index: 0 | 1) => void dispatchMove: (props: MoveDispatchProps, i: 0 | 1) => void
dispatchMove: (props: MoveDispatchProps, i: number) => void
} }
export interface ClientToServerEvents { export interface ClientToServerEvents {

View file

@ -14,6 +14,7 @@ export default function sendResponse<T>(
result: Result<T>, result: Result<T>,
) { ) {
if (result.redirectUrl) { if (result.redirectUrl) {
logging("Redirect | " + result.message, result.type ?? ["debug"], request)
return redirect(result.redirectUrl) return redirect(result.redirectUrl)
} else { } else {
logging(result.message, result.type ?? ["debug"], request) logging(result.message, result.type ?? ["debug"], request)

View file

@ -1,5 +1,4 @@
import { import {
activeIndex,
allowChat, allowChat,
allowMarkDraw, allowMarkDraw,
allowSpecials, allowSpecials,
@ -30,6 +29,5 @@ export function getPayloadFromProps() {
ships: user.ships(), ships: user.ships(),
hits: user.hits(), hits: user.hits(),
})), })),
activeIndex: activeIndex(),
} }
} }

View file

@ -98,7 +98,6 @@ export const CreateSchema = z.object({
.nullable(), .nullable(),
gamePin: z.string().nullable(), gamePin: z.string().nullable(),
users: z.object({ 0: PlayerSchema, 1: PlayerSchema }), users: z.object({ 0: PlayerSchema, 1: PlayerSchema }),
activeIndex: z.literal(0).or(z.literal(1)),
}) })
export const GamePropsSchema = z.object({ export const GamePropsSchema = z.object({

View file

@ -1,9 +1,10 @@
import { getSession } from "@auth/solid-start" import { getSession } from "@auth/solid-start"
import { and, eq, exists, ne } from "drizzle-orm" import { and, eq, exists, ne, notExists } from "drizzle-orm"
import { APIEvent } from "solid-start/api" import { APIEvent } from "solid-start/api"
import db from "~/drizzle" import db from "~/drizzle"
import { games, user_games } from "~/drizzle/schemas/Tables" import { games, user_games } from "~/drizzle/schemas/Tables"
import { rejectionErrors } from "~/lib/backend/errors" import { rejectionErrors } from "~/lib/backend/errors"
import logging from "~/lib/backend/logging"
import sendResponse from "~/lib/backend/sendResponse" import sendResponse from "~/lib/backend/sendResponse"
import { getPayloadwithChecksum } from "~/lib/getPayloadwithChecksum" import { getPayloadwithChecksum } from "~/lib/getPayloadwithChecksum"
import { GamePropsSchema } from "~/lib/zodSchemas" import { GamePropsSchema } from "~/lib/zodSchemas"
@ -87,7 +88,15 @@ export const getRunningGameToUser = async (userId: string) => {
and( and(
ne(game.state, "ended"), ne(game.state, "ended"),
exists( exists(
db.select().from(user_games).where(eq(user_games.userId, userId)), db
.select()
.from(user_games)
.where(
and(
eq(user_games.gameId, game.id),
eq(user_games.userId, userId),
),
),
), ),
), ),
...gameSelects, ...gameSelects,
@ -122,17 +131,10 @@ export function composeBody(
hits: [], hits: [],
}, },
} }
let activeIndex: 0 | 1 = 0
if (game.state === "running") {
const l1 = users[0]?.moves.length ?? 0
const l2 = users[1]?.moves.length ?? 0
activeIndex = l1 > l2 ? 1 : 0
}
const payload = { const payload = {
game: game, game: game,
gamePin: gamePin?.pin ?? null, gamePin: gamePin?.pin ?? null,
users: composedUsers, users: composedUsers,
activeIndex,
} }
return getPayloadwithChecksum(payload) return getPayloadwithChecksum(payload)
} }
@ -144,6 +146,26 @@ export async function GET({ request }: APIEvent) {
return sendResponse(request, rejectionErrors.unauthorized) return sendResponse(request, rejectionErrors.unauthorized)
} }
const abandonedGames = await db
.delete(games)
.where(
notExists(
db.select().from(user_games).where(eq(user_games.gameId, games.id)),
),
)
.returning()
if (abandonedGames.length) {
logging(
"Games were deleted because of missing players. Id's: " +
JSON.stringify(
abandonedGames.map((e) => e.id),
null,
2,
),
["error"],
)
}
const { email, id } = session.user const { email, id } = session.user
const game = await getRunningGameToUser(id) const game = await getRunningGameToUser(id)

View file

@ -39,11 +39,12 @@ export async function GET({
// io.use(authenticate) // io.use(authenticate)
io.use(async (socket, next) => { io.use(async (socket, next) => {
const request = socket.request
try { try {
const url = process.env.AUTH_URL! + socket.request.url const url = process.env.AUTH_URL! + request.url
const session = await getSession( const session = await getSession(
new Request(url, { new Request(url, {
headers: socket.request.headers as Record<string, string>, headers: request.headers as Record<string, string>,
}), }),
authOptions, authOptions,
) )
@ -53,10 +54,10 @@ export async function GET({
const game = await getRunningGameToUser(socket.data.user?.id ?? "") const game = await getRunningGameToUser(socket.data.user?.id ?? "")
if (!game) { if (!game) {
logging( logging(
"Forbidden, no game found: " + "Authentication forbidden, no game found for user: " +
JSON.stringify(Array.from(socket.rooms)), JSON.stringify([socket.data.user?.id, session.user]),
["debug"], ["debug"],
socket.request, request,
) )
return next(new Error(status["403"])) return next(new Error(status["403"]))
} }
@ -76,7 +77,7 @@ export async function GET({
next() next()
} catch { } catch {
logging("Unkonwn error - " + status["401"], ["warn"], socket.request) logging("Unkonwn error - " + status["401"], ["warn"], request)
next(new Error(status["401"])) next(new Error(status["401"]))
} }
}) })
@ -87,7 +88,7 @@ export async function GET({
", " + ", " +
socket.id.cyan, socket.id.cyan,
["infoGreen"], ["infoGreen"],
socket.request, request,
) )
socket.on("update", async (cb) => { socket.on("update", async (cb) => {
@ -224,8 +225,6 @@ export async function GET({
}) })
.where(eq(games.id, socket.data.gameId)) .where(eq(games.id, socket.data.gameId))
io.to(socket.data.gameId).emit("gameState", newState) io.to(socket.data.gameId).emit("gameState", newState)
if (newState === "running")
io.to(socket.data.gameId).emit("activeIndex", 0)
}) })
socket.on("ships", async (shipsData) => { socket.on("ships", async (shipsData) => {
@ -280,18 +279,14 @@ 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()
const game = user_Game.game
const l1 = game.users[0].moves.length
const l2 = game.users[1].moves.length
io.to(socket.data.gameId).emit("dispatchMove", props, socket.data.index) io.to(socket.data.gameId).emit("dispatchMove", props, socket.data.index)
io.to(socket.data.gameId).emit("activeIndex", l1 > l2 ? 1 : 0)
}) })
socket.on("disconnecting", async () => { socket.on("disconnecting", async () => {
logging( logging(
"Disconnecting: " + JSON.stringify(Array.from(socket.rooms)), "Disconnecting: " + JSON.stringify(Array.from(socket.rooms)),
["debug"], ["debug"],
socket.request, request,
) )
if (!socket.data.gameId) return if (!socket.data.gameId) return
socket.to(socket.data.gameId).emit("playerEvent", { socket.to(socket.data.gameId).emit("playerEvent", {
@ -301,7 +296,7 @@ export async function GET({
}) })
socket.on("disconnect", () => { socket.on("disconnect", () => {
logging("Disconnect: " + socket.id, ["debug"], socket.request) logging("Disconnect: " + socket.id, ["debug"], request)
}) })
}) })
} }