From bc609b62148b246e74286a0c5fc3b58df7808085 Mon Sep 17 00:00:00 2001 From: Adrien Burgun Date: Wed, 19 Jul 2023 21:24:46 +0200 Subject: [PATCH] :sparkles: Proper multiplayer system --- server.ts | 9 +++- src/components/Connect.module.css | 54 +++++++++++++++++++++++ src/components/Connect.tsx | 65 ++++++++++++++++++++++++++++ src/components/Document.astro | 18 ++++++++ src/components/Game.module.css | 21 +++++++++ src/components/Game.tsx | 44 +++++++++++-------- src/components/MatchMaker.module.css | 31 +++++++++++++ src/components/MatchMaker.tsx | 41 ++++++++++++++++++ src/components/Multiplayer.astro | 26 ----------- src/components/Multiplayer.tsx | 48 ++++++++++++++++++++ src/components/Status.module.css | 28 ++++++++++++ src/components/Status.tsx | 42 ++++++++++++++++++ src/consts.ts | 2 + src/game.ts | 18 ++++++-- src/pages/index.astro | 4 +- src/pages/play.astro | 4 +- 16 files changed, 403 insertions(+), 52 deletions(-) create mode 100644 src/components/Connect.module.css create mode 100644 src/components/Connect.tsx create mode 100644 src/components/Game.module.css create mode 100644 src/components/MatchMaker.module.css create mode 100644 src/components/MatchMaker.tsx delete mode 100644 src/components/Multiplayer.astro create mode 100644 src/components/Multiplayer.tsx create mode 100644 src/components/Status.module.css create mode 100644 src/components/Status.tsx diff --git a/server.ts b/server.ts index f9ac4d5..c9aa1d3 100644 --- a/server.ts +++ b/server.ts @@ -3,7 +3,12 @@ import { AcrossTheHex } from "./src/game.js"; const server = Server({ games: [AcrossTheHex], - origins: [Origins.LOCALHOST] + origins: [Origins.LOCALHOST], }); -server.run(3100); +server.run({ + port: 3100, + lobbyConfig: { + apiPort: 3200, + } +}); diff --git a/src/components/Connect.module.css b/src/components/Connect.module.css new file mode 100644 index 0000000..43cf169 --- /dev/null +++ b/src/components/Connect.module.css @@ -0,0 +1,54 @@ +.modal { + width: 100vw; + flex: 1; + display: flex; + justify-content: center; + align-items: center; +} + +.card { + padding: 0.5em 2em; + min-height: 4em; + width: fit-content; + border: 1px solid gray; + border-radius: 4px; + box-shadow: 0px 1em 1em -1em black; + display: flex; + flex-direction: column; + align-items: center; +} + +.card h2 { + margin-top: 0; + margin-bottom: 2rem; +} + +.inputs { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: 1em; +} + +.inputs input { + background: transparent; + color: inherit; + border: 1px solid gray; +} + +.setup-data { + text-align: center; + margin-top: 1em; +} + +.setup-data h3 { + font-size: inherit; + font-weight: bold; + margin-block: 0; +} + +.setup-data ul { + list-style: none; + padding-left: 0; +} diff --git a/src/components/Connect.tsx b/src/components/Connect.tsx new file mode 100644 index 0000000..287ac89 --- /dev/null +++ b/src/components/Connect.tsx @@ -0,0 +1,65 @@ +import { LobbyClient } from "boardgame.io/client"; +import { LOBBY_SERVER } from "../consts.js"; +import { Show, createResource, createSignal } from "solid-js"; +import classes from "./Connect.module.css"; +import Status from "./Status.tsx"; +import { DEFAULT_SETUP_DATA, type SetupData } from "../game.ts"; + +export type ConnectProps = { + gameName: string, + matchID: string, + setCredentials: (credentials: string) => void, + setPlayerID: (playerID: string) => void, + setSpectating: (spectating: boolean) => void, +}; + +export default function Connect(props: ConnectProps) { + const client = new LobbyClient({ + server: LOBBY_SERVER + }); + const [name, setName] = createSignal(""); + + const [match] = createResource(() => client.getMatch(props.gameName, props.matchID)); + + return
+
+

Join game

+ +
+ setName(event.currentTarget.value)} placeholder="Your name" /> + + +
+ + + + + {(setupData) => { + return (
+

Game settings:

+
    +
  • Size: {setupData().size ?? DEFAULT_SETUP_DATA.size}
  • +
  • Initial resources: {setupData().initialResources ?? DEFAULT_SETUP_DATA.initialResources}
  • +
+
); + }} +
+
+
; +} diff --git a/src/components/Document.astro b/src/components/Document.astro index 1938a86..95ea6a0 100644 --- a/src/components/Document.astro +++ b/src/components/Document.astro @@ -1,4 +1,5 @@ --- +import { BASE_URL } from "../consts.ts"; export type Props = { title?: string, @@ -21,9 +22,26 @@ export type Props = { background: #202020; color: white; } + + h1 { + font-weight: normal; + font-size: 16pt; + font-family: monospace; + text-align: center; + } + + h1 a { + color: inherit; + text-decoration: none; + } + + h1 a:hover { + text-decoration: underline; + } +

Across the Hex

diff --git a/src/components/Game.module.css b/src/components/Game.module.css new file mode 100644 index 0000000..25ff596 --- /dev/null +++ b/src/components/Game.module.css @@ -0,0 +1,21 @@ +.game { + display: grid; + grid-template: + "board players" auto + "toolbar toolbar" auto + / auto 20ch; + margin-left: 1em; + margin-right: 1em; +} + +.board { + grid-area: board; +} + +.players { + grid-area: players; +} + +.toolbar { + grid-area: toolbar; +} diff --git a/src/components/Game.tsx b/src/components/Game.tsx index 7302e17..ce4425b 100644 --- a/src/components/Game.tsx +++ b/src/components/Game.tsx @@ -8,12 +8,16 @@ import Toolbar, { Tool } from "./Toolbar.jsx"; import { GHOST_COLORS, PLAYER_COLORS, SOCKETIO_SERVER } from "../consts.js"; import { TileImages, loadTileImages } from "./tiles.js"; import clone from "../clone.js"; +import classes from "./Game.module.css"; +import Status from "./Status.tsx"; export type GameProps = ({ playerID: string, solo: true, + matchID?: undefined, } | { playerID: string, + credentials: string, solo?: false, matchID: string, } | { @@ -31,8 +35,9 @@ export default function Game(props: GameProps) { server: SOCKETIO_SERVER }), debug: props.debug ?? false, - ...("matchID" in props ? { matchID: props.matchID } : {}), - ...(props.playerID ? { playerID: props.playerID } : {}) + ...(props.matchID ? { matchID: props.matchID } : {}), + ...(props.playerID ? { playerID: props.playerID } : {}), + ...("credentials" in props ? { credentials: props.credentials } : {}), }); const [state, setState] = createStore(clone(client.getState()?.G) ?? { resources: {}, @@ -258,23 +263,28 @@ export default function Game(props: GameProps) { }); }); - return
+ return
- { - setWidth(width); - setHeight(height); - draw(); - }} - style={{ - width: "600px", - height: "600px", - }} - /> +
+ { + setWidth(width); + setHeight(height); + draw(); + }} + style={{ + width: "600px", + height: "600px", + }} + /> +
+ + {(matchID) =>
} +
{(playerID) => { - return "cells" in state ? game.getResourceGain(state.cells, playerID()) : 0} setReady={() => moves().setReady?.()} ready={() => stage() === "ready"} - />; + />
; }} diff --git a/src/components/MatchMaker.module.css b/src/components/MatchMaker.module.css new file mode 100644 index 0000000..d5767a9 --- /dev/null +++ b/src/components/MatchMaker.module.css @@ -0,0 +1,31 @@ +.matchmaker { + flex: 1; + display: flex; + flex-direction: column; + gap: 1em; + justify-content: center; + align-items: center; +} + +.setting > input[type="number"] { + appearance: textfield; + margin-left: 1em; + width: 4em; + background-color: transparent; + color: white; + border: 1px solid gray; + padding: 2px; + border-radius: 2px; + + transition: 0.2s background-color; +} + +.setting > input[type="number"]:hover { + background-color: black; +} + +.buttons { + display: flex; + flex-direction: row; + gap: 1em; +} diff --git a/src/components/MatchMaker.tsx b/src/components/MatchMaker.tsx new file mode 100644 index 0000000..58fc218 --- /dev/null +++ b/src/components/MatchMaker.tsx @@ -0,0 +1,41 @@ +import { LobbyClient } from "boardgame.io/client"; +import { BASE_URL, DOMAIN, LOBBY_SERVER } from "../consts.ts"; +import { AcrossTheHex, SetupData } from "../game.ts"; +import classes from "./MatchMaker.module.css"; +import { createSignal } from "solid-js"; + + +export default function MatchMaker() { + const client = new LobbyClient({ + server: LOBBY_SERVER + }); + + const [gridSize, setGridSize] = createSignal(4); + const [initialResources, setInitialResources] = createSignal(2); + + return
+
+ Grid size: + setGridSize(+event.currentTarget.value)} /> +
+
+ Initial resources: + setInitialResources(+event.currentTarget.value)} /> +
+
+ + +
+
; +} diff --git a/src/components/Multiplayer.astro b/src/components/Multiplayer.astro deleted file mode 100644 index 6a44073..0000000 --- a/src/components/Multiplayer.astro +++ /dev/null @@ -1,26 +0,0 @@ ---- -import Game from "./Game.tsx"; - -export type Props = { - playerID: string | undefined, - matchID: string, - debug: boolean, -}; - ---- - -{ - Astro.props.playerID - ? - : -} - diff --git a/src/components/Multiplayer.tsx b/src/components/Multiplayer.tsx new file mode 100644 index 0000000..6de5b17 --- /dev/null +++ b/src/components/Multiplayer.tsx @@ -0,0 +1,48 @@ +import { Match, Switch, createSignal } from "solid-js"; +import Game from "./Game.js"; +import Connect from "./Connect.tsx"; +import { AcrossTheHex } from "../game.ts"; + +export type MultiplayerProps = { + matchID: string, + debug: boolean, +}; + +export default function Multiplayer(props: MultiplayerProps) { + const [credentials, setCredentials] = createSignal(); + const [playerID, setPlayerID] = createSignal(); + const [spectating, setSpectating] = createSignal(false); + + + return ( + + + {(_) => { + return ; + }} + + + + + + {(_) => { + return ; + }} + + + ); +} diff --git a/src/components/Status.module.css b/src/components/Status.module.css new file mode 100644 index 0000000..2c8f7bb --- /dev/null +++ b/src/components/Status.module.css @@ -0,0 +1,28 @@ +.users { + list-style: none; + padding-left: 0; + border: 1px solid black; + width: 20ch; +} + +.users > li:not(:first-child) { + border-top: 1px solid black; +} + +.player-icon { + display: inline-block; + width: 0.75em; + height: 0.75em; + background: var(--color); + margin-inline: 0.5em; + line-height: 0.75em; + border-radius: 50%; +} + +.connected { + color: white; +} + +.logged-out { + color: gray; +} diff --git a/src/components/Status.tsx b/src/components/Status.tsx new file mode 100644 index 0000000..0b032a8 --- /dev/null +++ b/src/components/Status.tsx @@ -0,0 +1,42 @@ +import { LobbyClient } from "boardgame.io/client" +import { LOBBY_SERVER, PLAYER_COLORS } from "../consts.ts"; +import { For, Show, createEffect, createResource, onCleanup } from "solid-js"; +import classes from "./Status.module.css"; + +export type StatusProps = { + gameName: string, + matchID: string, +} + +export default function Status(props: StatusProps) { + const client = new LobbyClient({ + server: LOBBY_SERVER + }); + + const [match, actions] = createResource(() => client.getMatch(props.gameName, props.matchID)); + + createEffect(() => { + setTimeout(() => { + actions.refetch(); + }, 1000); + + const interval = setInterval(() => { + actions.refetch(); + }, 10000); + + onCleanup(() => clearInterval(interval)); + }); + + return
    + Loading...}> + {(player, index) => { + return
  • +
    + No player}>{player.name} +
  • ; + }} +
    +
; +} diff --git a/src/consts.ts b/src/consts.ts index 1e41db4..e9f028f 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -2,4 +2,6 @@ export const PLAYER_COLORS = ["#f06040", "#4050f0"]; export const GHOST_COLORS = ["#c05a47", "#4750c0"]; export const BASE_URL = "/"; +export const DOMAIN = "http://localhost:3000"; export const SOCKETIO_SERVER = "localhost:3100"; +export const LOBBY_SERVER = "http://localhost:3200/"; diff --git a/src/game.ts b/src/game.ts index 447b21c..11ab26a 100644 --- a/src/game.ts +++ b/src/game.ts @@ -143,11 +143,23 @@ export type Moves = Partial void, }>>; -export const AcrossTheHex: Game, { +export type SetupData = { size?: number, initialResources?: number -}> = { - setup({ ctx }, { size = 4, initialResources = 2 } = {}) { +}; + +export const DEFAULT_SETUP_DATA: Required = { + size: 4, + initialResources: 2, +} + +export const AcrossTheHex: Game, SetupData> = { + name: "across-the-hex", + setup({ ctx }, setupData = {}) { + const { size, initialResources } = { + ...DEFAULT_SETUP_DATA, + ...setupData + }; const cells = initGrid(size); const players = new Array(ctx.numPlayers).fill(null).map((_, id) => id.toString()); diff --git a/src/pages/index.astro b/src/pages/index.astro index 752804d..2e0e982 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,11 +1,11 @@ --- import Document from "../components/Document.astro"; -import Game from "../components/Game.tsx"; +import MatchMaker from "../components/MatchMaker.tsx"; export const prerender = true; --- -

Across the Hex

+
diff --git a/src/pages/play.astro b/src/pages/play.astro index 5b98bac..42577ea 100644 --- a/src/pages/play.astro +++ b/src/pages/play.astro @@ -1,6 +1,6 @@ --- import Document from "../components/Document.astro"; -import Multiplayer from "../components/Multiplayer.astro"; +import Multiplayer from "../components/Multiplayer.tsx"; import Singleplayer from "../components/Singleplayer.astro"; const playerID = Astro.url.searchParams.get("player") ?? undefined; @@ -10,5 +10,5 @@ const debug = Astro.url.searchParams.has("debug"); --- - {matchID ? : } + {matchID ? : }