Merge branch 'main' of github.com:leanprover-community/lean4game
This commit is contained in:
+1
-1
@@ -16,7 +16,7 @@ function App() {
|
|||||||
const params = useParams();
|
const params = useParams();
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<GameIdContext.Provider value={params.gameId}>
|
<GameIdContext.Provider value={"g/" + params.owner + "/" + params.repo}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</GameIdContext.Provider>
|
</GameIdContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,164 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Lean Game Server</title>
|
|
||||||
<link rel="stylesheet" href="frontpage.css">
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.7/dist/katex.min.css" integrity="sha384-3UiQGuEI4TTMaFmGIZumfRPtfKQ3trwQE2JgosJxCnGmQpL/lJdjpcHkaaFwHlcI" crossorigin="anonymous">
|
|
||||||
|
|
||||||
<!-- The loading of KaTeX is deferred to speed up page rendering -->
|
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.7/dist/katex.min.js" integrity="sha384-G0zcxDFp5LWZtDuRMnBkk3EphCK1lhEf4UEyEM693ka574TZGwo4IWwS6QLzM/2t" crossorigin="anonymous"></script>
|
|
||||||
|
|
||||||
<!-- To automatically render math in text elements, include the auto-render extension: -->
|
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.7/dist/contrib/auto-render.min.js" integrity="sha384-+VBxd3r6XgURycqtZ117nYw44OOcIax56Z4dCRWbxyPt0Koah1uHoK0o4+/RRE05" crossorigin="anonymous"
|
|
||||||
onload="renderMathInElement(document.body);"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<h1>Lean Game Server</h1>
|
|
||||||
|
|
||||||
<p>Welcome to the Lean Game Server where you can find interactive learning
|
|
||||||
games about <a target="_blank" href="https://leanprover-community.github.io/">Lean</a>.<p>
|
|
||||||
|
|
||||||
<div class="game-list">
|
|
||||||
|
|
||||||
<div class="game" onclick="location.href='#/game/adam';" style="cursor: pointer;">
|
|
||||||
<div class="wrapper">
|
|
||||||
<div class="title">Formaloversum</div>
|
|
||||||
<div class="short-description">Erkunde das Leansche Universum mit deinem Robo,
|
|
||||||
welcher dir bei der Verständigung mit den Formalosophen zur Seite steht.
|
|
||||||
</div>
|
|
||||||
<img class="image" src="data/formaloversum.png" alt="">
|
|
||||||
<div class="long description">
|
|
||||||
<p>Dieses Spiel führt die Grundlagen zur Beweisführung in Lean ein und schneidet danach
|
|
||||||
verschiedene Bereiche des Bachelorstudiums an.</p>
|
|
||||||
|
|
||||||
<p>(Das Spiel befindet sich noch in der Entstehungsphase.)</p>
|
|
||||||
|
|
||||||
<p>Das Spiel wurde im Rahmen des Projekts <a target="_blank" href="https://hhu-adam.github.io/">ADAM</a>
|
|
||||||
an der HHU in Düsseldorf
|
|
||||||
entwickelt.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<table class="info">
|
|
||||||
<tr>
|
|
||||||
<td title="consider playing these games first.">Prerequisites</td>
|
|
||||||
<td>-</td>
|
|
||||||
<!-- <td>
|
|
||||||
<ul>
|
|
||||||
<li>-</li>
|
|
||||||
</ul>
|
|
||||||
</td> -->
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Worlds</td>
|
|
||||||
<td>?</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Levels</td>
|
|
||||||
<td>?</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Language</td>
|
|
||||||
<td title="in German">🇩🇪</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="game" onclick="location.href='#/game/nng';" style="cursor: pointer;">
|
|
||||||
<div class="wrapper">
|
|
||||||
<div class="title">Natural Number Game</div>
|
|
||||||
<div class="short-description">
|
|
||||||
The classical introduction game for Lean.
|
|
||||||
</div>
|
|
||||||
<div class="image" src="data/formaloversum.png" alt=""></div>
|
|
||||||
<div class="long description">
|
|
||||||
<p>In this game you recreate the natural numbers
|
|
||||||
\(\mathbb{N}\) from the Peano axioms, learning the basics
|
|
||||||
about theorem proving in Lean.</p>
|
|
||||||
|
|
||||||
<p>This is a good first introduction to Lean!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<table class="info">
|
|
||||||
<tr>
|
|
||||||
<td title="consider playing these games first.">Prerequisites</td>
|
|
||||||
<td>-</td>
|
|
||||||
<!-- <td>
|
|
||||||
<ul>
|
|
||||||
<li>-</li>
|
|
||||||
</ul>
|
|
||||||
</td> -->
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Worlds</td>
|
|
||||||
<td>9</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Levels</td>
|
|
||||||
<td>72</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Language</td>
|
|
||||||
<td title="in English">🇬🇧</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Adding new games</h2>
|
|
||||||
<p>
|
|
||||||
If you consider writing your own game, you should use the
|
|
||||||
<a target="_blank" href="https://github.com/hhu-adam/NNG4">NNG Github Repo</a>
|
|
||||||
as a template.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
There will be an option to load and run games through the server
|
|
||||||
directly by specifying a URL, but this is still in development.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
To add games to this page, you should get in contact as
|
|
||||||
games will need to be added manually.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Impressum</h2>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
When running a game, our server collects metadata
|
|
||||||
(such as IP address, browser, operating system)
|
|
||||||
and the data that the user enters into the editor.
|
|
||||||
|
|
||||||
The data is used to compute the Lean output and display it to the user.
|
|
||||||
The information will be stored as long as the user stays on our
|
|
||||||
website and will be deleted immediately afterwards.
|
|
||||||
We keep logs to improve our software, but the contained
|
|
||||||
data is anonymized.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
We do not use cookies, but the game progress is stored in the
|
|
||||||
browser as site data. The game progress is not saved on the server;
|
|
||||||
if you delete your browser storage, it will be completely gone.
|
|
||||||
</p>
|
|
||||||
<p>Our server is located in Germany.</p>
|
|
||||||
|
|
||||||
<h3>Contact information</h3>
|
|
||||||
<p>
|
|
||||||
Jon Eugster<br>
|
|
||||||
Mathematisches Institut der Heinrich-Heine-Universität Düsseldorf<br>
|
|
||||||
Universitätsstr. 1<br>
|
|
||||||
40225 Düsseldorf<br>
|
|
||||||
Germany<br>
|
|
||||||
<a target="_blank" href="https://www.math.hhu.de/en/lehrstuehle-/-personen-/-ansprechpartner/innen/lehrstuehle-des-mathematischen-instituts/lehrstuhl-fuer-algebraische-geometrie/team/jon-eugster">Contact Details</a>
|
|
||||||
<p>
|
|
||||||
<div class="github-link">
|
|
||||||
<a target="_blank" href="https://github.com/leanprover-community/lean4game"
|
|
||||||
title="View the lean game server on github">
|
|
||||||
<svg height="32" aria-hidden="true" viewBox="0 0 16 16" version="1.1" width="24" data-view-component="true" class="octicon octicon-mark-github v-align-middle">
|
|
||||||
<path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -47,8 +47,7 @@ function GameTile({
|
|||||||
|
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
const routeChange = () =>{
|
const routeChange = () =>{
|
||||||
let path = `game/${gameId}`;
|
navigate(gameId);
|
||||||
navigate(path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="game" onClick={routeChange}>
|
return <div className="game" onClick={routeChange}>
|
||||||
@@ -108,7 +107,7 @@ function LandingPage() {
|
|||||||
|
|
||||||
<GameTile
|
<GameTile
|
||||||
title="Formaloversum"
|
title="Formaloversum"
|
||||||
gameId="adam"
|
gameId="g/hhu-adam/Robo"
|
||||||
intro="Erkunde das Leansche Universum mit deinem Robo, welcher dir bei der Verständigung mit den Formalosophen zur Seite steht."
|
intro="Erkunde das Leansche Universum mit deinem Robo, welcher dir bei der Verständigung mit den Formalosophen zur Seite steht."
|
||||||
description="
|
description="
|
||||||
Dieses Spiel führt die Grundlagen zur Beweisführung in Lean ein und schneidet danach verschiedene Bereiche des Bachelorstudiums an.
|
Dieses Spiel führt die Grundlagen zur Beweisführung in Lean ein und schneidet danach verschiedene Bereiche des Bachelorstudiums an.
|
||||||
@@ -120,7 +119,7 @@ Dieses Spiel führt die Grundlagen zur Beweisführung in Lean ein und schneidet
|
|||||||
|
|
||||||
<GameTile
|
<GameTile
|
||||||
title="Natural Number Game"
|
title="Natural Number Game"
|
||||||
gameId="nng"
|
gameId="g/hhu-adam/NNG4"
|
||||||
intro="The classical introduction game for Lean."
|
intro="The classical introduction game for Lean."
|
||||||
description="In this game you recreate the natural numbers $\mathbb{N}$ from the Peano axioms,
|
description="In this game you recreate the natural numbers $\mathbb{N}$ from the Peano axioms,
|
||||||
learning the basics about theorem proving in Lean.
|
learning the basics about theorem proving in Lean.
|
||||||
|
|||||||
@@ -221,8 +221,8 @@ function PlayableLevel({worldId, levelId}) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{levelId >= gameInfo.data?.worldSize[worldId] ?
|
{levelId >= gameInfo.data?.worldSize[worldId] ?
|
||||||
<Button to={`/game/${gameId}`}><FontAwesomeIcon icon={faHome} /></Button> :
|
<Button to={`/${gameId}`}><FontAwesomeIcon icon={faHome} /></Button> :
|
||||||
<Button to={`/game/${gameId}/world/${worldId}/level/${levelId + 1}`}>
|
<Button to={`/${gameId}/world/${worldId}/level/${levelId + 1}`}>
|
||||||
Next <FontAwesomeIcon icon={faArrowRight} /></Button>}
|
Next <FontAwesomeIcon icon={faArrowRight} /></Button>}
|
||||||
|
|
||||||
</div>}
|
</div>}
|
||||||
@@ -257,8 +257,8 @@ function Introduction({worldId}) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="conclusion">
|
<div className="conclusion">
|
||||||
{0 == gameInfo.data?.worldSize[worldId] ?
|
{0 == gameInfo.data?.worldSize[worldId] ?
|
||||||
<Button to={`/game/${gameId}`}><FontAwesomeIcon icon={faHome} /></Button> :
|
<Button to={`/${gameId}`}><FontAwesomeIcon icon={faHome} /></Button> :
|
||||||
<Button to={`/game/${gameId}/world/${worldId}/level/1`}>
|
<Button to={`/${gameId}/world/${worldId}/level/1`}>
|
||||||
Start <FontAwesomeIcon icon={faArrowRight} />
|
Start <FontAwesomeIcon icon={faArrowRight} />
|
||||||
</Button>}
|
</Button>}
|
||||||
</div>
|
</div>
|
||||||
@@ -272,7 +272,7 @@ function LevelAppBar({isLoading, levelId, worldId, levelTitle}) {
|
|||||||
|
|
||||||
return <div className="app-bar" style={isLoading ? {display: "none"} : null} >
|
return <div className="app-bar" style={isLoading ? {display: "none"} : null} >
|
||||||
<div>
|
<div>
|
||||||
<Button to={`/game/${gameId}`}><FontAwesomeIcon icon={faHome} /></Button>
|
<Button to={`/${gameId}`}><FontAwesomeIcon icon={faHome} /></Button>
|
||||||
<span className="app-bar-title">
|
<span className="app-bar-title">
|
||||||
{gameInfo.data?.worlds.nodes[worldId].title && `World: ${gameInfo.data?.worlds.nodes[worldId].title}`}
|
{gameInfo.data?.worlds.nodes[worldId].title && `World: ${gameInfo.data?.worlds.nodes[worldId].title}`}
|
||||||
</span>
|
</span>
|
||||||
@@ -282,10 +282,10 @@ function LevelAppBar({isLoading, levelId, worldId, levelTitle}) {
|
|||||||
{levelTitle}
|
{levelTitle}
|
||||||
</span>
|
</span>
|
||||||
<Button disabled={levelId <= 0} inverted={true}
|
<Button disabled={levelId <= 0} inverted={true}
|
||||||
to={`/game/${gameId}/world/${worldId}/level/${levelId - 1}`}
|
to={`/${gameId}/world/${worldId}/level/${levelId - 1}`}
|
||||||
><FontAwesomeIcon icon={faArrowLeft} /> Previous</Button>
|
><FontAwesomeIcon icon={faArrowLeft} /> Previous</Button>
|
||||||
<Button disabled={levelId >= gameInfo.data?.worldSize[worldId]} inverted={true}
|
<Button disabled={levelId >= gameInfo.data?.worldSize[worldId]} inverted={true}
|
||||||
to={`/game/${gameId}/world/${worldId}/level/${levelId + 1}`}
|
to={`/${gameId}/world/${worldId}/level/${levelId + 1}`}
|
||||||
>Next <FontAwesomeIcon icon={faArrowRight} /></Button>
|
>Next <FontAwesomeIcon icon={faArrowRight} /></Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ function LevelIcon({ worldId, levelId, position }) {
|
|||||||
|
|
||||||
// TODO: relative positioning?
|
// TODO: relative positioning?
|
||||||
return (
|
return (
|
||||||
<Link to={`/game/${gameId}/world/${worldId}/level/${levelId}`}>
|
<Link to={`/${gameId}/world/${worldId}/level/${levelId}`}>
|
||||||
<circle fill={completed ? "green" :"#999"} cx={x} cy={y} r={r} />
|
<circle fill={completed ? "green" :"#999"} cx={x} cy={y} r={r} />
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
@@ -76,13 +76,13 @@ function Welcome() {
|
|||||||
for (let i = 1; i <= gameInfo.data.worldSize[id]; i++) {
|
for (let i = 1; i <= gameInfo.data.worldSize[id]; i++) {
|
||||||
svgElements.push(
|
svgElements.push(
|
||||||
<LevelIcon
|
<LevelIcon
|
||||||
key={`/game/${gameId}/world/${id}/level/${i}`}
|
key={`/${gameId}/world/${id}/level/${i}`}
|
||||||
position={position} worldId={id} levelId={i} />
|
position={position} worldId={id} levelId={i} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
svgElements.push(
|
svgElements.push(
|
||||||
<Link key={`world${id}`} to={`/game/${gameId}/world/${id}/level/0`}>
|
<Link key={`world${id}`} to={`/${gameId}/world/${id}/level/0`}>
|
||||||
<circle className="world-circle" cx={s*position.x} cy={s*position.y} r={R}
|
<circle className="world-circle" cx={s*position.x} cy={s*position.y} r={R}
|
||||||
fill="#1976d2"/>
|
fill="#1976d2"/>
|
||||||
<foreignObject className="world-title-wrapper" x={s*position.x} y={s*position.y}
|
<foreignObject className="world-title-wrapper" x={s*position.x} y={s*position.y}
|
||||||
|
|||||||
@@ -22,19 +22,18 @@ const router = createHashRouter([
|
|||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
element: <LandingPage />,
|
element: <LandingPage />,
|
||||||
// loader: () => redirect("/game/adam")
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/game/:gameId",
|
path: "/g/:owner/:repo",
|
||||||
element: <App />,
|
element: <App />,
|
||||||
errorElement: <ErrorPage />,
|
errorElement: <ErrorPage />,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "/game/:gameId",
|
path: "/g/:owner/:repo",
|
||||||
element: <Welcome />,
|
element: <Welcome />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/game/:gameId/world/:worldId/level/:levelId",
|
path: "/g/:owner/:repo/world/:worldId/level/:levelId",
|
||||||
element: <Level />,
|
element: <Level />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
Generated
+1396
-20
File diff suppressed because it is too large
Load Diff
+7
-3
@@ -17,9 +17,11 @@
|
|||||||
"cytoscape-elk": "^2.1.0",
|
"cytoscape-elk": "^2.1.0",
|
||||||
"cytoscape-klay": "^3.1.4",
|
"cytoscape-klay": "^3.1.4",
|
||||||
"debounce": "^1.2.1",
|
"debounce": "^1.2.1",
|
||||||
|
"decompress": "^4.2.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"lean4-infoview": "https://gitpkg.now.sh/leanprover/vscode-lean4/lean4-infoview?de0062c",
|
"lean4-infoview": "https://gitpkg.now.sh/leanprover/vscode-lean4/lean4-infoview?de0062c",
|
||||||
"lean4web": "github:hhu-adam/lean4web",
|
"lean4web": "github:hhu-adam/lean4web",
|
||||||
|
"octokit": "^2.0.14",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
@@ -30,6 +32,8 @@
|
|||||||
"rehype-katex": "^6.0.2",
|
"rehype-katex": "^6.0.2",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remark-math": "^5.1.1",
|
"remark-math": "^5.1.1",
|
||||||
|
"request": "^2.88.2",
|
||||||
|
"request-progress": "^3.0.0",
|
||||||
"vscode-ws-jsonrpc": "^2.0.1",
|
"vscode-ws-jsonrpc": "^2.0.1",
|
||||||
"web-worker": "^1.2.0",
|
"web-worker": "^1.2.0",
|
||||||
"ws": "^8.11.0"
|
"ws": "^8.11.0"
|
||||||
@@ -62,10 +66,10 @@
|
|||||||
"start": "concurrently -n server,client -c blue,green \"npm run start_server\" \"npm run start_client\"",
|
"start": "concurrently -n server,client -c blue,green \"npm run start_server\" \"npm run start_client\"",
|
||||||
"start_server": "cd server && lake build && NODE_ENV=development nodemon -e mjs --exec \"node ./index.mjs\"",
|
"start_server": "cd server && lake build && NODE_ENV=development nodemon -e mjs --exec \"node ./index.mjs\"",
|
||||||
"start_client": "NODE_ENV=development webpack-dev-server --hot",
|
"start_client": "NODE_ENV=development webpack-dev-server --hot",
|
||||||
"build": "npm run build_server && npm run build_client",
|
"build": "NODE_ENV=production webpack",
|
||||||
"build_server": "server/build.sh",
|
|
||||||
"build_client": "NODE_ENV=production webpack",
|
|
||||||
"production": "NODE_ENV=production node server/index.mjs",
|
"production": "NODE_ENV=production node server/index.mjs",
|
||||||
|
"build_robo": "rm -rf ./Robo && git clone https://github.com/hhu-adam/Robo && docker build ./Robo --file ./Robo/Dockerfile --tag github-hhu-adam:Robo && rm -rf ./Robo",
|
||||||
|
"build_nng": "rm -rf ./NNG4 && git clone https://github.com/hhu-adam/NNG4 && docker build ./NNG4 --file ./NNG4/Dockerfile --tag github-hhu-adam:NNG4 && rm -rf ./NNG4",
|
||||||
"update_lean": "./UPDATE_LEAN.sh"
|
"update_lean": "./UPDATE_LEAN.sh"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
|
|
||||||
# Operate in the directory where this file is located
|
|
||||||
cd $(dirname $0)
|
|
||||||
|
|
||||||
# Build Adam
|
|
||||||
( rm -rf adam
|
|
||||||
git clone https://github.com/hhu-adam/Robo adam/
|
|
||||||
cd adam
|
|
||||||
docker rmi adam:latest || true
|
|
||||||
docker build \
|
|
||||||
--rm -f Dockerfile -t adam:latest .
|
|
||||||
)
|
|
||||||
|
|
||||||
# Build NNG
|
|
||||||
( rm -rf nng
|
|
||||||
git clone https://github.com/hhu-adam/NNG4 nng/
|
|
||||||
cd nng
|
|
||||||
docker rmi nng:latest || true
|
|
||||||
docker build \
|
|
||||||
--rm -f Dockerfile -t nng:latest .
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { spawn } from 'child_process'
|
||||||
|
import fs from 'fs';
|
||||||
|
import request from 'request'
|
||||||
|
import decompress from 'decompress'
|
||||||
|
import requestProgress from 'request-progress'
|
||||||
|
import { Octokit } from 'octokit';
|
||||||
|
|
||||||
|
const TOKEN = process.env.LEAN4GAME_GITHUB_TOKEN
|
||||||
|
const octokit = new Octokit({
|
||||||
|
auth: TOKEN
|
||||||
|
})
|
||||||
|
|
||||||
|
const progress = {}
|
||||||
|
|
||||||
|
async function runProcess(id, cmd, args, cwd) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const ls = spawn(cmd, args, {cwd});
|
||||||
|
|
||||||
|
ls.stdout.on('data', (data) => {
|
||||||
|
progress[id].output += data.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
ls.stderr.on('data', (data) => {
|
||||||
|
progress[id].output += data.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
ls.on('close', (code) => {
|
||||||
|
resolve()
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function download(id, url, dest) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// The options argument is optional so you can omit it
|
||||||
|
requestProgress(request({
|
||||||
|
url,
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'abentkamp',
|
||||||
|
'X-GitHub-Api-Version': '2022-11-28',
|
||||||
|
'Authorization': 'Bearer '+TOKEN
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.on('progress', function (state) {
|
||||||
|
progress[id].output += `Downloaded ${Math.round(state.size.transferred/1024/1024)}MB\n`
|
||||||
|
})
|
||||||
|
.on('error', function (err) {
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
.on('end', function () {
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
.pipe(fs.createWriteStream(dest));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doImport (owner, repo, id) {
|
||||||
|
progress[id].output += `Import starting in a few seconds...\n`
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||||
|
let artifactId = null
|
||||||
|
try {
|
||||||
|
const artifacts = await octokit.request('GET /repos/{owner}/{repo}/actions/artifacts', {
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
headers: {
|
||||||
|
'X-GitHub-Api-Version': '2022-11-28'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// choose latest artifact
|
||||||
|
const artifact = artifacts.data.artifacts
|
||||||
|
.reduce((acc, cur) => acc.created_at < cur.created_at ? cur : acc)
|
||||||
|
artifactId = artifact.id
|
||||||
|
const url = artifact.archive_download_url
|
||||||
|
if (!fs.existsSync("tmp")){
|
||||||
|
fs.mkdirSync("tmp");
|
||||||
|
}
|
||||||
|
progress[id].output += `Download from ${url}\n`
|
||||||
|
await download(id, url, `tmp/artifact_${artifactId}.zip`)
|
||||||
|
progress[id].output += `Download finished.\n`
|
||||||
|
progress[id].output += `Unpacking ZIP.\n`
|
||||||
|
const files = await decompress(`tmp/artifact_${artifactId}.zip`, `tmp/artifact_${artifactId}`)
|
||||||
|
if (files.length != 1) { throw Error(`Unexpected number of files in ZIP: ${files.length}`) }
|
||||||
|
progress[id].output += `Unpacking TAR.\n`
|
||||||
|
const files_inner = await decompress(`tmp/artifact_${artifactId}/${files[0].path}`, `tmp/artifact_${artifactId}_inner`)
|
||||||
|
let manifest = fs.readFileSync(`tmp/artifact_${artifactId}_inner/manifest.json`);
|
||||||
|
manifest = JSON.parse(manifest);
|
||||||
|
if (manifest.length !== 1) {
|
||||||
|
throw `Unexpected manifest: ${JSON.stringify(manifest)}`
|
||||||
|
}
|
||||||
|
manifest[0].RepoTags = [`github-${owner}:${repo}`]
|
||||||
|
fs.writeFileSync(`tmp/artifact_${artifactId}_inner/manifest.json`, JSON.stringify(manifest));
|
||||||
|
await runProcess(id, "tar", ["-cvf", `../archive_${artifactId}.tar`, "."], `tmp/artifact_${artifactId}_inner/`)
|
||||||
|
await runProcess(id, "docker", ["load", "-i", `tmp/archive_${artifactId}.tar`])
|
||||||
|
progress[id].done = true
|
||||||
|
progress[id].output += `Done.\n`
|
||||||
|
} catch (e) {
|
||||||
|
progress[id].output += `Error: ${e.toString()}\n${e.stack}`
|
||||||
|
} finally {
|
||||||
|
if (artifactId) {
|
||||||
|
fs.rmSync(`tmp/artifact_${artifactId}.zip`, {force: true, recursive: true});
|
||||||
|
fs.rmSync(`tmp/artifact_${artifactId}`, {force: true, recursive: true});
|
||||||
|
fs.rmSync(`tmp/artifact_${artifactId}_inner`, {force: true, recursive: true});
|
||||||
|
fs.rmSync(`tmp/archive_${artifactId}.tar`, {force: true, recursive: true});
|
||||||
|
}
|
||||||
|
progress[id].done = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const importTrigger = (req, res) => {
|
||||||
|
const owner = req.params.owner
|
||||||
|
const repo = req.params.repo
|
||||||
|
const id = req.params.owner + '/' + req.params.repo
|
||||||
|
if(!/^[\w.-]+\/[\w.-]+$/.test(id)) { res.send(`Invalid repo name ${id}`); return }
|
||||||
|
|
||||||
|
if(!progress[id] || progress[id].done) {
|
||||||
|
progress[id] = {output: "", done: false}
|
||||||
|
doImport(owner, repo, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect(`/import/status/${owner}/${repo}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const importStatus = (req, res) => {
|
||||||
|
const owner = req.params.owner
|
||||||
|
const repo = req.params.repo
|
||||||
|
const id = req.params.owner + '/' + req.params.repo
|
||||||
|
res.send(`<html><head><meta http-equiv="refresh" content="5"></head><body><pre>${progress[id]?.output ?? "Nothing here."}</pre></body></html>`)
|
||||||
|
}
|
||||||
+41
-24
@@ -7,15 +7,22 @@ import * as rpc from 'vscode-ws-jsonrpc';
|
|||||||
import * as jsonrpcserver from 'vscode-ws-jsonrpc/server';
|
import * as jsonrpcserver from 'vscode-ws-jsonrpc/server';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import anonymize from 'ip-anonymize';
|
import anonymize from 'ip-anonymize';
|
||||||
|
import { importTrigger, importStatus } from './import.mjs'
|
||||||
|
|
||||||
|
/** Preloaded games. The keys refer to the docker tags of the virtual machines.
|
||||||
|
* The number `queueLength` determines how many instances of the docker container
|
||||||
|
* get started before any user shows up to have them up and running immediately.
|
||||||
|
* The values `name`, `module`, and `dir` are just used for development where we
|
||||||
|
* use a project directory instead of a docker container.
|
||||||
|
*/
|
||||||
const games = {
|
const games = {
|
||||||
adam: {
|
"github-hhu-adam:Robo": {
|
||||||
name: "Adam",
|
name: "Adam",
|
||||||
module: "Adam",
|
module: "Adam",
|
||||||
dir: "../../../../Robo",
|
dir: "../../../../Robo",
|
||||||
queueLength: 5
|
queueLength: 5
|
||||||
},
|
},
|
||||||
nng: {
|
"github-hhu-adam:NNG4": {
|
||||||
name: "NNG",
|
name: "NNG",
|
||||||
module: "NNG",
|
module: "NNG",
|
||||||
dir: "../../../../NNG4",
|
dir: "../../../../NNG4",
|
||||||
@@ -30,8 +37,14 @@ const app = express()
|
|||||||
|
|
||||||
const PORT = process.env.PORT || 8080;
|
const PORT = process.env.PORT || 8080;
|
||||||
|
|
||||||
|
var router = express.Router();
|
||||||
|
|
||||||
|
router.get('/import/status/:owner/:repo', importStatus)
|
||||||
|
router.get('/import/trigger/:owner/:repo', importTrigger)
|
||||||
|
|
||||||
const server = app
|
const server = app
|
||||||
.use(express.static(path.join(__dirname, '../client/dist/')))
|
.use(express.static(path.join(__dirname, '../client/dist/')))
|
||||||
|
.use('/', router)
|
||||||
.listen(PORT, () => console.log(`Listening on ${PORT}`));
|
.listen(PORT, () => console.log(`Listening on ${PORT}`));
|
||||||
|
|
||||||
const wss = new WebSocketServer({ server })
|
const wss = new WebSocketServer({ server })
|
||||||
@@ -43,17 +56,18 @@ const isDevelopment = environment === 'development'
|
|||||||
|
|
||||||
/** We keep queues of started Lean Server processes to be ready when a user arrives */
|
/** We keep queues of started Lean Server processes to be ready when a user arrives */
|
||||||
const queue = {}
|
const queue = {}
|
||||||
const queueLength = 5
|
|
||||||
|
|
||||||
function startServerProcess(gameId) {
|
function startServerProcess(tag) {
|
||||||
const serverProcess = isDevelopment
|
let serverProcess
|
||||||
? cp.spawn("./gameserver",
|
if (isDevelopment && games[tag]?.dir) {
|
||||||
["--server", games[gameId].dir, games[gameId].module, games[gameId].name],
|
serverProcess = cp.spawn("./gameserver",
|
||||||
|
["--server", games[tag].dir, games[tag].module, games[tag].name],
|
||||||
{ cwd: "./build/bin/" })
|
{ cwd: "./build/bin/" })
|
||||||
: cp.spawn("docker",
|
} else {
|
||||||
["run", "--runtime=runsc", "--network=none", "--rm", "-i", `${gameId}:latest`,
|
serverProcess = cp.spawn("docker",
|
||||||
"./gameserver", "--server", "/game/", games[gameId].module, games[gameId].name],
|
["run", "--runtime=runsc", "--network=none", "--rm", "-i", `${tag}`],
|
||||||
{ cwd: "." })
|
{ cwd: "." })
|
||||||
|
}
|
||||||
serverProcess.on('error', error =>
|
serverProcess.on('error', error =>
|
||||||
console.error(`Launching Lean Server failed: ${error}`)
|
console.error(`Launching Lean Server failed: ${error}`)
|
||||||
);
|
);
|
||||||
@@ -66,32 +80,35 @@ function startServerProcess(gameId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** start Lean Server processes to refill the queue */
|
/** start Lean Server processes to refill the queue */
|
||||||
function fillQueue(gameId) {
|
function fillQueue(tag) {
|
||||||
while (queue[gameId].length < games[gameId].queueLength) {
|
while (queue[tag].length < games[tag].queueLength) {
|
||||||
const serverProcess = startServerProcess(gameId)
|
const serverProcess = startServerProcess(tag)
|
||||||
queue[gameId].push(serverProcess)
|
queue[tag].push(serverProcess)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let gameId in games) {
|
if (!isDevelopment) { // Don't use queue in development
|
||||||
queue[gameId] = []
|
for (let tag in games) {
|
||||||
fillQueue(gameId)
|
queue[tag] = []
|
||||||
|
fillQueue(tag)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlRegEx = new RegExp("^/websocket/(.*)$")
|
const urlRegEx = /^\/websocket\/g\/([\w.-]+)\/([\w.-]+)$/
|
||||||
|
|
||||||
wss.addListener("connection", function(ws, req) {
|
wss.addListener("connection", function(ws, req) {
|
||||||
const reRes = urlRegEx.exec(req.url)
|
const reRes = urlRegEx.exec(req.url)
|
||||||
if (!reRes) { console.error(`Connection refused because of invalid URL: ${req.url}`); return; }
|
if (!reRes) { console.error(`Connection refused because of invalid URL: ${req.url}`); return; }
|
||||||
const gameId = reRes[1]
|
const owner = reRes[1]
|
||||||
if (!games[gameId]) { console.error(`Unknown game: ${gameId}`); return; }
|
const repo = reRes[2]
|
||||||
|
const tag = `github-${owner}:${repo}`
|
||||||
|
|
||||||
let ps;
|
let ps;
|
||||||
if (isDevelopment) { // Don't use queue in development
|
if (!queue[tag] || queue[tag].length == 0) {
|
||||||
ps = startServerProcess(gameId)
|
ps = startServerProcess(tag)
|
||||||
} else {
|
} else {
|
||||||
ps = queue[gameId].shift() // Pick the first Lean process; it's likely to be ready immediately
|
ps = queue[tag].shift() // Pick the first Lean process; it's likely to be ready immediately
|
||||||
fillQueue(gameId)
|
fillQueue(tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
socketCounter += 1;
|
socketCounter += 1;
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ module.exports = env => {
|
|||||||
target: 'ws://localhost:8080',
|
target: 'ws://localhost:8080',
|
||||||
ws: true
|
ws: true
|
||||||
},
|
},
|
||||||
|
'/import': {
|
||||||
|
target: 'http://localhost:3000',
|
||||||
|
router: () => 'http://localhost:8080',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
static: path.join(__dirname, 'client/public/'),
|
static: path.join(__dirname, 'client/public/'),
|
||||||
port: 3000,
|
port: 3000,
|
||||||
|
|||||||
Reference in New Issue
Block a user