remove references to testgame on the server, add gameId to router

This commit is contained in:
Alexander Bentkamp
2023-03-23 16:23:06 +01:00
parent a4623a8241
commit bf2315b474
14 changed files with 159 additions and 93 deletions
+7 -2
View File
@@ -1,6 +1,6 @@
import * as React from 'react';
import { useState, useEffect } from 'react';
import { Outlet } from "react-router-dom";
import { Outlet, useParams } from "react-router-dom";
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
@@ -10,10 +10,15 @@ import '@fontsource/roboto/700.css';
import './reset.css';
import './app.css';
export const GameIdContext = React.createContext<string>(undefined);
function App() {
const params = useParams();
return (
<div className="app">
<Outlet />
<GameIdContext.Provider value={params.gameId}>
<Outlet />
</GameIdContext.Provider>
</div>
)
}
+3 -2
View File
@@ -5,6 +5,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faLock, faLockOpen, faBook, faHammer, faBan } from '@fortawesome/free-solid-svg-icons'
import Markdown from './Markdown';
import { useLoadDocQuery, ComputedInventoryItem } from '../state/api';
import { GameIdContext } from '../App';
export function Inventory({ tactics, lemmas, definitions, setInventoryDoc } :
{lemmas: ComputedInventoryItem[],
@@ -77,8 +78,8 @@ function InventoryItem({name, displayName, locked, disabled, showDoc}) {
}
export function Documentation({name, type}) {
const doc = useLoadDocQuery({type: type, name: name})
const gameId = React.useContext(GameIdContext)
const doc = useLoadDocQuery({game: gameId, type: type, name: name})
return <>
<h2 className="doc">{doc.data?.displayName}</h2>
+20 -15
View File
@@ -40,6 +40,7 @@ import Markdown from './Markdown';
import Split from 'react-split'
import { Alert } from '@mui/material';
import { GameIdContext } from '../App';
export const MonacoEditorContext = React.createContext<monaco.editor.IStandaloneCodeEditor>(null as any);
@@ -125,9 +126,9 @@ function PlayableLevel({worldId, levelId}) {
}]);
}
const gameInfo = useGetGameInfoQuery()
const level = useLoadLevelQuery({world: worldId, level: levelId})
const gameId = React.useContext(GameIdContext)
const gameInfo = useGetGameInfoQuery({game: gameId})
const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
const dispatch = useAppDispatch()
@@ -209,8 +210,8 @@ function PlayableLevel({worldId, levelId}) {
<Markdown>{level?.data?.conclusion}</Markdown>
</div>
{levelId >= gameInfo.data?.worldSize[worldId] ?
<Button to={`/`}><FontAwesomeIcon icon={faHome} /></Button> :
<Button to={`/world/${worldId}/level/${levelId + 1}`}>
<Button to={`/game/${gameId}`}><FontAwesomeIcon icon={faHome} /></Button> :
<Button to={`/game/${gameId}/world/${worldId}/level/${levelId + 1}`}>
Next&nbsp;<FontAwesomeIcon icon={faArrowRight} /></Button>}
</div>}
@@ -230,7 +231,8 @@ function PlayableLevel({worldId, levelId}) {
export default Level
function Introduction({worldId}) {
const gameInfo = useGetGameInfoQuery()
const gameId = React.useContext(GameIdContext)
const gameInfo = useGetGameInfoQuery({game: gameId})
return <>
<div style={gameInfo.isLoading ? null : {display: "none"}} className="app-content loading"><CircularProgress /></div>
@@ -245,8 +247,8 @@ function Introduction({worldId}) {
</div>
<div className="conclusion">
{0 == gameInfo.data?.worldSize[worldId] ?
<Button to={`/`}><FontAwesomeIcon icon={faHome} /></Button> :
<Button to={`/world/${worldId}/level/1`}>
<Button to={`/game/${gameId}`}><FontAwesomeIcon icon={faHome} /></Button> :
<Button to={`/game/${gameId}/world/${worldId}/level/1`}>
Start&nbsp;<FontAwesomeIcon icon={faArrowRight} />
</Button>}
</div>
@@ -255,11 +257,12 @@ function Introduction({worldId}) {
}
function LevelAppBar({isLoading, levelId, worldId, levelTitle}) {
const gameInfo = useGetGameInfoQuery()
const gameId = React.useContext(GameIdContext)
const gameInfo = useGetGameInfoQuery({game: gameId})
return <div className="app-bar" style={isLoading ? {display: "none"} : null} >
<div>
<Button to={`/`}><FontAwesomeIcon icon={faHome} /></Button>
<Button to={`/game/${gameId}`}><FontAwesomeIcon icon={faHome} /></Button>
<span className="app-bar-title">
{gameInfo.data?.worlds.nodes[worldId].title && `World: ${gameInfo.data?.worlds.nodes[worldId].title}`}
</span>
@@ -269,10 +272,10 @@ function LevelAppBar({isLoading, levelId, worldId, levelTitle}) {
{levelTitle}
</span>
<Button disabled={levelId <= 0} inverted={true}
to={`/world/${worldId}/level/${levelId - 1}`}
to={`/game/${gameId}/world/${worldId}/level/${levelId - 1}`}
><FontAwesomeIcon icon={faArrowLeft} />&nbsp;Previous</Button>
<Button disabled={levelId >= gameInfo.data?.worldSize[worldId]} inverted={true}
to={`/world/${worldId}/level/${levelId + 1}`}
to={`/game/${gameId}/world/${worldId}/level/${levelId + 1}`}
>Next&nbsp;<FontAwesomeIcon icon={faArrowRight} /></Button>
</div>
@@ -282,6 +285,7 @@ function LevelAppBar({isLoading, levelId, worldId, levelTitle}) {
function useLevelEditor(worldId: string, levelId: number, codeviewRef, initialCode, initialSelections, onDidChangeContent, onDidChangeSelection) {
const connection = React.useContext(ConnectionContext)
const gameId = React.useContext(GameIdContext)
const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor|null>(null)
const [infoProvider, setInfoProvider] = useState<null|InfoProvider>(null)
@@ -308,7 +312,7 @@ function useLevelEditor(worldId: string, levelId: number, codeviewRef, initialCo
theme: 'vs-code-theme-converted'
})
const infoProvider = new InfoProvider(connection.getLeanClient())
const infoProvider = new InfoProvider(connection.getLeanClient(gameId))
const editorApi = infoProvider.getApi()
@@ -358,7 +362,7 @@ function useLevelEditor(worldId: string, levelId: number, codeviewRef, initialCo
return () => { infoProvider.dispose(); editor.dispose() }
}, [])
const {leanClient, leanClientStarted} = useLeanClient()
const {leanClient, leanClientStarted} = useLeanClient(gameId)
// Create model when level changes
useEffect(() => {
@@ -391,7 +395,8 @@ function useLevelEditor(worldId: string, levelId: number, codeviewRef, initialCo
/** Open all files in this world on the server so that they will load faster when accessed */
function useLoadWorldFiles(worldId) {
const gameInfo = useGetGameInfoQuery()
const gameId = React.useContext(GameIdContext)
const gameInfo = useGetGameInfoQuery({game: gameId})
const store = useStore()
useEffect(() => {
+6 -3
View File
@@ -14,13 +14,15 @@ import { useGetGameInfoQuery } from '../state/api';
import { Link } from 'react-router-dom';
import Markdown from './Markdown';
import { selectCompleted } from '../state/progress';
import { GameIdContext } from '../App';
function LevelIcon({ worldId, levelId, position }) {
const gameId = React.useContext(GameIdContext)
const completed = useSelector(selectCompleted(worldId,levelId))
// TODO: relative positioning?
return (
<Link to={`/world/${worldId}/level/${levelId}`} key={`/world/${worldId}/level/${levelId}`}>
<Link to={`/game/${gameId}/${worldId}/level/${levelId}`} key={`/game/${gameId}/world/${worldId}/level/${levelId}`}>
<circle fill={completed ? "green" :"#aaa"} cx={position.x + Math.sin(levelId/5) * 9} cy={position.y - Math.cos(levelId/5) * 9} r="0.8" />
</Link>
)
@@ -29,7 +31,8 @@ function LevelIcon({ worldId, levelId, position }) {
function Welcome() {
const navigate = useNavigate();
const gameInfo = useGetGameInfoQuery()
const gameId = React.useContext(GameIdContext)
const gameInfo = useGetGameInfoQuery({game: gameId})
const { nodes, bounds }: any = gameInfo.data ? computeWorldLayout(gameInfo.data?.worlds) : {nodes: []}
@@ -55,7 +58,7 @@ function Welcome() {
let position: cytoscape.Position = nodes[id].position
svgElements.push(
<Link key={`world${id}`} to={`/world/${id}/level/0`}>
<Link key={`world${id}`} to={`/game/${gameId}/world/${id}/level/0`}>
<circle className="world-circle" cx={position.x} cy={position.y} r="8" />
<text className="world-name"
x={position.x} y={position.y}>{nodes[id].data.title ? nodes[id].data.title : id}</text>
+14 -9
View File
@@ -6,12 +6,17 @@ import { useState } from 'react';
export class Connection {
private leanClient = null
getLeanClient(): LeanClient {
if (this.leanClient === null) {
const socketUrl = ((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + '/websocket/'
private game: string = undefined // We only keep a connection to a single game at a time
private leanClient: LeanClient = null
getLeanClient(game): LeanClient {
if (this.game !== game) {
if (this.leanClient) {
this.leanClient.stop() // Stop previous Lean client
}
this.game = game
// Start a new Lean client for the new `gameId`.
const socketUrl = ((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + '/websocket/' + game
const uri = monaco.Uri.parse('file:///')
this.leanClient = new LeanClient(socketUrl, undefined, uri, () => {})
}
@@ -22,9 +27,9 @@ export class Connection {
/** If not already started, starts the Lean client. resolves the returned promise as soon as a
* Lean client is running.
*/
startLeanClient = () => {
startLeanClient = (game) => {
return new Promise<LeanClient>((resolve) => {
const leanClient = this.getLeanClient()
const leanClient = this.getLeanClient(game)
if (leanClient.isRunning()) {
resolve(leanClient)
} else {
@@ -47,8 +52,8 @@ export const connection = new Connection()
export const ConnectionContext = React.createContext(null);
export const useLeanClient = () => {
const leanClient = connection.getLeanClient()
export const useLeanClient = (gameId) => {
const leanClient = connection.getLeanClient(gameId)
const [leanClientStarted, setLeanClientStarted] = useState(leanClient.isStarted())
React.useEffect(() => {
+7 -2
View File
@@ -13,21 +13,26 @@ import ErrorPage from './ErrorPage';
import Welcome from './components/Welcome';
import Level from './components/Level';
import { monacoSetup } from 'lean4web/client/src/monacoSetup';
import { redirect } from 'react-router-dom';
monacoSetup()
const router = createHashRouter([
{
path: "/",
loader: () => redirect("/game/testgame")
},
{
path: "/game/:gameId",
element: <App />,
errorElement: <ErrorPage />,
children: [
{
path: "/",
path: "/game/:gameId",
element: <Welcome />,
},
{
path: "/world/:worldId/level/:levelId",
path: "/game/:gameId/world/:worldId/level/:levelId",
element: <Level />,
},
],
+8 -8
View File
@@ -38,13 +38,13 @@ interface Doc {
const customBaseQuery = async (
args : {method: string, params?: any},
args : {game: string, method: string, params?: any},
{ signal, dispatch, getState, extra },
extraOptions
) => {
try {
const connection : Connection = extra.connection
let leanClient = await connection.startLeanClient()
let leanClient = await connection.startLeanClient(args.game)
console.log(`Sending request ${args.method}`)
let res = await leanClient.sendRequest(args.method, args.params)
console.log('Received response', res)
@@ -59,14 +59,14 @@ export const apiSlice = createApi({
reducerPath: 'gameApi',
baseQuery: customBaseQuery,
endpoints: (builder) => ({
getGameInfo: builder.query<GameInfo, void>({
query: () => {return {method: 'info', params: {}}},
getGameInfo: builder.query<GameInfo, {game: string}>({
query: ({game}) => {return {game, method: 'info', params: {}}},
}),
loadLevel: builder.query<LevelInfo, {world: string, level: number}>({
query: ({world, level}) => {return {method: "loadLevel", params: {world, level}}},
loadLevel: builder.query<LevelInfo, {game: string, world: string, level: number}>({
query: ({game, world, level}) => {return {game, method: "loadLevel", params: {world, level}}},
}),
loadDoc: builder.query<Doc, {name: string, type: "lemma"|"tactic"}>({
query: ({name, type}) => {return {method: "loadDoc", params: {name, type}}},
loadDoc: builder.query<Doc, {game: string, name: string, type: "lemma"|"tactic"}>({
query: ({game, name, type}) => {return {game, method: "loadDoc", params: {name, type}}},
}),
}),
})