Compare commits
166
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb214f426b | ||
|
|
527f58e3a4 | ||
|
|
b239a5d3dc | ||
|
|
63cf5e8b72 | ||
|
|
25f166f57f | ||
|
|
c89e2e4020 | ||
|
|
f6738faf46 | ||
|
|
cb7224934c | ||
|
|
13d54ff0ff | ||
|
|
72e4011c62 | ||
|
|
4f5256fa88 | ||
|
|
c2b9175fe5 | ||
|
|
a1a6862b5a | ||
|
|
0a057913be | ||
|
|
bedb2ad5ec | ||
|
|
e02e73c1c0 | ||
|
|
c82a88867f | ||
|
|
8b43aed596 | ||
|
|
8c84d3fae7 | ||
|
|
d5697d052e | ||
|
|
0e652256f8 | ||
|
|
9492c1011a | ||
|
|
54bab2a016 | ||
|
|
3f8b180b04 | ||
|
|
23b1074aa2 | ||
|
|
0964a06f2f | ||
|
|
b17c8fc4cb | ||
|
|
0c4ae92856 | ||
|
|
088711b5d1 | ||
|
|
4c93b3a091 | ||
|
|
aa00e359c4 | ||
|
|
f6a2632d80 | ||
|
|
f2190d648f | ||
|
|
d7fd8709cb | ||
|
|
7a03c4fe0d | ||
|
|
333c9498f1 | ||
|
|
121b36b542 | ||
|
|
6bced7575b | ||
|
|
d18b48db2f | ||
|
|
97dc648452 | ||
|
|
472e2c66df | ||
|
|
622e9d3897 | ||
|
|
7f91ae7da8 | ||
|
|
28a7c65db2 | ||
|
|
56525c6234 | ||
|
|
44f7b6703e | ||
|
|
8851cd8b1f | ||
|
|
bae360874c | ||
|
|
07b6525c58 | ||
|
|
8e8026aa38 | ||
|
|
a3a421f504 | ||
|
|
084e25c0dc | ||
|
|
6580afb622 | ||
|
|
0c0a7ab400 | ||
|
|
dfb2f10219 | ||
|
|
a57a5af111 | ||
|
|
946a7fa673 | ||
|
|
244c373192 | ||
|
|
374afa318a | ||
|
|
d0636b1d85 | ||
|
|
2e121363b3 | ||
|
|
590d68ccfb | ||
|
|
f6727e5c9f | ||
|
|
241ef4b67a | ||
|
|
ea4250f38d | ||
|
|
1d2420331d | ||
|
|
2213862998 | ||
|
|
81073b24b8 | ||
|
|
33068f1183 | ||
|
|
8ab37d4344 | ||
|
|
9d9902cce1 | ||
|
|
f53316c591 | ||
|
|
427ce43e95 | ||
|
|
15d79244d4 | ||
|
|
f13027f75c | ||
|
|
5fcdee4f71 | ||
|
|
7ad23dab24 | ||
|
|
89e19cc019 | ||
|
|
4db5260ed8 | ||
|
|
d19046aebd | ||
|
|
6e0fcb1d50 | ||
|
|
04c70ba522 | ||
|
|
1b3993ad3e | ||
|
|
45a95bbdc4 | ||
|
|
7c9f3d7a0a | ||
|
|
823000d5d4 | ||
|
|
c706b66af1 | ||
|
|
933394bb6f | ||
|
|
e09c016c4c | ||
|
|
06d9656e88 | ||
|
|
b30164dec4 | ||
|
|
ea685f0b19 | ||
|
|
d71b895550 | ||
|
|
21070af13c | ||
|
|
2b9f791655 | ||
|
|
51ca5354dc | ||
|
|
ebcde9d588 | ||
|
|
335e7e6883 | ||
|
|
6f92d61381 | ||
|
|
506677ee02 | ||
|
|
3d97cff0f4 | ||
|
|
bda1f98693 | ||
|
|
7e0b82cb14 | ||
|
|
918151ade5 | ||
|
|
c2b5754371 | ||
|
|
d1cd0f80fd | ||
|
|
d52d63020c | ||
|
|
efdc9ee536 | ||
|
|
9b8e84cc9c | ||
|
|
3416b32ddc | ||
|
|
2951747d1a | ||
|
|
f0f998298f | ||
|
|
c4dc48292a | ||
|
|
d3a450f5b1 | ||
|
|
fe8f8b6df3 | ||
|
|
207bb88c59 | ||
|
|
5c41d5c83c | ||
|
|
8e4c993bd7 | ||
|
|
787d010b6c | ||
|
|
e740027144 | ||
|
|
53e015a2fe | ||
|
|
4266c090db | ||
|
|
b70ac78cf7 | ||
|
|
853f797157 | ||
|
|
84ad619537 | ||
|
|
b6bc77828d | ||
|
|
ceee1b38e1 | ||
|
|
b5927a1024 | ||
|
|
2b159f508f | ||
|
|
30f9d46489 | ||
|
|
5de8ce8be7 | ||
|
|
2de82a1106 | ||
|
|
ca58cb0e78 | ||
|
|
ec96f2c80e | ||
|
|
cafe74a22b | ||
|
|
cc4321ff3f | ||
|
|
19592668ae | ||
|
|
556c072cef | ||
|
|
bb0ba83f18 | ||
|
|
0c65a2e4f1 | ||
|
|
06aa5d5339 | ||
|
|
63a0d8d5f0 | ||
|
|
d66546a7f1 | ||
|
|
caee7195db | ||
|
|
2b5c42a3c5 | ||
|
|
4ca570e232 | ||
|
|
ae5413f82e | ||
|
|
60069191dd | ||
|
|
9cd44911a4 | ||
|
|
047c5ae268 | ||
|
|
04038a32c8 | ||
|
|
6970f10e30 | ||
|
|
165c0e176a | ||
|
|
7563963c34 | ||
|
|
7016173f53 | ||
|
|
6ce5131c63 | ||
|
|
0b1fe3baf1 | ||
|
|
b2936e0200 | ||
|
|
24eed37f89 | ||
|
|
3a3471c615 | ||
|
|
8730c2067c | ||
|
|
5257865b47 | ||
|
|
9689b8ca53 | ||
|
|
7b26280ae9 | ||
|
|
c3a36e2cf3 | ||
|
|
6e046d72b6 |
@@ -5,7 +5,17 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- name: install elan
|
||||||
|
run: |
|
||||||
|
set -o pipefail
|
||||||
|
curl -sSfL https://github.com/leanprover/elan/releases/download/v3.0.0/elan-x86_64-unknown-linux-gnu.tar.gz | tar xz
|
||||||
|
./elan-init -y
|
||||||
|
echo "$HOME/.elan/bin" >> $GITHUB_PATH
|
||||||
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
|
- name: print lean and lake versions
|
||||||
|
run: |
|
||||||
|
lean --version
|
||||||
|
lake --version
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
|
|||||||
+3
-2
@@ -1,4 +1,5 @@
|
|||||||
node_modules
|
node_modules
|
||||||
client/dist
|
client/dist
|
||||||
server/build
|
games/
|
||||||
**/lake-packages/
|
server/.lake
|
||||||
|
**/.DS_Store
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ npm install -g http-server
|
|||||||
|
|
||||||
```
|
```
|
||||||
git clone https://github.com/hhu-adam/NNG4.git
|
git clone https://github.com/hhu-adam/NNG4.git
|
||||||
cd NNG4
|
|
||||||
docker rmi nng4:latest
|
|
||||||
docker build --pull --rm -f "Dockerfile" -t nng4:latest "."
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@@ -146,12 +143,6 @@ Activate config:
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Install `unzip` for Importing Docker Images
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo apt-get install unzip
|
|
||||||
```
|
|
||||||
|
|
||||||
## Install bubblewrap (bwrap)
|
## Install bubblewrap (bwrap)
|
||||||
```
|
```
|
||||||
sudo apt-get install bubblewrap
|
sudo apt-get install bubblewrap
|
||||||
|
|||||||
@@ -1,33 +1,46 @@
|
|||||||
# Lean 4 Game
|
# Lean 4 Game
|
||||||
|
|
||||||
This is a prototype for a Lean 4 game platform. The project is based on ideas from the [Lean Game Maker](https://github.com/mpedramfar/Lean-game-maker) and the [Natural Number Game
|
This is the source code for a Lean 4 game platform hosted at [adam.math.hhu.de](https://adam.math.hhu.de).
|
||||||
(NNG)](https://www.ma.imperial.ac.uk/~buzzard/xena/natural_number_game/)
|
|
||||||
of Kevin Buzzard and Mohammad Pedramfar.
|
|
||||||
The project is based on Patrick Massot's prototype: [NNG4](https://github.com/PatrickMassot/NNG4).
|
|
||||||
|
|
||||||
### Progress & Contributing
|
## Creating a Game
|
||||||
Currently the interface is still undergoing bigger changes, contributions are of course welcome, but it might be better to wait with them for a bit until proper support for external games is implemented and the existing games are separated from this repository. (ca. Sept. 2023)
|
|
||||||
|
|
||||||
|
Please follow the tutorial [Creating a Game](doc/create_game.md). In particular, the following steps might be of interest:
|
||||||
|
|
||||||
### Documentation
|
* Step 5: [How to Run Games Locally](doc/running_locally.md)
|
||||||
|
* Step 7: [How to Update an existing Game](doc/update_game.md)
|
||||||
|
* Step 8: [How to Publishing a Game](doc/publish_game.md)
|
||||||
|
|
||||||
For game developers, there is a work-in-progress Documentation: [Create a Game](DOCUMENTATION.md).
|
## Documentation
|
||||||
Best to talk with us directly.
|
|
||||||
|
|
||||||
For the game engine itself, documentation is missing currently.
|
The documentation is very much work in progress but the linked documentation here
|
||||||
|
should be up-to-date:
|
||||||
|
|
||||||
## NPM Scripts
|
### Game creation API
|
||||||
|
|
||||||
* `npm start`: Start the project in development mode. The browser will automatically reload when client files get changed. The Lean server will get recompiled and restarted when lean files get changed. The Lean server will be started without a container. The client and server can be started separately using the scripts `npm run start_client` and `npm run start_server`. The project can be accessed via `http://localhost:3000`.
|
- [Creating a Game](doc/create_game.md): **the main document to consult**.
|
||||||
Internally, websocket requests to `ws://localhost:3000/websockets` will be forwarded to a Lean server running on port `8080`.
|
- [More about Hints](doc/hints.md): describes the `Hint` and `Branch` tactic.
|
||||||
|
|
||||||
* `npm run build`: Build the project in production mode. All assets of the client will be compiled into `client/dist`.
|
### Frontend API
|
||||||
On the server side, the command will set up a docker image containing the Lean server. The two parts can be built separately using `npm run build_client` and `npm run build_server`.
|
|
||||||
|
|
||||||
* `npm run production`: Start the project in production mode. This requires that the build script has been run. It will start a server on the port specified in the `PORT` environment variable or by default on `8080`. You can run on a specifiv port by running `PORT=80 npm run production`. The server will serve the files in `client/dist` via http and give access to the docker-contained Lean server via the web socket protocol.
|
* [How to Run Games Locally](doc/running_locally.md): play a game on your computer
|
||||||
|
* [How to Update an existing Game](doc/update_game.md): update to a new lean version
|
||||||
|
* [How to Publishing a Game](doc/publish_game.md): load your game to adam.math.hhu.de for others to play
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
not written yet
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions to `lean4game` are always welcome!
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
Providing the use access to a Lean instance running on the server is a severe security risk. That is why we start the Lean server in a Docker container
|
Providing the use access to a Lean instance running on the server is a severe security risk. That is why we start the Lean server with bubblewrap.
|
||||||
secured by [gVisor](https://gvisor.dev/).
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
The project is based on ideas from the [Lean Game Maker](https://github.com/mpedramfar/Lean-game-maker) and the [Natural Number Game
|
||||||
|
(NNG)](https://www.ma.imperial.ac.uk/~buzzard/xena/natural_number_game/)
|
||||||
|
by Kevin Buzzard and Mohammad Pedramfar.
|
||||||
|
The project is based on Patrick Massot's prototype: [NNG4](https://github.com/PatrickMassot/NNG4).
|
||||||
|
|||||||
+7
-12
@@ -6,33 +6,28 @@ import '@fontsource/roboto/400.css';
|
|||||||
import '@fontsource/roboto/500.css';
|
import '@fontsource/roboto/500.css';
|
||||||
import '@fontsource/roboto/700.css';
|
import '@fontsource/roboto/700.css';
|
||||||
|
|
||||||
import './reset.css';
|
import './css/reset.css';
|
||||||
import './app.css';
|
import './css/app.css';
|
||||||
import { MobileContext } from './components/infoview/context';
|
import { MobileContext } from './components/infoview/context';
|
||||||
import { useWindowDimensions } from './window_width';
|
import { useWindowDimensions } from './window_width';
|
||||||
import { selectOpenedIntro } from './state/progress';
|
import { connection } from './connection';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
export const GameIdContext = React.createContext<string>(undefined);
|
export const GameIdContext = React.createContext<string>(undefined);
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const gameId = "g/" + params.owner + "/" + params.repo
|
const gameId = "g/" + params.owner + "/" + params.repo
|
||||||
|
|
||||||
// TODO: Make mobileLayout be changeable in settings
|
|
||||||
// TODO: Handle resize Events
|
|
||||||
const {width, height} = useWindowDimensions()
|
const {width, height} = useWindowDimensions()
|
||||||
const [mobile, setMobile] = React.useState(width < 800)
|
const [mobile, setMobile] = React.useState(width < 800)
|
||||||
|
|
||||||
// On mobile, there are multiple pages on the welcome page to switch between
|
React.useEffect(() => {
|
||||||
const openedIntro = useSelector(selectOpenedIntro(gameId))
|
connection.startLeanClient(gameId);
|
||||||
// On mobile, there are multiple pages to switch between
|
}, [gameId])
|
||||||
const [pageNumber, setPageNumber] = React.useState(openedIntro ? 1 : 0)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<GameIdContext.Provider value={gameId}>
|
<GameIdContext.Provider value={gameId}>
|
||||||
<MobileContext.Provider value={{mobile, setMobile, pageNumber, setPageNumber}}>
|
<MobileContext.Provider value={{mobile, setMobile}}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</MobileContext.Provider>
|
</MobileContext.Provider>
|
||||||
</GameIdContext.Provider>
|
</GameIdContext.Provider>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 398 KiB |
+187
-333
@@ -1,45 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* @file contains the navigation bars of the app.
|
||||||
|
*/
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { faDownload, faUpload, faEraser, faBook, faBookOpen, faGlobe, faHome,
|
||||||
|
faArrowRight, faArrowLeft, faXmark, faBars, faCode,
|
||||||
|
faCircleInfo, faTerminal } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { GameIdContext } from "../app"
|
import { GameIdContext } from "../app"
|
||||||
import { InputModeContext, MobileContext, WorldLevelIdContext } from "./infoview/context"
|
import { InputModeContext, MobileContext, WorldLevelIdContext } from "./infoview/context"
|
||||||
import { GameInfo, useGetGameInfoQuery } from '../state/api'
|
import { GameInfo, useGetGameInfoQuery } from '../state/api'
|
||||||
import { changedOpenedIntro, selectCompleted, selectDifficulty, selectProgress } from '../state/progress'
|
import { changedOpenedIntro, selectCompleted, selectDifficulty, selectProgress } from '../state/progress'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import { useAppDispatch, useAppSelector } from '../hooks'
|
import { useAppDispatch, useAppSelector } from '../hooks'
|
||||||
import { Button } from './button'
|
import { Button } from './button'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { downloadProgress } from './popup/erase'
|
||||||
import { faDownload, faUpload, faEraser, faBook, faGlobe, faHome, faArrowRight, faArrowLeft, faXmark, faBars, faCode, faCircleInfo, faTerminal } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { PrivacyPolicyPopup } from './privacy_policy'
|
|
||||||
import { WorldSelectionMenu, downloadFile } from './world_tree'
|
|
||||||
|
|
||||||
/** navigation to switch between pages on mobile */
|
/** navigation buttons for mobile welcome page to switch between intro/tree/inventory. */
|
||||||
function MobileNav({pageNumber, setPageNumber}:
|
function MobileNavButtons({pageNumber, setPageNumber}:
|
||||||
{ pageNumber: number,
|
{ pageNumber: number,
|
||||||
setPageNumber: any}) {
|
setPageNumber: any}) {
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
let prevText = {0 : null, 1: "Intro", 2: null}[pageNumber]
|
// if `prevText` or `prevIcon` is set, show a button to go back
|
||||||
let prevIcon = {0 : null, 1: null, 2: faXmark}[pageNumber]
|
let prevText = {0: null, 1: "Intro", 2: null}[pageNumber]
|
||||||
let prevTitle = {
|
let prevIcon = {0: null, 1: null, 2: faBookOpen}[pageNumber]
|
||||||
0: null,
|
let prevTitle = {0: null, 1: "Game Introduction", 2: "World selection"}[pageNumber]
|
||||||
1: "Game Introduction",
|
// if `nextText` or `nextIcon` is set, show a button to go forward
|
||||||
2: "World selection"}[pageNumber]
|
let nextText = {0: "Start", 1: null, 2: null}[pageNumber]
|
||||||
let nextText = {0 : "Start", 1: null, 2: null}[pageNumber]
|
let nextIcon = {0: null, 1: faBook, 2: null}[pageNumber]
|
||||||
let nextIcon = {0 : null, 1: faBook, 2: null}[pageNumber]
|
let nextTitle = {0: "World selection", 1: "Inventory", 2: null}[pageNumber]
|
||||||
let nextTitle = {
|
|
||||||
0: "World selection",
|
|
||||||
1: "Inventory",
|
|
||||||
2: null}[pageNumber]
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
{(prevText || prevTitle || prevIcon) &&
|
{(prevText || prevIcon) &&
|
||||||
<Button className="btn btn-inverted toggle-width" to={pageNumber == 0 ? "/" : ""} inverted="true" title={prevTitle}
|
<Button className="btn btn-inverted toggle-width" to={pageNumber == 0 ? "/" : ""}
|
||||||
|
inverted="true" title={prevTitle}
|
||||||
onClick={() => {pageNumber == 0 ? null : setPageNumber(pageNumber - 1)}}>
|
onClick={() => {pageNumber == 0 ? null : setPageNumber(pageNumber - 1)}}>
|
||||||
{prevIcon && <FontAwesomeIcon icon={prevIcon} />}
|
{prevIcon && <FontAwesomeIcon icon={prevIcon} />}
|
||||||
{prevText && `${prevText}`}
|
{prevText && `${prevText}`}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
{(nextText || nextTitle || nextIcon) &&
|
{(nextText || nextIcon) &&
|
||||||
<Button className="btn btn-inverted toggle-width" to="" inverted="true"
|
<Button className="btn btn-inverted toggle-width" to="" inverted="true"
|
||||||
title={nextTitle} onClick={() => {
|
title={nextTitle} onClick={() => {
|
||||||
console.log(`page number: ${pageNumber}`)
|
console.log(`page number: ${pageNumber}`)
|
||||||
@@ -52,349 +52,203 @@ function MobileNav({pageNumber, setPageNumber}:
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WelcomeAppBar({gameInfo, toggleImpressum, openEraseMenu, openUploadMenu, toggleInfo} : {
|
/** button to toggle dropdown menu. */
|
||||||
|
function MenuButton({navOpen, setNavOpen}) {
|
||||||
|
return <Button to="" className="btn toggle-width" id="menu-btn" onClick={(ev) => {setNavOpen(!navOpen)}}>
|
||||||
|
{navOpen ? <FontAwesomeIcon icon={faXmark} /> : <FontAwesomeIcon icon={faBars} />}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** button to go one level futher.
|
||||||
|
* for the last level, this button turns into a button going back to the welcome page.
|
||||||
|
*/
|
||||||
|
function NextButton({worldSize, difficulty, completed, setNavOpen}) {
|
||||||
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
|
||||||
|
return (levelId < worldSize ?
|
||||||
|
<Button inverted="true"
|
||||||
|
to={`/${gameId}/world/${worldId}/level/${levelId + 1}`} title="next level"
|
||||||
|
disabled={difficulty >= 2 && !(completed || levelId == 0)}
|
||||||
|
onClick={() => setNavOpen(false)}>
|
||||||
|
<FontAwesomeIcon icon={faArrowRight} /> {levelId ? "Next" : "Start"}
|
||||||
|
</Button>
|
||||||
|
:
|
||||||
|
<Button to={`/${gameId}`} inverted="true" title="back to world selection" id="home-btn">
|
||||||
|
<FontAwesomeIcon icon={faHome} /> Leave World
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** button to go one level back.
|
||||||
|
* only renders if the current level id is > 0.
|
||||||
|
*/
|
||||||
|
function PreviousButton({setNavOpen}) {
|
||||||
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
|
||||||
|
return (levelId > 0 && <>
|
||||||
|
<Button disabled={levelId <= 0} inverted="true"
|
||||||
|
to={`/${gameId}/world/${worldId}/level/${levelId - 1}`}
|
||||||
|
title="previous level"
|
||||||
|
onClick={() => setNavOpen(false)}>
|
||||||
|
<FontAwesomeIcon icon={faArrowLeft} /> Previous
|
||||||
|
</Button>
|
||||||
|
</>)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** button to toggle between editor and typewriter */
|
||||||
|
function InputModeButton({setNavOpen, isDropdown}) {
|
||||||
|
const {levelId} = React.useContext(WorldLevelIdContext)
|
||||||
|
const {typewriterMode, setTypewriterMode, lockInputMode} = React.useContext(InputModeContext)
|
||||||
|
|
||||||
|
/** toggle input mode if allowed */
|
||||||
|
function toggleInputMode(ev: React.MouseEvent) {
|
||||||
|
if (!lockInputMode){
|
||||||
|
setTypewriterMode(!typewriterMode)
|
||||||
|
setNavOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Button
|
||||||
|
className={`btn btn-inverted ${isDropdown? '' : 'toggle-width'}`} disabled={levelId <= 0 || lockInputMode}
|
||||||
|
inverted="true" to=""
|
||||||
|
onClick={(ev) => toggleInputMode(ev)}
|
||||||
|
title={lockInputMode ? "Editor mode is enforced!" : typewriterMode ? "Editor mode" : "Typewriter mode"}>
|
||||||
|
<FontAwesomeIcon icon={typewriterMode ? faCode : faTerminal} />
|
||||||
|
{isDropdown && (typewriterMode ? <> Editor mode</> : <> Typewriter mode</>)}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** button to toggle iimpressum popup */
|
||||||
|
function ImpressumButton({setNavOpen, toggleImpressum, isDropdown}) {
|
||||||
|
return <Button className="btn btn-inverted toggle-width"
|
||||||
|
title="information, Impressum, privacy policy" inverted="true" to="" onClick={(ev) => {toggleImpressum(ev); setNavOpen(false)}}>
|
||||||
|
<FontAwesomeIcon icon={faCircleInfo} />
|
||||||
|
{isDropdown && <> Info & Impressum</>}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** button to go back to welcome page */
|
||||||
|
function HomeButton({isDropdown}) {
|
||||||
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
return <Button to={`/${gameId}`} inverted="true" title="back to world selection" id="home-btn">
|
||||||
|
<FontAwesomeIcon icon={faHome} />
|
||||||
|
{isDropdown && <> Home</>}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** button in mobile level to toggle inventory.
|
||||||
|
* only displays a button if `setPageNumber` is set.
|
||||||
|
*/
|
||||||
|
function InventoryButton({pageNumber, setPageNumber}) {
|
||||||
|
return (setPageNumber &&
|
||||||
|
<Button to="" className="btn btn-inverted toggle-width"
|
||||||
|
title={pageNumber ? "close inventory" : "show inventory"}
|
||||||
|
inverted="true" onClick={() => {setPageNumber(pageNumber ? 0 : 1)}}>
|
||||||
|
<FontAwesomeIcon icon={pageNumber ? faBookOpen : faBook} />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** the navigation bar on the welcome page */
|
||||||
|
export function WelcomeAppBar({pageNumber, setPageNumber, gameInfo, toggleImpressum, toggleEraseMenu, toggleUploadMenu, toggleInfo} : {
|
||||||
|
pageNumber: number,
|
||||||
|
setPageNumber: any,
|
||||||
gameInfo: GameInfo,
|
gameInfo: GameInfo,
|
||||||
toggleImpressum: any,
|
toggleImpressum: any,
|
||||||
openEraseMenu: any,
|
toggleEraseMenu: any,
|
||||||
openUploadMenu: any,
|
toggleUploadMenu: any,
|
||||||
toggleInfo: any
|
toggleInfo: any
|
||||||
}) {
|
}) {
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {mobile, pageNumber, setPageNumber} = React.useContext(MobileContext)
|
const gameProgress = useAppSelector(selectProgress(gameId))
|
||||||
|
const {mobile} = React.useContext(MobileContext)
|
||||||
const [navOpen, setNavOpen] = React.useState(false)
|
const [navOpen, setNavOpen] = React.useState(false)
|
||||||
|
|
||||||
|
return <div className="app-bar">
|
||||||
|
<div>
|
||||||
/** Download the current progress (i.e. what's saved in the browser store) */
|
<Button inverted="false" title="back to games selection" to="/">
|
||||||
const gameProgress = useSelector(selectProgress(gameId))
|
<FontAwesomeIcon icon={faArrowLeft} /> <FontAwesomeIcon icon={faGlobe} />
|
||||||
const downloadProgress = (e) => {
|
</Button>
|
||||||
e.preventDefault()
|
<span className="app-bar-title"></span>
|
||||||
downloadFile({
|
</div>
|
||||||
data: JSON.stringify(gameProgress, null, 2),
|
<div>
|
||||||
fileName: `lean4game-${gameId}-${new Date().toLocaleDateString()}.json`,
|
{!mobile && <span className="app-bar-title">{gameInfo?.title}</span>}
|
||||||
fileType: 'text/json',
|
</div>
|
||||||
})
|
<div className="nav-btns">
|
||||||
}
|
{mobile && <MobileNavButtons pageNumber={pageNumber} setPageNumber={setPageNumber} />}
|
||||||
|
<MenuButton navOpen={navOpen} setNavOpen={setNavOpen} />
|
||||||
|
</div>
|
||||||
return <div className="app-bar" >
|
<div className={'menu dropdown' + (navOpen ? '' : ' hidden')}>
|
||||||
<>
|
<Button title="Game Info & Credits" inverted="true" to="" onClick={() => {toggleInfo(); setNavOpen(false)}}>
|
||||||
<div>
|
<FontAwesomeIcon icon={faCircleInfo} /> Game Info
|
||||||
<Button inverted="false" title="back to games selection" to="/">
|
</Button>
|
||||||
<FontAwesomeIcon icon={faArrowLeft} /> <FontAwesomeIcon icon={faGlobe} />
|
<Button title="Clear Progress" inverted="true" to="" onClick={() => {toggleEraseMenu(); setNavOpen(false)}}>
|
||||||
</Button>
|
<FontAwesomeIcon icon={faEraser} /> Erase
|
||||||
<span className="app-bar-title"></span>
|
</Button>
|
||||||
</div>
|
<Button title="Download Progress" inverted="true" to="" onClick={(ev) => {downloadProgress(gameId, gameProgress, ev); setNavOpen(false)}}>
|
||||||
<div>
|
<FontAwesomeIcon icon={faDownload} /> Download
|
||||||
<span className="app-bar-title">
|
</Button>
|
||||||
{mobile ? '' : gameInfo?.title}
|
<Button title="Load Progress from JSON" inverted="true" to="" onClick={() => {toggleUploadMenu(); setNavOpen(false)}}>
|
||||||
</span>
|
<FontAwesomeIcon icon={faUpload} /> Upload
|
||||||
</div>
|
</Button>
|
||||||
<div className="nav-btns">
|
<Button title="Impressum, privacy policy" inverted="true" to="" onClick={() => {toggleImpressum(); setNavOpen(false)}}>
|
||||||
{mobile && <>
|
<FontAwesomeIcon icon={faCircleInfo} /> Impressum
|
||||||
{/* BUTTONS for MOBILE */}
|
</Button>
|
||||||
<MobileNav pageNumber={pageNumber} setPageNumber={setPageNumber} />
|
</div>
|
||||||
|
|
||||||
</>}
|
|
||||||
|
|
||||||
<Button to="" className="btn toggle-width" id="menu-btn" onClick={(ev) => {setNavOpen(!navOpen)}} >
|
|
||||||
{navOpen ? <FontAwesomeIcon icon={faXmark} /> : <FontAwesomeIcon icon={faBars} />}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div className={'menu dropdown' + (navOpen ? '' : ' hidden')}>
|
|
||||||
{/* {levelId < gameInfo.data?.worldSize[worldId] &&
|
|
||||||
<Button inverted="true"
|
|
||||||
to={`/${gameId}/world/${worldId}/level/${levelId + 1}`} title="next level"
|
|
||||||
disabled={difficulty >= 2 && !(completed || levelId == 0)}
|
|
||||||
onClick={() => setNavOpen(false)}>
|
|
||||||
<FontAwesomeIcon icon={faArrowRight} /> {levelId ? "Next" : "Start"}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
{levelId > 0 && <>
|
|
||||||
<Button disabled={levelId <= 0} inverted="true"
|
|
||||||
to={`/${gameId}/world/${worldId}/level/${levelId - 1}`}
|
|
||||||
title="previous level"
|
|
||||||
onClick={() => setNavOpen(false)}>
|
|
||||||
<FontAwesomeIcon icon={faArrowLeft} /> Previous
|
|
||||||
</Button>
|
|
||||||
</>}
|
|
||||||
<Button to={`/${gameId}`} inverted="true" title="back to world selection">
|
|
||||||
<FontAwesomeIcon icon={faHome} /> Home
|
|
||||||
</Button>
|
|
||||||
<Button disabled={levelId <= 0} inverted="true" to=""
|
|
||||||
onClick={(ev) => { setCommandLineMode(!commandLineMode); setNavOpen(false) }}
|
|
||||||
title="toggle Editor mode">
|
|
||||||
<FontAwesomeIcon icon={faCode} /> Toggle Editor
|
|
||||||
</Button> */}
|
|
||||||
|
|
||||||
<Button title="Game Info & Credits" inverted="true" to="" onClick={(ev) => {toggleInfo(); setNavOpen(false)}}>
|
|
||||||
<FontAwesomeIcon icon={faCircleInfo} /> Game Info
|
|
||||||
</Button>
|
|
||||||
<Button title="Clear Progress" inverted="true" to="" onClick={(ev) => {openEraseMenu(); setNavOpen(false)}}>
|
|
||||||
<FontAwesomeIcon icon={faEraser} /> Erase
|
|
||||||
</Button>
|
|
||||||
<Button title="Download Progress" inverted="true" to="" onClick={(ev) => {downloadProgress(ev); setNavOpen(false)}}>
|
|
||||||
<FontAwesomeIcon icon={faDownload} /> Download
|
|
||||||
</Button>
|
|
||||||
<Button title="Load Progress from JSON" inverted="true" to="" onClick={(ev) => {openUploadMenu(); setNavOpen(false)}}>
|
|
||||||
<FontAwesomeIcon icon={faUpload} /> Upload
|
|
||||||
</Button>
|
|
||||||
<Button title="Impressum, privacy policy" inverted="true" to="" onClick={(ev) => {toggleImpressum(); setNavOpen(false)}}>
|
|
||||||
<FontAwesomeIcon icon={faCircleInfo} /> Impressum
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// /** The menu that is shown next to the world selection graph */
|
/** the navigation bar in a level */
|
||||||
// function WorldSelectionMenu() {
|
export function LevelAppBar({isLoading, levelTitle, toggleImpressum, pageNumber=undefined, setPageNumber=undefined} : {
|
||||||
// const [file, setFile] = React.useState<File>();
|
isLoading: boolean,
|
||||||
|
levelTitle: string,
|
||||||
// const gameId = React.useContext(GameIdContext)
|
toggleImpressum: any,
|
||||||
// const store = useStore()
|
pageNumber?: number,
|
||||||
// const difficulty = useSelector(selectDifficulty(gameId))
|
setPageNumber?: any,
|
||||||
|
}) {
|
||||||
|
|
||||||
// /* state variables to toggle the pop-up menus */
|
|
||||||
// const [eraseMenu, setEraseMenu] = React.useState(false);
|
|
||||||
// const openEraseMenu = () => setEraseMenu(true);
|
|
||||||
// const closeEraseMenu = () => setEraseMenu(false);
|
|
||||||
// const [uploadMenu, setUploadMenu] = React.useState(false);
|
|
||||||
// const openUploadMenu = () => setUploadMenu(true);
|
|
||||||
// const closeUploadMenu = () => setUploadMenu(false);
|
|
||||||
|
|
||||||
// const gameProgress = useSelector(selectProgress(gameId))
|
|
||||||
// const dispatch = useAppDispatch()
|
|
||||||
|
|
||||||
// /** Download the current progress (i.e. what's saved in the browser store) */
|
|
||||||
// const downloadProgress = (e) => {
|
|
||||||
// e.preventDefault()
|
|
||||||
// downloadFile({
|
|
||||||
// data: JSON.stringify(gameProgress, null, 2),
|
|
||||||
// fileName: `lean4game-${gameId}-${new Date().toLocaleDateString()}.json`,
|
|
||||||
// fileType: 'text/json',
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const handleFileChange = (e) => {
|
|
||||||
// if (e.target.files) {
|
|
||||||
// setFile(e.target.files[0])
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /** Upload progress from a */
|
|
||||||
// const uploadProgress = (e) => {
|
|
||||||
// if (!file) {return}
|
|
||||||
// const fileReader = new FileReader()
|
|
||||||
// fileReader.readAsText(file, "UTF-8")
|
|
||||||
// fileReader.onload = (e) => {
|
|
||||||
// const data = JSON.parse(e.target.result.toString()) as GameProgressState
|
|
||||||
// console.debug("Json Data", data)
|
|
||||||
// dispatch(loadProgress({game: gameId, data: data}))
|
|
||||||
// }
|
|
||||||
// closeUploadMenu()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const eraseProgress = () => {
|
|
||||||
// dispatch(deleteProgress({game: gameId}))
|
|
||||||
// closeEraseMenu()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const downloadAndErase = (e) => {
|
|
||||||
// downloadProgress(e)
|
|
||||||
// eraseProgress()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// function label(x : number) {
|
|
||||||
// return x == 0 ? 'none' : x == 1 ? 'lax' : 'regular'
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return <nav className="world-selection-menu">
|
|
||||||
// <Button onClick={downloadProgress} title="Download game progress" to=""><FontAwesomeIcon icon={faDownload} /></Button>
|
|
||||||
// <Button title="Load game progress from JSON" onClick={openUploadMenu} to=""><FontAwesomeIcon icon={faUpload} /></Button>
|
|
||||||
// <Button title="Clear game progress" to="" onClick={openEraseMenu}><FontAwesomeIcon icon={faEraser} /></Button>
|
|
||||||
// <div className="slider-wrap">
|
|
||||||
// <span className="difficulty-label">Game Rules:</span>
|
|
||||||
// <Slider
|
|
||||||
// title="Game Rules: - regular: 🔐 levels, 🔐 tactics - lax: 🔓 levels, 🔐 tactics - none: 🔓 levels, 🔓 tactics"
|
|
||||||
// min={0} max={2}
|
|
||||||
// aria-label="Game Rules"
|
|
||||||
// defaultValue={difficulty}
|
|
||||||
// marks={[
|
|
||||||
// {value: 0, label: label(0)},
|
|
||||||
// {value: 1, label: label(1)},
|
|
||||||
// {value: 2, label: label(2)}
|
|
||||||
// ]}
|
|
||||||
// valueLabelFormat={label}
|
|
||||||
// getAriaValueText={label}
|
|
||||||
// valueLabelDisplay="auto"
|
|
||||||
// onChange={(ev, val: number) => {
|
|
||||||
// dispatch(changedDifficulty({game: gameId, difficulty: val}))
|
|
||||||
// }}
|
|
||||||
// ></Slider>
|
|
||||||
|
|
||||||
// </div>
|
|
||||||
// {eraseMenu?
|
|
||||||
// <div className="modal-wrapper">
|
|
||||||
// <div className="modal-backdrop" onClick={closeEraseMenu} />
|
|
||||||
// <div className="modal">
|
|
||||||
// <div className="codicon codicon-close modal-close" onClick={closeEraseMenu}></div>
|
|
||||||
// <h2>Delete Progress?</h2>
|
|
||||||
|
|
||||||
// <p>Do you want to delete your saved progress irreversibly?</p>
|
|
||||||
// <p>
|
|
||||||
// (This deletes your proofs and your collected inventory.
|
|
||||||
// Saves from other games are not deleted.)
|
|
||||||
// </p>
|
|
||||||
|
|
||||||
// <Button onClick={eraseProgress} to="">Delete</Button>
|
|
||||||
// <Button onClick={downloadAndErase} to="">Download & Delete</Button>
|
|
||||||
// <Button onClick={closeEraseMenu} to="">Cancel</Button>
|
|
||||||
// </div>
|
|
||||||
// </div> : null}
|
|
||||||
// {uploadMenu ?
|
|
||||||
// <div className="modal-wrapper">
|
|
||||||
// <div className="modal-backdrop" onClick={closeUploadMenu} />
|
|
||||||
// <div className="modal">
|
|
||||||
// <div className="codicon codicon-close modal-close" onClick={closeUploadMenu}></div>
|
|
||||||
// <h2>Upload Saved Progress</h2>
|
|
||||||
|
|
||||||
// <p>Select a JSON file with the saved game progress to load your progress.</p>
|
|
||||||
|
|
||||||
// <p><b>Warning:</b> This will delete your current game progress!
|
|
||||||
// Consider <a className="download-link" onClick={downloadProgress} >downloading your current progress</a> first!</p>
|
|
||||||
|
|
||||||
// <input type="file" onChange={handleFileChange}/>
|
|
||||||
|
|
||||||
// <Button to="" onClick={uploadProgress}>Load selected file</Button>
|
|
||||||
// </div>
|
|
||||||
// </div> : null}
|
|
||||||
// </nav>
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
/** The top-navigation bar */
|
|
||||||
export function LevelAppBar({
|
|
||||||
isLoading, levelTitle, impressum, toggleImpressum,
|
|
||||||
pageNumber = undefined, setPageNumber = undefined}) {
|
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
|
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
|
||||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
|
||||||
|
|
||||||
const {mobile} = React.useContext(MobileContext)
|
const {mobile} = React.useContext(MobileContext)
|
||||||
|
|
||||||
const difficulty = useSelector(selectDifficulty(gameId))
|
|
||||||
const completed = useAppSelector(selectCompleted(gameId, worldId, levelId))
|
|
||||||
|
|
||||||
const { commandLineMode, setCommandLineMode } = React.useContext(InputModeContext)
|
|
||||||
|
|
||||||
const [navOpen, setNavOpen] = React.useState(false)
|
const [navOpen, setNavOpen] = React.useState(false)
|
||||||
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||||
|
const completed = useAppSelector(selectCompleted(gameId, worldId, levelId))
|
||||||
|
const difficulty = useAppSelector(selectDifficulty(gameId))
|
||||||
|
|
||||||
|
let worldTitle = gameInfo.data?.worlds.nodes[worldId].title
|
||||||
|
|
||||||
return <div className="app-bar" style={isLoading ? {display: "none"} : null} >
|
return <div className="app-bar" style={isLoading ? {display: "none"} : null} >
|
||||||
{mobile ?
|
{mobile ?
|
||||||
<>
|
<>
|
||||||
{/* MOBILE VERSION */}
|
{/* MOBILE VERSION */}
|
||||||
<div>
|
<div>
|
||||||
<span className="app-bar-title">
|
<span className="app-bar-title">{levelTitle}</span>
|
||||||
{levelTitle}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="nav-btns">
|
<div className="nav-btns">
|
||||||
{mobile && pageNumber == 0 ?
|
<InventoryButton pageNumber={pageNumber} setPageNumber={setPageNumber}/>
|
||||||
<Button to="" className="btn btn-inverted toggle-width"
|
<MenuButton navOpen={navOpen} setNavOpen={setNavOpen}/>
|
||||||
title="show inventory" inverted="true" onClick={() => {setPageNumber(1)}}>
|
|
||||||
<FontAwesomeIcon icon={faBook}/>
|
|
||||||
</Button>
|
|
||||||
: pageNumber == 1 &&
|
|
||||||
<Button className="btn btn-inverted toggle-width" to=""
|
|
||||||
title="show inventory" inverted="true" onClick={() => {setPageNumber(0)}}>
|
|
||||||
<FontAwesomeIcon icon={faXmark}/>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
<Button to="" className="btn toggle-width" id="menu-btn" onClick={(ev) => {setNavOpen(!navOpen)}} >
|
|
||||||
{navOpen ? <FontAwesomeIcon icon={faXmark} /> : <FontAwesomeIcon icon={faBars} />}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={'menu dropdown' + (navOpen ? '' : ' hidden')}>
|
<div className={'menu dropdown' + (navOpen ? '' : ' hidden')}>
|
||||||
{levelId < gameInfo.data?.worldSize[worldId] &&
|
<NextButton worldSize={gameInfo.data?.worldSize[worldId]} difficulty={difficulty} completed={completed} setNavOpen={setNavOpen} />
|
||||||
<Button inverted="true"
|
<PreviousButton setNavOpen={setNavOpen} />
|
||||||
to={`/${gameId}/world/${worldId}/level/${levelId + 1}`} title="next level"
|
<HomeButton isDropdown={true} />
|
||||||
disabled={difficulty >= 2 && !(completed || levelId == 0)}
|
<InputModeButton setNavOpen={setNavOpen} isDropdown={true}/>
|
||||||
onClick={() => setNavOpen(false)}>
|
<ImpressumButton setNavOpen={setNavOpen} toggleImpressum={toggleImpressum} isDropdown={true} />
|
||||||
<FontAwesomeIcon icon={faArrowRight} /> {levelId ? "Next" : "Start"}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
{levelId > 0 && <>
|
|
||||||
<Button disabled={levelId <= 0} inverted="true"
|
|
||||||
to={`/${gameId}/world/${worldId}/level/${levelId - 1}`}
|
|
||||||
title="previous level"
|
|
||||||
onClick={() => setNavOpen(false)}>
|
|
||||||
<FontAwesomeIcon icon={faArrowLeft} /> Previous
|
|
||||||
</Button>
|
|
||||||
</>}
|
|
||||||
<Button to={`/${gameId}`} inverted="true" title="back to world selection">
|
|
||||||
<FontAwesomeIcon icon={faHome} /> Home
|
|
||||||
</Button>
|
|
||||||
<Button disabled={levelId <= 0} inverted="true" to=""
|
|
||||||
onClick={(ev) => { setCommandLineMode(!commandLineMode); setNavOpen(false) }}
|
|
||||||
title="toggle Editor mode">
|
|
||||||
<FontAwesomeIcon icon={faCode} /> Toggle Editor
|
|
||||||
</Button>
|
|
||||||
<Button title="information, Impressum, privacy policy" inverted="true" to="" onClick={(ev) => {toggleImpressum(ev); setNavOpen(false)}}>
|
|
||||||
<FontAwesomeIcon icon={faCircleInfo} /> Info & Impressum
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</> :
|
||||||
:
|
|
||||||
<>
|
<>
|
||||||
{/* DESKTOP VERSION */}
|
{/* DESKTOP VERSION */}
|
||||||
<div>
|
<div>
|
||||||
<Button to={`/${gameId}`} inverted="true" title="back to world selection" id="home-btn">
|
<HomeButton isDropdown={false} />
|
||||||
<FontAwesomeIcon icon={faHome} />
|
<span className="app-bar-title">{worldTitle && `World: ${worldTitle}`}</span>
|
||||||
</Button>
|
|
||||||
<span className="app-bar-title">
|
|
||||||
{gameInfo.data?.worlds.nodes[worldId].title && `World: ${gameInfo.data?.worlds.nodes[worldId].title}`}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="app-bar-title">
|
<span className="app-bar-title">{levelTitle}</span>
|
||||||
{levelTitle}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="nav-btns">
|
<div className="nav-btns">
|
||||||
{levelId > 0 && <>
|
<PreviousButton setNavOpen={setNavOpen} />
|
||||||
<Button disabled={levelId <= 0} inverted="true"
|
<NextButton worldSize={gameInfo.data?.worldSize[worldId]} difficulty={difficulty} completed={completed} setNavOpen={setNavOpen} />
|
||||||
to={`/${gameId}/world/${worldId}/level/${levelId - 1}`}
|
<InputModeButton setNavOpen={setNavOpen} isDropdown={false}/>
|
||||||
title="previous level"
|
<ImpressumButton setNavOpen={setNavOpen} toggleImpressum={toggleImpressum} isDropdown={false} />
|
||||||
onClick={() => setNavOpen(false)}>
|
|
||||||
<FontAwesomeIcon icon={faArrowLeft} /> Previous
|
|
||||||
</Button>
|
|
||||||
</>}
|
|
||||||
{levelId < gameInfo.data?.worldSize[worldId] ?
|
|
||||||
<Button inverted="true"
|
|
||||||
to={`/${gameId}/world/${worldId}/level/${levelId + 1}`} title="next level"
|
|
||||||
disabled={difficulty >= 2 && !(completed || levelId == 0)}
|
|
||||||
onClick={() => setNavOpen(false)}>
|
|
||||||
<FontAwesomeIcon icon={faArrowRight} /> {levelId ? "Next" : "Start"}
|
|
||||||
</Button>
|
|
||||||
:
|
|
||||||
<Button to={`/${gameId}`} inverted="true" title="back to world selection" id="home-btn">
|
|
||||||
<FontAwesomeIcon icon={faHome} /> Leave World
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
<Button className="btn btn-inverted toggle-width" disabled={levelId <= 0} inverted="true" to=""
|
|
||||||
onClick={(ev) => { setCommandLineMode(!commandLineMode); setNavOpen(false) }}
|
|
||||||
title="toggle Editor mode">
|
|
||||||
<FontAwesomeIcon icon={commandLineMode ? faCode : faTerminal} />
|
|
||||||
</Button>
|
|
||||||
<Button className="btn btn-inverted toggle-width" title="information, Impressum, privacy policy" inverted="true" to="" onClick={(ev) => {toggleImpressum(ev); setNavOpen(false)}}>
|
|
||||||
<FontAwesomeIcon icon={impressum ? faXmark : faCircleInfo} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { GameHint } from "./infoview/rpc_api";
|
import { GameHint } from "./infoview/rpc_api";
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Markdown from './markdown';
|
import Markdown from './markdown';
|
||||||
|
import { ProofStep } from "./infoview/context";
|
||||||
|
|
||||||
export function Hint({hint, step, selected, toggleSelection, lastLevel} : {hint: GameHint, step: number, selected: number, toggleSelection: any, lastLevel?: boolean}) {
|
export function Hint({hint, step, selected, toggleSelection, lastLevel} : {hint: GameHint, step: number, selected: number, toggleSelection: any, lastLevel?: boolean}) {
|
||||||
return <div className={`message information step-${step}` + (step == selected ? ' selected' : '') + (lastLevel ? ' recent' : '')} onClick={toggleSelection}>
|
return <div className={`message information step-${step}` + (step == selected ? ' selected' : '') + (lastLevel ? ' recent' : '')} onClick={toggleSelection}>
|
||||||
@@ -43,3 +44,24 @@ export function DeletedHints({hints} : {hints: GameHint[]}) {
|
|||||||
{hiddenHints.map((hint, i) => <DeletedHint key={`deleted-hidden-hint-${i}`} hint={hint}/>)}
|
{hiddenHints.map((hint, i) => <DeletedHint key={`deleted-hidden-hint-${i}`} hint={hint}/>)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Filter hints to not show consequtive identical hints twice.
|
||||||
|
*
|
||||||
|
* This function takes a `ProofStep[]` and extracts the hints in form of an
|
||||||
|
* element of type `GameHint[][]` where it removes hints that are identical to hints
|
||||||
|
* appearing in the previous step. Hidden hints are not filtered.
|
||||||
|
*
|
||||||
|
* This effectively means we prevent consequtive identical hints from being shown.
|
||||||
|
*/
|
||||||
|
export function filterHints(proof: ProofStep[]): GameHint[][] {
|
||||||
|
return proof.map((step, i) => {
|
||||||
|
if (i == 0){
|
||||||
|
return step.hints
|
||||||
|
} else {
|
||||||
|
// TODO: Writing all fields explicitely is somewhat fragile to changes, is there a
|
||||||
|
// good way to shallow-compare objects?
|
||||||
|
return step.hints.filter((hint) => hint.hidden ||
|
||||||
|
(proof[i-1].hints.find((x) => (x.text == hint.text && x.hidden == hint.hidden)) === undefined))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -65,13 +65,9 @@ export const ProofStateContext = React.createContext<{
|
|||||||
export const MobileContext = React.createContext<{
|
export const MobileContext = React.createContext<{
|
||||||
mobile : boolean,
|
mobile : boolean,
|
||||||
setMobile: React.Dispatch<React.SetStateAction<Boolean>>,
|
setMobile: React.Dispatch<React.SetStateAction<Boolean>>,
|
||||||
pageNumber: number,
|
|
||||||
setPageNumber: React.Dispatch<React.SetStateAction<Number>>
|
|
||||||
}>({
|
}>({
|
||||||
mobile : false,
|
mobile : false,
|
||||||
setMobile: () => {},
|
setMobile: () => {},
|
||||||
pageNumber: 0,
|
|
||||||
setPageNumber: () => {}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const WorldLevelIdContext = React.createContext<{
|
export const WorldLevelIdContext = React.createContext<{
|
||||||
@@ -105,13 +101,17 @@ export const DeletedChatContext = React.createContext<{
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const InputModeContext = React.createContext<{
|
export const InputModeContext = React.createContext<{
|
||||||
commandLineMode: boolean,
|
typewriterMode: boolean,
|
||||||
setCommandLineMode: React.Dispatch<React.SetStateAction<boolean>>,
|
setTypewriterMode: React.Dispatch<React.SetStateAction<boolean>>,
|
||||||
commandLineInput: string,
|
typewriterInput: string,
|
||||||
setCommandLineInput: React.Dispatch<React.SetStateAction<string>>
|
setTypewriterInput: React.Dispatch<React.SetStateAction<string>>,
|
||||||
|
lockInputMode: boolean,
|
||||||
|
setLockInputMode: React.Dispatch<React.SetStateAction<boolean>>,
|
||||||
}>({
|
}>({
|
||||||
commandLineMode: true,
|
typewriterMode: true,
|
||||||
setCommandLineMode: () => {},
|
setTypewriterMode: () => {},
|
||||||
commandLineInput: "",
|
typewriterInput: "",
|
||||||
setCommandLineInput: () => {},
|
setTypewriterInput: () => {},
|
||||||
|
lockInputMode: false,
|
||||||
|
setLockInputMode: () => {},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ interface GoalProps {
|
|||||||
goal: InteractiveGoal
|
goal: InteractiveGoal
|
||||||
filter: GoalFilterState
|
filter: GoalFilterState
|
||||||
showHints?: boolean
|
showHints?: boolean
|
||||||
commandLine: boolean
|
typewriter: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProofDisplayProps {
|
interface ProofDisplayProps {
|
||||||
@@ -136,7 +136,7 @@ interface ProofDisplayProps {
|
|||||||
* Displays the hypotheses, target type and optional case label of a goal according to the
|
* Displays the hypotheses, target type and optional case label of a goal according to the
|
||||||
* provided `filter`. */
|
* provided `filter`. */
|
||||||
export const Goal = React.memo((props: GoalProps) => {
|
export const Goal = React.memo((props: GoalProps) => {
|
||||||
const { goal, filter, showHints, commandLine } = props
|
const { goal, filter, showHints, typewriter } = props
|
||||||
|
|
||||||
// TODO: Apparently `goal` can be `undefined`
|
// TODO: Apparently `goal` can be `undefined`
|
||||||
if (!goal) {return <></>}
|
if (!goal) {return <></>}
|
||||||
@@ -163,18 +163,18 @@ export const Goal = React.memo((props: GoalProps) => {
|
|||||||
// const hints = <Hints hints={goal.hints} key={goal.mvarId} />
|
// const hints = <Hints hints={goal.hints} key={goal.mvarId} />
|
||||||
const objectHyps = hyps.filter(hyp => !hyp.isAssumption)
|
const objectHyps = hyps.filter(hyp => !hyp.isAssumption)
|
||||||
const assumptionHyps = hyps.filter(hyp => hyp.isAssumption)
|
const assumptionHyps = hyps.filter(hyp => hyp.isAssumption)
|
||||||
const {commandLineMode} = React.useContext(InputModeContext)
|
const {typewriterMode} = React.useContext(InputModeContext)
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
{/* {goal.userName && <div><strong className="goal-case">case </strong>{goal.userName}</div>} */}
|
{/* {goal.userName && <div><strong className="goal-case">case </strong>{goal.userName}</div>} */}
|
||||||
{filter.reverse && goalLi}
|
{filter.reverse && goalLi}
|
||||||
{! commandLine && objectHyps.length > 0 &&
|
{! typewriter && objectHyps.length > 0 &&
|
||||||
<div className="hyp-group"><div className="hyp-group-title">Objects:</div>
|
<div className="hyp-group"><div className="hyp-group-title">Objects:</div>
|
||||||
{objectHyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)}</div> }
|
{objectHyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)}</div> }
|
||||||
{!commandLine && assumptionHyps.length > 0 &&
|
{!typewriter && assumptionHyps.length > 0 &&
|
||||||
<div className="hyp-group"><div className="hyp-group-title">Assumptions:</div>
|
<div className="hyp-group"><div className="hyp-group-title">Assumptions:</div>
|
||||||
{assumptionHyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)}</div> }
|
{assumptionHyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)}</div> }
|
||||||
{/* {commandLine && commandLineMode && <CommandLine />} */}
|
{/* {typewriter && typewriterMode && <Typewriter />} */}
|
||||||
{!filter.reverse && goalLi}
|
{!filter.reverse && goalLi}
|
||||||
{/* {showHints && hints} */}
|
{/* {showHints && hints} */}
|
||||||
</div>
|
</div>
|
||||||
@@ -229,7 +229,7 @@ export const OtherGoals = React.memo((props: GoalProps2) => {
|
|||||||
<summary>
|
<summary>
|
||||||
<InteractiveCode fmt={goal.type} />
|
<InteractiveCode fmt={goal.type} />
|
||||||
</summary>
|
</summary>
|
||||||
<Goal commandLine={false} filter={filter} goal={goal} />
|
<Goal typewriter={false} filter={filter} goal={goal} />
|
||||||
</details>)}
|
</details>)}
|
||||||
</div>}
|
</div>}
|
||||||
</>
|
</>
|
||||||
@@ -264,7 +264,7 @@ export function Goals({ goals, filter }: GoalsProps) {
|
|||||||
return <>No goals</>
|
return <>No goals</>
|
||||||
} else {
|
} else {
|
||||||
return <>
|
return <>
|
||||||
{goals.goals.map((g, i) => <Goal commandLine={false} key={i} goal={g} filter={filter} />)}
|
{goals.goals.map((g, i) => <Goal typewriter={false} key={i} goal={g} filter={filter} />)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{ goals && (goals.goals.length > 0
|
{ goals && (goals.goals.length > 0
|
||||||
? <Goal commandLine={true} filter={goalFilter} key='mainGoal' goal={goals.goals[0]} showHints={true} />
|
? <Goal typewriter={true} filter={goalFilter} key='mainGoal' goal={goals.goals[0]} showHints={true} />
|
||||||
: <div className="goals-section-title">No Goals</div>
|
: <div className="goals-section-title">No Goals</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -156,7 +156,7 @@ const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => {
|
|||||||
<div className="goals-section-title">Weitere Goals</div>
|
<div className="goals-section-title">Weitere Goals</div>
|
||||||
|
|
||||||
{goals.goals.slice(1).map((goal, i) =>
|
{goals.goals.slice(1).map((goal, i) =>
|
||||||
<details key={i}><summary><InteractiveCode fmt={goal.type} /></summary> <Goal commandLine={false} filter={goalFilter} goal={goal} /></details>)}
|
<details key={i}><summary><InteractiveCode fmt={goal.type} /></summary> <Goal typewriter={false} filter={goalFilter} goal={goal} /></details>)}
|
||||||
</div>}
|
</div>}
|
||||||
</LocationsContext.Provider> */}
|
</LocationsContext.Provider> */}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { DidCloseTextDocumentParams, DidChangeTextDocumentParams, Location,
|
|||||||
import 'tachyons/css/tachyons.css';
|
import 'tachyons/css/tachyons.css';
|
||||||
import '@vscode/codicons/dist/codicon.css';
|
import '@vscode/codicons/dist/codicon.css';
|
||||||
import '../../../../node_modules/lean4-infoview/src/infoview/index.css';
|
import '../../../../node_modules/lean4-infoview/src/infoview/index.css';
|
||||||
import './infoview.css'
|
import '../../css/infoview.css'
|
||||||
|
|
||||||
import { LeanFileProgressParams, LeanFileProgressProcessingInfo, defaultInfoviewConfig, EditorApi, InfoviewApi } from '@leanprover/infoview-api';
|
import { LeanFileProgressParams, LeanFileProgressProcessingInfo, defaultInfoviewConfig, EditorApi, InfoviewApi } from '@leanprover/infoview-api';
|
||||||
import { useClientNotificationEffect, useServerNotificationEffect, useEventResult, useServerNotificationState } from '../../../../node_modules/lean4-infoview/src/infoview/util';
|
import { useClientNotificationEffect, useServerNotificationEffect, useEventResult, useServerNotificationState } from '../../../../node_modules/lean4-infoview/src/infoview/util';
|
||||||
@@ -21,39 +21,36 @@ import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
|
|||||||
import { GameIdContext } from '../../app';
|
import { GameIdContext } from '../../app';
|
||||||
import { useAppDispatch, useAppSelector } from '../../hooks';
|
import { useAppDispatch, useAppSelector } from '../../hooks';
|
||||||
import { LevelInfo } from '../../state/api';
|
import { LevelInfo } from '../../state/api';
|
||||||
import { changedInventory, levelCompleted, selectCompleted, selectInventory } from '../../state/progress';
|
import { changedInventory, levelCompleted, selectCode, selectCompleted, selectInventory } from '../../state/progress';
|
||||||
import Markdown from '../markdown';
|
import Markdown from '../markdown';
|
||||||
|
|
||||||
import { Infos } from './infos';
|
import { Infos } from './infos';
|
||||||
import { AllMessages, Errors, WithLspDiagnosticsContext } from './messages';
|
import { AllMessages, Errors, WithLspDiagnosticsContext } from './messages';
|
||||||
import { Goal } from './goals';
|
import { Goal } from './goals';
|
||||||
import { DeletedChatContext, InputModeContext, MobileContext, MonacoEditorContext, ProofContext, ProofStep, SelectionContext } from './context';
|
import { DeletedChatContext, InputModeContext, MobileContext, MonacoEditorContext, ProofContext, ProofStep, SelectionContext, WorldLevelIdContext } from './context';
|
||||||
import { CommandLine, hasErrors, hasInteractiveErrors } from './command_line';
|
import { Typewriter, hasErrors, hasInteractiveErrors } from './typewriter';
|
||||||
import { InteractiveDiagnostic } from '@leanprover/infoview/*';
|
import { InteractiveDiagnostic } from '@leanprover/infoview/*';
|
||||||
import { Button } from '../button';
|
import { Button } from '../button';
|
||||||
import { CircularProgress } from '@mui/material';
|
import { CircularProgress } from '@mui/material';
|
||||||
import { GameHint } from './rpc_api';
|
import { GameHint } from './rpc_api';
|
||||||
import { store } from '../../state/store';
|
import { store } from '../../state/store';
|
||||||
import { Hints } from '../hints';
|
import { Hints, filterHints } from '../hints';
|
||||||
|
|
||||||
/** Wrapper for the two editors. It is important that the `div` with `codeViewRef` is
|
/** Wrapper for the two editors. It is important that the `div` with `codeViewRef` is
|
||||||
* always present, or the monaco editor cannot start.
|
* always present, or the monaco editor cannot start.
|
||||||
*/
|
*/
|
||||||
export function DualEditor({ level, codeviewRef, levelId, worldId, worldSize }) {
|
export function DualEditor({ level, codeviewRef, levelId, worldId, worldSize }) {
|
||||||
const ec = React.useContext(EditorContext)
|
const ec = React.useContext(EditorContext)
|
||||||
const { commandLineMode } = React.useContext(InputModeContext)
|
const { typewriterMode } = React.useContext(InputModeContext)
|
||||||
return <>
|
return <>
|
||||||
<div className={commandLineMode ? 'hidden' : ''}>
|
<div className={typewriterMode ? 'hidden' : ''}>
|
||||||
<ExerciseStatement data={level} />
|
<ExerciseStatement data={level} showLeanStatement={true} />
|
||||||
<div ref={codeviewRef} className={'codeview'}></div>
|
<div ref={codeviewRef} className={'codeview'}></div>
|
||||||
</div>
|
</div>
|
||||||
{ec ?
|
{ec ?
|
||||||
<DualEditorMain worldId={worldId} levelId={levelId} level={level} worldSize={worldSize} /> :
|
<DualEditorMain worldId={worldId} levelId={levelId} level={level} worldSize={worldSize} /> :
|
||||||
// TODO: Style this if relevant.
|
// TODO: Style this if relevant.
|
||||||
<>
|
<></>
|
||||||
<p>Editor is starting up...</p>
|
|
||||||
<CircularProgress />
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@@ -62,7 +59,7 @@ export function DualEditor({ level, codeviewRef, levelId, worldId, worldSize })
|
|||||||
function DualEditorMain({ worldId, levelId, level, worldSize }: { worldId: string, levelId: number, level: LevelInfo, worldSize: number }) {
|
function DualEditorMain({ worldId, levelId, level, worldSize }: { worldId: string, levelId: number, level: LevelInfo, worldSize: number }) {
|
||||||
const ec = React.useContext(EditorContext)
|
const ec = React.useContext(EditorContext)
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const { commandLineMode } = React.useContext(InputModeContext)
|
const { typewriterMode } = React.useContext(InputModeContext)
|
||||||
|
|
||||||
// Mark level as completed when server gives notification
|
// Mark level as completed when server gives notification
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
@@ -70,7 +67,7 @@ function DualEditorMain({ worldId, levelId, level, worldSize }: { worldId: strin
|
|||||||
'$/game/completed',
|
'$/game/completed',
|
||||||
(params: any) => {
|
(params: any) => {
|
||||||
if (ec.events.changedCursorLocation.current &&
|
if (ec.events.changedCursorLocation.current &&
|
||||||
ec.events.changedCursorLocation.current.uri === params.uri) {
|
ec.events.changedCursorLocation.current.uri === params.uri) {
|
||||||
dispatch(levelCompleted({ game: gameId, world: worldId, level: levelId }))
|
dispatch(levelCompleted({ game: gameId, world: worldId, level: levelId }))
|
||||||
|
|
||||||
// On completion, add the names of all new items to the local storage
|
// On completion, add the names of all new items to the local storage
|
||||||
@@ -80,6 +77,11 @@ function DualEditorMain({ worldId, levelId, level, worldSize }: { worldId: strin
|
|||||||
...level?.definitions
|
...level?.definitions
|
||||||
].filter((tile) => tile.new).map((tile) => tile.name)
|
].filter((tile) => tile.new).map((tile) => tile.name)
|
||||||
|
|
||||||
|
// Add the proven statement to the local storage as well.
|
||||||
|
if (level?.statementName != null) {
|
||||||
|
newTiles.push(level?.statementName)
|
||||||
|
}
|
||||||
|
|
||||||
let inv: string[] = selectInventory(gameId)(store.getState())
|
let inv: string[] = selectInventory(gameId)(store.getState())
|
||||||
|
|
||||||
// add new items and remove duplicates
|
// add new items and remove duplicates
|
||||||
@@ -108,10 +110,10 @@ function DualEditorMain({ worldId, levelId, level, worldSize }: { worldId: strin
|
|||||||
<WithRpcSessions>
|
<WithRpcSessions>
|
||||||
<WithLspDiagnosticsContext>
|
<WithLspDiagnosticsContext>
|
||||||
<ProgressContext.Provider value={allProgress}>
|
<ProgressContext.Provider value={allProgress}>
|
||||||
{commandLineMode ?
|
{typewriterMode ?
|
||||||
<CommandLineInterface world={worldId} level={levelId} data={level} worldSize={worldSize}/>
|
<TypewriterInterfaceWrapper world={worldId} level={levelId} data={level} worldSize={worldSize}/>
|
||||||
:
|
:
|
||||||
<Main key={`${worldId}/${levelId}`} world={worldId} level={levelId} />
|
<Main key={`${worldId}/${levelId}`} world={worldId} level={levelId} data={level} />
|
||||||
}
|
}
|
||||||
</ProgressContext.Provider>
|
</ProgressContext.Provider>
|
||||||
</WithLspDiagnosticsContext>
|
</WithLspDiagnosticsContext>
|
||||||
@@ -126,22 +128,45 @@ function DualEditorMain({ worldId, levelId, level, worldSize }: { worldId: strin
|
|||||||
* - Theorem xyz: description
|
* - Theorem xyz: description
|
||||||
* - Theorem xyz
|
* - Theorem xyz
|
||||||
* - Exercises: description
|
* - Exercises: description
|
||||||
|
*
|
||||||
|
* If `showLeanStatement` is true, it will additionally display the lean code.
|
||||||
*/
|
*/
|
||||||
function ExerciseStatement({ data }) {
|
function ExerciseStatement({ data, showLeanStatement = false }) {
|
||||||
if (!data?.descrText) { return <></> }
|
if (!(data?.descrText || data?.descrFormat)) { return <></> }
|
||||||
return <div className="exercise-statement"><Markdown>
|
return <>
|
||||||
{(data?.statementName ? `**Theorem** \`${data?.statementName}\`: ` : data?.descrText && "**Exercise**: ") + `${data?.descrText}`}
|
<div className="exercise-statement">
|
||||||
</Markdown></div>
|
{data?.descrText &&
|
||||||
|
<Markdown>
|
||||||
|
{(data?.displayName ? `**Theorem** \`${data?.displayName}\`: ` : '') + data?.descrText}
|
||||||
|
</Markdown>
|
||||||
|
}
|
||||||
|
{data?.descrFormat && showLeanStatement &&
|
||||||
|
<p><code className="lean-code">{data?.descrFormat}</code></p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This is only used in `EditorInterface`
|
// TODO: This is only used in `EditorInterface`
|
||||||
// while `CommandLineInterface` has this copy-pasted in.
|
// while `TypewriterInterface` has this copy-pasted in.
|
||||||
export function Main(props: { world: string, level: number }) {
|
export function Main(props: { world: string, level: number, data: LevelInfo}) {
|
||||||
const ec = React.useContext(EditorContext);
|
const ec = React.useContext(EditorContext);
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
|
||||||
|
|
||||||
const completed = useAppSelector(selectCompleted(gameId, props.world, props.level))
|
const completed = useAppSelector(selectCompleted(gameId, props.world, props.level))
|
||||||
|
|
||||||
|
console.debug(`template: ${props.data?.template}`)
|
||||||
|
|
||||||
|
// React.useEffect (() => {
|
||||||
|
// if (props.data.template) {
|
||||||
|
// let code: string = selectCode(gameId, worldId, levelId)(store.getState())
|
||||||
|
// if (!code.length) {
|
||||||
|
// //models.push(monaco.editor.createModel(code, 'lean4', uri))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }, [props.data.template])
|
||||||
|
|
||||||
/* Set up updates to the global infoview state on editor events. */
|
/* Set up updates to the global infoview state on editor events. */
|
||||||
const config = useEventResult(ec.events.changedInfoviewConfig) ?? defaultInfoviewConfig;
|
const config = useEventResult(ec.events.changedInfoviewConfig) ?? defaultInfoviewConfig;
|
||||||
|
|
||||||
@@ -203,8 +228,8 @@ function Command({ command, deleteProof }: { command: string, deleteProof: any }
|
|||||||
if (!command) { return <></> }
|
if (!command) { return <></> }
|
||||||
return <div className="command">
|
return <div className="command">
|
||||||
<div className="command-text">{command}</div>
|
<div className="command-text">{command}</div>
|
||||||
<Button to="" className="undo-button btn btn-inverted" title="Delete this and future commands" onClick={deleteProof}>
|
<Button to="" className="undo-button btn btn-inverted" title="Retry proof from here" onClick={deleteProof}>
|
||||||
<FontAwesomeIcon icon={faDeleteLeft} /> Delete
|
<FontAwesomeIcon icon={faDeleteLeft} /> Retry
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -232,7 +257,7 @@ function Command({ command, deleteProof }: { command: string, deleteProof: any }
|
|||||||
// message = diag.message
|
// message = diag.message
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// const { commandLineMode } = React.useContext(InputModeContext)
|
// const { typewriterMode } = React.useContext(InputModeContext)
|
||||||
|
|
||||||
// return (
|
// return (
|
||||||
// // <details open>
|
// // <details open>
|
||||||
@@ -251,7 +276,7 @@ function Command({ command, deleteProof }: { command: string, deleteProof: any }
|
|||||||
// // </span>
|
// // </span>
|
||||||
// // </summary>
|
// // </summary>
|
||||||
// <div className={severityClass + ' ml1 message'}>
|
// <div className={severityClass + ' ml1 message'}>
|
||||||
// {!commandLineMode && <p className="mv2">{title}</p>}
|
// {!typewriterMode && <p className="mv2">{title}</p>}
|
||||||
// <pre className="font-code pre-wrap">
|
// <pre className="font-code pre-wrap">
|
||||||
// <InteractiveMessage fmt={message} />
|
// <InteractiveMessage fmt={message} />
|
||||||
// </pre>
|
// </pre>
|
||||||
@@ -261,103 +286,29 @@ function Command({ command, deleteProof }: { command: string, deleteProof: any }
|
|||||||
// }, fastIsEqual)
|
// }, fastIsEqual)
|
||||||
|
|
||||||
/** The tabs of goals that lean ahs after the command of this step has been processed */
|
/** The tabs of goals that lean ahs after the command of this step has been processed */
|
||||||
function GoalsTab({ proofStep, last}: { proofStep: ProofStep, last : boolean }) {
|
function GoalsTabs({ proofStep, last, onClick, onGoalChange=(n)=>{}}: { proofStep: ProofStep, last : boolean, onClick? : any, onGoalChange?: (n?: number) => void }) {
|
||||||
|
|
||||||
const [selectedGoal, setSelectedGoal] = React.useState<number>(0)
|
const [selectedGoal, setSelectedGoal] = React.useState<number>(0)
|
||||||
|
|
||||||
if (!proofStep.goals.length) { return <></> }
|
return <div className="goal-tabs" onClick={onClick}>
|
||||||
|
|
||||||
return <div>
|
|
||||||
<div className={`tab-bar ${last ? 'current' : ''}`}>
|
<div className={`tab-bar ${last ? 'current' : ''}`}>
|
||||||
{proofStep.goals.map((goal, i) => (
|
{proofStep.goals.map((goal, i) => (
|
||||||
// TODO: Should not use index as key.
|
// TODO: Should not use index as key.
|
||||||
<div key={`proof-goal-${i}`} className={`tab ${i == (selectedGoal) ? "active" : ""}`} onClick={(ev) => { setSelectedGoal(i); ev.stopPropagation() }}>
|
<div key={`proof-goal-${i}`} className={`tab ${i == (selectedGoal) ? "active" : ""}`} onClick={(ev) => { onGoalChange(i); setSelectedGoal(i); ev.stopPropagation() }}>
|
||||||
{i ? `Goal ${i + 1}` : "Active Goal"}
|
{i ? `Goal ${i + 1}` : "Active Goal"}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="goal-tab vscode-light">
|
<div className="goal-tab vscode-light">
|
||||||
<Goal commandLine={false} filter={goalFilter} goal={proofStep.goals[selectedGoal]} />
|
<Goal typewriter={false} filter={goalFilter} goal={proofStep.goals[selectedGoal]} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The interface in command line mode */
|
// Splitting up Typewriter into two parts is a HACK
|
||||||
export function CommandLineInterface(props: { world: string, level: number, data: LevelInfo, worldSize: number }) {
|
export function TypewriterInterfaceWrapper(props: { world: string, level: number, data: LevelInfo, worldSize: number }) {
|
||||||
|
|
||||||
const ec = React.useContext(EditorContext)
|
const ec = React.useContext(EditorContext)
|
||||||
const editor = React.useContext(MonacoEditorContext)
|
|
||||||
const model = editor.getModel()
|
|
||||||
const uri = model.uri.toString()
|
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const { proof } = React.useContext(ProofContext)
|
|
||||||
const { selectedStep, setSelectedStep } = React.useContext(SelectionContext)
|
|
||||||
const { setDeletedChat, showHelp, setShowHelp } = React.useContext(DeletedChatContext)
|
|
||||||
|
|
||||||
const {mobile} = React.useContext(MobileContext)
|
|
||||||
|
|
||||||
const proofPanelRef = React.useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
/** Delete all proof lines starting from a given line.
|
|
||||||
* Note that the first line (i.e. deleting everything) is `1`!
|
|
||||||
*/
|
|
||||||
function deleteProof(line: number) {
|
|
||||||
return (ev) => {
|
|
||||||
let deletedChat: Array<GameHint> = []
|
|
||||||
proof.slice(line).map((step, i) => {
|
|
||||||
// Only add these hidden hints to the deletion stack which were visible
|
|
||||||
deletedChat = [...deletedChat, ...step.hints.filter(hint => (!hint.hidden || showHelp.has(line + i)))]
|
|
||||||
})
|
|
||||||
setDeletedChat(deletedChat)
|
|
||||||
|
|
||||||
editor.executeEdits("command-line", [{
|
|
||||||
range: monaco.Selection.fromPositions(
|
|
||||||
{ lineNumber: line, column: 1 },
|
|
||||||
editor.getModel().getFullModelRange().getEndPosition()
|
|
||||||
),
|
|
||||||
text: '',
|
|
||||||
forceMoveMarkers: false
|
|
||||||
}])
|
|
||||||
setSelectedStep(undefined)
|
|
||||||
ev.stopPropagation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleSelectStep(line: number) {
|
|
||||||
return (ev) => {
|
|
||||||
if (mobile) {return}
|
|
||||||
if (selectedStep == line) {
|
|
||||||
setSelectedStep(undefined)
|
|
||||||
console.debug(`unselected step`)
|
|
||||||
} else {
|
|
||||||
setSelectedStep(line)
|
|
||||||
console.debug(`step ${line} selected`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scroll to the end of the proof if it is updated.
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (proof?.length > 1) {
|
|
||||||
proofPanelRef.current?.lastElementChild?.scrollIntoView() //scrollTo(0,0)
|
|
||||||
} else {
|
|
||||||
proofPanelRef.current?.scrollTo(0,0)
|
|
||||||
}
|
|
||||||
}, [proof])
|
|
||||||
|
|
||||||
// Scroll to element if selection changes
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (typeof selectedStep !== 'undefined') {
|
|
||||||
Array.from(proofPanelRef.current?.getElementsByClassName(`step-${selectedStep}`)).map((elem) => {
|
|
||||||
elem.scrollIntoView({ block: "center" })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [selectedStep])
|
|
||||||
|
|
||||||
const completed = useAppSelector(selectCompleted(gameId, props.world, props.level))
|
|
||||||
|
|
||||||
const config = useEventResult(ec.events.changedInfoviewConfig) ?? defaultInfoviewConfig;
|
|
||||||
|
|
||||||
const curUri = useEventResult(ec.events.changedCursorLocation, loc => loc?.uri);
|
|
||||||
|
|
||||||
useClientNotificationEffect(
|
useClientNotificationEffect(
|
||||||
'textDocument/didClose',
|
'textDocument/didClose',
|
||||||
@@ -376,7 +327,7 @@ export function CommandLineInterface(props: { world: string, level: number, data
|
|||||||
// it's important not to reconstruct the `WithBlah` wrappers below since they contain state
|
// it's important not to reconstruct the `WithBlah` wrappers below since they contain state
|
||||||
// that we want to persist.
|
// that we want to persist.
|
||||||
|
|
||||||
if (!serverVersion) { return <p>Waiting for Lean server to start...</p> }
|
if (!serverVersion) { return <></> }
|
||||||
if (serverStoppedResult) {
|
if (serverStoppedResult) {
|
||||||
return <div>
|
return <div>
|
||||||
<p>{serverStoppedResult.message}</p>
|
<p>{serverStoppedResult.message}</p>
|
||||||
@@ -384,6 +335,94 @@ export function CommandLineInterface(props: { world: string, level: number, data
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return <TypewriterInterface props={props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The interface in command line mode */
|
||||||
|
export function TypewriterInterface({props}) {
|
||||||
|
const ec = React.useContext(EditorContext)
|
||||||
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
const editor = React.useContext(MonacoEditorContext)
|
||||||
|
const model = editor.getModel()
|
||||||
|
const uri = model.uri.toString()
|
||||||
|
|
||||||
|
const [disableInput, setDisableInput] = React.useState<boolean>(false)
|
||||||
|
const [loadingProgress, setLoadingProgress] = React.useState<number>(0)
|
||||||
|
const { setDeletedChat, showHelp, setShowHelp } = React.useContext(DeletedChatContext)
|
||||||
|
const {mobile} = React.useContext(MobileContext)
|
||||||
|
const { proof } = React.useContext(ProofContext)
|
||||||
|
const { setTypewriterInput } = React.useContext(InputModeContext)
|
||||||
|
const { selectedStep, setSelectedStep } = React.useContext(SelectionContext)
|
||||||
|
|
||||||
|
const proofPanelRef = React.useRef<HTMLDivElement>(null)
|
||||||
|
const completed = useAppSelector(selectCompleted(gameId, props.world, props.level))
|
||||||
|
// const config = useEventResult(ec.events.changedInfoviewConfig) ?? defaultInfoviewConfig;
|
||||||
|
// const curUri = useEventResult(ec.events.changedCursorLocation, loc => loc?.uri);
|
||||||
|
|
||||||
|
const rpcSess = useRpcSessionAtPos({uri: uri, line: 0, character: 0})
|
||||||
|
|
||||||
|
/** Delete all proof lines starting from a given line.
|
||||||
|
* Note that the first line (i.e. deleting everything) is `1`!
|
||||||
|
*/
|
||||||
|
function deleteProof(line: number) {
|
||||||
|
return (ev) => {
|
||||||
|
let deletedChat: Array<GameHint> = []
|
||||||
|
filterHints(proof).slice(line).map((hintsAtStep, i) => {
|
||||||
|
// Only add these hidden hints to the deletion stack which were visible
|
||||||
|
deletedChat = [...deletedChat, ...hintsAtStep.filter(hint => (!hint.hidden || showHelp.has(line + i)))]
|
||||||
|
})
|
||||||
|
setDeletedChat(deletedChat)
|
||||||
|
|
||||||
|
editor.executeEdits("typewriter", [{
|
||||||
|
range: monaco.Selection.fromPositions(
|
||||||
|
{ lineNumber: line, column: 1 },
|
||||||
|
editor.getModel().getFullModelRange().getEndPosition()
|
||||||
|
),
|
||||||
|
text: '',
|
||||||
|
forceMoveMarkers: false
|
||||||
|
}])
|
||||||
|
setSelectedStep(undefined)
|
||||||
|
setTypewriterInput(proof[line].command)
|
||||||
|
ev.stopPropagation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSelectStep(line: number) {
|
||||||
|
return (ev) => {
|
||||||
|
if (mobile) {return}
|
||||||
|
if (selectedStep == line) {
|
||||||
|
setSelectedStep(undefined)
|
||||||
|
console.debug(`unselected step`)
|
||||||
|
} else {
|
||||||
|
setSelectedStep(line)
|
||||||
|
console.debug(`step ${line} selected`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll to the end of the proof if it is updated.
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (proof?.length > 1) {
|
||||||
|
proofPanelRef.current?.lastElementChild?.scrollIntoView() //scrollTo(0,0)
|
||||||
|
} else {
|
||||||
|
proofPanelRef.current?.scrollTo(0,0)
|
||||||
|
}
|
||||||
|
// also reenable the commandline when the proof changes
|
||||||
|
|
||||||
|
// BUG: If selecting 2nd goal on a intermediate proofstep and then delete proof to there,
|
||||||
|
// the commandline is not displaying disabled even though it should.
|
||||||
|
setDisableInput(false)
|
||||||
|
}, [proof])
|
||||||
|
|
||||||
|
// Scroll to element if selection changes
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (typeof selectedStep !== 'undefined') {
|
||||||
|
Array.from(proofPanelRef.current?.getElementsByClassName(`step-${selectedStep}`)).map((elem) => {
|
||||||
|
elem.scrollIntoView({ block: "center" })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [selectedStep])
|
||||||
|
|
||||||
// TODO: This about hidden hints is all copied from `level.tsx`. Can we move that into `hints.tsx`?
|
// TODO: This about hidden hints is all copied from `level.tsx`. Can we move that into `hints.tsx`?
|
||||||
|
|
||||||
// If the last step has errors, we want to treat it as if it is part of the second-to-last step
|
// If the last step has errors, we want to treat it as if it is part of the second-to-last step
|
||||||
@@ -417,16 +456,21 @@ export function CommandLineInterface(props: { world: string, level: number, data
|
|||||||
|
|
||||||
let lastStepErrors = proof.length ? hasInteractiveErrors(proof[proof.length - 1].errors) : false
|
let lastStepErrors = proof.length ? hasInteractiveErrors(proof[proof.length - 1].errors) : false
|
||||||
|
|
||||||
// TODO: does the position matter at all?
|
|
||||||
const rpcSess = useRpcSessionAtPos({uri: uri, line: 0, character: 0})
|
|
||||||
|
|
||||||
return <div className="commandline-interface">
|
useServerNotificationEffect("$/game/loading", (params : any) => {
|
||||||
|
if (params.kind == "loadConstants") {
|
||||||
|
setLoadingProgress(params.counter/100*50)
|
||||||
|
} else if (params.kind == "finalizeExtensions") {
|
||||||
|
setLoadingProgress(50 + params.counter/150*50)
|
||||||
|
} else {
|
||||||
|
console.error(`Unknown loading kind: ${params.kind}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return <div className="typewriter-interface">
|
||||||
<RpcContext.Provider value={rpcSess}>
|
<RpcContext.Provider value={rpcSess}>
|
||||||
<div className="content">
|
<div className="content">
|
||||||
<div className="tmp-pusher">
|
<div className="tmp-pusher">
|
||||||
{!proof.length &&
|
|
||||||
<CircularProgress />
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div className='proof' ref={proofPanelRef}>
|
<div className='proof' ref={proofPanelRef}>
|
||||||
<ExerciseStatement data={props.data} />
|
<ExerciseStatement data={props.data} />
|
||||||
@@ -438,12 +482,12 @@ export function CommandLineInterface(props: { world: string, level: number, data
|
|||||||
// entered command as it is still present in the command line.
|
// entered command as it is still present in the command line.
|
||||||
// TODO: Should not use index as key.
|
// TODO: Should not use index as key.
|
||||||
return <div key={`proof-step-${i}`}>
|
return <div key={`proof-step-${i}`}>
|
||||||
<Errors errors={step.errors} commandLineMode={true} />
|
<Errors errors={step.errors} typewriterMode={true} />
|
||||||
</div>
|
</div>
|
||||||
} else {
|
} else {
|
||||||
return <div key={`proof-step-${i}`} className={`step step-${i}` + (selectedStep == i ? ' selected' : '')} onClick={toggleSelectStep(i)}>
|
return <div key={`proof-step-${i}`} className={`step step-${i}` + (selectedStep == i ? ' selected' : '')}>
|
||||||
<Command command={step.command} deleteProof={deleteProof(i)} />
|
<Command command={step.command} deleteProof={deleteProof(i)} />
|
||||||
<Errors errors={step.errors} commandLineMode={true} />
|
<Errors errors={step.errors} typewriterMode={true} />
|
||||||
{mobile && i == 0 && props.data?.introduction &&
|
{mobile && i == 0 && props.data?.introduction &&
|
||||||
<div className={`message information step-0${selectedStep === 0 ? ' selected' : ''}`} onClick={toggleSelectStep(0)}>
|
<div className={`message information step-0${selectedStep === 0 ? ' selected' : ''}`} onClick={toggleSelectStep(0)}>
|
||||||
<Markdown>{props.data?.introduction}</Markdown>
|
<Markdown>{props.data?.introduction}</Markdown>
|
||||||
@@ -460,7 +504,7 @@ export function CommandLineInterface(props: { world: string, level: number, data
|
|||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
<GoalsTab proofStep={step} last={i == proof.length - (lastStepErrors ? 2 : 1)} />
|
<GoalsTabs proofStep={step} last={i == proof.length - (lastStepErrors ? 2 : 1)} onClick={toggleSelectStep(i)} onGoalChange={i == proof.length - 1 - withErr ? (n) => setDisableInput(n > 0) : (n) => {}}/>
|
||||||
{/* Show a message that there are no goals left */}
|
{/* Show a message that there are no goals left */}
|
||||||
{!step.goals.length && (
|
{!step.goals.length && (
|
||||||
<div className="message information">
|
<div className="message information">
|
||||||
@@ -489,11 +533,11 @@ export function CommandLineInterface(props: { world: string, level: number, data
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</> : <></>
|
</> : <CircularProgress variant="determinate" value={loadingProgress} />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CommandLine hidden={!withErr && proof[proof.length - 1]?.goals.length == 0}/>
|
<Typewriter hidden={!withErr && proof[proof.length - 1]?.goals.length == 0} disabled={disableInput || !proof.length}/>
|
||||||
</RpcContext.Provider>
|
</RpcContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ interface MessageViewProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** A list of messages (info/warning/error) that are produced after this command */
|
/** A list of messages (info/warning/error) that are produced after this command */
|
||||||
function Error({error, commandLineMode} : {error : InteractiveDiagnostic, commandLineMode : boolean}) {
|
function Error({error, typewriterMode} : {error : InteractiveDiagnostic, typewriterMode : boolean}) {
|
||||||
// The first step will always have an empty command
|
// The first step will always have an empty command
|
||||||
|
|
||||||
const severityClass = error.severity ? {
|
const severityClass = error.severity ? {
|
||||||
@@ -41,7 +41,7 @@ function Error({error, commandLineMode} : {error : InteractiveDiagnostic, comman
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <div className={severityClass + ' ml1 message'}>
|
return <div className={severityClass + ' ml1 message'}>
|
||||||
{!commandLineMode && <p className="mv2">{title}</p>}
|
{!typewriterMode && <p className="mv2">{title}</p>}
|
||||||
<pre className="font-code pre-wrap">
|
<pre className="font-code pre-wrap">
|
||||||
<InteractiveMessage fmt={message} />
|
<InteractiveMessage fmt={message} />
|
||||||
</pre>
|
</pre>
|
||||||
@@ -50,9 +50,9 @@ function Error({error, commandLineMode} : {error : InteractiveDiagnostic, comman
|
|||||||
|
|
||||||
// TODO: Should not use index as key.
|
// TODO: Should not use index as key.
|
||||||
/** A list of messages (info/warning/error) that are produced after this command */
|
/** A list of messages (info/warning/error) that are produced after this command */
|
||||||
export function Errors ({errors, commandLineMode} : {errors : InteractiveDiagnostic[], commandLineMode : boolean}) {
|
export function Errors ({errors, typewriterMode} : {errors : InteractiveDiagnostic[], typewriterMode : boolean}) {
|
||||||
return <div>
|
return <div>
|
||||||
{errors.map((err, i) => (<Error key={`error-${i}`} error={err} commandLineMode={commandLineMode}/>))}
|
{errors.map((err, i) => (<Error key={`error-${i}`} error={err} typewriterMode={typewriterMode}/>))}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ const MessageView = React.memo(({uri, diag}: MessageViewProps) => {
|
|||||||
message = diag.message
|
message = diag.message
|
||||||
}
|
}
|
||||||
|
|
||||||
const { commandLineMode } = React.useContext(InputModeContext)
|
const { typewriterMode } = React.useContext(InputModeContext)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// <details open>
|
// <details open>
|
||||||
@@ -98,7 +98,7 @@ const MessageView = React.memo(({uri, diag}: MessageViewProps) => {
|
|||||||
// </span>
|
// </span>
|
||||||
// </summary>
|
// </summary>
|
||||||
<div className={severityClass + ' ml1 message'}>
|
<div className={severityClass + ' ml1 message'}>
|
||||||
{!commandLineMode && <p className="mv2">{title}</p>}
|
{!typewriterMode && <p className="mv2">{title}</p>}
|
||||||
<pre className="font-code pre-wrap">
|
<pre className="font-code pre-wrap">
|
||||||
<InteractiveMessage fmt={message} />
|
<InteractiveMessage fmt={message} />
|
||||||
</pre>
|
</pre>
|
||||||
|
|||||||
+26
-19
@@ -64,7 +64,7 @@ config.autoClosingPairs = config.autoClosingPairs.map(
|
|||||||
monaco.languages.setLanguageConfiguration('lean4cmd', config);
|
monaco.languages.setLanguageConfiguration('lean4cmd', config);
|
||||||
|
|
||||||
/** The input field */
|
/** The input field */
|
||||||
export function CommandLine({hidden}: {hidden?: boolean}) {
|
export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boolean}) {
|
||||||
|
|
||||||
/** Reference to the hidden multi-line editor */
|
/** Reference to the hidden multi-line editor */
|
||||||
const editor = React.useContext(MonacoEditorContext)
|
const editor = React.useContext(MonacoEditorContext)
|
||||||
@@ -74,7 +74,7 @@ export function CommandLine({hidden}: {hidden?: boolean}) {
|
|||||||
const [oneLineEditor, setOneLineEditor] = useState<monaco.editor.IStandaloneCodeEditor>(null)
|
const [oneLineEditor, setOneLineEditor] = useState<monaco.editor.IStandaloneCodeEditor>(null)
|
||||||
const [processing, setProcessing] = useState(false)
|
const [processing, setProcessing] = useState(false)
|
||||||
|
|
||||||
const {commandLineInput, setCommandLineInput} = React.useContext(InputModeContext)
|
const {typewriterInput, setTypewriterInput} = React.useContext(InputModeContext)
|
||||||
|
|
||||||
const inputRef = useRef()
|
const inputRef = useRef()
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ export function CommandLine({hidden}: {hidden?: boolean}) {
|
|||||||
rpcSess.call('Game.getInteractiveGoals', DocumentPosition.toTdpp({line: i, character: 0, uri: uri}))
|
rpcSess.call('Game.getInteractiveGoals', DocumentPosition.toTdpp({line: i, character: 0, uri: uri}))
|
||||||
)
|
)
|
||||||
msgCalls.push(
|
msgCalls.push(
|
||||||
getInteractiveDiagnostics(rpcSess, {start: i, end: i+1})
|
getInteractiveDiagnostics(rpcSess, {start: i, end: i+1}).catch((error) => {console.debug("promise broken")})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,8 +178,8 @@ export function CommandLine({hidden}: {hidden?: boolean}) {
|
|||||||
})
|
})
|
||||||
// Save the proof to the context
|
// Save the proof to the context
|
||||||
setProof(tmpProof)
|
setProof(tmpProof)
|
||||||
})
|
}).catch((error) => {console.debug("promise broken")})
|
||||||
})
|
}).catch((error) => {console.debug("promise broken")})
|
||||||
}, [editor, rpcSess, uri, model])
|
}, [editor, rpcSess, uri, model])
|
||||||
|
|
||||||
// Run the command
|
// Run the command
|
||||||
@@ -190,26 +190,33 @@ export function CommandLine({hidden}: {hidden?: boolean}) {
|
|||||||
setDeletedChat([])
|
setDeletedChat([])
|
||||||
|
|
||||||
const pos = editor.getPosition()
|
const pos = editor.getPosition()
|
||||||
if (commandLineInput) {
|
if (typewriterInput) {
|
||||||
setProcessing(true)
|
setProcessing(true)
|
||||||
editor.executeEdits("command-line", [{
|
editor.executeEdits("typewriter", [{
|
||||||
range: monaco.Selection.fromPositions(
|
range: monaco.Selection.fromPositions(
|
||||||
pos,
|
pos,
|
||||||
editor.getModel().getFullModelRange().getEndPosition()
|
editor.getModel().getFullModelRange().getEndPosition()
|
||||||
),
|
),
|
||||||
text: commandLineInput.trim() + "\n",
|
text: typewriterInput.trim() + "\n",
|
||||||
forceMoveMarkers: false
|
forceMoveMarkers: false
|
||||||
}])
|
}])
|
||||||
|
setTypewriterInput('')
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.setPosition(pos)
|
editor.setPosition(pos)
|
||||||
}, [commandLineInput, editor])
|
}, [typewriterInput, editor])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (oneLineEditor && oneLineEditor.getValue() !== commandLineInput) {
|
if (oneLineEditor && oneLineEditor.getValue() !== typewriterInput) {
|
||||||
oneLineEditor.setValue(commandLineInput)
|
oneLineEditor.setValue(typewriterInput)
|
||||||
}
|
}
|
||||||
}, [commandLineInput])
|
}, [typewriterInput])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (proof.length && hasInteractiveErrors(proof[proof.length - 1].errors)) {
|
||||||
|
setTypewriterInput(proof[proof.length - 1].command)
|
||||||
|
}
|
||||||
|
}, [proof])
|
||||||
|
|
||||||
// React when answer from the server comes back
|
// React when answer from the server comes back
|
||||||
useServerNotificationEffect('textDocument/publishDiagnostics', (params: PublishDiagnosticsParams) => {
|
useServerNotificationEffect('textDocument/publishDiagnostics', (params: PublishDiagnosticsParams) => {
|
||||||
@@ -217,7 +224,7 @@ export function CommandLine({hidden}: {hidden?: boolean}) {
|
|||||||
setProcessing(false)
|
setProcessing(false)
|
||||||
loadAllGoals()
|
loadAllGoals()
|
||||||
if (!hasErrors(params.diagnostics)) {
|
if (!hasErrors(params.diagnostics)) {
|
||||||
setCommandLineInput("")
|
//setTypewriterInput("")
|
||||||
editor.setPosition(editor.getModel().getFullModelRange().getEndPosition())
|
editor.setPosition(editor.getModel().getFullModelRange().getEndPosition())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -231,7 +238,7 @@ export function CommandLine({hidden}: {hidden?: boolean}) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const myEditor = monaco.editor.create(inputRef.current!, {
|
const myEditor = monaco.editor.create(inputRef.current!, {
|
||||||
value: commandLineInput,
|
value: typewriterInput,
|
||||||
language: "lean4cmd",
|
language: "lean4cmd",
|
||||||
quickSuggestions: false,
|
quickSuggestions: false,
|
||||||
lightbulb: {
|
lightbulb: {
|
||||||
@@ -274,14 +281,14 @@ export function CommandLine({hidden}: {hidden?: boolean}) {
|
|||||||
// Ensure that our one-line editor can only have a single line
|
// Ensure that our one-line editor can only have a single line
|
||||||
const l = oneLineEditor.getModel().onDidChangeContent((e) => {
|
const l = oneLineEditor.getModel().onDidChangeContent((e) => {
|
||||||
const value = oneLineEditor.getValue()
|
const value = oneLineEditor.getValue()
|
||||||
setCommandLineInput(value)
|
setTypewriterInput(value)
|
||||||
const newValue = value.replace(/[\n\r]/g, '')
|
const newValue = value.replace(/[\n\r]/g, '')
|
||||||
if (value != newValue) {
|
if (value != newValue) {
|
||||||
oneLineEditor.setValue(newValue)
|
oneLineEditor.setValue(newValue)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return () => { l.dispose() }
|
return () => { l.dispose() }
|
||||||
}, [oneLineEditor, setCommandLineInput])
|
}, [oneLineEditor, setTypewriterInput])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!oneLineEditor) return
|
if (!oneLineEditor) return
|
||||||
@@ -308,10 +315,10 @@ export function CommandLine({hidden}: {hidden?: boolean}) {
|
|||||||
runCommand()
|
runCommand()
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={`command-line${hidden ? ' hidden' : ''}`}>
|
return <div className={`typewriter${hidden ? ' hidden' : ''}${disabled ? ' disabled' : ''}`}>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="command-line-input-wrapper">
|
<div className="typewriter-input-wrapper">
|
||||||
<div ref={inputRef} className="command-line-input" />
|
<div ref={inputRef} className="typewriter-input" />
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" disabled={processing} className="btn btn-inverted">
|
<button type="submit" disabled={processing} className="btn btn-inverted">
|
||||||
<FontAwesomeIcon icon={faWandMagicSparkles} /> Execute
|
<FontAwesomeIcon icon={faWandMagicSparkles} /> Execute
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import './inventory.css'
|
import '../css/inventory.css'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faLock, faLockOpen, faBook, faHammer, faBan } from '@fortawesome/free-solid-svg-icons'
|
import { faLock, faBan } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { GameIdContext } from '../app';
|
import { GameIdContext } from '../app';
|
||||||
import Markdown from './markdown';
|
import Markdown from './markdown';
|
||||||
import { useLoadDocQuery, InventoryTile, LevelInfo, InventoryOverview, useLoadInventoryOverviewQuery } from '../state/api';
|
import { useLoadDocQuery, InventoryTile, LevelInfo, InventoryOverview, useLoadInventoryOverviewQuery, WorldOverview } from '../state/api';
|
||||||
import { selectDifficulty, selectInventory } from '../state/progress';
|
import { selectDifficulty, selectInventory } from '../state/progress';
|
||||||
import { store } from '../state/store';
|
import { store } from '../state/store';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
@@ -29,7 +29,7 @@ export function Inventory({levelInfo, openDoc, enableAll=false} :
|
|||||||
{levelInfo?.definitions &&
|
{levelInfo?.definitions &&
|
||||||
<InventoryList items={levelInfo?.definitions} docType="Definition" openDoc={openDoc} enableAll={enableAll}/>
|
<InventoryList items={levelInfo?.definitions} docType="Definition" openDoc={openDoc} enableAll={enableAll}/>
|
||||||
}
|
}
|
||||||
<h2>Lemmas</h2>
|
<h2>Theorems</h2>
|
||||||
{levelInfo?.lemmas &&
|
{levelInfo?.lemmas &&
|
||||||
<InventoryList items={levelInfo?.lemmas} docType="Lemma" openDoc={openDoc} defaultTab={levelInfo?.lemmaTab} level={levelInfo} enableAll={enableAll}/>
|
<InventoryList items={levelInfo?.lemmas} docType="Lemma" openDoc={openDoc} defaultTab={levelInfo?.lemmaTab} level={levelInfo} enableAll={enableAll}/>
|
||||||
}
|
}
|
||||||
@@ -37,6 +37,79 @@ export function Inventory({levelInfo, openDoc, enableAll=false} :
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function OverviewInventory({data, openDoc, enableAll=false, showOverview=true} :
|
||||||
|
{
|
||||||
|
data: WorldOverview[],
|
||||||
|
openDoc: (props: {name: string, type: string}) => void,
|
||||||
|
enableAll?: boolean,
|
||||||
|
showOverview?: boolean
|
||||||
|
}) {
|
||||||
|
|
||||||
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
const difficulty = useSelector(selectDifficulty(gameId))
|
||||||
|
const [tab, setTab] = useState<string>()
|
||||||
|
let inv: string[] = selectInventory(gameId)(store.getState())
|
||||||
|
|
||||||
|
let levelInfo : InventoryOverview = {
|
||||||
|
tactics : data ? data.map(world => (world.tactics.map(tile => inv.includes(tile.name) ? {...tile, locked: false} : tile))).flat() : [],
|
||||||
|
lemmas : data ? data.map(world => (world.lemmas.map(tile => inv.includes(tile.name) ? {...tile, locked: false} : tile))).flat() : [],
|
||||||
|
definitions : data ? data.map(world => (world.definitions.map(tile => inv.includes(tile.name) ? {...tile, locked: false} : tile))).flat() : [],
|
||||||
|
lemmaTab: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Inventory levelInfo={levelInfo} openDoc={openDoc} enableAll={enableAll}/>
|
||||||
|
{showOverview &&
|
||||||
|
<div className="inventory">
|
||||||
|
{data && <>
|
||||||
|
<h2>Overviews</h2>
|
||||||
|
<div className="tab-bar">
|
||||||
|
{data.map(world => {
|
||||||
|
return <div key={`category-${world.world}`} className={`tab ${world.world == tab ? "active": ""}`} onClick={() => { setTab((tab == world.world) ? null : world.world) }}>{world.world}</div>
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="inventory-list">
|
||||||
|
{/* TODO: a bit hacky, do we need to redesign inventory completely and provide it in a better order? */}
|
||||||
|
{data.map(world => {
|
||||||
|
if (world.world == tab) {
|
||||||
|
return [
|
||||||
|
...world.tactics.map(item => {
|
||||||
|
return <InventoryItem
|
||||||
|
key={`${world.world}-${item.name}`}
|
||||||
|
showDoc={() => {openDoc({name: item.name, type: 'Tactic'})}}
|
||||||
|
name={item.name} displayName={item.displayName}
|
||||||
|
locked={difficulty > 0 ? !inv.includes(item.name) : false}
|
||||||
|
disabled={item.disabled} newly={false} enableAll={enableAll} />
|
||||||
|
}),
|
||||||
|
...world.lemmas.map(item => {
|
||||||
|
return <InventoryItem
|
||||||
|
key={`${world.world}-${item.name}`}
|
||||||
|
showDoc={() => {openDoc({name: item.name, type: 'Lemma'})}}
|
||||||
|
name={item.name} displayName={item.displayName}
|
||||||
|
locked={difficulty > 0 ? item.locked : false}
|
||||||
|
disabled={item.disabled} newly={false} enableAll={enableAll} />
|
||||||
|
}),
|
||||||
|
...world.definitions.map(item => {
|
||||||
|
return <InventoryItem
|
||||||
|
key={`${world.world}-${item.name}`}
|
||||||
|
showDoc={() => {openDoc({name: item.name, type: 'Definition'})}}
|
||||||
|
name={item.name} displayName={item.displayName}
|
||||||
|
locked={difficulty > 0 ? item.locked : false}
|
||||||
|
disabled={item.disabled} newly={false} enableAll={enableAll} />
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function InventoryList({items, docType, openDoc, defaultTab=null, level=undefined, enableAll=false} :
|
function InventoryList({items, docType, openDoc, defaultTab=null, level=undefined, enableAll=false} :
|
||||||
{
|
{
|
||||||
items: InventoryTile[],
|
items: InventoryTile[],
|
||||||
@@ -86,7 +159,7 @@ function InventoryList({items, docType, openDoc, defaultTab=null, level=undefine
|
|||||||
{[...modifiedItems].sort(
|
{[...modifiedItems].sort(
|
||||||
// For lemas, sort entries `available > disabled > locked`
|
// For lemas, sort entries `available > disabled > locked`
|
||||||
// otherwise alphabetically
|
// otherwise alphabetically
|
||||||
(x, y) => +(docType == "Lemma") * (+x.locked - +y.locked || +x.disabled - +y.disabled)
|
(x, y) => +(docType == "Lemma") * (+x.locked - +y.locked || +x.disabled - +y.disabled) || x.displayName.localeCompare(y.displayName)
|
||||||
).filter(item => !item.hidden && ((tab ?? categories[0]) == item.category)).map((item, i) => {
|
).filter(item => !item.hidden && ((tab ?? categories[0]) == item.category)).map((item, i) => {
|
||||||
return <InventoryItem key={`${item.category}-${item.name}`}
|
return <InventoryItem key={`${item.category}-${item.name}`}
|
||||||
showDoc={() => {openDoc({name: item.name, type: docType})}}
|
showDoc={() => {openDoc({name: item.name, type: docType})}}
|
||||||
@@ -144,3 +217,21 @@ export function InventoryPanel({levelInfo, visible = true}) {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The panel (on the welcome page) showing the user's inventory with tactics, definitions, and lemmas */
|
||||||
|
export function InventoryOverviewPanel({data, visible = true, showOverview=true} : {data : WorldOverview[], visible?: boolean, showOverview?: boolean}) {
|
||||||
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
|
||||||
|
// The inventory is overlayed by the doc entry of a clicked item
|
||||||
|
const [inventoryDoc, setInventoryDoc] = useState<{name: string, type: string}>(null)
|
||||||
|
// Set `inventoryDoc` to `null` to close the doc
|
||||||
|
function closeInventoryDoc() {setInventoryDoc(null)}
|
||||||
|
|
||||||
|
return <div className={`column inventory-panel ${visible ? '' : 'hidden'}`}>
|
||||||
|
{inventoryDoc ?
|
||||||
|
<Documentation name={inventoryDoc.name} type={inventoryDoc.type} handleClose={closeInventoryDoc}/>
|
||||||
|
:
|
||||||
|
<OverviewInventory data={data} openDoc={setInventoryDoc} enableAll={true} showOverview={showOverview}/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ import '@fontsource/roboto/400.css';
|
|||||||
import '@fontsource/roboto/500.css';
|
import '@fontsource/roboto/500.css';
|
||||||
import '@fontsource/roboto/700.css';
|
import '@fontsource/roboto/700.css';
|
||||||
|
|
||||||
import './landing_page.css'
|
import '../css/landing_page.css'
|
||||||
import coverRobo from '../assets/covers/formaloversum.png'
|
|
||||||
import bgImage from '../assets/bg.jpg'
|
import bgImage from '../assets/bg.jpg'
|
||||||
|
|
||||||
import Markdown from './markdown';
|
import Markdown from './markdown';
|
||||||
import {PrivacyPolicyPopup} from './privacy_policy'
|
import {PrivacyPolicyPopup} from './popup/privacy_policy'
|
||||||
|
import { GameTile, useGetGameInfoQuery } from '../state/api'
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
const flag = {
|
const flag = {
|
||||||
'Dutch': '🇳🇱',
|
'Dutch': '🇳🇱',
|
||||||
@@ -19,6 +20,7 @@ const flag = {
|
|||||||
'French': '🇫🇷',
|
'French': '🇫🇷',
|
||||||
'German': '🇩🇪',
|
'German': '🇩🇪',
|
||||||
'Italian': '🇮🇹',
|
'Italian': '🇮🇹',
|
||||||
|
'Spanish': '🇪🇸',
|
||||||
}
|
}
|
||||||
|
|
||||||
function GithubIcon({url='https://github.com'}) {
|
function GithubIcon({url='https://github.com'}) {
|
||||||
@@ -32,47 +34,44 @@ function GithubIcon({url='https://github.com'}) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
function GameTile({
|
function Tile({gameId, data}: {gameId: string, data: GameTile|undefined}) {
|
||||||
title,
|
|
||||||
gameId,
|
|
||||||
intro, // Catchy intro phrase.
|
|
||||||
image=null,
|
|
||||||
worlds='?',
|
|
||||||
levels='?',
|
|
||||||
prereq='–', // Optional list of games that this game builds on. Use markdown.
|
|
||||||
description, // Longer description. Supports Markdown.
|
|
||||||
language}) {
|
|
||||||
|
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
const routeChange = () =>{
|
const routeChange = () =>{
|
||||||
navigate(gameId);
|
navigate(gameId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof data === 'undefined') {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
|
||||||
return <div className="game" onClick={routeChange}>
|
return <div className="game" onClick={routeChange}>
|
||||||
<div className="wrapper">
|
<div className="wrapper">
|
||||||
<div className="title">{title}</div>
|
<div className="title">{data.title}</div>
|
||||||
<div className="short-description">{intro}
|
<div className="short-description">{data.short}
|
||||||
</div>
|
</div>
|
||||||
{ image ? <img className="image" src={image} alt="" /> : <div className="image"/> }
|
{ data.image ? <img className="image" src={path.join("data", gameId, data.image)} alt="" /> : <div className="image"/> }
|
||||||
<div className="long description"><Markdown>{description}</Markdown></div>
|
<div className="long description"><Markdown>{data.long}</Markdown></div>
|
||||||
</div>
|
</div>
|
||||||
<table className="info">
|
<table className="info">
|
||||||
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td title="consider playing these games first.">Prerequisites</td>
|
<td title="consider playing these games first.">Prerequisites</td>
|
||||||
<td><Markdown>{prereq}</Markdown></td>
|
<td><Markdown>{data.prerequisites.join(', ')}</Markdown></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Worlds</td>
|
<td>Worlds</td>
|
||||||
<td>{worlds}</td>
|
<td>{data.worlds}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Levels</td>
|
<td>Levels</td>
|
||||||
<td>{levels}</td>
|
<td>{data.levels}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Language</td>
|
<td>Language</td>
|
||||||
<td title={`in ${language}`}>{flag[language]}</td>
|
<td title={`in ${data.languages.join(', ')}`}>{data.languages.map((lan) => flag[lan]).join(', ')}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -86,6 +85,58 @@ function LandingPage() {
|
|||||||
const openImpressum = () => setImpressum(true);
|
const openImpressum = () => setImpressum(true);
|
||||||
const closeImpressum = () => setImpressum(false);
|
const closeImpressum = () => setImpressum(false);
|
||||||
|
|
||||||
|
// const [allGames, setAllGames] = React.useState([])
|
||||||
|
// const [allTiles, setAllTiles] = React.useState([])
|
||||||
|
|
||||||
|
// const getTiles=()=>{
|
||||||
|
// fetch('featured_games.json', {
|
||||||
|
// headers : {
|
||||||
|
// 'Content-Type': 'application/json',
|
||||||
|
// 'Accept': 'application/json'
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ).then(function(response){
|
||||||
|
// return response.json()
|
||||||
|
// }).then(function(data) {
|
||||||
|
// setAllGames(data.featured_games)
|
||||||
|
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// React.useEffect(()=>{
|
||||||
|
// getTiles()
|
||||||
|
// },[])
|
||||||
|
|
||||||
|
// React.useEffect(()=>{
|
||||||
|
|
||||||
|
// Promise.allSettled(
|
||||||
|
// allGames.map((gameId) => (
|
||||||
|
// fetch(`data/g/${gameId}/game.json`).catch(err => {return undefined})))
|
||||||
|
// ).then(responses =>
|
||||||
|
// responses.forEach((result) => console.log(result)))
|
||||||
|
// // Promise.all(responses.map(res => {
|
||||||
|
// // if (res.status == "fulfilled") {
|
||||||
|
// // console.log(res.value.json())
|
||||||
|
// // return res.value.json()
|
||||||
|
// // } else {
|
||||||
|
// // return undefined
|
||||||
|
// // }
|
||||||
|
// // }))
|
||||||
|
// // ).then(allData => {
|
||||||
|
// // setAllTiles(allData.map(data => data?.tile))
|
||||||
|
// // })
|
||||||
|
// },[allGames])
|
||||||
|
|
||||||
|
// TODO: I would like to read the supported games list form a JSON,
|
||||||
|
// Then load all these games in
|
||||||
|
//
|
||||||
|
let allGames = [
|
||||||
|
"leanprover-community/nng4",
|
||||||
|
"hhu-adam/robo",
|
||||||
|
"djvelleman/stg4",
|
||||||
|
"miguelmarco/STG4",
|
||||||
|
]
|
||||||
|
let allTiles = allGames.map((gameId) => (useGetGameInfoQuery({game: `g/${gameId}`}).data?.tile))
|
||||||
|
|
||||||
return <div className="landing-page">
|
return <div className="landing-page">
|
||||||
<header style={{backgroundImage: `url(${bgImage})`}}>
|
<header style={{backgroundImage: `url(${bgImage})`}}>
|
||||||
@@ -102,40 +153,26 @@ function LandingPage() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="game-list">
|
<div className="game-list">
|
||||||
|
{allTiles.length == 0 ?
|
||||||
<GameTile
|
<p>No Games loaded. Use <a>http://localhost:3000/#/g/local/FOLDER</a> to open a
|
||||||
title="Natural Number Game"
|
game directly from a local folder.
|
||||||
gameId="g/hhu-adam/NNG4"
|
</p>
|
||||||
intro="The classical introduction game for Lean."
|
: allGames.map((id, i) => (
|
||||||
description="In this game you recreate the natural numbers $\mathbb{N}$ from the Peano axioms,
|
<Tile
|
||||||
learning the basics about theorem proving in Lean.
|
key={id}
|
||||||
|
gameId={`g/${id}`}
|
||||||
This is a good first introduction to Lean!"
|
data={allTiles[i]}
|
||||||
worlds="9"
|
/>
|
||||||
levels="72"
|
))
|
||||||
language="English"
|
}
|
||||||
/>
|
|
||||||
|
|
||||||
<GameTile
|
|
||||||
title="Formaloversum"
|
|
||||||
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."
|
|
||||||
description="
|
|
||||||
Dieses Spiel führt die Grundlagen zur Beweisführung in Lean ein und schneidet danach verschiedene Bereiche des Bachelorstudiums an.
|
|
||||||
|
|
||||||
(Das Spiel befindet sich noch in der Entstehungsphase.)"
|
|
||||||
image={coverRobo}
|
|
||||||
language="German"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<section>
|
<section>
|
||||||
<div className="wrapper">
|
<div className="wrapper">
|
||||||
<h2>Development notes</h2>
|
<h2>Development notes</h2>
|
||||||
<p>
|
<p>
|
||||||
As this server runs lean on our university machines, it has a limited capacity.
|
As this server runs lean on our university machines, it has a limited capacity.
|
||||||
Our current estimate is about 55 copies of the NNG or 25 copies of games importing
|
Our current estimate is about 70 simultaneous games.
|
||||||
mathlib. We hope to address this limitation in the future.
|
We hope to address and test this limitation better in the future.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Most aspects of the games and the infrastructure are still in development. Feel free to
|
Most aspects of the games and the infrastructure are still in development. Feel free to
|
||||||
@@ -149,18 +186,19 @@ Dieses Spiel führt die Grundlagen zur Beweisführung in Lean ein und schneidet
|
|||||||
<h2>Adding new games</h2>
|
<h2>Adding new games</h2>
|
||||||
<p>
|
<p>
|
||||||
If you are considering writing your own game, you should use
|
If you are considering writing your own game, you should use
|
||||||
the <a target="_blank" href="https://github.com/hhu-adam/NNG4">NNG Github Repo</a> as
|
the <a target="_blank" href="https://github.com/hhu-adam/GameSkeleton">GameSkeleton Github Repo</a> as
|
||||||
a template.
|
a template and read <a target="_blank" href="https://github.com/leanprover-community/lean4game/">How to Create a Game</a>.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
There is an option to load and run your own games direclty on the server,
|
You can directly load your games into the server and play it using
|
||||||
instructions are in the NNG repo. Since this is still in development we'd like to
|
the correct URL. The <a target="_blank" href="https://github.com/leanprover-community/lean4game/">instructions above</a> also
|
||||||
encourage you to contact us for support creating your own game. The documentation is
|
explain the details for how to load your game to the server.
|
||||||
not polished yet.
|
|
||||||
|
We'd like to encourage you to contact us if you have any questions.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
To add games to this main page, you should get in contact as
|
Featured games on this page are added manually.
|
||||||
games will need to be added manually.
|
Please get in contact and we-ll happily add yours.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -178,9 +216,6 @@ Dieses Spiel führt die Grundlagen zur Beweisführung in Lean ein und schneidet
|
|||||||
<a className="link" onClick={openImpressum}>Impressum</a>
|
<a className="link" onClick={openImpressum}>Impressum</a>
|
||||||
{impressum? <PrivacyPolicyPopup handleClose={closeImpressum} />: null}
|
{impressum? <PrivacyPolicyPopup handleClose={closeImpressum} />: null}
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
{/* <PrivacyPolicy/> */}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+126
-91
@@ -4,7 +4,7 @@ import { useSelector, useStore } from 'react-redux'
|
|||||||
import Split from 'react-split'
|
import Split from 'react-split'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faBars, faBook, faCode, faXmark, faHome, faCircleInfo, faArrowRight, faArrowLeft, faTerminal } from '@fortawesome/free-solid-svg-icons'
|
import { faHome, faArrowRight } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { CircularProgress } from '@mui/material'
|
import { CircularProgress } from '@mui/material'
|
||||||
import type { Location } from 'vscode-languageserver-protocol'
|
import type { Location } from 'vscode-languageserver-protocol'
|
||||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
|
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
|
||||||
@@ -22,18 +22,19 @@ import { ConnectionContext, connection, useLeanClient } from '../connection'
|
|||||||
import { useAppDispatch, useAppSelector } from '../hooks'
|
import { useAppDispatch, useAppSelector } from '../hooks'
|
||||||
import { useGetGameInfoQuery, useLoadInventoryOverviewQuery, useLoadLevelQuery } from '../state/api'
|
import { useGetGameInfoQuery, useLoadInventoryOverviewQuery, useLoadLevelQuery } from '../state/api'
|
||||||
import { changedSelection, codeEdited, selectCode, selectSelections, selectCompleted, helpEdited,
|
import { changedSelection, codeEdited, selectCode, selectSelections, selectCompleted, helpEdited,
|
||||||
selectHelp, selectDifficulty, selectInventory } from '../state/progress'
|
selectHelp, selectDifficulty, selectInventory, selectTypewriterMode, changeTypewriterMode } from '../state/progress'
|
||||||
import { store } from '../state/store'
|
import { store } from '../state/store'
|
||||||
import { Button } from './button'
|
import { Button } from './button'
|
||||||
import Markdown from './markdown'
|
import Markdown from './markdown'
|
||||||
import {InventoryPanel} from './inventory'
|
import {InventoryOverviewPanel, InventoryPanel} from './inventory'
|
||||||
import { hasInteractiveErrors } from './infoview/command_line'
|
import { hasInteractiveErrors } from './infoview/typewriter'
|
||||||
import { DeletedChatContext, InputModeContext, MobileContext, MonacoEditorContext,
|
import { DeletedChatContext, InputModeContext, MobileContext, MonacoEditorContext,
|
||||||
ProofContext, ProofStep, SelectionContext, WorldLevelIdContext } from './infoview/context'
|
ProofContext, ProofStep, SelectionContext, WorldLevelIdContext } from './infoview/context'
|
||||||
import { DualEditor } from './infoview/main'
|
import { DualEditor } from './infoview/main'
|
||||||
import { GameHint } from './infoview/rpc_api'
|
import { GameHint } from './infoview/rpc_api'
|
||||||
import { DeletedHints, Hint, Hints } from './hints'
|
import { DeletedHints, Hint, Hints, filterHints } from './hints'
|
||||||
import { Impressum } from './privacy_policy'
|
import { PrivacyPolicyPopup } from './popup/privacy_policy'
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
import '@fontsource/roboto/300.css'
|
import '@fontsource/roboto/300.css'
|
||||||
import '@fontsource/roboto/400.css'
|
import '@fontsource/roboto/400.css'
|
||||||
@@ -41,17 +42,25 @@ import '@fontsource/roboto/500.css'
|
|||||||
import '@fontsource/roboto/700.css'
|
import '@fontsource/roboto/700.css'
|
||||||
import 'lean4web/client/src/editor/infoview.css'
|
import 'lean4web/client/src/editor/infoview.css'
|
||||||
import 'lean4web/client/src/editor/vscode.css'
|
import 'lean4web/client/src/editor/vscode.css'
|
||||||
import './level.css'
|
import '../css/level.css'
|
||||||
import { LevelAppBar } from './app_bar'
|
import { LevelAppBar } from './app_bar'
|
||||||
|
|
||||||
function Level() {
|
function Level() {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const levelId = parseInt(params.levelId)
|
const levelId = parseInt(params.levelId)
|
||||||
const worldId = params.worldId
|
const worldId = params.worldId
|
||||||
// useLoadWorldFiles(worldId)
|
|
||||||
|
const [impressum, setImpressum] = React.useState(false)
|
||||||
|
|
||||||
|
const closeImpressum = () => {
|
||||||
|
setImpressum(false)
|
||||||
|
}
|
||||||
|
|
||||||
return <WorldLevelIdContext.Provider value={{worldId, levelId}}>
|
return <WorldLevelIdContext.Provider value={{worldId, levelId}}>
|
||||||
{levelId == 0 ? <Introduction /> : <PlayableLevel key={`${worldId}/${levelId}`}/>}
|
{levelId == 0 ?
|
||||||
|
<Introduction impressum={impressum} setImpressum={setImpressum} /> :
|
||||||
|
<PlayableLevel key={`${worldId}/${levelId}`} impressum={impressum} setImpressum={setImpressum} />}
|
||||||
|
{impressum ? <PrivacyPolicyPopup handleClose={closeImpressum} /> : null}
|
||||||
</WorldLevelIdContext.Provider>
|
</WorldLevelIdContext.Provider>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,20 +136,26 @@ function ChatPanel({lastLevel}) {
|
|||||||
// // chatRef.current!.scrollTo(0,0)
|
// // chatRef.current!.scrollTo(0,0)
|
||||||
// }, [gameId, worldId, levelId])
|
// }, [gameId, worldId, levelId])
|
||||||
|
|
||||||
|
let introText: Array<string> = level?.data?.introduction.split(/\n(\s*\n)+/)
|
||||||
|
|
||||||
|
// experimental: Remove all hints that appeared identically in the previous step
|
||||||
|
// This effectively prevent consequtive hints being shown.
|
||||||
|
let modifiedHints : GameHint[][] = filterHints(proof)
|
||||||
|
|
||||||
return <div className="chat-panel">
|
return <div className="chat-panel">
|
||||||
<div ref={chatRef} className="chat">
|
<div ref={chatRef} className="chat">
|
||||||
{level?.data?.introduction &&
|
{introText?.filter(t => t.trim()).map(((t, i) =>
|
||||||
<div className={`message information step-0${selectedStep === 0 ? ' selected' : ''}`} onClick={toggleSelection(0)}>
|
// Show the level's intro text as hints, too
|
||||||
<Markdown>{level?.data?.introduction}</Markdown>
|
<Hint key={`intro-p-${i}`}
|
||||||
</div>
|
hint={{text: t, hidden: false}} step={0} selected={selectedStep} toggleSelection={toggleSelection(0)} />
|
||||||
}
|
))}
|
||||||
{proof.map((step, i) => {
|
{modifiedHints.map((step, i) => {
|
||||||
// It the last step has errors, it will have the same hints
|
// It the last step has errors, it will have the same hints
|
||||||
// as the second-to-last step. Therefore we should not display them.
|
// as the second-to-last step. Therefore we should not display them.
|
||||||
if (!(i == proof.length - 1 && withErr)) {
|
if (!(i == proof.length - 1 && withErr)) {
|
||||||
// TODO: Should not use index as key.
|
// TODO: Should not use index as key.
|
||||||
return <Hints key={`hints-${i}`}
|
return <Hints key={`hints-${i}`}
|
||||||
hints={step.hints} showHidden={showHelp.has(i)} step={i}
|
hints={step} showHidden={showHelp.has(i)} step={i}
|
||||||
selected={selectedStep} toggleSelection={toggleSelection(i)} lastLevel={i == proof.length - 1}/>
|
selected={selectedStep} toggleSelection={toggleSelection(i)} lastLevel={i == proof.length - 1}/>
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
@@ -176,7 +191,7 @@ function ChatPanel({lastLevel}) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
function ExercisePanel({codeviewRef, impressum, closeImpressum, visible=true}) {
|
function ExercisePanel({codeviewRef, visible=true}) {
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
||||||
const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
||||||
@@ -185,21 +200,25 @@ function ExercisePanel({codeviewRef, impressum, closeImpressum, visible=true}) {
|
|||||||
<div className="exercise">
|
<div className="exercise">
|
||||||
<DualEditor level={level?.data} codeviewRef={codeviewRef} levelId={levelId} worldId={worldId} worldSize={gameInfo.data?.worldSize[worldId]}/>
|
<DualEditor level={level?.data} codeviewRef={codeviewRef} levelId={levelId} worldId={worldId} worldSize={gameInfo.data?.worldSize[worldId]}/>
|
||||||
</div>
|
</div>
|
||||||
{impressum ? <Impressum handleClose={closeImpressum} /> : null}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
function PlayableLevel() {
|
function PlayableLevel({impressum, setImpressum}) {
|
||||||
const codeviewRef = useRef<HTMLDivElement>(null)
|
const codeviewRef = useRef<HTMLDivElement>(null)
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
||||||
const {mobile} = React.useContext(MobileContext)
|
const {mobile} = React.useContext(MobileContext)
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
const difficulty = useSelector(selectDifficulty(gameId))
|
const difficulty = useSelector(selectDifficulty(gameId))
|
||||||
const initialCode = useAppSelector(selectCode(gameId, worldId, levelId))
|
const initialCode = useAppSelector(selectCode(gameId, worldId, levelId))
|
||||||
const initialSelections = useAppSelector(selectSelections(gameId, worldId, levelId))
|
const initialSelections = useAppSelector(selectSelections(gameId, worldId, levelId))
|
||||||
const inventory: Array<String> = useSelector(selectInventory(gameId))
|
const inventory: Array<String> = useSelector(selectInventory(gameId))
|
||||||
|
|
||||||
|
const typewriterMode = useSelector(selectTypewriterMode(gameId))
|
||||||
|
const setTypewriterMode = (newTypewriterMode: boolean) => dispatch(changeTypewriterMode({game: gameId, typewriterMode: newTypewriterMode}))
|
||||||
|
|
||||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||||
const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
||||||
|
|
||||||
@@ -212,14 +231,13 @@ function PlayableLevel() {
|
|||||||
const [showHelp, setShowHelp] = useState<Set<number>>(new Set())
|
const [showHelp, setShowHelp] = useState<Set<number>>(new Set())
|
||||||
// Only for mobile layout
|
// Only for mobile layout
|
||||||
const [pageNumber, setPageNumber] = useState(0)
|
const [pageNumber, setPageNumber] = useState(0)
|
||||||
const [commandLineMode, setCommandLineMode] = useState(true)
|
|
||||||
const [commandLineInput, setCommandLineInput] = useState("")
|
// set to true to prevent switching between typewriter and editor
|
||||||
|
const [lockInputMode, setLockInputMode] = useState(false)
|
||||||
|
const [typewriterInput, setTypewriterInput] = useState("")
|
||||||
const lastLevel = levelId >= gameInfo.data?.worldSize[worldId]
|
const lastLevel = levelId >= gameInfo.data?.worldSize[worldId]
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
|
|
||||||
// impressum pop-up
|
// impressum pop-up
|
||||||
const [impressum, setImpressum] = React.useState(false)
|
|
||||||
function closeImpressum() {setImpressum(false)}
|
|
||||||
function toggleImpressum() {setImpressum(!impressum)}
|
function toggleImpressum() {setImpressum(!impressum)}
|
||||||
|
|
||||||
// When clicking on an inventory item, the inventory is overlayed by the item's doc.
|
// When clicking on an inventory item, the inventory is overlayed by the item's doc.
|
||||||
@@ -246,7 +264,7 @@ function PlayableLevel() {
|
|||||||
useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChangeContent, onDidChangeSelection)
|
useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChangeContent, onDidChangeSelection)
|
||||||
|
|
||||||
/** Unused. Was implementing an undo button, which has been replaced by `deleteProof` inside
|
/** Unused. Was implementing an undo button, which has been replaced by `deleteProof` inside
|
||||||
* `CommandLineInterface`.
|
* `TypewriterInterface`.
|
||||||
*/
|
*/
|
||||||
const handleUndo = () => {
|
const handleUndo = () => {
|
||||||
const endPos = editor.getModel().getFullModelRange().getEndPosition()
|
const endPos = editor.getModel().getFullModelRange().getEndPosition()
|
||||||
@@ -278,29 +296,63 @@ function PlayableLevel() {
|
|||||||
// if the user inventory changes, notify the server
|
// if the user inventory changes, notify the server
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let leanClient = connection.getLeanClient(gameId)
|
let leanClient = connection.getLeanClient(gameId)
|
||||||
leanClient.sendNotification('$/game/setInventory', {inventory: inventory, checkEnabled: difficulty > 0})
|
leanClient.sendNotification('$/game/setInventory', {inventory: inventory, difficulty: difficulty})
|
||||||
}, [inventory])
|
}, [inventory])
|
||||||
|
|
||||||
|
useEffect (() => {
|
||||||
|
// Lock editor mode
|
||||||
|
if (level?.data?.template) {
|
||||||
|
setTypewriterMode(false)
|
||||||
|
|
||||||
|
if (editor) {
|
||||||
|
let code = editor.getModel().getLinesContent()
|
||||||
|
|
||||||
|
// console.log(`insert. code: ${code}`)
|
||||||
|
// console.log(`insert. join: ${code.join('')}`)
|
||||||
|
// console.log(`insert. trim: ${code.join('').trim()}`)
|
||||||
|
// console.log(`insert. length: ${code.join('').trim().length}`)
|
||||||
|
// console.log(`insert. range: ${editor.getModel().getFullModelRange()}`)
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: It does seem that the template is always indented by spaces.
|
||||||
|
// This is a hack, assuming there are exactly two.
|
||||||
|
if (!code.join('').trim().length) {
|
||||||
|
console.debug(`inserting template:\n${level.data.template}`)
|
||||||
|
// TODO: This does not work! HERE
|
||||||
|
// Probably overwritten by a query to the server
|
||||||
|
editor.executeEdits("template-writer", [{
|
||||||
|
range: editor.getModel().getFullModelRange(),
|
||||||
|
text: level.data.template + `\n`,
|
||||||
|
forceMoveMarkers: true
|
||||||
|
}])
|
||||||
|
} else {
|
||||||
|
console.debug(`not inserting template.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [level, levelId, worldId, gameId, editor])
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// TODO: That's a problem if the saved proof contains an error
|
// TODO: That's a problem if the saved proof contains an error
|
||||||
// Reset command line input when loading a new level
|
// Reset command line input when loading a new level
|
||||||
setCommandLineInput("")
|
setTypewriterInput("")
|
||||||
|
|
||||||
// Load the selected help steps from the store
|
// Load the selected help steps from the store
|
||||||
setShowHelp(new Set(selectHelp(gameId, worldId, levelId)(store.getState())))
|
setShowHelp(new Set(selectHelp(gameId, worldId, levelId)(store.getState())))
|
||||||
}, [gameId, worldId, levelId])
|
}, [gameId, worldId, levelId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!commandLineMode) {
|
if (!typewriterMode && editor) {
|
||||||
// Delete last input attempt from command line
|
// Delete last input attempt from command line
|
||||||
editor.executeEdits("command-line", [{
|
editor.executeEdits("typewriter", [{
|
||||||
range: editor.getSelection(),
|
range: editor.getSelection(),
|
||||||
text: "",
|
text: "",
|
||||||
forceMoveMarkers: false
|
forceMoveMarkers: false
|
||||||
}]);
|
}]);
|
||||||
editor.focus()
|
editor.focus()
|
||||||
}
|
}
|
||||||
}, [commandLineMode])
|
}, [typewriterMode])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Forget whether hidden hints are displayed for steps that don't exist yet
|
// Forget whether hidden hints are displayed for steps that don't exist yet
|
||||||
@@ -320,46 +372,50 @@ function PlayableLevel() {
|
|||||||
|
|
||||||
// Effect when command line mode gets enabled
|
// Effect when command line mode gets enabled
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editor && commandLineMode) {
|
if (editor && typewriterMode) {
|
||||||
let endPos = editor.getModel().getFullModelRange().getEndPosition()
|
let code = editor.getModel().getLinesContent().filter(line => line.trim())
|
||||||
if (editor.getModel().getLineContent(endPos.lineNumber).trim() !== "") {
|
editor.executeEdits("typewriter", [{
|
||||||
editor.executeEdits("command-line", [{
|
range: editor.getModel().getFullModelRange(),
|
||||||
range: monaco.Selection.fromPositions(endPos, endPos),
|
text: code.length ? code.join('\n') + '\n' : '',
|
||||||
text: "\n",
|
forceMoveMarkers: true
|
||||||
forceMoveMarkers: true
|
}]);
|
||||||
}]);
|
|
||||||
}
|
// let endPos = editor.getModel().getFullModelRange().getEndPosition()
|
||||||
endPos = editor.getModel().getFullModelRange().getEndPosition()
|
// if (editor.getModel().getLineContent(endPos.lineNumber).trim() !== "") {
|
||||||
let currPos = editor.getPosition()
|
// editor.executeEdits("typewriter", [{
|
||||||
if (currPos.column != 1 || (currPos.lineNumber != endPos.lineNumber && currPos.lineNumber != endPos.lineNumber - 1)) {
|
// range: monaco.Selection.fromPositions(endPos, endPos),
|
||||||
// This is not a position that would naturally occur from CommandLine, reset:
|
// text: "\n",
|
||||||
editor.setSelection(monaco.Selection.fromPositions(endPos, endPos))
|
// forceMoveMarkers: true
|
||||||
}
|
// }]);
|
||||||
|
// }
|
||||||
|
// let endPos = editor.getModel().getFullModelRange().getEndPosition()
|
||||||
|
// let currPos = editor.getPosition()
|
||||||
|
// if (currPos.column != 1 || (currPos.lineNumber != endPos.lineNumber && currPos.lineNumber != endPos.lineNumber - 1)) {
|
||||||
|
// // This is not a position that would naturally occur from Typewriter, reset:
|
||||||
|
// editor.setSelection(monaco.Selection.fromPositions(endPos, endPos))
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}, [editor, commandLineMode])
|
}, [editor, typewriterMode])
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<div style={level.isLoading ? null : {display: "none"}} className="app-content loading"><CircularProgress /></div>
|
<div style={level.isLoading ? null : {display: "none"}} className="app-content loading"><CircularProgress /></div>
|
||||||
<DeletedChatContext.Provider value={{deletedChat, setDeletedChat, showHelp, setShowHelp}}>
|
<DeletedChatContext.Provider value={{deletedChat, setDeletedChat, showHelp, setShowHelp}}>
|
||||||
<SelectionContext.Provider value={{selectedStep, setSelectedStep}}>
|
<SelectionContext.Provider value={{selectedStep, setSelectedStep}}>
|
||||||
<InputModeContext.Provider value={{commandLineMode, setCommandLineMode, commandLineInput, setCommandLineInput}}>
|
<InputModeContext.Provider value={{typewriterMode, setTypewriterMode, typewriterInput, setTypewriterInput, lockInputMode, setLockInputMode}}>
|
||||||
<ProofContext.Provider value={{proof, setProof}}>
|
<ProofContext.Provider value={{proof, setProof}}>
|
||||||
<EditorContext.Provider value={editorConnection}>
|
<EditorContext.Provider value={editorConnection}>
|
||||||
<MonacoEditorContext.Provider value={editor}>
|
<MonacoEditorContext.Provider value={editor}>
|
||||||
<LevelAppBar
|
<LevelAppBar
|
||||||
|
pageNumber={pageNumber} setPageNumber={setPageNumber}
|
||||||
isLoading={level.isLoading}
|
isLoading={level.isLoading}
|
||||||
levelTitle={`${mobile ? '' : 'Level '}${levelId} / ${gameInfo.data?.worldSize[worldId]}` +
|
levelTitle={`${mobile ? '' : 'Level '}${levelId} / ${gameInfo.data?.worldSize[worldId]}` +
|
||||||
(level?.data?.title && ` : ${level?.data?.title}`)}
|
(level?.data?.title && ` : ${level?.data?.title}`)}
|
||||||
impressum={impressum}
|
toggleImpressum={toggleImpressum} />
|
||||||
toggleImpressum={toggleImpressum}
|
|
||||||
pageNumber={pageNumber} setPageNumber={setPageNumber} />
|
|
||||||
{mobile?
|
{mobile?
|
||||||
// TODO: This is copied from the `Split` component below...
|
// TODO: This is copied from the `Split` component below...
|
||||||
<>
|
<>
|
||||||
<div className={`app-content level-mobile ${level.isLoading ? 'hidden' : ''}`}>
|
<div className={`app-content level-mobile ${level.isLoading ? 'hidden' : ''}`}>
|
||||||
<ExercisePanel
|
<ExercisePanel
|
||||||
impressum={impressum}
|
|
||||||
closeImpressum={closeImpressum}
|
|
||||||
codeviewRef={codeviewRef}
|
codeviewRef={codeviewRef}
|
||||||
visible={pageNumber == 0} />
|
visible={pageNumber == 0} />
|
||||||
<InventoryPanel levelInfo={level?.data} visible={pageNumber == 1} />
|
<InventoryPanel levelInfo={level?.data} visible={pageNumber == 1} />
|
||||||
@@ -369,8 +425,6 @@ function PlayableLevel() {
|
|||||||
<Split minSize={0} snapOffset={200} sizes={[25, 50, 25]} className={`app-content level ${level.isLoading ? 'hidden' : ''}`}>
|
<Split minSize={0} snapOffset={200} sizes={[25, 50, 25]} className={`app-content level ${level.isLoading ? 'hidden' : ''}`}>
|
||||||
<ChatPanel lastLevel={lastLevel}/>
|
<ChatPanel lastLevel={lastLevel}/>
|
||||||
<ExercisePanel
|
<ExercisePanel
|
||||||
impressum={impressum}
|
|
||||||
closeImpressum={closeImpressum}
|
|
||||||
codeviewRef={codeviewRef} />
|
codeviewRef={codeviewRef} />
|
||||||
<InventoryPanel levelInfo={level?.data} />
|
<InventoryPanel levelInfo={level?.data} />
|
||||||
</Split>
|
</Split>
|
||||||
@@ -384,7 +438,7 @@ function PlayableLevel() {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
function IntroductionPanel({gameInfo, impressum, closeImpressum}) {
|
function IntroductionPanel({gameInfo}) {
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {worldId} = useContext(WorldLevelIdContext)
|
const {worldId} = useContext(WorldLevelIdContext)
|
||||||
|
|
||||||
@@ -396,7 +450,6 @@ function IntroductionPanel({gameInfo, impressum, closeImpressum}) {
|
|||||||
<Hint key={`intro-p-${i}`}
|
<Hint key={`intro-p-${i}`}
|
||||||
hint={{text: t, hidden: false}} step={0} selected={null} toggleSelection={undefined} />
|
hint={{text: t, hidden: false}} step={0} selected={null} toggleSelection={undefined} />
|
||||||
))}
|
))}
|
||||||
{impressum ? <Impressum handleClose={closeImpressum} /> : null}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="button-row">
|
<div className="button-row">
|
||||||
{gameInfo.data?.worldSize[worldId] == 0 ?
|
{gameInfo.data?.worldSize[worldId] == 0 ?
|
||||||
@@ -412,7 +465,7 @@ function IntroductionPanel({gameInfo, impressum, closeImpressum}) {
|
|||||||
export default Level
|
export default Level
|
||||||
|
|
||||||
/** The site with the introduction text of a world */
|
/** The site with the introduction text of a world */
|
||||||
function Introduction() {
|
function Introduction({impressum, setImpressum}) {
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {mobile} = useContext(MobileContext)
|
const {mobile} = useContext(MobileContext)
|
||||||
|
|
||||||
@@ -420,27 +473,32 @@ function Introduction() {
|
|||||||
|
|
||||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||||
|
|
||||||
const [impressum, setImpressum] = React.useState(false)
|
const {worldId} = useContext(WorldLevelIdContext)
|
||||||
|
|
||||||
|
let image: string = gameInfo.data?.worlds.nodes[worldId].image
|
||||||
|
|
||||||
const closeImpressum = () => {
|
|
||||||
setImpressum(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleImpressum = () => {
|
const toggleImpressum = () => {
|
||||||
setImpressum(!impressum)
|
setImpressum(!impressum)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<LevelAppBar isLoading={gameInfo.isLoading} levelTitle="Introduction" impressum={impressum} toggleImpressum={toggleImpressum}/>
|
<LevelAppBar isLoading={gameInfo.isLoading} levelTitle="Introduction" toggleImpressum={toggleImpressum}/>
|
||||||
{gameInfo.isLoading ?
|
{gameInfo.isLoading ?
|
||||||
<div className="app-content loading"><CircularProgress /></div>
|
<div className="app-content loading"><CircularProgress /></div>
|
||||||
: mobile ?
|
: mobile ?
|
||||||
<IntroductionPanel gameInfo={gameInfo} impressum={impressum} closeImpressum={closeImpressum} />
|
<IntroductionPanel gameInfo={gameInfo} />
|
||||||
:
|
:
|
||||||
<Split minSize={0} snapOffset={200} sizes={[25, 50, 25]} className={`app-content level`}>
|
<Split minSize={0} snapOffset={200} sizes={[25, 50, 25]} className={`app-content level`}>
|
||||||
<IntroductionPanel gameInfo={gameInfo} impressum={impressum} closeImpressum={closeImpressum} />
|
<IntroductionPanel gameInfo={gameInfo} />
|
||||||
<div className="world-image-container empty"></div>
|
<div className="world-image-container empty">
|
||||||
<InventoryPanel levelInfo={inventory?.data} />
|
{image &&
|
||||||
|
// TODO: Temporary for testing
|
||||||
|
<img className={worldId=="Proposition" ? "cover" : "contain"} src={path.join("data", gameId, image)} alt="" />
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<InventoryOverviewPanel data={inventory?.data} />
|
||||||
</Split>
|
</Split>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,7 +633,8 @@ function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChange
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
editorConnection.api.sendClientNotification(uriStr, "textDocument/didClose", {textDocument: {uri: uriStr}})
|
editorConnection.api.sendClientNotification(uriStr, "textDocument/didClose", {textDocument: {uri: uriStr}})
|
||||||
model.dispose(); }
|
model.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [editor, levelId, connection, leanClientStarted])
|
}, [editor, levelId, connection, leanClientStarted])
|
||||||
|
|
||||||
@@ -597,27 +656,3 @@ function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChange
|
|||||||
|
|
||||||
return {editor, infoProvider, editorConnection}
|
return {editor, infoProvider, editorConnection}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Open all files in this world on the server so that they will load faster when accessed */
|
|
||||||
function useLoadWorldFiles(worldId) {
|
|
||||||
const gameId = React.useContext(GameIdContext)
|
|
||||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
|
||||||
const store = useStore()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (gameInfo.data) {
|
|
||||||
const models = []
|
|
||||||
for (let levelId = 1; levelId <= gameInfo.data.worldSize[worldId]; levelId++) {
|
|
||||||
const uri = monaco.Uri.parse(`file:///${worldId}/${levelId}`)
|
|
||||||
let model = monaco.editor.getModel(uri)
|
|
||||||
if (model) {
|
|
||||||
models.push(model)
|
|
||||||
} else {
|
|
||||||
const code = selectCode(gameId, worldId, levelId)(store.getState())
|
|
||||||
models.push(monaco.editor.createModel(code, 'lean4', uri))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return () => { for (let model of models) { model.dispose() } }
|
|
||||||
}
|
|
||||||
}, [gameInfo.data, worldId])
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* @fileOverview
|
||||||
|
*/
|
||||||
|
import * as React from 'react'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { GameIdContext } from '../../app'
|
||||||
|
import { useAppDispatch } from '../../hooks'
|
||||||
|
import { deleteProgress, selectProgress } from '../../state/progress'
|
||||||
|
import { downloadFile } from '../world_tree'
|
||||||
|
import { Button } from '../button'
|
||||||
|
|
||||||
|
/** download the current progress (i.e. what's saved in the browser store) */
|
||||||
|
export function downloadProgress(gameId: string, gameProgress: any, ev: React.MouseEvent) {
|
||||||
|
ev.preventDefault()
|
||||||
|
downloadFile({
|
||||||
|
data: JSON.stringify(gameProgress, null, 2),
|
||||||
|
fileName: `lean4game-${gameId}-${new Date().toLocaleDateString()}.json`,
|
||||||
|
fileType: 'text/json',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pop-up to delete game progress.
|
||||||
|
*
|
||||||
|
* `handleClose` is the function to close it again because it's open/closed state is
|
||||||
|
* controlled by the containing element.
|
||||||
|
*/
|
||||||
|
export function ErasePopup ({handleClose}) {
|
||||||
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
const gameProgress = useSelector(selectProgress(gameId))
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
const eraseProgress = () => {
|
||||||
|
dispatch(deleteProgress({game: gameId}))
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadAndErase = (ev) => {
|
||||||
|
downloadProgress(gameId, gameProgress, ev)
|
||||||
|
eraseProgress()
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="modal-wrapper">
|
||||||
|
<div className="modal-backdrop" onClick={handleClose} />
|
||||||
|
<div className="modal">
|
||||||
|
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
||||||
|
<h2>Delete Progress?</h2>
|
||||||
|
|
||||||
|
<p>Do you want to delete your saved progress irreversibly?</p>
|
||||||
|
<p>
|
||||||
|
(This deletes your proofs and your collected inventory.
|
||||||
|
Saves from other games are not deleted.)
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Button onClick={eraseProgress} to="">Delete</Button>
|
||||||
|
<Button onClick={downloadAndErase} to="">Download & Delete</Button>
|
||||||
|
<Button onClick={handleClose} to="">Cancel</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* @fileOverview
|
||||||
|
*/
|
||||||
|
import * as React from 'react'
|
||||||
|
import { Typography } from '@mui/material'
|
||||||
|
import Markdown from '../markdown'
|
||||||
|
|
||||||
|
/** Pop-up that is displaying the Game Info.
|
||||||
|
*
|
||||||
|
* `handleClose` is the function to close it again because it's open/closed state is
|
||||||
|
* controlled by the containing element.
|
||||||
|
*/
|
||||||
|
export function InfoPopup ({info, handleClose}: {info: string, handleClose: () => void}) {
|
||||||
|
return <div className="modal-wrapper">
|
||||||
|
<div className="modal-backdrop" onClick={handleClose} />
|
||||||
|
<div className="modal">
|
||||||
|
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
||||||
|
<Typography variant="body1" component="div" className="welcome-text">
|
||||||
|
<Markdown>{info}</Markdown>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
+17
-48
@@ -11,7 +11,7 @@ import * as React from 'react'
|
|||||||
* controlled by the containing element.
|
* controlled by the containing element.
|
||||||
*/
|
*/
|
||||||
export function PrivacyPolicyPopup ({handleClose}: {handleClose: () => void}) {
|
export function PrivacyPolicyPopup ({handleClose}: {handleClose: () => void}) {
|
||||||
return <div className="modal-wrapper">
|
return <div className="privacy-policy modal-wrapper">
|
||||||
<div className="modal-backdrop" onClick={handleClose} />
|
<div className="modal-backdrop" onClick={handleClose} />
|
||||||
<div className="modal">
|
<div className="modal">
|
||||||
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
||||||
@@ -31,7 +31,7 @@ export function PrivacyPolicyPopup ({handleClose}: {handleClose: () => void}) {
|
|||||||
<p>Our server is located in Germany.</p>
|
<p>Our server is located in Germany.</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Contact information:</strong><br />
|
<strong>Contact information:</strong><br />
|
||||||
Jon Eugster<br />
|
Alexander Bentkamp, Jon Eugster<br />
|
||||||
Mathematisches Institut der Heinrich-Heine-Universität Düsseldorf<br />
|
Mathematisches Institut der Heinrich-Heine-Universität Düsseldorf<br />
|
||||||
Universitätsstr. 1<br />
|
Universitätsstr. 1<br />
|
||||||
40225 Düsseldorf<br />
|
40225 Düsseldorf<br />
|
||||||
@@ -39,6 +39,21 @@ export function PrivacyPolicyPopup ({handleClose}: {handleClose: () => void}) {
|
|||||||
+49 211 81-12173<br />
|
+49 211 81-12173<br />
|
||||||
<a 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>
|
<a 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>
|
</p>
|
||||||
|
<h2>Development & Funding</h2>
|
||||||
|
<p>
|
||||||
|
The source code of this Lean game engine
|
||||||
|
is <a href="https://github.com/leanprover-community/lean4game" target="_blank">available on Github</a>.
|
||||||
|
If you experience any problems, please
|
||||||
|
file an <a href="https://github.com/leanprover-community/lean4game/issues" target="_blank">Issue on Github</a> or
|
||||||
|
get directly in contact.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The game engine has been developed as part of the
|
||||||
|
project <a href="https://hhu-adam.github.io/" target="_blank">ADAM: Anticipating the Digital
|
||||||
|
Age of Mathematics</a> at
|
||||||
|
Heinrich-Heine-Universität Düsseldorf. It is funded by
|
||||||
|
the <i>Stiftung Innovation in der Hochschullehre</i> as part of project <i>Freiraum 2022</i>.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -58,49 +73,3 @@ export const PrivacyPolicy: React.FC = () => {
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Impressum({handleClose}) {
|
|
||||||
return <div className="impressum">
|
|
||||||
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
|
||||||
<h2>Funding</h2>
|
|
||||||
<p>
|
|
||||||
This Lean game engine has been developed as part of the
|
|
||||||
project <a href="https://hhu-adam.github.io/" target="_blank">ADAM: Anticipating the Digital
|
|
||||||
Age of Mathematics</a> at
|
|
||||||
Heinrich-Heine-Universität Düsseldorf. It is funded by
|
|
||||||
the <i>Stiftung Innovation in der Hochschullehre</i> as part of project <i>Freiraum 2022</i>.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Development</h2>
|
|
||||||
<p>
|
|
||||||
The source code is <a href="https://github.com/leanprover-community/lean4game" target="_blank">available on Github</a>.
|
|
||||||
If you experience any problems, please
|
|
||||||
file an <a href="https://github.com/leanprover-community/lean4game/issues" target="_blank">Issue on Github</a> or
|
|
||||||
get directly in contact.
|
|
||||||
</p>
|
|
||||||
<h2>Privacy Policy & Impressum</h2>
|
|
||||||
<p>
|
|
||||||
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 anonymised.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
We do not use cookies, but your game progress is stored in the browser storage
|
|
||||||
as site data. Your game progress is not saved on the server; if you delete
|
|
||||||
your browser storage, it is completely gone.
|
|
||||||
</p>
|
|
||||||
<p>Our server is located in Germany.</p>
|
|
||||||
<p>
|
|
||||||
<strong>Contact information:</strong><br />
|
|
||||||
Jon Eugster<br />
|
|
||||||
Mathematisches Institut der Heinrich-Heine-Universität Düsseldorf<br />
|
|
||||||
Universitätsstr. 1<br />
|
|
||||||
40225 Düsseldorf<br />
|
|
||||||
Germany<br />
|
|
||||||
+49 211 81-12173<br />
|
|
||||||
<a 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>
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* @fileOverview
|
||||||
|
*/
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
/** Pop-up that is displayed when opening the help explaining the game rules.
|
||||||
|
*
|
||||||
|
* `handleClose` is the function to close it again because it's open/closed state is
|
||||||
|
* controlled by the containing element.
|
||||||
|
*/
|
||||||
|
export function RulesHelpPopup ({handleClose}: {handleClose: () => void}) {
|
||||||
|
return <div className="privacy-policy modal-wrapper">
|
||||||
|
<div className="modal-backdrop" onClick={handleClose} />
|
||||||
|
<div className="modal">
|
||||||
|
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
||||||
|
<h2>Game Rules</h2>
|
||||||
|
<p>
|
||||||
|
Game rules determine if it is allowed to skip levels and if the games runs checks to only
|
||||||
|
allow unlocked tactics and theorems in proofs.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Note: "Unlocked" tactics (or theorems) are determined by two things: The set of minimal
|
||||||
|
tactics needed to solve a level, plus any tactics you unlocked in another level. That means
|
||||||
|
if you unlock <code>simp</code> in a level, you can use it henceforth in any level.
|
||||||
|
</p>
|
||||||
|
<p>The options are:</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col"></th>
|
||||||
|
<th scope="col">levels</th>
|
||||||
|
<th scope="col">tactics</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">regular</th>
|
||||||
|
<td>🔐</td>
|
||||||
|
<td>🔐</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">relaxed</th>
|
||||||
|
<td>🔓</td>
|
||||||
|
<td>🔐</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">none</th>
|
||||||
|
<td>🔓</td>
|
||||||
|
<td>🔓</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* @fileOverview
|
||||||
|
*/
|
||||||
|
import * as React from 'react'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { GameIdContext } from '../../app'
|
||||||
|
import { useAppDispatch } from '../../hooks'
|
||||||
|
import { GameProgressState, loadProgress, selectProgress } from '../../state/progress'
|
||||||
|
import { downloadFile } from '../world_tree'
|
||||||
|
import { Button } from '../button'
|
||||||
|
|
||||||
|
/** Pop-up that is displaying the Game Info.
|
||||||
|
*
|
||||||
|
* `handleClose` is the function to close it again because it's open/closed state is
|
||||||
|
* controlled by the containing element.
|
||||||
|
*/
|
||||||
|
export function UploadPopup ({handleClose}) {
|
||||||
|
const [file, setFile] = React.useState<File>();
|
||||||
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
const gameProgress = useSelector(selectProgress(gameId))
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
const handleFileChange = (e) => {
|
||||||
|
if (e.target.files) {
|
||||||
|
setFile(e.target.files[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Upload progress from a */
|
||||||
|
const uploadProgress = (e) => {
|
||||||
|
if (!file) {return}
|
||||||
|
const fileReader = new FileReader()
|
||||||
|
fileReader.readAsText(file, "UTF-8")
|
||||||
|
fileReader.onload = (e) => {
|
||||||
|
const data = JSON.parse(e.target.result.toString()) as GameProgressState
|
||||||
|
console.debug("Json Data", data)
|
||||||
|
dispatch(loadProgress({game: gameId, data: data}))
|
||||||
|
}
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Download the current progress (i.e. what's saved in the browser store) */
|
||||||
|
const downloadProgress = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
downloadFile({
|
||||||
|
data: JSON.stringify(gameProgress, null, 2),
|
||||||
|
fileName: `lean4game-${gameId}-${new Date().toLocaleDateString()}.json`,
|
||||||
|
fileType: 'text/json',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return <div className="modal-wrapper">
|
||||||
|
<div className="modal-backdrop" onClick={handleClose} />
|
||||||
|
<div className="modal">
|
||||||
|
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
||||||
|
<h2>Upload Saved Progress</h2>
|
||||||
|
|
||||||
|
<p>Select a JSON file with the saved game progress to load your progress.</p>
|
||||||
|
|
||||||
|
<p><b>Warning:</b> This will delete your current game progress!
|
||||||
|
Consider <a className="download-link" onClick={downloadProgress} >downloading your current progress</a> first!</p>
|
||||||
|
<p>
|
||||||
|
<input type="file" onChange={handleFileChange}/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Button to="" onClick={uploadProgress}>Load selected file</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -1,29 +1,32 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useState, useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import Split from 'react-split'
|
import Split from 'react-split'
|
||||||
import { Box, Typography, CircularProgress } from '@mui/material'
|
import { Box, CircularProgress } from '@mui/material'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faGlobe, faBook, faArrowRight, faArrowLeft } from '@fortawesome/free-solid-svg-icons'
|
import { faArrowRight } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
import { GameIdContext } from '../app'
|
import { GameIdContext } from '../app'
|
||||||
import { useAppDispatch } from '../hooks'
|
import { useAppDispatch, useAppSelector } from '../hooks'
|
||||||
import { GameProgressState, changedOpenedIntro, deleteProgress, loadProgress, selectOpenedIntro, selectProgress } from '../state/progress'
|
import { changedOpenedIntro, selectOpenedIntro } from '../state/progress'
|
||||||
import { useGetGameInfoQuery, useLoadInventoryOverviewQuery } from '../state/api'
|
import { useGetGameInfoQuery, useLoadInventoryOverviewQuery } from '../state/api'
|
||||||
import { Button } from './button'
|
import { Button } from './button'
|
||||||
import { MobileContext } from './infoview/context'
|
import { MobileContext } from './infoview/context'
|
||||||
import { InventoryPanel } from './inventory'
|
import { InventoryOverviewPanel, InventoryPanel } from './inventory'
|
||||||
import Markdown from './markdown'
|
import { ErasePopup } from './popup/erase'
|
||||||
import {PrivacyPolicyPopup} from './privacy_policy'
|
import { InfoPopup } from './popup/game_info'
|
||||||
import { WorldTreePanel, downloadFile } from './world_tree'
|
import { PrivacyPolicyPopup } from './popup/privacy_policy'
|
||||||
|
import { RulesHelpPopup } from './popup/rules_help'
|
||||||
|
import { UploadPopup } from './popup/upload'
|
||||||
|
import { WorldTreePanel } from './world_tree'
|
||||||
|
|
||||||
import './welcome.css'
|
import '../css/welcome.css'
|
||||||
import { WelcomeAppBar } from './app_bar'
|
import { WelcomeAppBar } from './app_bar'
|
||||||
import { Hint } from './hints'
|
import { Hint } from './hints'
|
||||||
|
|
||||||
/** The panel showing the game's introduction text */
|
|
||||||
function IntroductionPanel({introduction}: {introduction: string}) {
|
/** the panel showing the game's introduction text */
|
||||||
const {mobile, setPageNumber} = React.useContext(MobileContext)
|
function IntroductionPanel({introduction, setPageNumber}: {introduction: string, setPageNumber}) {
|
||||||
|
const {mobile} = React.useContext(MobileContext)
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
@@ -43,11 +46,6 @@ function IntroductionPanel({introduction}: {introduction: string}) {
|
|||||||
: <></>
|
: <></>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{/* <Typography variant="body1" component="div" className="welcome-text">
|
|
||||||
<h1>{title}</h1>
|
|
||||||
<Markdown>{introduction}</Markdown>
|
|
||||||
</Typography>
|
|
||||||
*/}
|
|
||||||
{mobile &&
|
{mobile &&
|
||||||
<div className="button-row">
|
<div className="button-row">
|
||||||
<Button className="btn" to=""
|
<Button className="btn" to=""
|
||||||
@@ -62,142 +60,32 @@ function IntroductionPanel({introduction}: {introduction: string}) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InfoPopup ({info, handleClose}: {info: string, handleClose: () => void}) {
|
/** main page of the game showing among others the tree of worlds/levels */
|
||||||
return <div className="modal-wrapper">
|
|
||||||
<div className="modal-backdrop" onClick={handleClose} />
|
|
||||||
<div className="modal">
|
|
||||||
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
|
||||||
<Typography variant="body1" component="div" className="welcome-text">
|
|
||||||
<Markdown>{info}</Markdown>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function ErasePopup ({handleClose}) {
|
|
||||||
const gameId = React.useContext(GameIdContext)
|
|
||||||
const gameProgress = useSelector(selectProgress(gameId))
|
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
|
|
||||||
/** Download the current progress (i.e. what's saved in the browser store) */
|
|
||||||
const downloadProgress = (e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
downloadFile({
|
|
||||||
data: JSON.stringify(gameProgress, null, 2),
|
|
||||||
fileName: `lean4game-${gameId}-${new Date().toLocaleDateString()}.json`,
|
|
||||||
fileType: 'text/json',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const eraseProgress = () => {
|
|
||||||
dispatch(deleteProgress({game: gameId}))
|
|
||||||
handleClose()
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadAndErase = (e) => {
|
|
||||||
downloadProgress(e)
|
|
||||||
eraseProgress()
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className="modal-wrapper">
|
|
||||||
<div className="modal-backdrop" onClick={handleClose} />
|
|
||||||
<div className="modal">
|
|
||||||
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
|
||||||
<h2>Delete Progress?</h2>
|
|
||||||
|
|
||||||
<p>Do you want to delete your saved progress irreversibly?</p>
|
|
||||||
<p>
|
|
||||||
(This deletes your proofs and your collected inventory.
|
|
||||||
Saves from other games are not deleted.)
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<Button onClick={eraseProgress} to="">Delete</Button>
|
|
||||||
<Button onClick={downloadAndErase} to="">Download & Delete</Button>
|
|
||||||
<Button onClick={handleClose} to="">Cancel</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
function UploadPopup ({handleClose}) {
|
|
||||||
const [file, setFile] = React.useState<File>();
|
|
||||||
const gameId = React.useContext(GameIdContext)
|
|
||||||
const gameProgress = useSelector(selectProgress(gameId))
|
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
|
|
||||||
const handleFileChange = (e) => {
|
|
||||||
if (e.target.files) {
|
|
||||||
setFile(e.target.files[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Upload progress from a */
|
|
||||||
const uploadProgress = (e) => {
|
|
||||||
if (!file) {return}
|
|
||||||
const fileReader = new FileReader()
|
|
||||||
fileReader.readAsText(file, "UTF-8")
|
|
||||||
fileReader.onload = (e) => {
|
|
||||||
const data = JSON.parse(e.target.result.toString()) as GameProgressState
|
|
||||||
console.debug("Json Data", data)
|
|
||||||
dispatch(loadProgress({game: gameId, data: data}))
|
|
||||||
}
|
|
||||||
handleClose()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Download the current progress (i.e. what's saved in the browser store) */
|
|
||||||
const downloadProgress = (e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
downloadFile({
|
|
||||||
data: JSON.stringify(gameProgress, null, 2),
|
|
||||||
fileName: `lean4game-${gameId}-${new Date().toLocaleDateString()}.json`,
|
|
||||||
fileType: 'text/json',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return <div className="modal-wrapper">
|
|
||||||
<div className="modal-backdrop" onClick={handleClose} />
|
|
||||||
<div className="modal">
|
|
||||||
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
|
||||||
<h2>Upload Saved Progress</h2>
|
|
||||||
|
|
||||||
<p>Select a JSON file with the saved game progress to load your progress.</p>
|
|
||||||
|
|
||||||
<p><b>Warning:</b> This will delete your current game progress!
|
|
||||||
Consider <a className="download-link" onClick={downloadProgress} >downloading your current progress</a> first!</p>
|
|
||||||
|
|
||||||
<input type="file" onChange={handleFileChange}/>
|
|
||||||
|
|
||||||
<Button to="" onClick={uploadProgress}>Load selected file</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
/** main page of the game showing amoung others the tree of worlds/levels */
|
|
||||||
function Welcome() {
|
function Welcome() {
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {mobile, pageNumber, setPageNumber} = React.useContext(MobileContext)
|
const {mobile} = React.useContext(MobileContext)
|
||||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||||
const inventory = useLoadInventoryOverviewQuery({game: gameId})
|
const inventory = useLoadInventoryOverviewQuery({game: gameId})
|
||||||
|
|
||||||
// impressum pop-up
|
// For mobile only
|
||||||
|
const openedIntro = useAppSelector(selectOpenedIntro(gameId))
|
||||||
|
const [pageNumber, setPageNumber] = React.useState(openedIntro ? 1 : 0)
|
||||||
|
|
||||||
|
// pop-ups
|
||||||
|
const [eraseMenu, setEraseMenu] = React.useState(false)
|
||||||
const [impressum, setImpressum] = React.useState(false)
|
const [impressum, setImpressum] = React.useState(false)
|
||||||
function closeImpressum() {setImpressum(false)}
|
|
||||||
function toggleImpressum() {setImpressum(!impressum)}
|
|
||||||
|
|
||||||
const [info, setInfo] = React.useState(false)
|
const [info, setInfo] = React.useState(false)
|
||||||
function closeInfo() {setInfo(false)}
|
const [rulesHelp, setRulesHelp] = React.useState(false)
|
||||||
function toggleInfo() {setInfo(!impressum)}
|
const [uploadMenu, setUploadMenu] = React.useState(false)
|
||||||
|
function closeEraseMenu() {setEraseMenu(false)}
|
||||||
|
function closeImpressum() {setImpressum(false)}
|
||||||
/* state variables to toggle the pop-up menus */
|
function closeInfo() {setInfo(false)}
|
||||||
const [eraseMenu, setEraseMenu] = React.useState(false);
|
function closeRulesHelp() {setRulesHelp(false)}
|
||||||
const openEraseMenu = () => setEraseMenu(true);
|
function closeUploadMenu() {setUploadMenu(false)}
|
||||||
const closeEraseMenu = () => setEraseMenu(false);
|
function toggleEraseMenu() {setEraseMenu(!eraseMenu)}
|
||||||
const [uploadMenu, setUploadMenu] = React.useState(false);
|
function toggleImpressum() {setImpressum(!impressum)}
|
||||||
const openUploadMenu = () => setUploadMenu(true);
|
function toggleInfo() {setInfo(!info)}
|
||||||
const closeUploadMenu = () => setUploadMenu(false);
|
function toggleUploadMenu() {setUploadMenu(!uploadMenu)}
|
||||||
|
|
||||||
|
|
||||||
// set the window title
|
// set the window title
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -211,28 +99,32 @@ function Welcome() {
|
|||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
</Box>
|
</Box>
|
||||||
: <>
|
: <>
|
||||||
<WelcomeAppBar gameInfo={gameInfo.data} toggleImpressum={toggleImpressum} openEraseMenu={openEraseMenu}
|
<WelcomeAppBar pageNumber={pageNumber} setPageNumber={setPageNumber} gameInfo={gameInfo.data} toggleImpressum={toggleImpressum}
|
||||||
openUploadMenu={openUploadMenu} toggleInfo={toggleInfo} />
|
toggleEraseMenu={toggleEraseMenu} toggleUploadMenu={toggleUploadMenu}
|
||||||
|
toggleInfo={toggleInfo} />
|
||||||
<div className="app-content">
|
<div className="app-content">
|
||||||
{ mobile ?
|
{ mobile ?
|
||||||
<div className="welcome mobile">
|
<div className="welcome mobile">
|
||||||
{(pageNumber == 0 ?
|
{(pageNumber == 0 ?
|
||||||
<IntroductionPanel introduction={gameInfo.data?.introduction} />
|
<IntroductionPanel introduction={gameInfo.data?.introduction} setPageNumber={setPageNumber} />
|
||||||
: pageNumber == 1 ?
|
: pageNumber == 1 ?
|
||||||
<WorldTreePanel worlds={gameInfo.data?.worlds} worldSize={gameInfo.data?.worldSize} />
|
<WorldTreePanel worlds={gameInfo.data?.worlds} worldSize={gameInfo.data?.worldSize}
|
||||||
|
rulesHelp={rulesHelp} setRulesHelp={setRulesHelp} />
|
||||||
:
|
:
|
||||||
<InventoryPanel levelInfo={inventory?.data} />
|
<InventoryOverviewPanel data={inventory?.data} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
<Split className="welcome" minSize={0} snapOffset={200} sizes={[25, 50, 25]}>
|
<Split className="welcome" minSize={0} snapOffset={200} sizes={[25, 50, 25]}>
|
||||||
<IntroductionPanel introduction={gameInfo.data?.introduction} />
|
<IntroductionPanel introduction={gameInfo.data?.introduction} setPageNumber={setPageNumber} />
|
||||||
<WorldTreePanel worlds={gameInfo.data?.worlds} worldSize={gameInfo.data?.worldSize} />
|
<WorldTreePanel worlds={gameInfo.data?.worlds} worldSize={gameInfo.data?.worldSize}
|
||||||
<InventoryPanel levelInfo={inventory?.data} />
|
rulesHelp={rulesHelp} setRulesHelp={setRulesHelp} />
|
||||||
|
<InventoryOverviewPanel data={inventory?.data} />
|
||||||
</Split>
|
</Split>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{impressum ? <PrivacyPolicyPopup handleClose={closeImpressum} /> : null}
|
{impressum ? <PrivacyPolicyPopup handleClose={closeImpressum} /> : null}
|
||||||
|
{rulesHelp ? <RulesHelpPopup handleClose={closeRulesHelp} /> : null}
|
||||||
{eraseMenu? <ErasePopup handleClose={closeEraseMenu}/> : null}
|
{eraseMenu? <ErasePopup handleClose={closeEraseMenu}/> : null}
|
||||||
{uploadMenu? <UploadPopup handleClose={closeUploadMenu}/> : null}
|
{uploadMenu? <UploadPopup handleClose={closeUploadMenu}/> : null}
|
||||||
{info ? <InfoPopup info={gameInfo.data?.info} handleClose={closeInfo}/> : null}
|
{info ? <InfoPopup info={gameInfo.data?.info} handleClose={closeInfo}/> : null}
|
||||||
|
|||||||
@@ -8,16 +8,14 @@ import { Slider } from '@mui/material'
|
|||||||
import cytoscape, { LayoutOptions } from 'cytoscape'
|
import cytoscape, { LayoutOptions } from 'cytoscape'
|
||||||
import klay from 'cytoscape-klay'
|
import klay from 'cytoscape-klay'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faDownload, faUpload, faEraser, faGlobe, faArrowLeft } from '@fortawesome/free-solid-svg-icons'
|
import { faXmark, faCircleQuestion } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
import { GameIdContext } from '../app'
|
import { GameIdContext } from '../app'
|
||||||
import { useAppDispatch } from '../hooks'
|
import { useAppDispatch } from '../hooks'
|
||||||
import { deleteProgress, selectProgress, loadProgress, GameProgressState,
|
import { selectDifficulty, changedDifficulty, selectCompleted } from '../state/progress'
|
||||||
selectDifficulty, changedDifficulty, selectCompleted } from '../state/progress'
|
|
||||||
import { store } from '../state/store'
|
import { store } from '../state/store'
|
||||||
import { Button } from './button'
|
|
||||||
|
|
||||||
import './world_tree.css'
|
import '../css/world_tree.css'
|
||||||
|
|
||||||
// Settings for the world tree
|
// Settings for the world tree
|
||||||
cytoscape.use( klay )
|
cytoscape.use( klay )
|
||||||
@@ -28,8 +26,11 @@ const lineWidth = 10 //
|
|||||||
const ds = .75 // scale the resulting svg image
|
const ds = .75 // scale the resulting svg image
|
||||||
|
|
||||||
const NMIN = 5 // min. worldsize
|
const NMIN = 5 // min. worldsize
|
||||||
const NLABEL = 9 // max. world size to display label below the world
|
const NLABEL = 8 // max. world size to display label below the world
|
||||||
const NMAX = 16 // max. world size. Level icons start spiraling out if the world has more levels.
|
const NMAX = 16 // max. world size. Level icons start spiraling out if the world has more levels.
|
||||||
|
const NSPIRAL = 12 // world size if NMAX has been passed and need to spiral.
|
||||||
|
|
||||||
|
const MINFONT = 12
|
||||||
|
|
||||||
// colours
|
// colours
|
||||||
const grey = '#999'
|
const grey = '#999'
|
||||||
@@ -54,8 +55,9 @@ export function LevelIcon({ world, level, position, completed, unlocked, worldSi
|
|||||||
|
|
||||||
const N = Math.max(worldSize, NMIN)
|
const N = Math.max(worldSize, NMIN)
|
||||||
|
|
||||||
// divide circle into `N+2` equal pieces
|
// divide circle into `N+2` equal pieces.
|
||||||
const beta = 2 * Math.PI / Math.min(N+2, (NMAX+1))
|
// only for non-spiraling case
|
||||||
|
const beta = 2 * Math.PI / Math.min(N+2, ((N < (NMAX+1) ? NMAX : NSPIRAL)+1))
|
||||||
|
|
||||||
// We want distance between two level icons to be `2.2*r`, therefore:
|
// We want distance between two level icons to be `2.2*r`, therefore:
|
||||||
// Sinus-Satz: (1.1*r) / sin(β/2) = R / sin(π/2)
|
// Sinus-Satz: (1.1*r) / sin(β/2) = R / sin(π/2)
|
||||||
@@ -70,19 +72,19 @@ export function LevelIcon({ world, level, position, completed, unlocked, worldSi
|
|||||||
* works fine in tests up to `N=30`.
|
* works fine in tests up to `N=30`.
|
||||||
*/
|
*/
|
||||||
function betaSpiral(level) {
|
function betaSpiral(level) {
|
||||||
return 2 * Math.PI / ((NMAX+1) + Math.max(0, (level-2)) / (NMAX+1))
|
return 2 * Math.PI / ((NSPIRAL+1) + 2 * Math.max(0, (level-2)) / (NSPIRAL+1))
|
||||||
}
|
}
|
||||||
|
|
||||||
const x = N < (NMAX+1) ?
|
const x = N < (NMAX+1) ?
|
||||||
// normal case
|
// normal case
|
||||||
s * position.x + Math.sin(level * beta) * R :
|
s * position.x + Math.sin(level * beta) * R :
|
||||||
// spiraling case
|
// spiraling case
|
||||||
s * position.x + Math.sin(level * betaSpiral(level)) * (R + 2*r*(level-1)/(NMAX+1))
|
s * position.x + Math.sin(level * betaSpiral(level)) * (R + 2*r*(level-1)/(NSPIRAL+1))
|
||||||
const y = N < (NMAX+1) ?
|
const y = N < (NMAX+1) ?
|
||||||
// normal case
|
// normal case
|
||||||
s * position.y - Math.cos(level * beta) * R :
|
s * position.y - Math.cos(level * beta) * R :
|
||||||
// spiraling case
|
// spiraling case
|
||||||
s * position.y - Math.cos(level * betaSpiral(level)) * (R + 2*r*(level-1)/(NMAX+1))
|
s * position.y - Math.cos(level * betaSpiral(level)) * (R + 2*r*(level-1)/(NSPIRAL+1))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={levelDisabled ? '' : `/${gameId}/world/${world}/level/${level == 1 ? 0 : level}`}
|
<Link to={levelDisabled ? '' : `/${gameId}/world/${world}/level/${level == 1 ? 0 : level}`}
|
||||||
@@ -112,9 +114,11 @@ export function WorldIcon({world, title, position, completedLevels, difficulty,
|
|||||||
|
|
||||||
// See level icons. Match radius computed there minus `1.2*r`
|
// See level icons. Match radius computed there minus `1.2*r`
|
||||||
const N = Math.max(worldSize, NMIN)
|
const N = Math.max(worldSize, NMIN)
|
||||||
const betaHalf = Math.PI / Math.min(N+2, (NMAX + 1))
|
const betaHalf = Math.PI / Math.min(N+2, ((N < (NMAX+1) ? NMAX : NSPIRAL) + 1))
|
||||||
let R = 1.1 * r / Math.sin(betaHalf) - 1.2 * r
|
let R = 1.1 * r / Math.sin(betaHalf) - 1.2 * r
|
||||||
|
|
||||||
|
let fontSize = Math.floor(R/4)
|
||||||
|
|
||||||
// Offset for the labels for small worlds
|
// Offset for the labels for small worlds
|
||||||
let labelOffset = R + 2.5 * r
|
let labelOffset = R + 2.5 * r
|
||||||
|
|
||||||
@@ -137,12 +141,14 @@ export function WorldIcon({world, title, position, completedLevels, difficulty,
|
|||||||
className={playable ? '' : 'disabled'}>
|
className={playable ? '' : 'disabled'}>
|
||||||
<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={completed ? green : unlocked ? blue : grey}/>
|
fill={completed ? green : unlocked ? blue : grey}/>
|
||||||
{worldSize > NLABEL ?
|
{ false ? // fontSize >= MINFONT ?
|
||||||
|
// NOTE: This code would display the world names inside the bubble, but currently
|
||||||
|
// it isn't used.
|
||||||
// Label for large worlds
|
// Label for large worlds
|
||||||
<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}
|
||||||
width={1.42*R} height={1.42*R} transform={"translate("+ -.71*R +","+ -.71*R +")"}>
|
width={1.42*R} height={1.42*R} transform={"translate("+ -.71*R +","+ -.71*R +")"}>
|
||||||
<div className={unlocked && !completed ? "playable-world" : ''}>
|
<div className={unlocked && !completed ? "playable-world" : ''}>
|
||||||
<p className="world-title" style={{fontSize: Math.floor(R/4) + "px"}}>
|
<p className="world-title" style={{fontSize: fontSize + "px"}}>
|
||||||
{title ? title : world}
|
{title ? title : world}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,7 +159,7 @@ export function WorldIcon({world, title, position, completedLevels, difficulty,
|
|||||||
width='150px' height='2em' style={{overflow: 'visible'}}
|
width='150px' height='2em' style={{overflow: 'visible'}}
|
||||||
>
|
>
|
||||||
<div className='world-label' style={{backgroundColor: completed ? darkgreen : unlocked ? darkblue : darkgrey}}>
|
<div className='world-label' style={{backgroundColor: completed ? darkgreen : unlocked ? darkblue : darkgrey}}>
|
||||||
<p className='world-title'>
|
<p className='world-title' style={{fontSize: MINFONT + "px"}}>
|
||||||
{title ? title : world}
|
{title ? title : world}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -187,7 +193,7 @@ export const downloadFile = ({ data, fileName, fileType } :
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The menu that is shown next to the world selection graph */
|
/** The menu that is shown next to the world selection graph */
|
||||||
export function WorldSelectionMenu() {
|
export function WorldSelectionMenu({rulesHelp, setRulesHelp}) {
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const difficulty = useSelector(selectDifficulty(gameId))
|
const difficulty = useSelector(selectDifficulty(gameId))
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
@@ -196,12 +202,15 @@ export function WorldSelectionMenu() {
|
|||||||
return x == 0 ? 'none' : x == 1 ? 'relaxed' : 'regular'
|
return x == 0 ? 'none' : x == 1 ? 'relaxed' : 'regular'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return <nav className="world-selection-menu">
|
return <nav className="world-selection-menu">
|
||||||
<div className="slider-wrap">
|
<div className="slider-wrap">
|
||||||
<span className="difficulty-label">Game Rules</span>
|
<span className="difficulty-label">Rules
|
||||||
|
<FontAwesomeIcon icon={rulesHelp ? faXmark : faCircleQuestion} className='helpButton' onClick={() => (setRulesHelp(!rulesHelp))}/>
|
||||||
|
</span>
|
||||||
<Slider
|
<Slider
|
||||||
orientation="vertical"
|
orientation="vertical"
|
||||||
title="Game Rules: - regular: 🔐 levels, 🔐 tactics - relaxed: 🔓 levels, 🔐 tactics - none: 🔓 levels, 🔓 tactics"
|
title="Game Rules"
|
||||||
min={0} max={2}
|
min={0} max={2}
|
||||||
aria-label="Game Rules"
|
aria-label="Game Rules"
|
||||||
defaultValue={difficulty}
|
defaultValue={difficulty}
|
||||||
@@ -256,9 +265,12 @@ export function computeWorldLayout(worlds) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function WorldTreePanel({worlds, worldSize}:
|
export function WorldTreePanel({worlds, worldSize, rulesHelp, setRulesHelp}:
|
||||||
{ worlds: any,
|
{ worlds: any,
|
||||||
worldSize: any}) {
|
worldSize: any,
|
||||||
|
rulesHelp: boolean,
|
||||||
|
setRulesHelp: any,
|
||||||
|
}) {
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const difficulty = useSelector(selectDifficulty(gameId))
|
const difficulty = useSelector(selectDifficulty(gameId))
|
||||||
const {nodes, bounds}: any = worlds ? computeWorldLayout(worlds) : {nodes: []}
|
const {nodes, bounds}: any = worlds ? computeWorldLayout(worlds) : {nodes: []}
|
||||||
@@ -342,7 +354,7 @@ export function WorldTreePanel({worlds, worldSize}:
|
|||||||
let dx = bounds ? s*(bounds.x2 - bounds.x1) + 2*padding : null
|
let dx = bounds ? s*(bounds.x2 - bounds.x1) + 2*padding : null
|
||||||
|
|
||||||
return <div className="column">
|
return <div className="column">
|
||||||
<WorldSelectionMenu />
|
<WorldSelectionMenu rulesHelp={rulesHelp} setRulesHelp={setRulesHelp} />
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink"
|
<svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||||
width={bounds ? `${ds * dx}` : ''}
|
width={bounds ? `${ds * dx}` : ''}
|
||||||
viewBox={bounds ? `${s*bounds.x1 - padding} ${s*bounds.y1 - padding} ${dx} ${s*(bounds.y2 - bounds.y1) + 2 * padding}` : ''}
|
viewBox={bounds ? `${s*bounds.x1 - padding} ${s*bounds.y1 - padding} ${dx} ${s*(bounds.y2 - bounds.y1) + 2 * padding}` : ''}
|
||||||
|
|||||||
@@ -42,12 +42,6 @@ body {
|
|||||||
border-radius: .3em;
|
border-radius: .3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Hide monaco editor notifications */
|
|
||||||
.monaco-workbench > .notifications-toasts.visible {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -108,6 +102,7 @@ em {
|
|||||||
flex: 0;
|
flex: 0;
|
||||||
background: var(--clr-primary);
|
background: var(--clr-primary);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: relative;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 1.1em;
|
padding: 1.1em;
|
||||||
@@ -139,3 +134,7 @@ em {
|
|||||||
.markdown ul, .markdown ol {
|
.markdown ul, .markdown ol {
|
||||||
margin:0 1.5em 1.5em 1.5em;
|
margin:0 1.5em 1.5em 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.privacy-policy {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.command-line {
|
.typewriter {
|
||||||
background: var(--clr-primary);
|
background: var(--clr-primary);
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
font-family: var(--ff-primary);
|
font-family: var(--ff-primary);
|
||||||
@@ -51,17 +51,33 @@
|
|||||||
/* margin: 0.2em 0 0; */
|
/* margin: 0.2em 0 0; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.command-line form {
|
.typewriter.disabled{
|
||||||
|
background: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typewriter.disabled .btn-inverted,
|
||||||
|
.typewriter.disabled .mtk1 {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typewriter.disabled .typewriter-input-wrapper,
|
||||||
|
.typewriter.disabled .monaco-editor,
|
||||||
|
.typewriter.disabled .monaco-editor-background,
|
||||||
|
.typewriter.disabled .btn-inverted {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typewriter form {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.command-line button, .undo-button {
|
.typewriter button, .undo-button {
|
||||||
display: block;
|
display: block;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex: 0;
|
flex: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.command-line .command-line-input-wrapper{
|
.typewriter .typewriter-input-wrapper{
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0.4em .6em 0;
|
padding: 0.4em .6em 0;
|
||||||
@@ -71,15 +87,22 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.command-line .command-line-input {
|
.typewriter-interface .proof .MuiCircularProgress-root {
|
||||||
|
left: 50%;
|
||||||
|
position: relative;
|
||||||
|
margin-left: -20px;
|
||||||
|
margin-bottom: 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typewriter .typewriter-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Turn off some monaco decorations: */
|
/* Turn off some monaco decorations: */
|
||||||
.command-line-input .monaco-editor .view-overlays .current-line {
|
.typewriter-input .monaco-editor .view-overlays .current-line {
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
.command-line-input .monaco-editor .scroll-decoration {
|
.typewriter-input .monaco-editor .scroll-decoration {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +161,7 @@
|
|||||||
|
|
||||||
|
|
||||||
/* Push the goals to the bottom for now, until we insert the proof history above. */
|
/* Push the goals to the bottom for now, until we insert the proof history above. */
|
||||||
.commandline-interface .content {
|
.typewriter-interface .content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
@@ -149,7 +172,7 @@
|
|||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commandline-interface .content .tmp-pusher {
|
.typewriter-interface .content .tmp-pusher {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +180,10 @@
|
|||||||
background-color: #bbb;
|
background-color: #bbb;
|
||||||
padding: .5em;
|
padding: .5em;
|
||||||
border-radius: .2em;
|
border-radius: .2em;
|
||||||
margin-bottom: 2rem;
|
/* TODO: It seems my browsers merge the margings of the proof steps,
|
||||||
|
so that it only shows once 0.5rem instead of twice. Thus have 1.5 here now.
|
||||||
|
*/
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
@@ -48,6 +48,7 @@ a {
|
|||||||
border: 1px solid rgb(140, 140, 140);
|
border: 1px solid rgb(140, 140, 140);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
box-shadow: 5px 5px 8px rgb(140, 140, 140);
|
box-shadow: 5px 5px 8px rgb(140, 140, 140);
|
||||||
|
width: 100%;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -41,6 +41,20 @@
|
|||||||
padding-right: .5em;
|
padding-right: .5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.exercise-statement {
|
||||||
|
padding-top: 1em;
|
||||||
|
padding-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exercise-statement p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exercise-statement .lean-code {
|
||||||
|
color: rgba(0, 32, 90, 0.87);
|
||||||
|
font-size: 12px; /* TODO: is the monaco font-size hardcoded? */
|
||||||
|
}
|
||||||
|
|
||||||
.conclusion {
|
.conclusion {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
}
|
}
|
||||||
@@ -218,17 +232,17 @@ td code {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
} */
|
} */
|
||||||
|
|
||||||
.commandline-interface {
|
.typewriter-interface {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.command-line {
|
.typewriter {
|
||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commandline-interface .content {
|
.typewriter-interface .content {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -257,7 +271,7 @@ td code {
|
|||||||
box-shadow: .0em .0em .4em .1em #8cbbe9;
|
box-shadow: .0em .0em .4em .1em #8cbbe9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.exercise .step.selected, .chat .selected {
|
.exercise .step.selected .goal-tabs, .chat .selected {
|
||||||
/* border: 3px solid #5191d1; */
|
/* border: 3px solid #5191d1; */
|
||||||
box-shadow: .0em .0em .4em .1em var(--clr-primary);
|
box-shadow: .0em .0em .4em .1em var(--clr-primary);
|
||||||
}
|
}
|
||||||
@@ -318,11 +332,26 @@ td code {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commandline-interface .content, .world-image-container.empty {
|
.typewriter-interface .content, .world-image-container.empty {
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commandline-interface .proof {
|
.world-image-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.world-image-container img.contain {
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.world-image-container img.cover {
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typewriter-interface .proof {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +49,6 @@ svg .disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.world-selection-menu {
|
.world-selection-menu {
|
||||||
padding: .5em 1em;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 1em;
|
right: 1em;
|
||||||
top: 1em;
|
top: 1em;
|
||||||
@@ -75,7 +74,7 @@ svg .disabled {
|
|||||||
/* width: 100%; */
|
/* width: 100%; */
|
||||||
height: 7em;
|
height: 7em;
|
||||||
/* min-width: 16em; */
|
/* min-width: 16em; */
|
||||||
padding-bottom: .8em;
|
padding: .5em 1em 1.3em 1em;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
@@ -109,3 +108,10 @@ svg .disabled {
|
|||||||
padding: .2em;
|
padding: .2em;
|
||||||
border-radius: .5em;
|
border-radius: .5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.world-selection-menu .helpButton {
|
||||||
|
position: absolute;
|
||||||
|
right: .5em;
|
||||||
|
top: .5em;
|
||||||
|
color: var(--clr-primary);
|
||||||
|
}
|
||||||
+32
-20
@@ -1,32 +1,44 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react'
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client'
|
||||||
import App from './app';
|
import App from './app'
|
||||||
import { ConnectionContext, connection } from './connection'
|
import { ConnectionContext, connection } from './connection'
|
||||||
import { store } from './state/store';
|
import { store } from './state/store'
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux'
|
||||||
import {
|
import type { RouteObject } from "react-router"
|
||||||
createHashRouter,
|
import { createHashRouter, RouterProvider, Route, redirect } from "react-router-dom"
|
||||||
RouterProvider,
|
import ErrorPage from './components/error_page'
|
||||||
Route,
|
import Welcome from './components/welcome'
|
||||||
} from "react-router-dom";
|
import LandingPage from './components/landing_page'
|
||||||
import ErrorPage from './components/error_page';
|
import Level from './components/level'
|
||||||
import Welcome from './components/welcome';
|
import { monacoSetup } from 'lean4web/client/src/monacoSetup'
|
||||||
import LandingPage from './components/landing_page';
|
|
||||||
import Level from './components/level';
|
|
||||||
import { monacoSetup } from 'lean4web/client/src/monacoSetup';
|
|
||||||
import { redirect } from 'react-router-dom';
|
|
||||||
|
|
||||||
monacoSetup()
|
monacoSetup()
|
||||||
|
|
||||||
|
|
||||||
|
// If `VITE_LEAN4GAME_SINGLE` is set to true, then `/` should be redirected to
|
||||||
|
// `/g/local/game`. This is used for the devcontainer setup
|
||||||
|
let single_game = (import.meta.env.VITE_LEAN4GAME_SINGLE == "true")
|
||||||
|
let root_object: RouteObject = single_game ? {
|
||||||
|
path: "/",
|
||||||
|
loader: () => redirect("/g/local/game")
|
||||||
|
} : {
|
||||||
|
path: "/",
|
||||||
|
element: <LandingPage />,
|
||||||
|
}
|
||||||
|
|
||||||
const router = createHashRouter([
|
const router = createHashRouter([
|
||||||
|
root_object,
|
||||||
{
|
{
|
||||||
path: "/",
|
// For backwards compatibility
|
||||||
element: <LandingPage />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/game/nng",
|
path: "/game/nng",
|
||||||
loader: () => redirect("/g/hhu-adam/NNG4")
|
loader: () => redirect("/g/hhu-adam/NNG4")
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// For backwards compatibility
|
||||||
|
path: "/g/hhu-adam/NNG4",
|
||||||
|
loader: () => redirect("/g/leanprover-community/NNG4")
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/g/:owner/:repo",
|
path: "/g/:owner/:repo",
|
||||||
element: <App />,
|
element: <App />,
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
// This file is a copy of `index.tsx` where the path "/" is redirected to "/g/local/game".
|
|
||||||
// It is used for the dev. setup where there is only one game in a folder called `game`.
|
|
||||||
import * as React from 'react';
|
|
||||||
import { createRoot } from 'react-dom/client';
|
|
||||||
import App from './app';
|
|
||||||
import { ConnectionContext, connection } from './connection'
|
|
||||||
import { store } from './state/store';
|
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
import {
|
|
||||||
createHashRouter,
|
|
||||||
RouterProvider,
|
|
||||||
Route,
|
|
||||||
} from "react-router-dom";
|
|
||||||
import ErrorPage from './components/error_page';
|
|
||||||
import Welcome from './components/welcome';
|
|
||||||
import LandingPage from './components/landing_page';
|
|
||||||
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("/g/local/game")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/g/:owner/:repo",
|
|
||||||
element: <App />,
|
|
||||||
errorElement: <ErrorPage />,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "/g/:owner/:repo",
|
|
||||||
element: <Welcome />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/g/:owner/:repo/world/:worldId/level/:levelId",
|
|
||||||
element: <Level />,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const container = document.getElementById('root');
|
|
||||||
const root = createRoot(container!);
|
|
||||||
root.render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<Provider store={store}>
|
|
||||||
<ConnectionContext.Provider value={connection}>
|
|
||||||
<RouterProvider router={router} />
|
|
||||||
</ConnectionContext.Provider>
|
|
||||||
</Provider>
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
|
||||||
+29
-25
@@ -2,16 +2,29 @@
|
|||||||
* @fileOverview Define API of the server-client communication
|
* @fileOverview Define API of the server-client communication
|
||||||
*/
|
*/
|
||||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
||||||
import { Connection } from '../connection'
|
|
||||||
|
|
||||||
|
export interface GameTile {
|
||||||
|
title: string
|
||||||
|
short: string
|
||||||
|
long: string
|
||||||
|
languages: Array<string>
|
||||||
|
prerequisites: Array<string>
|
||||||
|
worlds: number
|
||||||
|
levels: number
|
||||||
|
image: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface GameInfo {
|
export interface GameInfo {
|
||||||
title: null|string,
|
title: null|string,
|
||||||
introduction: null|string,
|
introduction: null|string,
|
||||||
info: null|string,
|
info: null|string,
|
||||||
worlds: null|{nodes: {[id:string]: {id: string, title: string, introduction: string}}, edges: string[][]},
|
worlds: null|{nodes: {[id:string]: {id: string, title: string, introduction: string, image: string}}, edges: string[][]},
|
||||||
worldSize: null|{[key: string]: number},
|
worldSize: null|{[key: string]: number},
|
||||||
authors: null|string[],
|
authors: null|string[],
|
||||||
conclusion: null|string,
|
conclusion: null|string,
|
||||||
|
tile: null|GameTile,
|
||||||
|
image: null|string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InventoryTile {
|
export interface InventoryTile {
|
||||||
@@ -36,7 +49,9 @@ export interface LevelInfo {
|
|||||||
descrFormat: null|string,
|
descrFormat: null|string,
|
||||||
lemmaTab: null|string,
|
lemmaTab: null|string,
|
||||||
statementName: null|string,
|
statementName: null|string,
|
||||||
template: null|string
|
displayName: null|string,
|
||||||
|
template: null|string,
|
||||||
|
image: null|string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Used to display the inventory on the welcome page */
|
/** Used to display the inventory on the welcome page */
|
||||||
@@ -56,40 +71,29 @@ interface Doc {
|
|||||||
category: string,
|
category: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WorldOverview {
|
||||||
const customBaseQuery = async (
|
world: string
|
||||||
args : {game: string, method: string, params?: any},
|
tactics: InventoryTile[]
|
||||||
{ signal, dispatch, getState, extra },
|
lemmas: InventoryTile[]
|
||||||
extraOptions
|
definitions: InventoryTile[]
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const connection : Connection = extra.connection
|
|
||||||
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)
|
|
||||||
return {'data': res}
|
|
||||||
} catch (e) {
|
|
||||||
return {'error': e}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define a service using a base URL and expected endpoints
|
// Define a service using a base URL and expected endpoints
|
||||||
export const apiSlice = createApi({
|
export const apiSlice = createApi({
|
||||||
reducerPath: 'gameApi',
|
reducerPath: 'gameApi',
|
||||||
baseQuery: customBaseQuery,
|
baseQuery: fetchBaseQuery({ baseUrl: window.location.origin + "/data" }),
|
||||||
endpoints: (builder) => ({
|
endpoints: (builder) => ({
|
||||||
getGameInfo: builder.query<GameInfo, {game: string}>({
|
getGameInfo: builder.query<GameInfo, {game: string}>({
|
||||||
query: ({game}) => {return {game, method: 'info', params: {}}},
|
query: ({game}) => `${game}/game.json`,
|
||||||
}),
|
}),
|
||||||
loadLevel: builder.query<LevelInfo, {game: string, world: string, level: number}>({
|
loadLevel: builder.query<LevelInfo, {game: string, world: string, level: number}>({
|
||||||
query: ({game, world, level}) => {return {game, method: "loadLevel", params: {world, level}}},
|
query: ({game, world, level}) => `${game}/level__${world}__${level}.json`,
|
||||||
}),
|
}),
|
||||||
loadInventoryOverview: builder.query<InventoryOverview, {game: string}>({
|
loadInventoryOverview: builder.query<WorldOverview[], {game: string}>({
|
||||||
query: ({game}) => {return {game, method: "loadInventoryOverview", params: {}}},
|
query: ({game}) => `${game}/inventory.json`,
|
||||||
}),
|
}),
|
||||||
loadDoc: builder.query<Doc, {game: string, name: string, type: "lemma"|"tactic"}>({
|
loadDoc: builder.query<Doc, {game: string, name: string, type: "lemma"|"tactic"}>({
|
||||||
query: ({game, name, type}) => {return {game, method: "loadDoc", params: {name, type}}},
|
query: ({game, type, name}) => `${game}/doc__${type}__${name}.json`,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ export interface GameProgressState {
|
|||||||
inventory: string[],
|
inventory: string[],
|
||||||
difficulty: number,
|
difficulty: number,
|
||||||
openedIntro: boolean,
|
openedIntro: boolean,
|
||||||
data: WorldProgressState
|
data: WorldProgressState,
|
||||||
|
typewriterMode?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -126,6 +127,11 @@ export const progressSlice = createSlice({
|
|||||||
addGameProgress(state, action)
|
addGameProgress(state, action)
|
||||||
state.games[action.payload.game].openedIntro = action.payload.openedIntro
|
state.games[action.payload.game].openedIntro = action.payload.openedIntro
|
||||||
},
|
},
|
||||||
|
/** set the typewriter mode */
|
||||||
|
changeTypewriterMode(state: ProgressState, action: PayloadAction<{game: string, typewriterMode: boolean}>) {
|
||||||
|
addGameProgress(state, action)
|
||||||
|
state.games[action.payload.game].typewriterMode = action.payload.typewriterMode
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -196,7 +202,14 @@ export function selectOpenedIntro(game: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** return typewriter mode for the current game if it exists */
|
||||||
|
export function selectTypewriterMode(game: string) {
|
||||||
|
return (state) => {
|
||||||
|
return state.progress.games[game]?.typewriterMode ?? true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Export actions to modify the progress */
|
/** Export actions to modify the progress */
|
||||||
export const { changedSelection, codeEdited, levelCompleted, deleteProgress,
|
export const { changedSelection, codeEdited, levelCompleted, deleteProgress,
|
||||||
deleteLevelProgress, loadProgress, helpEdited, changedInventory, changedOpenedIntro,
|
deleteLevelProgress, loadProgress, helpEdited, changedInventory, changedOpenedIntro,
|
||||||
changedDifficulty } = progressSlice.actions
|
changedDifficulty, changeTypewriterMode} = progressSlice.actions
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
**NOTE! This document is deprecated! The current documentation is [How To Create A Game](create_game.md)**
|
||||||
|
|
||||||
# Creating a game.
|
# Creating a game.
|
||||||
|
|
||||||
Ideally one takes the [NNG template](https://github.com/hhu-adam/NNG4) to create a new game.
|
Ideally one takes the [GameSkeleton template](https://github.com/hhu-adam/GameSkeleton) to create a new game.
|
||||||
|
|
||||||
## Game Structure
|
## Game Structure
|
||||||
|
|
||||||
@@ -8,9 +10,7 @@ A game consist of worlds which have multiple levels each. In the following we de
|
|||||||
|
|
||||||
### Level
|
### Level
|
||||||
|
|
||||||
#### Preample
|
A level file is a lean file that imports at least `import GameServer.Commands` and starts with the following Lean commands.
|
||||||
|
|
||||||
A level file is a lean file that imports at least `GameServer.Commands` and starts with the four lean commands
|
|
||||||
|
|
||||||
```lean
|
```lean
|
||||||
Game "NNG"
|
Game "NNG"
|
||||||
@@ -19,12 +19,12 @@ Level 1
|
|||||||
Title "The rfl tactic"
|
Title "The rfl tactic"
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that the levels inside a world must have consequtive numbering starting with `1`. The `Game`
|
Note that the levels inside a world must have consecutive numbering starting with `1`. The `Game`
|
||||||
and `World` strings can be anything.
|
and `World` strings can be anything, see below.
|
||||||
|
|
||||||
#### Statement
|
#### Statement
|
||||||
|
|
||||||
The core of a level is the `Statement`, which is the exercise that should be proven:
|
The core of a level is the `Statement`, which is the exercise that should be proven.
|
||||||
|
|
||||||
```lean
|
```lean
|
||||||
/-- For all natural numbers $n$, we have $0 + n = n$. -/
|
/-- For all natural numbers $n$, we have $0 + n = n$. -/
|
||||||
@@ -53,10 +53,12 @@ There are a few extra tactics that help you structuring the proof:
|
|||||||
|
|
||||||
- `Hint`: You can use `Hint "text"` to display text if the goal state in-game matches
|
- `Hint`: You can use `Hint "text"` to display text if the goal state in-game matches
|
||||||
the one where `Hint` is placed. For more options about hints, see below.
|
the one where `Hint` is placed. For more options about hints, see below.
|
||||||
- `Branch`: In the proof you can add a `Branch` that runs an alternativ tactic sequence, which
|
- `Branch`: In the proof you can add a `Branch` that runs an alternative tactic sequence, which
|
||||||
helps setting `Hints` in different places. The `Branch` does not affect the main
|
helps setting `Hints` in different places. The `Branch` does not affect the main
|
||||||
proof and does not need to finish any goals.
|
proof and does not need to finish any goals.
|
||||||
- `Template`/`Hole`: not implemented yet, but used to provide a sample proof template.
|
- `Template`/`Hole`: Used to provide a sample proof template. Anything inside `Template`
|
||||||
|
will be copied into the editor with all `Hole`s replaced with `sorry`. Note that
|
||||||
|
having a `Template` will force the user to use Editor-mode for this level.
|
||||||
|
|
||||||
##### Statement Name (optional)
|
##### Statement Name (optional)
|
||||||
|
|
||||||
@@ -77,26 +79,27 @@ There are three places where the documentation comment appears:
|
|||||||
description there including examples etc.
|
description there including examples etc.
|
||||||
|
|
||||||
Both latter points support Markdown (including katex).
|
Both latter points support Markdown (including katex).
|
||||||
|
|
||||||
##### Attributes (optional)
|
##### Attributes (optional)
|
||||||
|
|
||||||
the `@[ attributes ]` prefix should work just like you know it from the `theorem` keyword.
|
the `@[ attributes ]` prefix should work just like you know it from the `theorem` keyword.
|
||||||
|
|
||||||
#### Introduction/Conclusion
|
#### Introduction/Conclusion
|
||||||
|
|
||||||
Optionally, you can add a `Introduction "some text"` and `Conclusion "some text"` to your level.
|
Optionally, you can add an `Introduction "some text"` and `Conclusion "some text"` to your level.
|
||||||
The introduction will be shown at the beginning, the conclusion is displayed once the level
|
The introduction will be shown at the beginning, the conclusion is displayed once the level
|
||||||
is solved.
|
is solved.
|
||||||
|
|
||||||
#### Lemmas/Tactics/Definitions
|
#### Theorems/Tactics/Definitions
|
||||||
|
|
||||||
Only enabled lemmas/tactics/definitions (called "items" here) are available in a level.
|
Only enabled theorems/tactics/definitions (called "items" here) are available in a level.
|
||||||
|
|
||||||
To add a new item in a level, you can add
|
To add a new item in a level, you can add
|
||||||
|
|
||||||
```lean
|
```lean
|
||||||
NewTactic rfl simp
|
NewTactic rfl simp
|
||||||
NewLemma MyNat.add_zero
|
NewLemma MyNat.add_zero MyNat.add_succ
|
||||||
NewDefinition Nat
|
NewDefinition Nat Pow Mul
|
||||||
```
|
```
|
||||||
|
|
||||||
Once added, items will be available in all future levels/worlds,
|
Once added, items will be available in all future levels/worlds,
|
||||||
@@ -107,7 +110,7 @@ DisabledTactic tauto
|
|||||||
DisabledLemma MyNat.add_zero
|
DisabledLemma MyNat.add_zero
|
||||||
```
|
```
|
||||||
|
|
||||||
or specify explicitely which items should be available with
|
or specify explicitly which items should be available with
|
||||||
|
|
||||||
```lean
|
```lean
|
||||||
OnlyTactic rw rfl apply
|
OnlyTactic rw rfl apply
|
||||||
@@ -134,10 +137,9 @@ Introduction "some text"
|
|||||||
```
|
```
|
||||||
|
|
||||||
The `Title` is the world's display title. The `Introduction` is displayed before loading level 1.
|
The `Title` is the world's display title. The `Introduction` is displayed before loading level 1.
|
||||||
Note that all levels of a world should be imported by the world file and they **must not** be
|
Note that all levels of a world should be imported by the world file.
|
||||||
imported in another world's levels. (bug?)
|
|
||||||
|
|
||||||
However, you can import an entire world in a different world's level: `import NNG.Levels.Addition`
|
BUG: A level **must not** be imported in a different world's level. Instead, you have to import an entire world there: `import NNG.Levels.Addition`
|
||||||
|
|
||||||
### Game
|
### Game
|
||||||
|
|
||||||
@@ -152,17 +154,18 @@ Game "NNG"
|
|||||||
Title "Natural Number Game"
|
Title "Natural Number Game"
|
||||||
Introduction "some text"
|
Introduction "some text"
|
||||||
|
|
||||||
Path Addition → Multiplication → Power
|
|
||||||
|
|
||||||
MakeGame
|
MakeGame
|
||||||
```
|
```
|
||||||
|
|
||||||
There can be several `Path` statements to define the relation between the worlds. The worlds
|
The game will automatically compute the order of the worlds depending on the sample proofs of the Levels (ignoring anything inside a `Branch`). You can add additional dependencies manually by adding `Dependency PowerWorld → ImpossibleWorld` before `MakeGame`.
|
||||||
must form a directed acyclic graph, i.e. have no loops. The order of worlds influences which tactics and lemmas will be unlocked in a given level.
|
The order of worlds influences which tactics and lemmas will be unlocked in a given level.
|
||||||
|
|
||||||
|
`MakeGame` will display warnings about things in the game that need to be fixed, like missing
|
||||||
|
documentation or if a tactic is never introduced.
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
Each tactic, lemma, or definition (all called items here) that is introduced in the game
|
Each tactic, theorem, or definition (all called items here) that is introduced in the game
|
||||||
needs a documentation entry. These are statements of the following form:
|
needs a documentation entry. These are statements of the following form:
|
||||||
|
|
||||||
```lean
|
```lean
|
||||||
@@ -194,7 +197,7 @@ There will be features added to get automatic information from mathlib!
|
|||||||
|
|
||||||
Inside the doc comment you don't need to escape the backslashes:
|
Inside the doc comment you don't need to escape the backslashes:
|
||||||
|
|
||||||
```
|
```lean
|
||||||
/-- $\operatorname{succ}(n)$. notation for naturals is `\N`. -/
|
/-- $\operatorname{succ}(n)$. notation for naturals is `\N`. -/
|
||||||
Statement ...
|
Statement ...
|
||||||
```
|
```
|
||||||
@@ -203,11 +206,17 @@ However, inside interpolated strings (e.g. in `Hint`, `Introduction` and `Conclu
|
|||||||
you do need to escape backslashes
|
you do need to escape backslashes
|
||||||
with `\\` and `{` with `\{`:
|
with `\\` and `{` with `\{`:
|
||||||
|
|
||||||
```
|
```lean
|
||||||
Hint "This code has some $\\operatorname\{succ}(n)$ math. The value of `h` is {h}.
|
Hint "This code has some $\\operatorname\{succ}(n)$ math. The value of `h` is {h}.
|
||||||
Notation for naturals is `\\N`."
|
Notation for naturals is `\\N`."
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Game design
|
||||||
|
Here are some things you should consider designing a new game:
|
||||||
|
|
||||||
|
* A world with more than 16 levels will be displayed with the levels spiraling outwards,
|
||||||
|
it might be desirable to stay below that bound. Above 22 levels the spiral start getting out
|
||||||
|
of control.
|
||||||
|
|
||||||
# Running Games Locally
|
# Running Games Locally
|
||||||
|
|
||||||
@@ -217,7 +226,7 @@ The installation instructions are not yet tested on Mac/Windows. Comments very w
|
|||||||
|
|
||||||
1. **Install Docker and Dev Containers** *(once)*:<br/>
|
1. **Install Docker and Dev Containers** *(once)*:<br/>
|
||||||
See [official instructions](https://code.visualstudio.com/docs/devcontainers/containers#_getting-started).
|
See [official instructions](https://code.visualstudio.com/docs/devcontainers/containers#_getting-started).
|
||||||
Explicitely this means:
|
Explicitly this means:
|
||||||
* Install docker engine if you have not yet: [Instructions](https://docs.docker.com/engine/install/).
|
* Install docker engine if you have not yet: [Instructions](https://docs.docker.com/engine/install/).
|
||||||
I followed the "Server" instructions for linux.
|
I followed the "Server" instructions for linux.
|
||||||
* Note that on Linux you need to add your user to the `docker` group
|
* Note that on Linux you need to add your user to the `docker` group
|
||||||
@@ -244,29 +253,30 @@ The installation instructions are not yet tested on Mac/Windows. Comments very w
|
|||||||
running the `remote-containers.showReopenInContainerNotificationReset` command in vscode.
|
running the `remote-containers.showReopenInContainerNotificationReset` command in vscode.
|
||||||
* If the starting the container fails, in particular with a message `Error: network xyz not found`,
|
* If the starting the container fails, in particular with a message `Error: network xyz not found`,
|
||||||
you might have deleted stuff from docker via your shell. Try deleting the container and image
|
you might have deleted stuff from docker via your shell. Try deleting the container and image
|
||||||
explicitely in VSCode (left side, "Docker" icon). Then reopen vscode and let it rebuild the
|
explicitly in VSCode (left side, "Docker" icon). Then reopen vscode and let it rebuild the
|
||||||
container. (this will again take some time)
|
container. (this will again take some time)
|
||||||
|
|
||||||
|
|
||||||
## Without Dev Containers
|
## Without Dev Containers
|
||||||
Install `nvm`:
|
Install `nvm`:
|
||||||
```
|
```bash
|
||||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.2/install.sh | bash
|
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.2/install.sh | bash
|
||||||
```
|
```
|
||||||
then reopen bash and test with `command -v nvm` if it is available (Should print "nvm").
|
then reopen bash and test with `command -v nvm` if it is available (Should print "nvm").
|
||||||
|
|
||||||
Now install node:
|
Now install node:
|
||||||
```
|
```bash
|
||||||
nvm install node
|
nvm install node
|
||||||
```
|
```
|
||||||
|
|
||||||
Clone the game (e.g. `NNG4` here):
|
Clone the game (e.g. `NNG4` here):
|
||||||
```
|
```bash
|
||||||
git clone git@github.com:hhu-adam/NNG4.git
|
git clone https://github.com/hhu-adam/NNG4.git
|
||||||
|
# or: git clone git@github.com:hhu-adam/NNG4.git
|
||||||
```
|
```
|
||||||
|
|
||||||
Download dependencies and build the game:
|
Download dependencies and build the game:
|
||||||
```
|
```bash
|
||||||
cd NNG4
|
cd NNG4
|
||||||
lake update
|
lake update
|
||||||
lake exe cache get # if your game depends on mathlib
|
lake exe cache get # if your game depends on mathlib
|
||||||
@@ -274,21 +284,23 @@ lake build
|
|||||||
```
|
```
|
||||||
|
|
||||||
Clone the game repository into a directory next to the game:
|
Clone the game repository into a directory next to the game:
|
||||||
```
|
```bash
|
||||||
cd ..
|
cd ..
|
||||||
git clone git@github.com:leanprover-community/lean4game.git
|
git clone https://github.com/leanprover-community/lean4game.git
|
||||||
|
# or: git clone git@github.com:leanprover-community/lean4game.git
|
||||||
```
|
```
|
||||||
The folders `NNG4` and `lean4game` must be in the same directory!
|
The folders `NNG4` and `lean4game` must be in the same directory!
|
||||||
|
|
||||||
In `lean4game`, install dependencies:
|
In `lean4game`, install dependencies:
|
||||||
```
|
```bash
|
||||||
cd lean4game
|
cd lean4game
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
TODO: This is outdated!
|
||||||
If you are developing a game other than `Robo` or `NNG4`, adapt the
|
If you are developing a game other than `Robo` or `NNG4`, adapt the
|
||||||
code at the beginning of `lean4game/server/index.mjs`:
|
code at the beginning of `lean4game/relay/index.mjs`:
|
||||||
```
|
```typescript
|
||||||
const games = {
|
const games = {
|
||||||
"g/hhu-adam/robo": {
|
"g/hhu-adam/robo": {
|
||||||
dir: "../../../../Robo",
|
dir: "../../../../Robo",
|
||||||
@@ -302,7 +314,7 @@ const games = {
|
|||||||
```
|
```
|
||||||
|
|
||||||
Run the game:
|
Run the game:
|
||||||
```
|
```bash
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -314,7 +326,7 @@ and the game is available on http://localhost:3000/#/g/hhu-adam/NNG4.
|
|||||||
When modifying the game engine itself (in particular the content in `lean4game/server`) you can test it live with this
|
When modifying the game engine itself (in particular the content in `lean4game/server`) you can test it live with this
|
||||||
setup by setting `export NODE_ENV=development` inside your local game before building it:
|
setup by setting `export NODE_ENV=development` inside your local game before building it:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
cd NNG4
|
cd NNG4
|
||||||
export NODE_ENV=development
|
export NODE_ENV=development
|
||||||
lake update
|
lake update
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
# Creating a new game
|
||||||
|
|
||||||
|
This tutorial walks you through creating a new game for lean4. It covers from writing new levels, over testing the game locally, to publishing the game online.
|
||||||
|
|
||||||
|
## 1. Create the project
|
||||||
|
|
||||||
|
1. Use the [GameSkeleton template](https://github.com/hhu-adam/GameSkeleton) to create a new github repo for your game: On github, click on "Use this template" > "Create a new repository".
|
||||||
|
2. Clone the game repo.
|
||||||
|
3. Call `lake update && lake exe cache get && lake build` to build the Lean project.
|
||||||
|
|
||||||
|
Note that you need to host your game's code on github to publish it online later on. If you only
|
||||||
|
want to play it locally, you can simply clone the NNG repo and start modifying that one.
|
||||||
|
|
||||||
|
|
||||||
|
## 2. Game.lean
|
||||||
|
|
||||||
|
The file `Game.lean` is the backbone of the game putting everything together.
|
||||||
|
|
||||||
|
1. `MakeGame`: This command is where the game gets built. If there are things to fix, they will be shown here as warnings or errors (orange/red squigglies in VSCode). Note that you might have to call "Lean: Restart File" (Ctrl+Shift+X) to reload the file and see changes made in other files.
|
||||||
|
1. `Title`: Set the display title of your game.
|
||||||
|
1. `Introduction`: This is the text displayed at the beginning next to the world selection tree.
|
||||||
|
1. `Info`: This will be displayed in the game's menu as "Game Info". Use it for credits, funding and other meta information about the game.
|
||||||
|
1. Imports: The file needs to import all world files, which we look at in a second. If you add a new world, remember to import it here!
|
||||||
|
1. `Dependency`: (optional) This command adds another edge in the world tree. The game computes them automatically based on used tactics/theorems. However, sometimes you need a dependency the game can't compute, for example you explained `≠` in one world and in another world, the user is expected to know it already. The syntax is `Dependency World₁ → World₂ → World₃`.
|
||||||
|
|
||||||
|
So the `Game.lean` has the following structure:
|
||||||
|
|
||||||
|
```lean
|
||||||
|
import GameServer.Commands
|
||||||
|
|
||||||
|
-- import all worlds
|
||||||
|
import Game.Levels.Tutorial
|
||||||
|
|
||||||
|
Title "My Game"
|
||||||
|
|
||||||
|
Introduction "
|
||||||
|
Hi, nice you're here! Click on a world to start.
|
||||||
|
"
|
||||||
|
|
||||||
|
Info "
|
||||||
|
This game has been developed by me.
|
||||||
|
"
|
||||||
|
|
||||||
|
-- Dependency World₁ → World₂ -- because of `≠`
|
||||||
|
|
||||||
|
MakeGame
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Creating a Level
|
||||||
|
|
||||||
|
In this tutorial we first learn about Levels. A game consists of a tree of worlds, each world has
|
||||||
|
a number of levels, enumerated 1 to n. Here we create level 1 of a world `MyWorld`. We add this world to the game in the next section.
|
||||||
|
|
||||||
|
A minimal level file looks like the following. There are many options to add, which we will dive
|
||||||
|
into in a later section
|
||||||
|
|
||||||
|
```lean
|
||||||
|
import GameServer.Commands
|
||||||
|
|
||||||
|
World "MyWorld"
|
||||||
|
Level 1
|
||||||
|
Title "Hello World"
|
||||||
|
|
||||||
|
Introduction "
|
||||||
|
A message shown at the beginning of the level. Use it to explain any new concepts.
|
||||||
|
"
|
||||||
|
|
||||||
|
/-- The exercise statement in natural language using latex: $\iff$. -/
|
||||||
|
Statement (n : Nat) : 0 + n = n := by
|
||||||
|
sorry
|
||||||
|
|
||||||
|
Conclusion "
|
||||||
|
The message shown when the level is completed
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Create a folder `Game/Levels/MyWorld`
|
||||||
|
1. Create a file `Game/Levels/MyWorld/L01_hello.lean`
|
||||||
|
1. Copy the above inside your first level.
|
||||||
|
|
||||||
|
## 4. Creating a World
|
||||||
|
|
||||||
|
Now we create a world. For that we create a file `MyWorld.lean` that imports all levels of this world:
|
||||||
|
|
||||||
|
```lean
|
||||||
|
import Game.Levels.MyWorld.L01_hello
|
||||||
|
|
||||||
|
World "MyWorld"
|
||||||
|
Title "My First World"
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
"
|
||||||
|
This introduction text is shown when one first enters a world.
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Create a file `Game/Levels/MyWorld.lean`
|
||||||
|
1. Use the template above and make sure you import all levels of this world.
|
||||||
|
1. In `Game.lean` import the world with `import Game.Levels.MyWorld`
|
||||||
|
|
||||||
|
Now you created a world with one level and added it🎉 The command `MakeGame` in `Game.lean` shows you any problems you might need to fix. Currently it shows
|
||||||
|
|
||||||
|
```text
|
||||||
|
No world introducing sorry, but required by MyWorld
|
||||||
|
```
|
||||||
|
|
||||||
|
which means that the world `MyWorld` uses the tactic `sorry` in a proof, but `sorry` has not been
|
||||||
|
introduced anywhere.
|
||||||
|
|
||||||
|
## 5. Testing the Game Locally
|
||||||
|
|
||||||
|
Now it's time to test the game locally and play it.
|
||||||
|
|
||||||
|
There are multiple ways how you can start the game locally to test-play it described at [How to Run the Game Locally](running_locally.md). If you have problems getting one of the setups to work, please get in contact!
|
||||||
|
|
||||||
|
|
||||||
|
## 6. Dive into Level creation
|
||||||
|
|
||||||
|
Now that you have a running game, we have a closer look at the level files and all the options
|
||||||
|
you have to design your game.
|
||||||
|
|
||||||
|
### 6. a) Inventory
|
||||||
|
|
||||||
|
The player has an inventory with tactics, theorems, and definitions that unlock during the game. You can unlock/introduce such items in a Level by adding one of the following below the `Statement`.
|
||||||
|
|
||||||
|
```lean
|
||||||
|
NewTactic induction simp
|
||||||
|
NewLemma Nat.zero_mul
|
||||||
|
NewDefinition Pow
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Doc entries
|
||||||
|
|
||||||
|
You'll see a warning about a missing Lemma documentation. You can fix it by adding doc-entries like the following somewhere above it.
|
||||||
|
|
||||||
|
```lean
|
||||||
|
LemmaDoc Nat.zero_mul as "zero_mul" in "Mul"
|
||||||
|
"some description"
|
||||||
|
|
||||||
|
TacticDoc simp
|
||||||
|
"some description"
|
||||||
|
|
||||||
|
DefinitionDoc Pow as "^"
|
||||||
|
"some description"
|
||||||
|
```
|
||||||
|
|
||||||
|
(e.g. you could also create a file `Game/Doc/MyTheorems.lean`, add there your documentation and import it)
|
||||||
|
|
||||||
|
If you do not provide any content for the inventory, the game will try to find a docstring of that item and display that docstring.
|
||||||
|
|
||||||
|
#### Inventory management
|
||||||
|
|
||||||
|
You have a few options to disable inventory items that have been unlocked in previous levels:
|
||||||
|
|
||||||
|
```lean
|
||||||
|
DisableTactic, DisableLemma, OnlyTactic, OnlyLemma
|
||||||
|
```
|
||||||
|
|
||||||
|
have the same syntax as above. The former two disable items for this level, the latter two
|
||||||
|
disable all items except the ones specified.
|
||||||
|
|
||||||
|
#### Theorem Tab
|
||||||
|
|
||||||
|
Theorems are sorted into tabs. with `LemmaTab "Mul"` you specify which tab should be open by default in this level.
|
||||||
|
|
||||||
|
#### HiddenTactic
|
||||||
|
|
||||||
|
`NewHiddenTactic` lets you add a tactic without adding it to the inventory. This can be useful for variants of tactics. E.g. you could do
|
||||||
|
|
||||||
|
```lean
|
||||||
|
NewTactic rw
|
||||||
|
NewHiddenTactic rewrite nth_rewrite rwa
|
||||||
|
```
|
||||||
|
|
||||||
|
and only `rw` would show up in the inventory.
|
||||||
|
|
||||||
|
### 6. b) Statement
|
||||||
|
|
||||||
|
The statement is the exercise of the level. the basics work the same as they would in `example` or `theorem`. Note however, that you **must** do a tactic proof, i.e. the `:= by` is a hard-coded part of the syntax
|
||||||
|
|
||||||
|
#### Name
|
||||||
|
|
||||||
|
You can give your exercise a name: `Statement my_first_exercise (n : Nat) ...`. If you do so, it will be added to the inventory and be available in future levels.
|
||||||
|
|
||||||
|
#### Doc String / Exercise statement
|
||||||
|
|
||||||
|
Add a docstring that contains the exercise statement in natural language. If you do this, it will appear at the top of the exercise. It supports Latex.
|
||||||
|
|
||||||
|
```lean
|
||||||
|
/-- The exercise statement in natural language using latex: $\iff$. -/
|
||||||
|
Statement ...
|
||||||
|
sorry
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Attributes
|
||||||
|
|
||||||
|
You can add attributes as you would for a `theorem`. Most notably, you can make your named exercise a `simp` lemma:
|
||||||
|
|
||||||
|
```lean
|
||||||
|
@[simp]
|
||||||
|
Statement my_simp_lemma ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. c) Proof
|
||||||
|
|
||||||
|
The proof must always be a tactic proof, i.e. `:= by` is a mandatory part of the syntax.
|
||||||
|
|
||||||
|
There are a few extra tactics that help you structuring the proof:
|
||||||
|
|
||||||
|
- `Hint`: You can use `Hint "text"` to display text if the goal state in-game matches
|
||||||
|
the one where `Hint` is placed. For more options about hints, see below.
|
||||||
|
- `Branch`: In the proof you can add a `Branch` that runs an alternative tactic sequence, which
|
||||||
|
helps setting `Hints` in different places. The `Branch` does not affect the main
|
||||||
|
proof and does not need to finish any goals.
|
||||||
|
- `Template`/`Hole`: Used to provide a sample proof template. Anything inside `Template`
|
||||||
|
will be copied into the editor with all `Hole`s replaced with `sorry`. Note that
|
||||||
|
having a `Template` will force the user to use Editor-mode for this level.
|
||||||
|
|
||||||
|
One thing to keep in mind is that the game will look at the main proof to figure out which tactics and theorems are needed, but it ignores any `Branch`es.
|
||||||
|
|
||||||
|
### 6. d) Hints
|
||||||
|
|
||||||
|
Most important for game development are probably the `Hints`.
|
||||||
|
|
||||||
|
The hints will be displayed whenever the player's current goal matches the goal the hint is
|
||||||
|
placed at inside the sample proof. You can use `Branch` to place hints in dead ends or alternative proof strands.
|
||||||
|
|
||||||
|
Read [More about Hints](doc/hints.md) for how they work and what the options are.
|
||||||
|
|
||||||
|
### 6. e) Extra: Images
|
||||||
|
You can add images on any layer of the game (i.e. game/world/level). These will be displayed in your game.
|
||||||
|
|
||||||
|
The images need to be placed in `images/` and you need to add a command like `Image "images/path/to/myWorldImage.png"`
|
||||||
|
in one of the files you created in 2), 3), or 4) (i.e. game/world/level).
|
||||||
|
|
||||||
|
NOTE: At present, only the images for a world are displayed. They appear in the introduction of the world.
|
||||||
|
|
||||||
|
## 7. Update your game
|
||||||
|
|
||||||
|
In principle, it is as simple as modifying `lean-toolchain` to update your game to a new Lean version. However, you should read about the details in [Update An Existing Game](doc/update_game.md).
|
||||||
|
|
||||||
|
## 8. Publish your game
|
||||||
|
|
||||||
|
To publish your game on the official server, see [Publishing a game](doc/publish_game.md)
|
||||||
|
|
||||||
|
There are a few more options you can add in `Game.lean` before the `MakeGame` command, which describe the tile that is visible on the server's landing page:
|
||||||
|
|
||||||
|
```lean
|
||||||
|
Languages "English"
|
||||||
|
CaptionShort "Game Template"
|
||||||
|
CaptionLong "You should use this game as a template for your own game and add your own levels."
|
||||||
|
Prerequisites "NNG"
|
||||||
|
CoverImage "images/cover.png"
|
||||||
|
```
|
||||||
|
* `Languages`: Currently only a single language (capital English name). The tile will show a corresponding flag.
|
||||||
|
* `CaptionShort`: One catch phrase. Appears above the image.
|
||||||
|
* `CaptionLong`: 2-4 sentences to describe the game.
|
||||||
|
* `Prerequisites` a list of other games you should play before this one, e.g. `Prerequisites "NNG" "STG"`. The game names are free-text.
|
||||||
|
* `CoverImage`: You can create a folder `images/` and put images there for the game to use. The maximal ratio is ca. 500x200 (W x H) but it might be cropped horizontally on narrow screens.
|
||||||
|
|
||||||
|
## Further Notes
|
||||||
|
|
||||||
|
Here are some random further things you should consider designing a new game:
|
||||||
|
|
||||||
|
* Inside strings, you need to escape backslashes, but not inside doc-strings, therefore you
|
||||||
|
would write `Introduction "some latex here: $\\iff$."` but
|
||||||
|
`/-- some latex here: $\iff$. -/ Statement ...`
|
||||||
|
* A world with more than 16 levels will be displayed with the levels spiraling outwards,
|
||||||
|
it might be desirable to stay below that bound. Above 22 levels the spiral starts getting out
|
||||||
|
of control.
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# Hints
|
||||||
|
|
||||||
|
Most important for game development are probably the "Hints". You can add Hints at any place in your proof using the `Hint` tactic
|
||||||
|
|
||||||
|
```
|
||||||
|
Statement .... := by
|
||||||
|
Hint "Hint to show at the start"
|
||||||
|
rw [h]
|
||||||
|
Hint "some tip after using rw"
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1. When do hints show?
|
||||||
|
|
||||||
|
A hint will be displayed if the player's goal matches the one where the hint was placed in the
|
||||||
|
sample solutions and the entire context from the sample solutions is present in the
|
||||||
|
player's context. The player's context may contain additional items.
|
||||||
|
|
||||||
|
This means if you have multiple identical
|
||||||
|
subgoals, you should only place a single hint in one of them and it will be displayed in
|
||||||
|
all of them.
|
||||||
|
|
||||||
|
However, identical (non-hidden) hints which where already present in the step
|
||||||
|
before are omitted. This is to allow a player to add new assumptions to context, for example
|
||||||
|
with `have`, without seeing the same hint over and over again.
|
||||||
|
Hidden hints are not filtered.
|
||||||
|
|
||||||
|
## 2. Alternative Proofs / `Branch`
|
||||||
|
|
||||||
|
You can use `Branch` to place hints
|
||||||
|
in dead ends or alternative proof strands.
|
||||||
|
|
||||||
|
A proof inside a `Branch`-block is normally evaluated by lean but it's discarded at the end
|
||||||
|
so that no progress has been made on proofing the goal.
|
||||||
|
|
||||||
|
```
|
||||||
|
Statement .... := by
|
||||||
|
Hint "Huse `rw` or `rewrite`."
|
||||||
|
Branch
|
||||||
|
rewrite [h]
|
||||||
|
Hint "now you still need `rfl`"
|
||||||
|
rw [h]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Variables names
|
||||||
|
|
||||||
|
Put variables in the hint text inside brackets like this: `{h}`! This way the server can replace
|
||||||
|
the variable's name with the one the user actually used.
|
||||||
|
|
||||||
|
For example, if the sample proof contains
|
||||||
|
|
||||||
|
```
|
||||||
|
have h : True := trivial
|
||||||
|
Hint "Now use `rw [{h}]` to use your assumption `{h}`."
|
||||||
|
```
|
||||||
|
|
||||||
|
but the player writes `have g : True := trivial`, they will see a hint saying
|
||||||
|
"Now use `rw [g]` to use your assumption `g`."
|
||||||
|
|
||||||
|
## 4. Hidden hints
|
||||||
|
|
||||||
|
Some hints can be hidden, and only show after the user clicks on a button to get additional
|
||||||
|
help. You mark a hint as hidden with `(hidden := true)`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hint (hidden := true) "some hidden hint"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Strict context matching
|
||||||
|
|
||||||
|
If you use the attribute `(strict := true)` a hint is only shown if the entire context
|
||||||
|
matches exactly the one where the hint is placed. With `(hint := false)`, which is the default,
|
||||||
|
it does not matter if additional assumptions are present in the player's context.
|
||||||
|
|
||||||
|
```
|
||||||
|
Hint (strict := true) "now use `have` to create a new assumption."
|
||||||
|
```
|
||||||
|
|
||||||
|
You should probably use `(strict := true)` if you want to give fine-grained details about
|
||||||
|
tactics like `have` which do not modify the goal or any existing assumptions, but only
|
||||||
|
create new assumptions.
|
||||||
|
|
||||||
|
## 6. Formatting
|
||||||
|
|
||||||
|
You can add use markdown to format your hints, for example you can use KaTex: `$\\iff$`
|
||||||
|
|
||||||
|
TODO: Write a doc about latex/markdown options available.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
## NPM Scripts
|
||||||
|
|
||||||
|
* `npm start`: Start the project in development mode. The browser will automatically reload when client files get changed. The Lean server will get recompiled and restarted when lean files get changed. The Lean server will be started without a container. The client and server can be started separately using the scripts `npm run start_client` and `npm run start_server`. The project can be accessed via `http://localhost:3000`.
|
||||||
|
Internally, websocket requests to `ws://localhost:3000/websockets` will be forwarded to a Lean server running on port `8080`.
|
||||||
|
|
||||||
|
* `npm run build`: Build the project in production mode. All assets of the client will be compiled into `client/dist`.
|
||||||
|
On the server side, the command will set up a docker image containing the Lean server. The two parts can be built separately using `npm run build_client` and `npm run build_server`.
|
||||||
|
|
||||||
|
* `npm run production`: Start the project in production mode. This requires that the build script has been run. It will start a server on the port specified in the `PORT` environment variable or by default on `8080`. You can run on a specific port by running `PORT=80 npm run production`. The server will serve the files in `client/dist` via http and give access to the bubblewrapped Lean server via the web socket protocol.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Publishing games
|
||||||
|
|
||||||
|
You can publish your game on the official (Lean Game Server)[https://adam.math.hhu.de] in a few simple
|
||||||
|
steps.
|
||||||
|
|
||||||
|
## 1. Upload Game to github
|
||||||
|
|
||||||
|
First, you need your game in a public Github repository and make sure the github action has run.
|
||||||
|
You can check this by spotting the green checkmark on the start page, or by looking at the "Actions"
|
||||||
|
tab.
|
||||||
|
|
||||||
|
## 2. Import the game
|
||||||
|
|
||||||
|
You call the URL that's listed under "What's Next?" in the latest action run. Explicitely you call
|
||||||
|
the URL of the form
|
||||||
|
|
||||||
|
> adam.math.hhu.de/import/trigger/{USER}/{REPOSITORY}
|
||||||
|
|
||||||
|
where `{USER}` and `{REPOSITORY}` are replaced with the github user and repository name.
|
||||||
|
|
||||||
|
You should see a white screen which shows import updates and eventually reports "Done."
|
||||||
|
|
||||||
|
## 3. Play the game
|
||||||
|
|
||||||
|
Now you can immediately play the game at `adam.math.hhu.de/#/g/{USER}/{REPOSITORY}`!
|
||||||
|
|
||||||
|
## 4. Main page
|
||||||
|
|
||||||
|
Adding games to the main page happens manually by the server maintainers. Tell us if you want us
|
||||||
|
to add a tile for your game!
|
||||||
|
|
||||||
|
For example, you can [contact Jon on Zulip](https://leanprover.zulipchat.com/#narrow/dm/385895-Jon-Eugster). Or [via Email](https://www.math.hhu.de/en/lehrstuehle-/-personen-/-ansprechpartner/innen/lehrstuehle-des-mathematischen-instituts/lehrstuhl-fuer-algebraische-geometrie/team/jon-eugster).
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# Running Games Locally
|
||||||
|
|
||||||
|
The installation instructions are not yet tested on Mac/Windows. Comments very welcome!
|
||||||
|
|
||||||
|
There are several options to play a game locally:
|
||||||
|
|
||||||
|
1. VSCode Dev Container: needs `docker` installed on your machine
|
||||||
|
2. Codespaces: Needs active internet connection and computing time is limited.
|
||||||
|
3. Gitpod: does not work yet (Is that true?)
|
||||||
|
4. Manual installation: Needs `npm` installed on your system
|
||||||
|
|
||||||
|
The recommended option is "VSCode Dev containers" but you may choose any option above depending on your setup.
|
||||||
|
|
||||||
|
The template game [GameSkeleton](https://github.com/hhu-adam/GameSkeleton) contains all the relevant files to make your local setup (dev container / gitpod / codespaces) work. You might need to update these files manually by copying them from there if you need any new improvements to the dev setup you're using in an existing game.
|
||||||
|
|
||||||
|
## VSCode Dev Containers
|
||||||
|
|
||||||
|
1. **Install Docker and Dev Containers** *(once)*:<br/>
|
||||||
|
See [official instructions](https://code.visualstudio.com/docs/devcontainers/containers#_getting-started).
|
||||||
|
Explicitly this means:
|
||||||
|
* Install docker engine if you have not yet: [Instructions](https://docs.docker.com/engine/install/).
|
||||||
|
I followed the "Server" instructions for linux.
|
||||||
|
* Note that on Linux you need to add your user to the `docker` group
|
||||||
|
([see instructions](https://docs.docker.com/engine/install/linux-postinstall/)) and probably reboot.
|
||||||
|
* Open the games folder in VSCode: `cd NNG4 && code .` or "Open Folder" within VSCode
|
||||||
|
* a message appears prompting you to install the "Dev Containers" extension (by Microsoft). (Alternatively install it from the VSCode extensions).
|
||||||
|
|
||||||
|
2. **Open Project in Dev Container** *(everytime)*:<br/>
|
||||||
|
Once you have the Dev Containers Extension installed, (re)open the project folder of your game in VSCode.
|
||||||
|
A message appears asking you to "Reopen in Container".
|
||||||
|
|
||||||
|
* The first start will take a while, ca. 2-15 minutes. After the first
|
||||||
|
start this should be very quickly.
|
||||||
|
* Once built, you can open http://localhost:3000 in your browser. which should load the game.
|
||||||
|
|
||||||
|
3. **Editing Files** *(everytime)*:<br/>
|
||||||
|
After editing some Lean files in VSCode, open VSCode's terminal (View > Terminal) and run `lake build`. Now you can reload your browser to see the changes.
|
||||||
|
|
||||||
|
### Errors
|
||||||
|
|
||||||
|
* If you don't get the pop-up, you might have disabled them and you can reenable it by
|
||||||
|
running the `remote-containers.showReopenInContainerNotificationReset` command in vscode.
|
||||||
|
* If the starting the container fails, in particular with a message `Error: network xyz not found`,
|
||||||
|
you might have deleted stuff from docker via your shell. Try deleting the container and image
|
||||||
|
explicitely in VSCode (left side, "Docker" icon). Then reopen vscode and let it rebuild the
|
||||||
|
container. (this will again take some time)
|
||||||
|
* On a working dev container setup, http://localhost:3000 should directly redirect you to http://localhost:3000/#/g/local/game, try if the latter is accessible.
|
||||||
|
|
||||||
|
## Codespaces
|
||||||
|
|
||||||
|
You can work on your game using Github codespaces (click "Code" and then "Codespaces" and then "create codespace on main"). It it should run the game locally in the background. You can open it for example under "Ports" and clicking on "Open in Browser".
|
||||||
|
|
||||||
|
Note: You have to wait until npm started properly, which might take a good while.
|
||||||
|
|
||||||
|
As with devcontainers, you need to run `lake build` after changing any lean files and then reload the browser.
|
||||||
|
|
||||||
|
## Gitpod
|
||||||
|
|
||||||
|
TODO: Not sure this works yet!
|
||||||
|
|
||||||
|
Open the game clicking on the gitpod link. Again you will need to wait until it is fully built and then enter `lake build` in the shell whenever you made changes.
|
||||||
|
|
||||||
|
## Manual installation
|
||||||
|
|
||||||
|
This installs the `lean4game` manually on your computer. (It is the same installation as one would
|
||||||
|
use to setup a proper server online, only the run command (i.e. `npm start`) is different.)
|
||||||
|
|
||||||
|
Install `nvm`:
|
||||||
|
```bash
|
||||||
|
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.2/install.sh | bash
|
||||||
|
```
|
||||||
|
then reopen bash and test with `command -v nvm` if it is available (Should print "nvm").
|
||||||
|
|
||||||
|
Now install node:
|
||||||
|
```bash
|
||||||
|
nvm install node
|
||||||
|
```
|
||||||
|
|
||||||
|
Clone the game (e.g. `GameSkeleton` here):
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/hhu-adam/GameSkeleton.git
|
||||||
|
# or: git clone git@github.com:hhu-adam/GameSkeleton.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Download dependencies and build the game:
|
||||||
|
```bash
|
||||||
|
cd GameSkeleton
|
||||||
|
lake update -R
|
||||||
|
lake build
|
||||||
|
```
|
||||||
|
|
||||||
|
Clone the server repository into a directory next to the game:
|
||||||
|
```bash
|
||||||
|
cd ..
|
||||||
|
git clone https://github.com/leanprover-community/lean4game.git
|
||||||
|
# or: git clone git@github.com:leanprover-community/lean4game.git
|
||||||
|
```
|
||||||
|
The folders `GameSkeleton` and `lean4game` must be in the same directory!
|
||||||
|
|
||||||
|
In `lean4game`, install dependencies:
|
||||||
|
```bash
|
||||||
|
cd lean4game
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the game:
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
This takes a little time. Eventually, the game is available on http://localhost:3000/#/g/local/GameSkeleton. Replace `GameSkeleton` with the folder name of your local game.
|
||||||
|
|
||||||
|
## Modifying the GameServer
|
||||||
|
|
||||||
|
When modifying the game engine itself (in particular the content in `lean4game/server`) you can test it live with the same setup as above (manual installation) by using `lake update -R -Klean4game.local`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd NNG4
|
||||||
|
lake update -R -Klean4game.local
|
||||||
|
lake build
|
||||||
|
```
|
||||||
|
This causes lake to search locally for the `GameServer` lake package instead of using the version from github. Therefore, you can the local copy of the edit `GameServer` in `../lean4game` and
|
||||||
|
`lake build` will then directly use this modified copy to build your game.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
# Notes for Server maintainer
|
||||||
|
|
||||||
|
In order to set up the server to allow imports, one needs to create a
|
||||||
|
[Github Access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens). A fine-grained access token with only reading rights for public
|
||||||
|
repos will suffice.
|
||||||
|
|
||||||
|
You need to set the environment variables `LEAN4GAME_GITHUB_USER` and `LEAN4GAME_GITHUB_TOKEN`
|
||||||
|
with your user name and access token. For example, you can seet these in `ecosystem.config.cjs` if
|
||||||
|
you're using `pm2`
|
||||||
|
|
||||||
|
Then people can call:
|
||||||
|
|
||||||
|
> https://{website}/import/trigger/{owner}/{repo}
|
||||||
|
|
||||||
|
where you replace:
|
||||||
|
- website: The website your server runs on, e.g. `localhost:3000`
|
||||||
|
- owner, repo: The owner and repository name of the game you want to load from github.
|
||||||
|
|
||||||
|
will trigger to download the latest version of your game from github onto your server.
|
||||||
|
Once this import reports "Done", you should be able to play your game under:
|
||||||
|
|
||||||
|
> https://{website}/#/g/{owner}/{repo}
|
||||||
|
|
||||||
|
## data management
|
||||||
|
Everything downloaded remains in the folder `lean4game/games`.
|
||||||
|
the subfolder `tmp` contains downloaded artifacts and can be deleted without loss.
|
||||||
|
The other folders should only contain the built lean-games, sorted by owner and repo.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# How to update your Game
|
||||||
|
|
||||||
|
## New Lean version
|
||||||
|
|
||||||
|
You can update the game to any Lean version by simply editing the `lean-toolchain` in your game repo to contain the
|
||||||
|
new lean version `leanprover/lean4:v4.X.0`.
|
||||||
|
|
||||||
|
Before you continue, make sure there [exists a `v4.X.0`-tag in this repo](https://github.com/leanprover-community/lean4game/tags).
|
||||||
|
|
||||||
|
Then, depending on the setup you use, do one of the following:
|
||||||
|
|
||||||
|
* Dev Container: Rebuild the VSCode Devcontainer.
|
||||||
|
* Local Setup: in your game's folder run the following:
|
||||||
|
```
|
||||||
|
lake update -R
|
||||||
|
lake build
|
||||||
|
```
|
||||||
|
|
||||||
|
* Additionally, if you have a local copy of the server `lean4game`,
|
||||||
|
you should update this one to the matching version. Run the following in the folder `lean4game/`:
|
||||||
|
```
|
||||||
|
git fetch
|
||||||
|
git checkout {VERSION_TAG}
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
where `{VERSION_TAG}` is the tag from above of the form `v4.X.0`
|
||||||
|
* Gitpod/Codespaces: Create a fresh one
|
||||||
|
|
||||||
|
This will update your game (and the mathlib version you might be using) to the new lean version.
|
||||||
|
|
||||||
|
## Newest developing setup
|
||||||
|
|
||||||
|
There are a few files in your game repository which are used for the developing setup
|
||||||
|
(dev container/codespaces/gitpod). If you need to update your developing setup, for example because it doesn't work
|
||||||
|
anymore, you will need to copy the relevant files from the [GameSkeleton](https://github.com/hhu-adam/GameSkeleton) template into your game repo.
|
||||||
|
|
||||||
|
The relevant files are:
|
||||||
|
|
||||||
|
```
|
||||||
|
.devcontainer/
|
||||||
|
.docker/
|
||||||
|
.github/
|
||||||
|
.gitpod/
|
||||||
|
.vscode/
|
||||||
|
lakefile.lean
|
||||||
|
```
|
||||||
|
|
||||||
|
simply copy them from the `GameSkeleton` into your game and proceed as above,
|
||||||
|
i.e. `lake update -R && lake build`.
|
||||||
|
|
||||||
|
(Note: You should not need to modify any of these files, with the exception of the `lakefile.lean`,
|
||||||
|
where you need to add any dependencies of your game, or remove mathlib if you don't need it.)
|
||||||
@@ -2,10 +2,12 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
apps : [{
|
apps : [{
|
||||||
name : "lean4game",
|
name : "lean4game",
|
||||||
script : "server/index.mjs",
|
script : "relay/index.mjs",
|
||||||
env: {
|
env: {
|
||||||
NODE_ENV: "production",
|
LEAN4GAME_GITHUB_USER: "",
|
||||||
PORT: 8002
|
LEAN4GAME_GITHUB_TOKEN: "",
|
||||||
|
NODE_ENV: "production",
|
||||||
|
PORT: 8002
|
||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_LEAN4GAME_SINGLE: string
|
||||||
|
// more env variables...
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</noscript>
|
</noscript>
|
||||||
<script src="bundle.js"></script>
|
<script type="module" src="/client/src/index.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
Generated
+4434
-2710
File diff suppressed because it is too large
Load Diff
+16
-19
@@ -1,9 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "lean4-game",
|
"name": "lean4-game",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": ".",
|
"homepage": ".",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@babel/plugin-transform-optional-chaining": "^7.23.0",
|
||||||
"@emotion/react": "^11.10.5",
|
"@emotion/react": "^11.10.5",
|
||||||
"@emotion/styled": "^11.10.5",
|
"@emotion/styled": "^11.10.5",
|
||||||
"@fontsource/roboto": "^4.5.8",
|
"@fontsource/roboto": "^4.5.8",
|
||||||
@@ -14,6 +16,7 @@
|
|||||||
"@reduxjs/toolkit": "^1.9.1",
|
"@reduxjs/toolkit": "^1.9.1",
|
||||||
"@types/cytoscape": "^3.19.9",
|
"@types/cytoscape": "^3.19.9",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.4.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cytoscape": "^3.23.0",
|
"cytoscape": "^3.23.0",
|
||||||
"cytoscape-elk": "^2.1.0",
|
"cytoscape-elk": "^2.1.0",
|
||||||
@@ -34,45 +37,39 @@
|
|||||||
"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",
|
"request-progress": "^3.0.0",
|
||||||
|
"vite": "^4.5.0",
|
||||||
|
"vite-plugin-static-copy": "^0.17.0",
|
||||||
|
"vite-plugin-svgr": "^4.1.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"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.19.3",
|
|
||||||
"@babel/core": "^7.20.5",
|
|
||||||
"@babel/preset-env": "^7.20.2",
|
|
||||||
"@babel/preset-react": "^7.18.6",
|
|
||||||
"@babel/preset-typescript": "^7.18.6",
|
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
|
||||||
"@redux-devtools/core": "^3.13.1",
|
"@redux-devtools/core": "^3.13.1",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@types/debounce": "^1.2.1",
|
"@types/debounce": "^1.2.1",
|
||||||
"babel-loader": "^8.3.0",
|
|
||||||
"concurrently": "^7.6.0",
|
"concurrently": "^7.6.0",
|
||||||
"css-loader": "^6.7.3",
|
"css-loader": "^6.7.3",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"nodemon": "^2.0.20",
|
"nodemon": "^3.0.1",
|
||||||
"react-refresh": "^0.14.0",
|
"react-refresh": "^0.14.0",
|
||||||
"style-loader": "^3.3.1",
|
"style-loader": "^3.3.1",
|
||||||
"ts-loader": "^9.4.2",
|
"ts-loader": "^9.4.2",
|
||||||
"typescript": "^4.9.4",
|
"typescript": "^4.9.4",
|
||||||
"url-loader": "^4.1.1",
|
"url-loader": "^4.1.1"
|
||||||
"webpack": "^5.75.0",
|
|
||||||
"webpack-cli": "^4.10.0",
|
|
||||||
"webpack-dev-server": "^4.11.1",
|
|
||||||
"webpack-shell-plugin-next": "^2.3.1"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"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 && cross-env NODE_ENV=development nodemon -e mjs --exec \"node ./index.mjs\"",
|
"start_server": "(cd server && lake build) && (cd relay && cross-env NODE_ENV=development nodemon -e mjs --exec \"node ./index.mjs\")",
|
||||||
"start_client": "cross-env NODE_ENV=development webpack-dev-server --hot",
|
"start_client": "cross-env NODE_ENV=development vite --host",
|
||||||
"build": "cross-env NODE_ENV=production webpack",
|
"build": "npm run build_server && npm run build_client",
|
||||||
"production": "cross-env NODE_ENV=production node server/index.mjs",
|
"preview": "vite preview",
|
||||||
"build_robo": "rm -rf ./Robo && git clone https://github.com/hhu-adam/Robo && docker build ./Robo --file ./Robo/Dockerfile --tag g/hhu-adam/robo && rm -rf ./Robo",
|
"build_server": "cd server && lake build",
|
||||||
"build_nng": "rm -rf ./NNG4 && git clone https://github.com/hhu-adam/NNG4 && docker build ./NNG4 --file ./NNG4/Dockerfile --tag g/hhu-adam/nng4 && rm -rf ./NNG4",
|
"build_client": "cross-env NODE_ENV=production vite build",
|
||||||
"update_lean": "./UPDATE_LEAN.sh"
|
"production": "cross-env NODE_ENV=production node relay/index.mjs"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
|
|||||||
@@ -2,11 +2,15 @@
|
|||||||
|
|
||||||
ELAN_HOME=$(lake env printenv ELAN_HOME)
|
ELAN_HOME=$(lake env printenv ELAN_HOME)
|
||||||
|
|
||||||
|
# $1 : the game directory
|
||||||
|
# $2 : the lean4game folder
|
||||||
|
# $3 : the gameserver executable
|
||||||
|
|
||||||
(exec bwrap\
|
(exec bwrap\
|
||||||
--ro-bind ../../lean4game /lean4game \
|
--bind $2 /lean4game \
|
||||||
--ro-bind ../../$1 /game \
|
--bind $1 /game \
|
||||||
--ro-bind $ELAN_HOME /elan \
|
--bind $ELAN_HOME /elan \
|
||||||
--ro-bind /usr /usr \
|
--bind /usr /usr \
|
||||||
--dev /dev \
|
--dev /dev \
|
||||||
--proc /proc \
|
--proc /proc \
|
||||||
--symlink usr/lib /lib\
|
--symlink usr/lib /lib\
|
||||||
@@ -22,6 +26,6 @@ ELAN_HOME=$(lake env printenv ELAN_HOME)
|
|||||||
--unshare-uts \
|
--unshare-uts \
|
||||||
--unshare-cgroup \
|
--unshare-cgroup \
|
||||||
--die-with-parent \
|
--die-with-parent \
|
||||||
--chdir "/lean4game/server/build/bin/" \
|
--chdir "/game/.lake/packages/GameServer/server/.lake/build/bin/" \
|
||||||
./gameserver --server /game
|
./gameserver --server /game
|
||||||
)
|
)
|
||||||
@@ -11,6 +11,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
const TOKEN = process.env.LEAN4GAME_GITHUB_TOKEN
|
const TOKEN = process.env.LEAN4GAME_GITHUB_TOKEN
|
||||||
|
const USERNAME = process.env.LEAN4GAME_GITHUB_USER
|
||||||
const octokit = new Octokit({
|
const octokit = new Octokit({
|
||||||
auth: TOKEN
|
auth: TOKEN
|
||||||
})
|
})
|
||||||
@@ -41,9 +42,10 @@ async function download(id, url, dest) {
|
|||||||
requestProgress(request({
|
requestProgress(request({
|
||||||
url,
|
url,
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'abentkamp',
|
'accept': 'application/vnd.github+json',
|
||||||
|
'User-Agent': USERNAME,
|
||||||
'X-GitHub-Api-Version': '2022-11-28',
|
'X-GitHub-Api-Version': '2022-11-28',
|
||||||
'Authorization': 'Bearer '+TOKEN
|
'Authorization': 'Bearer ' + TOKEN
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.on('progress', function (state) {
|
.on('progress', function (state) {
|
||||||
@@ -76,35 +78,44 @@ async function doImport (owner, repo, id) {
|
|||||||
.reduce((acc, cur) => acc.created_at < cur.created_at ? cur : acc)
|
.reduce((acc, cur) => acc.created_at < cur.created_at ? cur : acc)
|
||||||
artifactId = artifact.id
|
artifactId = artifact.id
|
||||||
const url = artifact.archive_download_url
|
const url = artifact.archive_download_url
|
||||||
if (!fs.existsSync("tmp")){
|
// Make sure the download folder exists
|
||||||
fs.mkdirSync("tmp");
|
if (!fs.existsSync(path.join(__dirname, "..", "games"))){
|
||||||
|
fs.mkdirSync(path.join(__dirname, "..", "games"));
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(path.join(__dirname, "..", "games", "tmp"))){
|
||||||
|
fs.mkdirSync(path.join(__dirname, "..", "games", "tmp"));
|
||||||
}
|
}
|
||||||
progress[id].output += `Download from ${url}\n`
|
progress[id].output += `Download from ${url}\n`
|
||||||
await download(id, url, `tmp/artifact_${artifactId}.zip`)
|
await download(id, url, path.join(__dirname, "..", "games", "tmp", `${owner.toLowerCase()}_${repo.toLowerCase()}_${artifactId}.zip`))
|
||||||
progress[id].output += `Download finished.\n`
|
progress[id].output += `Download finished.\n`
|
||||||
await runProcess(id, "/bin/bash", [`${__dirname}/unpack.sh`, artifactId],".")
|
|
||||||
let manifest = fs.readFileSync(`tmp/artifact_${artifactId}_inner/manifest.json`);
|
await runProcess(id, "/bin/bash", [path.join(__dirname, "unpack.sh"), artifactId, owner.toLowerCase(), repo.toLowerCase()], path.join(__dirname, ".."))
|
||||||
manifest = JSON.parse(manifest);
|
|
||||||
if (manifest.length !== 1) {
|
|
||||||
throw `Unexpected manifest: ${JSON.stringify(manifest)}`
|
// let manifest = fs.readFileSync(`tmp/artifact_${artifactId}_inner/manifest.json`);
|
||||||
}
|
// manifest = JSON.parse(manifest);
|
||||||
manifest[0].RepoTags = [`g/${owner.toLowerCase()}/${repo.toLowerCase()}:latest`]
|
// if (manifest.length !== 1) {
|
||||||
fs.writeFileSync(`tmp/artifact_${artifactId}_inner/manifest.json`, JSON.stringify(manifest));
|
// throw `Unexpected manifest: ${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`])
|
// manifest[0].RepoTags = [`g/${owner.toLowerCase()}/${repo.toLowerCase()}:latest`]
|
||||||
|
// 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].done = true
|
||||||
progress[id].output += `Done.\n`
|
progress[id].output += `Done!\n`
|
||||||
|
progress[id].output += `Play the game at: {your website}/#/g/${owner}/${repo}\n`
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
progress[id].output += `Error: ${e.toString()}\n${e.stack}`
|
progress[id].output += `Error: ${e.toString()}\n${e.stack}`
|
||||||
} finally {
|
} finally {
|
||||||
|
// clean-up temp. files
|
||||||
if (artifactId) {
|
if (artifactId) {
|
||||||
fs.rmSync(`tmp/artifact_${artifactId}.zip`, {force: true, recursive: true});
|
fs.rmSync(path.join(__dirname, "..", "games", "tmp", `${owner}_${repo}_${artifactId}.zip`), {force: true, recursive: false});
|
||||||
fs.rmSync(`tmp/artifact_${artifactId}`, {force: true, recursive: true});
|
fs.rmSync(path.join(__dirname, "..", "games", "tmp", `${owner}_${repo}_${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
|
progress[id].done = true
|
||||||
}
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10000))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const importTrigger = (req, res) => {
|
export const importTrigger = (req, res) => {
|
||||||
+203
@@ -0,0 +1,203 @@
|
|||||||
|
import { WebSocketServer } from 'ws';
|
||||||
|
import express from 'express'
|
||||||
|
import path from 'path'
|
||||||
|
import * as cp from 'child_process';
|
||||||
|
import * as url from 'url';
|
||||||
|
import * as rpc from 'vscode-ws-jsonrpc';
|
||||||
|
import * as jsonrpcserver from 'vscode-ws-jsonrpc/server';
|
||||||
|
import os from 'os';
|
||||||
|
import fs from 'fs';
|
||||||
|
import anonymize from 'ip-anonymize';
|
||||||
|
import { importTrigger, importStatus } from './import.mjs'
|
||||||
|
// import fs from 'fs'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a game here if the server should keep a queue of pre-loaded games ready at all times.
|
||||||
|
*
|
||||||
|
* IMPORTANT! Tags here need to be lower case!
|
||||||
|
*/
|
||||||
|
const queueLength = {
|
||||||
|
"g/hhu-adam/robo": 2,
|
||||||
|
"g/hhu-adam/nng4": 5,
|
||||||
|
"g/djvelleman/stg4": 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
const __filename = url.fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
|
||||||
|
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
|
||||||
|
.use(express.static(path.join(__dirname, '..', 'client', 'dist'))) // TODO: add a dist folder from inside the game
|
||||||
|
.use('/data/g/:owner/:repo/*', (req, res, next) => {
|
||||||
|
const owner = req.params.owner;
|
||||||
|
const repo = req.params.repo
|
||||||
|
const filename = req.params[0];
|
||||||
|
req.url = filename;
|
||||||
|
express.static(path.join(getGameDir(owner,repo),".lake","gamedata"))(req, res, next);
|
||||||
|
})
|
||||||
|
.use('/', router)
|
||||||
|
.listen(PORT, () => console.log(`Listening on ${PORT}`));
|
||||||
|
|
||||||
|
const wss = new WebSocketServer({ server })
|
||||||
|
|
||||||
|
var socketCounter = 0
|
||||||
|
|
||||||
|
const environment = process.env.NODE_ENV
|
||||||
|
const isDevelopment = environment === 'development'
|
||||||
|
|
||||||
|
/** We keep queues of started Lean Server processes to be ready when a user arrives */
|
||||||
|
const queue = {}
|
||||||
|
|
||||||
|
function getTag(owner, repo) {
|
||||||
|
return `g/${owner.toLowerCase()}/${repo.toLowerCase()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGameDir(owner, repo) {
|
||||||
|
owner = owner.toLowerCase()
|
||||||
|
if (owner == 'local') {
|
||||||
|
if(!isDevelopment) {
|
||||||
|
console.error(`No local games in production mode.`)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(!fs.existsSync(path.join(__dirname, '..', 'games'))) {
|
||||||
|
console.error(`Did not find the following folder: ${path.join(__dirname, '..', 'games')}`)
|
||||||
|
console.error('Did you already import any games?')
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let game_dir = (owner == 'local') ?
|
||||||
|
path.join(__dirname, '..', '..', repo) : // note: here we need `repo` to be case sensitive
|
||||||
|
path.join(__dirname, '..', 'games', `${owner}`, `${repo.toLowerCase()}`)
|
||||||
|
|
||||||
|
if(!fs.existsSync(game_dir)) {
|
||||||
|
console.error(`Game '${game_dir}' does not exist!`)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return game_dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startServerProcess(owner, repo) {
|
||||||
|
|
||||||
|
let game_dir = getGameDir(owner, repo)
|
||||||
|
if (!game_dir) return;
|
||||||
|
|
||||||
|
let serverProcess
|
||||||
|
if (isDevelopment) {
|
||||||
|
let args = ["--server", game_dir]
|
||||||
|
let binDir = path.join(game_dir, ".lake", "packages", "GameServer", "server", ".lake", "build", "bin")
|
||||||
|
// Note: `cwd` is important to be the `bin` directory as `Watchdog` calls `./gameserver` again
|
||||||
|
if (fs.existsSync(binDir)) {
|
||||||
|
// Try to use the game's own copy of `gameserver`.
|
||||||
|
serverProcess = cp.spawn("./gameserver", args, { cwd: binDir })
|
||||||
|
} else {
|
||||||
|
// If the game is built with `-Klean4game.local` there is no copy in the lake packages.
|
||||||
|
serverProcess = cp.spawn("./gameserver", args,
|
||||||
|
{ cwd: path.join(__dirname, "..", "server", ".lake", "build", "bin") })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
serverProcess = cp.spawn("./bubblewrap.sh",
|
||||||
|
[ game_dir, path.join(__dirname, '..')],
|
||||||
|
{ cwd: __dirname })
|
||||||
|
}
|
||||||
|
|
||||||
|
serverProcess.on('error', error =>
|
||||||
|
console.error(`Launching Lean Server failed: ${error}`)
|
||||||
|
)
|
||||||
|
if (serverProcess.stderr !== null) {
|
||||||
|
serverProcess.stderr.on('data', data =>
|
||||||
|
console.error(`Lean Server: ${data}`)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return serverProcess
|
||||||
|
}
|
||||||
|
|
||||||
|
/** start Lean Server processes to refill the queue */
|
||||||
|
function fillQueue(tag) {
|
||||||
|
while (queue[tag].length < queueLength[tag]) {
|
||||||
|
let serverProcess
|
||||||
|
serverProcess = startServerProcess(tag)
|
||||||
|
if (serverProcess == null) {
|
||||||
|
console.error('serverProcess was undefined/null')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
queue[tag].push(serverProcess)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// // TODO: We disabled queue for now
|
||||||
|
// if (!isDevelopment) { // Don't use queue in development
|
||||||
|
// for (let tag in queueLength) {
|
||||||
|
// queue[tag] = []
|
||||||
|
// fillQueue(tag)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
const urlRegEx = /^\/websocket\/g\/([\w.-]+)\/([\w.-]+)$/
|
||||||
|
|
||||||
|
wss.addListener("connection", function(ws, req) {
|
||||||
|
const reRes = urlRegEx.exec(req.url)
|
||||||
|
if (!reRes) { console.error(`Connection refused because of invalid URL: ${req.url}`); return; }
|
||||||
|
const owner = reRes[1]
|
||||||
|
const repo = reRes[2]
|
||||||
|
|
||||||
|
const tag = getTag(owner, repo)
|
||||||
|
|
||||||
|
let ps
|
||||||
|
if (!queue[tag] || queue[tag].length == 0) {
|
||||||
|
ps = startServerProcess(owner, repo)
|
||||||
|
} else {
|
||||||
|
console.info('Got process from the queue')
|
||||||
|
ps = queue[tag].shift() // Pick the first Lean process; it's likely to be ready immediately
|
||||||
|
fillQueue(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps == null) {
|
||||||
|
console.error('server process is undefined/null')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
socketCounter += 1;
|
||||||
|
const ip = anonymize(req.headers['x-forwarded-for'] || req.socket.remoteAddress)
|
||||||
|
console.log(`[${new Date()}] Socket opened - ${ip}`)
|
||||||
|
|
||||||
|
const socket = {
|
||||||
|
onMessage: (cb) => { ws.on("message", cb) },
|
||||||
|
onError: (cb) => { ws.on("error", cb) },
|
||||||
|
onClose: (cb) => { ws.on("close", cb) },
|
||||||
|
send: (data, cb) => { ws.send(data,cb) }
|
||||||
|
}
|
||||||
|
const reader = new rpc.WebSocketMessageReader(socket)
|
||||||
|
const writer = new rpc.WebSocketMessageWriter(socket)
|
||||||
|
const socketConnection = jsonrpcserver.createConnection(reader, writer, () => ws.close())
|
||||||
|
const serverConnection = jsonrpcserver.createProcessStreamConnection(ps)
|
||||||
|
socketConnection.forward(serverConnection, message => {
|
||||||
|
if (isDevelopment) {console.log(`CLIENT: ${JSON.stringify(message)}`)}
|
||||||
|
return message;
|
||||||
|
})
|
||||||
|
serverConnection.forward(socketConnection, message => {
|
||||||
|
if (isDevelopment) {console.log(`SERVER: ${JSON.stringify(message)}`)}
|
||||||
|
return message;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[${new Date()}] Number of open sockets - ${socketCounter}`)
|
||||||
|
console.log(`[${new Date()}] Free RAM - ${Math.round(os.freemem() / 1024 / 1024)} / ${Math.round(os.totalmem() / 1024 / 1024)} MB`)
|
||||||
|
|
||||||
|
ws.on('close', () => {
|
||||||
|
console.log(`[${new Date()}] Socket closed - ${ip}`)
|
||||||
|
socketCounter -= 1
|
||||||
|
})
|
||||||
|
|
||||||
|
socketConnection.onClose(() => serverConnection.dispose())
|
||||||
|
serverConnection.onClose(() => socketConnection.dispose())
|
||||||
|
})
|
||||||
Executable
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#/bin/bash
|
||||||
|
|
||||||
|
ARTIFACT_ID=$1
|
||||||
|
OWNER=$2
|
||||||
|
REPO=$3
|
||||||
|
|
||||||
|
# mkdir -p games
|
||||||
|
cd games
|
||||||
|
|
||||||
|
# mkdir -p tmp
|
||||||
|
mkdir -p ${OWNER}
|
||||||
|
|
||||||
|
echo "Unpacking ZIP."
|
||||||
|
unzip -o tmp/${OWNER}_${REPO}_${ARTIFACT_ID}.zip -d tmp/${OWNER}_${REPO}_${ARTIFACT_ID}
|
||||||
|
echo "Unpacking game."
|
||||||
|
|
||||||
|
# exit the npm project to avoid reloading. TODO: Where should we actually save these?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
echo "Delete old version of the game"
|
||||||
|
rm -rf ${OWNER}/${REPO}
|
||||||
|
mkdir -p ${OWNER}/${REPO}
|
||||||
|
|
||||||
|
for f in tmp/${OWNER}_${REPO}_${ARTIFACT_ID}/* #Should only be one file
|
||||||
|
do
|
||||||
|
echo "Unpacking $f"
|
||||||
|
#tar -xvzf $f -C games/${OWNER}/${REPO}
|
||||||
|
unzip -q -o $f -d ${OWNER}/${REPO}
|
||||||
|
done
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
build
|
|
||||||
adam
|
|
||||||
nng
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import Lean
|
import Lean
|
||||||
|
import GameServer.Utils
|
||||||
|
|
||||||
section AbstractCtx
|
section AbstractCtx
|
||||||
open Lean
|
open Lean
|
||||||
|
|||||||
+250
-107
@@ -10,6 +10,14 @@ register_option lean4game.showDependencyReasons : Bool := {
|
|||||||
descr := "show reasons for calculated world dependencies."
|
descr := "show reasons for calculated world dependencies."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/-- Let `MakeGame` print the reasons why the worlds depend on each other.
|
||||||
|
|
||||||
|
Note: currently unused in favour of setting `set_option trace.debug true`. -/
|
||||||
|
register_option lean4game.verbose : Bool := {
|
||||||
|
defValue := false
|
||||||
|
descr := "display more info messages to help developing the game."
|
||||||
|
}
|
||||||
|
|
||||||
/-! # Game metadata -/
|
/-! # Game metadata -/
|
||||||
|
|
||||||
/-- Switch to the specified `Game` (and create it if non-existent). Example: `Game "NNG"` -/
|
/-- Switch to the specified `Game` (and create it if non-existent). Example: `Game "NNG"` -/
|
||||||
@@ -38,7 +46,9 @@ elab "Title" t:str : command => do
|
|||||||
match ← getCurLayer with
|
match ← getCurLayer with
|
||||||
| .Level => modifyCurLevel fun level => pure {level with title := t.getString}
|
| .Level => modifyCurLevel fun level => pure {level with title := t.getString}
|
||||||
| .World => modifyCurWorld fun world => pure {world with title := t.getString}
|
| .World => modifyCurWorld fun world => pure {world with title := t.getString}
|
||||||
| .Game => modifyCurGame fun game => pure {game with title := t.getString}
|
| .Game => modifyCurGame fun game => pure {game with
|
||||||
|
title := t.getString
|
||||||
|
tile := {game.tile with title := t.getString}}
|
||||||
|
|
||||||
/-- Define the introduction of the current game/world/level. -/
|
/-- Define the introduction of the current game/world/level. -/
|
||||||
elab "Introduction" t:str : command => do
|
elab "Introduction" t:str : command => do
|
||||||
@@ -50,10 +60,32 @@ elab "Introduction" t:str : command => do
|
|||||||
/-- Define the info of the current game. Used for e.g. credits -/
|
/-- Define the info of the current game. Used for e.g. credits -/
|
||||||
elab "Info" t:str : command => do
|
elab "Info" t:str : command => do
|
||||||
match ← getCurLayer with
|
match ← getCurLayer with
|
||||||
| .Level => pure ()
|
| .Level =>
|
||||||
| .World => pure ()
|
logError "Can't use `Info` in a level!"
|
||||||
|
pure ()
|
||||||
|
| .World =>
|
||||||
|
logError "Can't use `Info` in a world"
|
||||||
|
pure ()
|
||||||
| .Game => modifyCurGame fun game => pure {game with info := t.getString}
|
| .Game => modifyCurGame fun game => pure {game with info := t.getString}
|
||||||
|
|
||||||
|
/-- Provide the location of the image for the current game/world/level.
|
||||||
|
Paths are relative to the lean project's root. -/
|
||||||
|
elab "Image" t:str : command => do
|
||||||
|
let file := t.getString
|
||||||
|
if not <| ← System.FilePath.pathExists file then
|
||||||
|
logWarningAt t s!"Make sure the cover image '{file}' exists."
|
||||||
|
if not <| file.startsWith "images/" then
|
||||||
|
logWarningAt t s!"The file name should start with `images/`. Make sure all images are in that folder."
|
||||||
|
|
||||||
|
match ← getCurLayer with
|
||||||
|
| .Level =>
|
||||||
|
logWarning "Level-images not implemented yet" -- TODO
|
||||||
|
modifyCurLevel fun level => pure {level with image := file}
|
||||||
|
| .World =>
|
||||||
|
modifyCurWorld fun world => pure {world with image := file}
|
||||||
|
| .Game =>
|
||||||
|
logWarning "Main image of the game not implemented yet" -- TODO
|
||||||
|
modifyCurGame fun game => pure {game with image := file}
|
||||||
|
|
||||||
/-- Define the conclusion of the current game or current level if some
|
/-- Define the conclusion of the current game or current level if some
|
||||||
building a level. -/
|
building a level. -/
|
||||||
@@ -63,6 +95,38 @@ elab "Conclusion" t:str : command => do
|
|||||||
| .World => modifyCurWorld fun world => pure {world with conclusion := t.getString}
|
| .World => modifyCurWorld fun world => pure {world with conclusion := t.getString}
|
||||||
| .Game => modifyCurGame fun game => pure {game with conclusion := t.getString}
|
| .Game => modifyCurGame fun game => pure {game with conclusion := t.getString}
|
||||||
|
|
||||||
|
/-- A list of games that should be played before this one. Example `Prerequisites "NNG" "STG"`. -/
|
||||||
|
elab "Prerequisites" t:str* : command => do
|
||||||
|
modifyCurGame fun game => pure {game with
|
||||||
|
tile := {game.tile with prerequisites := t.map (·.getString) |>.toList}}
|
||||||
|
|
||||||
|
/-- Short caption for the game (1 sentence) -/
|
||||||
|
elab "CaptionShort" t:str : command => do
|
||||||
|
modifyCurGame fun game => pure {game with
|
||||||
|
tile := {game.tile with short := t.getString}}
|
||||||
|
|
||||||
|
/-- More detailed description what the game is about (2-4 sentences). -/
|
||||||
|
elab "CaptionLong" t:str : command => do
|
||||||
|
modifyCurGame fun game => pure {game with
|
||||||
|
tile := {game.tile with long := t.getString}}
|
||||||
|
|
||||||
|
/-- A list of Languages the game is translated to. For example `Languages "German" "English"`.
|
||||||
|
NOTE: For the time being, only a single language is supported.
|
||||||
|
-/
|
||||||
|
elab "Languages" t:str* : command => do
|
||||||
|
modifyCurGame fun game => pure {game with
|
||||||
|
tile := {game.tile with languages := t.map (·.getString) |>.toList}}
|
||||||
|
|
||||||
|
/-- The Image of the game (optional). TODO: Not impementeds -/
|
||||||
|
elab "CoverImage" t:str : command => do
|
||||||
|
let file := t.getString
|
||||||
|
if not <| ← System.FilePath.pathExists file then
|
||||||
|
logWarningAt t s!"Make sure the cover image '{file}' exists."
|
||||||
|
if not <| file.startsWith "images/" then
|
||||||
|
logWarningAt t s!"The file name should start with `images/`. Make sure all images are in that folder."
|
||||||
|
|
||||||
|
modifyCurGame fun game => pure {game with
|
||||||
|
tile := {game.tile with image := file}}
|
||||||
|
|
||||||
/-! # Inventory
|
/-! # Inventory
|
||||||
|
|
||||||
@@ -72,6 +136,64 @@ in the first level and get enabled during the game.
|
|||||||
|
|
||||||
/-! ## Doc entries -/
|
/-! ## Doc entries -/
|
||||||
|
|
||||||
|
/-- Copied from `Mathlib.Tactic.HelpCmd`.
|
||||||
|
|
||||||
|
Gets the initial string token in a parser description. For example, for a declaration like
|
||||||
|
`syntax "bla" "baz" term : tactic`, it returns `some "bla"`. Returns `none` for syntax declarations
|
||||||
|
that don't start with a string constant. -/
|
||||||
|
partial def getHeadTk (e : Expr) : Option String :=
|
||||||
|
match (Expr.withApp e λ e a => (e.constName?.getD Name.anonymous, a)) with
|
||||||
|
| (``ParserDescr.node, #[_, _, p]) => getHeadTk p
|
||||||
|
| (``ParserDescr.unary, #[.app _ (.lit (.strVal "withPosition")), p]) => getHeadTk p
|
||||||
|
| (``ParserDescr.unary, #[.app _ (.lit (.strVal "atomic")), p]) => getHeadTk p
|
||||||
|
| (``ParserDescr.binary, #[.app _ (.lit (.strVal "andthen")), p, _]) => getHeadTk p
|
||||||
|
| (``ParserDescr.nonReservedSymbol, #[.lit (.strVal tk), _]) => some tk
|
||||||
|
| (``ParserDescr.symbol, #[.lit (.strVal tk)]) => some tk
|
||||||
|
| (``Parser.withAntiquot, #[_, p]) => getHeadTk p
|
||||||
|
| (``Parser.leadingNode, #[_, _, p]) => getHeadTk p
|
||||||
|
| (``HAndThen.hAndThen, #[_, _, _, _, p, _]) => getHeadTk p
|
||||||
|
| (``Parser.nonReservedSymbol, #[.lit (.strVal tk), _]) => some tk
|
||||||
|
| (``Parser.symbol, #[.lit (.strVal tk)]) => some tk
|
||||||
|
| _ => none
|
||||||
|
|
||||||
|
/-- Modified from `#help` in `Mathlib.Tactic.HelpCmd` -/
|
||||||
|
def getTacticDocstring (env : Environment) (name: Name) : CommandElabM (Option String) := do
|
||||||
|
let name := name.toString (escape := false)
|
||||||
|
let mut decls : Lean.RBMap String (Array SyntaxNodeKind) compare := {}
|
||||||
|
|
||||||
|
let catName : Name := `tactic
|
||||||
|
let catStx : Ident := mkIdent catName -- TODO
|
||||||
|
let some cat := (Parser.parserExtension.getState env).categories.find? catName
|
||||||
|
| throwErrorAt catStx "{catStx} is not a syntax category"
|
||||||
|
liftTermElabM <| Term.addCategoryInfo catStx catName
|
||||||
|
for (k, _) in cat.kinds do
|
||||||
|
let mut used := false
|
||||||
|
if let some tk := do getHeadTk (← (← env.find? k).value?) then
|
||||||
|
let tk := tk.trim
|
||||||
|
if name ≠ tk then -- was `!name.isPrefixOf tk`
|
||||||
|
continue
|
||||||
|
used := true
|
||||||
|
decls := decls.insert tk ((decls.findD tk #[]).push k)
|
||||||
|
for (_name, ks) in decls do
|
||||||
|
for k in ks do
|
||||||
|
if let some doc ← findDocString? env k then
|
||||||
|
return doc
|
||||||
|
|
||||||
|
logWarning <| m!"Could not find a docstring for tactic {name}, consider adding one " ++
|
||||||
|
m!"using `TacticDoc {name} \"some doc\"`"
|
||||||
|
return none
|
||||||
|
|
||||||
|
/-- Retrieve the docstring associated to an inventory item. For Tactics, this
|
||||||
|
is not guaranteed to work. -/
|
||||||
|
def getDocstring (env : Environment) (name : Name) (type : InventoryType) :
|
||||||
|
CommandElabM (Option String) :=
|
||||||
|
match type with
|
||||||
|
-- for tactics it's a lookup following mathlib's `#help`. not guaranteed to be the correct one.
|
||||||
|
| .Tactic => getTacticDocstring env name
|
||||||
|
| .Lemma => findDocString? env name
|
||||||
|
-- TODO: for definitions not implemented yet, does it work?
|
||||||
|
| .Definition => findDocString? env name
|
||||||
|
|
||||||
/-- Checks if `inventoryTemplateExt` contains an entry with `(type, name)` and yields
|
/-- Checks if `inventoryTemplateExt` contains an entry with `(type, name)` and yields
|
||||||
a warning otherwise. If `template` is provided, it will add such an entry instead of yielding a
|
a warning otherwise. If `template` is provided, it will add such an entry instead of yielding a
|
||||||
warning.
|
warning.
|
||||||
@@ -94,13 +216,22 @@ def checkInventoryDoc (type : InventoryType) (ref : Ident) (name : Name := ref.g
|
|||||||
match template with
|
match template with
|
||||||
-- Warn about missing documentation
|
-- Warn about missing documentation
|
||||||
| none =>
|
| none =>
|
||||||
|
let docstring ← match (← getDocstring env name type) with
|
||||||
|
| some ds =>
|
||||||
|
logInfoAt ref (m!"Missing {type} Documentation. Using existing docstring. " ++
|
||||||
|
m!"Add {name}\nAdd `{type}Doc {name}` somewhere above this statement.")
|
||||||
|
pure s!"*(lean docstring)*\\\n{ds}"
|
||||||
|
| none =>
|
||||||
|
logWarningAt ref (m!"Missing {type} Documentation: {name}\nAdd `{type}Doc {name}` " ++
|
||||||
|
m!"somewhere above this statement.")
|
||||||
|
pure "(missing)"
|
||||||
|
|
||||||
-- We just add a dummy entry
|
-- We just add a dummy entry
|
||||||
modifyEnv (inventoryTemplateExt.addEntry · {
|
modifyEnv (inventoryTemplateExt.addEntry · {
|
||||||
type := type
|
type := type
|
||||||
name := name
|
name := name
|
||||||
category := if type == .Lemma then s!"{n.getPrefix}" else "" })
|
category := if type == .Lemma then s!"{n.getPrefix}" else ""
|
||||||
logWarningAt ref (m!"Missing {type} Documentation: {name}\nAdd `{type}Doc {name}` " ++
|
content := docstring})
|
||||||
m!"somewhere above this statement.")
|
|
||||||
-- Add the default documentation
|
-- Add the default documentation
|
||||||
| some s =>
|
| some s =>
|
||||||
modifyEnv (inventoryTemplateExt.addEntry · {
|
modifyEnv (inventoryTemplateExt.addEntry · {
|
||||||
@@ -109,8 +240,8 @@ def checkInventoryDoc (type : InventoryType) (ref : Ident) (name : Name := ref.g
|
|||||||
category := if type == .Lemma then s!"{n.getPrefix}" else ""
|
category := if type == .Lemma then s!"{n.getPrefix}" else ""
|
||||||
content := s })
|
content := s })
|
||||||
logInfoAt ref (m!"Missing {type} Documentation: {name}, used default (e.g. provided " ++
|
logInfoAt ref (m!"Missing {type} Documentation: {name}, used default (e.g. provided " ++
|
||||||
m!"docstring) instead. If you want to write a different description, add " ++
|
m!"docstring) instead. If you want to write a different description, add " ++
|
||||||
m!"`{type}Doc {name}` somewhere above this statement.")
|
m!"`{type}Doc {name}` somewhere above this statement.")
|
||||||
|
|
||||||
/-- Documentation entry of a tactic. Example:
|
/-- Documentation entry of a tactic. Example:
|
||||||
|
|
||||||
@@ -140,7 +271,7 @@ LemmaDoc Nat.succ_pos as "succ_pos" in "Nat" "says `0 < n.succ`, etc."
|
|||||||
* The identifier after `in` is the category to group lemmas by (in the Inventory).
|
* The identifier after `in` is the category to group lemmas by (in the Inventory).
|
||||||
* The description is a string supporting Markdown.
|
* The description is a string supporting Markdown.
|
||||||
|
|
||||||
Use `[[mathlib_doc]]` in the string to insert a link to the mathlib doc page. This requries
|
Use `[[mathlib_doc]]` in the string to insert a link to the mathlib doc page. This requires
|
||||||
The lemma/definition to have the same fully qualified name as in mathlib.
|
The lemma/definition to have the same fully qualified name as in mathlib.
|
||||||
-/
|
-/
|
||||||
elab "LemmaDoc" name:ident "as" displayName:str "in" category:str content:str : command =>
|
elab "LemmaDoc" name:ident "as" displayName:str "in" category:str content:str : command =>
|
||||||
@@ -168,7 +299,7 @@ DefinitionDoc Function.Bijective as "Bijective" "defined as `Injective f ∧ Sur
|
|||||||
* The string following `as` is the displayed name (in the Inventory).
|
* The string following `as` is the displayed name (in the Inventory).
|
||||||
* The description is a string supporting Markdown.
|
* The description is a string supporting Markdown.
|
||||||
|
|
||||||
Use `[[mathlib_doc]]` in the string to insert a link to the mathlib doc page. This requries
|
Use `[[mathlib_doc]]` in the string to insert a link to the mathlib doc page. This requires
|
||||||
The lemma/definition to have the same fully qualified name as in mathlib.
|
The lemma/definition to have the same fully qualified name as in mathlib.
|
||||||
-/
|
-/
|
||||||
elab "DefinitionDoc" name:ident "as" displayName:str template:str : command =>
|
elab "DefinitionDoc" name:ident "as" displayName:str template:str : command =>
|
||||||
@@ -180,31 +311,17 @@ elab "DefinitionDoc" name:ident "as" displayName:str template:str : command =>
|
|||||||
|
|
||||||
/-! ## Add inventory items -/
|
/-! ## Add inventory items -/
|
||||||
|
|
||||||
-- namespace Lean.PrettyPrinter
|
|
||||||
-- def ppSignature' (c : Name) : MetaM String := do
|
|
||||||
-- let decl ← getConstInfo c
|
|
||||||
-- let e := .const c (decl.levelParams.map mkLevelParam)
|
|
||||||
-- let (stx, _) ← delabCore e (delab := Delaborator.delabConstWithSignature)
|
|
||||||
-- let f ← ppTerm stx
|
|
||||||
-- return toString f
|
|
||||||
-- end Lean.PrettyPrinter
|
|
||||||
|
|
||||||
def getStatement (name : Name) : CommandElabM MessageData := do
|
def getStatement (name : Name) : CommandElabM MessageData := do
|
||||||
-- let c := name.getId
|
|
||||||
|
|
||||||
let decl ← getConstInfo name
|
|
||||||
-- -- TODO: How to go between CommandElabM and MetaM
|
|
||||||
|
|
||||||
-- addCompletionInfo <| .id name c (danglingDot := false) {} none
|
|
||||||
return ← addMessageContextPartial (.ofPPFormat { pp := fun
|
return ← addMessageContextPartial (.ofPPFormat { pp := fun
|
||||||
| some ctx => ctx.runMetaM <| ppExpr decl.type
|
| some ctx => ctx.runMetaM <| PrettyPrinter.ppSignature name
|
||||||
-- PrettyPrinter.ppSignature' c
|
|
||||||
-- PrettyPrinter.ppSignature c
|
|
||||||
| none => return "that's a bug." })
|
| none => return "that's a bug." })
|
||||||
|
|
||||||
-- Note: We use `String` because we can't send `MessageData` as json, but
|
-- Note: We use `String` because we can't send `MessageData` as json, but
|
||||||
-- `MessageData` might be better for interactive highlighting.
|
-- `MessageData` might be better for interactive highlighting.
|
||||||
/-- Get a string of the form `my_lemma (n : ℕ) : n + n = 2 * n`. -/
|
/-- Get a string of the form `my_lemma (n : ℕ) : n + n = 2 * n`.
|
||||||
|
|
||||||
|
Note: A statement like `theorem abc : ∀ x : Nat, x ≥ 0` would be turned into
|
||||||
|
`theorem abc (x : Nat) : x ≥ 0` by `PrettyPrinter.ppSignature`. -/
|
||||||
def getStatementString (name : Name) : CommandElabM String := do
|
def getStatementString (name : Name) : CommandElabM String := do
|
||||||
try
|
try
|
||||||
return ← (← getStatement name).toString
|
return ← (← getStatement name).toString
|
||||||
@@ -214,12 +331,15 @@ def getStatementString (name : Name) : CommandElabM String := do
|
|||||||
|
|
||||||
/-- Declare tactics that are introduced by this level. -/
|
/-- Declare tactics that are introduced by this level. -/
|
||||||
elab "NewTactic" args:ident* : command => do
|
elab "NewTactic" args:ident* : command => do
|
||||||
for name in ↑args do checkInventoryDoc .Tactic name -- TODO: Add (template := "[docstring]")
|
for name in ↑args do
|
||||||
|
checkInventoryDoc .Tactic name -- TODO: Add (template := "[docstring]")
|
||||||
modifyCurLevel fun level => pure {level with
|
modifyCurLevel fun level => pure {level with
|
||||||
tactics := {level.tactics with new := level.tactics.new ++ args.map (·.getId)}}
|
tactics := {level.tactics with new := level.tactics.new ++ args.map (·.getId)}}
|
||||||
|
|
||||||
/-- Declare tactics that are introduced by this level. -/
|
/-- Declare tactics that are introduced by this level but do not show up in inventory. -/
|
||||||
elab "NewHiddenTactic" args:ident* : command => do
|
elab "NewHiddenTactic" args:ident* : command => do
|
||||||
|
for name in ↑args do
|
||||||
|
checkInventoryDoc .Tactic name (template := "")
|
||||||
modifyCurLevel fun level => pure {level with
|
modifyCurLevel fun level => pure {level with
|
||||||
tactics := {level.tactics with new := level.tactics.new ++ args.map (·.getId),
|
tactics := {level.tactics with new := level.tactics.new ++ args.map (·.getId),
|
||||||
hidden := level.tactics.hidden ++ args.map (·.getId)}}
|
hidden := level.tactics.hidden ++ args.map (·.getId)}}
|
||||||
@@ -227,6 +347,8 @@ elab "NewHiddenTactic" args:ident* : command => do
|
|||||||
/-- Declare lemmas that are introduced by this level. -/
|
/-- Declare lemmas that are introduced by this level. -/
|
||||||
elab "NewLemma" args:ident* : command => do
|
elab "NewLemma" args:ident* : command => do
|
||||||
for name in ↑args do
|
for name in ↑args do
|
||||||
|
try let _decl ← getConstInfo name.getId catch
|
||||||
|
| _ => logErrorAt name m!"unknown identifier '{name}'."
|
||||||
checkInventoryDoc .Lemma name -- TODO: Add (template := "[mathlib]")
|
checkInventoryDoc .Lemma name -- TODO: Add (template := "[mathlib]")
|
||||||
modifyCurLevel fun level => pure {level with
|
modifyCurLevel fun level => pure {level with
|
||||||
lemmas := {level.lemmas with new := args.map (·.getId)}}
|
lemmas := {level.lemmas with new := args.map (·.getId)}}
|
||||||
@@ -409,13 +531,11 @@ elab doc:docComment ? attrs:Parser.Term.attributes ?
|
|||||||
-- the namespace?
|
-- the namespace?
|
||||||
let currNamespace ← getCurrNamespace
|
let currNamespace ← getCurrNamespace
|
||||||
|
|
||||||
let st ← match statementName with
|
-- Format theorem statement for displaying
|
||||||
| some name => getStatementString <| currNamespace ++ name.getId
|
let sigString := sig.raw.reprint.getD ""
|
||||||
| none => getStatementString <| currNamespace ++ (defaultDeclName).getId
|
let descrFormat : String := match statementName with
|
||||||
|
| some name => s!"theorem {name.getId} {sigString} := by"
|
||||||
let head := match statementName with
|
| none => s!"example {sigString} := by"
|
||||||
| some name => Format.join ["theorem ", (currNamespace ++ name.getId).toString]
|
|
||||||
| none => "example"
|
|
||||||
|
|
||||||
modifyCurLevel fun level => pure { level with
|
modifyCurLevel fun level => pure { level with
|
||||||
module := env.header.mainModule
|
module := env.header.mainModule
|
||||||
@@ -425,7 +545,7 @@ elab doc:docComment ? attrs:Parser.Term.attributes ?
|
|||||||
statementName := match statementName with
|
statementName := match statementName with
|
||||||
| none => default
|
| none => default
|
||||||
| some name => currNamespace ++ name.getId
|
| some name => currNamespace ++ name.getId
|
||||||
descrFormat := (Format.join [head, " ", st, " := by"]).pretty 10
|
descrFormat := descrFormat
|
||||||
hints := hints
|
hints := hints
|
||||||
tactics := {level.tactics with used := usedInventory.tactics.toArray}
|
tactics := {level.tactics with used := usedInventory.tactics.toArray}
|
||||||
definitions := {level.definitions with used := usedInventory.definitions.toArray}
|
definitions := {level.definitions with used := usedInventory.definitions.toArray}
|
||||||
@@ -457,7 +577,7 @@ elab (name := GameServer.Tactic.Hint) "Hint" args:hintArg* msg:interpolatedStr(t
|
|||||||
let mut strict := false
|
let mut strict := false
|
||||||
let mut hidden := false
|
let mut hidden := false
|
||||||
|
|
||||||
-- remove spaces at the beginngng of new lines
|
-- remove spaces at the beginning of new lines
|
||||||
let msg := TSyntax.mk $ msg.raw.setArgs $ ← msg.raw.getArgs.mapM fun m => do
|
let msg := TSyntax.mk $ msg.raw.setArgs $ ← msg.raw.getArgs.mapM fun m => do
|
||||||
match m with
|
match m with
|
||||||
| Syntax.node info k args =>
|
| Syntax.node info k args =>
|
||||||
@@ -511,9 +631,9 @@ elab (name := GameServer.Tactic.Branch) "Branch" t:tacticSeq : tactic => do
|
|||||||
-- Show an info whether the branch proofs all remaining goals.
|
-- Show an info whether the branch proofs all remaining goals.
|
||||||
let gs ← Tactic.getUnsolvedGoals
|
let gs ← Tactic.getUnsolvedGoals
|
||||||
if gs.isEmpty then
|
if gs.isEmpty then
|
||||||
logInfo "This branch finishes the proof."
|
trace[debug] "This branch finishes the proof."
|
||||||
else
|
else
|
||||||
logInfo "This branch leaves open goals."
|
trace[debug] "This branch leaves open goals."
|
||||||
|
|
||||||
let msgs ← Core.getMessageLog
|
let msgs ← Core.getMessageLog
|
||||||
b.restore
|
b.restore
|
||||||
@@ -525,7 +645,7 @@ elab (name := GameServer.Tactic.Hole) "Hole" t:tacticSeq : tactic => do
|
|||||||
|
|
||||||
/--
|
/--
|
||||||
Iterate recursively through the Syntax, replace `Hole` with `sorry` and remove all
|
Iterate recursively through the Syntax, replace `Hole` with `sorry` and remove all
|
||||||
`Hint`/`Branch` occurences.
|
`Hint`/`Branch` occurrences.
|
||||||
-/
|
-/
|
||||||
def replaceHoles (tacs : Syntax) : Syntax :=
|
def replaceHoles (tacs : Syntax) : Syntax :=
|
||||||
match tacs with
|
match tacs with
|
||||||
@@ -539,7 +659,7 @@ where filterArgs (args : List Syntax) : List Syntax :=
|
|||||||
-- replace `Hole` with `sorry`.
|
-- replace `Hole` with `sorry`.
|
||||||
| Syntax.node info `GameServer.Tactic.Hole _ :: r =>
|
| Syntax.node info `GameServer.Tactic.Hole _ :: r =>
|
||||||
Syntax.node info `Lean.Parser.Tactic.tacticSorry #[Syntax.atom info "sorry"] :: filterArgs r
|
Syntax.node info `Lean.Parser.Tactic.tacticSorry #[Syntax.atom info "sorry"] :: filterArgs r
|
||||||
-- delete all `Hint` and `Branch` occurences in the middle.
|
-- delete all `Hint` and `Branch` occurrences in the middle.
|
||||||
| Syntax.node _ `GameServer.Tactic.Hint _ :: _ :: r
|
| Syntax.node _ `GameServer.Tactic.Hint _ :: _ :: r
|
||||||
| Syntax.node _ `GameServer.Tactic.Branch _ :: _ :: r =>
|
| Syntax.node _ `GameServer.Tactic.Branch _ :: _ :: r =>
|
||||||
filterArgs r
|
filterArgs r
|
||||||
@@ -558,10 +678,31 @@ elab "Template" tacs:tacticSeq : tactic => do
|
|||||||
Tactic.evalTactic tacs
|
Tactic.evalTactic tacs
|
||||||
let newTacs : TSyntax `Lean.Parser.Tactic.tacticSeq := ⟨replaceHoles tacs.raw⟩
|
let newTacs : TSyntax `Lean.Parser.Tactic.tacticSeq := ⟨replaceHoles tacs.raw⟩
|
||||||
let template ← PrettyPrinter.ppCategory `Lean.Parser.Tactic.tacticSeq newTacs
|
let template ← PrettyPrinter.ppCategory `Lean.Parser.Tactic.tacticSeq newTacs
|
||||||
logInfo s!"Template:\n{template}"
|
trace[debug] s!"Template:\n{template}"
|
||||||
modifyLevel (←getCurLevelId) fun level => do
|
modifyLevel (←getCurLevelId) fun level => do
|
||||||
return {level with template := s!"{template}"}
|
return {level with template := s!"{template}"}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
open IO.FS System FilePath in
|
||||||
|
/-- Copies the folder `images/` to `.lake/gamedata/images/` -/
|
||||||
|
def copyImages : IO Unit := do
|
||||||
|
let target : FilePath := ".lake" / "gamedata"
|
||||||
|
if ← FilePath.pathExists "images" then
|
||||||
|
for file in ← walkDir "images" do
|
||||||
|
let outFile := target.join file
|
||||||
|
-- create the directories
|
||||||
|
if ← file.isDir then
|
||||||
|
createDirAll outFile
|
||||||
|
else
|
||||||
|
if let some parent := outFile.parent then
|
||||||
|
createDirAll parent
|
||||||
|
-- copy file
|
||||||
|
let content ← readBinFile file
|
||||||
|
writeBinFile outFile content
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- TODO: Notes for testing if a declaration has the simp attribute
|
-- TODO: Notes for testing if a declaration has the simp attribute
|
||||||
|
|
||||||
-- -- Test: From zulip
|
-- -- Test: From zulip
|
||||||
@@ -580,6 +721,47 @@ elab "Template" tacs:tacticSeq : tactic => do
|
|||||||
|
|
||||||
/-! # Make Game -/
|
/-! # Make Game -/
|
||||||
|
|
||||||
|
#eval IO.FS.createDirAll ".lake/gamedata/"
|
||||||
|
|
||||||
|
-- TODO: register all of this as ToJson instance?
|
||||||
|
def saveGameData (allItemsByType : HashMap InventoryType (HashSet Name)) : CommandElabM Unit := do
|
||||||
|
let game ← getCurGame
|
||||||
|
let env ← getEnv
|
||||||
|
let path : System.FilePath := s!"{← IO.currentDir}" / ".lake" / "gamedata"
|
||||||
|
|
||||||
|
if ← path.isDir then
|
||||||
|
IO.FS.removeDirAll path
|
||||||
|
IO.FS.createDirAll path
|
||||||
|
|
||||||
|
-- copy the images folder
|
||||||
|
copyImages
|
||||||
|
|
||||||
|
for (worldId, world) in game.worlds.nodes.toArray do
|
||||||
|
for (levelId, level) in world.levels.toArray do
|
||||||
|
IO.FS.writeFile (path / s!"level__{worldId}__{levelId}.json") (toString (toJson (level.toInfo env)))
|
||||||
|
|
||||||
|
IO.FS.writeFile (path / s!"game.json") (toString (getGameJson game))
|
||||||
|
|
||||||
|
for inventoryType in [InventoryType.Lemma, .Tactic, .Definition] do
|
||||||
|
for name in allItemsByType.findD inventoryType {} do
|
||||||
|
let some item ← getInventoryItem? name inventoryType
|
||||||
|
| throwError "Expected item to exist: {name}"
|
||||||
|
IO.FS.writeFile (path / s!"doc__{inventoryType}__{name}.json") (toString (toJson item))
|
||||||
|
|
||||||
|
let getTiles (type : InventoryType) : CommandElabM (Array InventoryTile) := do
|
||||||
|
(allItemsByType.findD type {}).toArray.mapM (fun name => do
|
||||||
|
let some item ← getInventoryItem? name type
|
||||||
|
| throwError "Expected item to exist: {name}"
|
||||||
|
return item.toTile)
|
||||||
|
let inventory : InventoryOverview := {
|
||||||
|
lemmas := ← getTiles .Lemma
|
||||||
|
tactics := ← getTiles .Tactic
|
||||||
|
definitions := ← getTiles .Definition
|
||||||
|
lemmaTab := none
|
||||||
|
}
|
||||||
|
IO.FS.writeFile (path / s!"inventory.json") (toString (toJson inventory))
|
||||||
|
|
||||||
|
|
||||||
def GameLevel.getInventory (level : GameLevel) : InventoryType → InventoryInfo
|
def GameLevel.getInventory (level : GameLevel) : InventoryType → InventoryInfo
|
||||||
| .Tactic => level.tactics
|
| .Tactic => level.tactics
|
||||||
| .Definition => level.definitions
|
| .Definition => level.definitions
|
||||||
@@ -591,65 +773,6 @@ def GameLevel.setComputedInventory (level : GameLevel) :
|
|||||||
| .Definition, v => {level with definitions := {level.definitions with tiles := v}}
|
| .Definition, v => {level with definitions := {level.definitions with tiles := v}}
|
||||||
| .Lemma, v => {level with lemmas := {level.lemmas with tiles := v}}
|
| .Lemma, v => {level with lemmas := {level.lemmas with tiles := v}}
|
||||||
|
|
||||||
/-- Copied from `Mathlib.Tactic.HelpCmd`.
|
|
||||||
|
|
||||||
Gets the initial string token in a parser description. For example, for a declaration like
|
|
||||||
`syntax "bla" "baz" term : tactic`, it returns `some "bla"`. Returns `none` for syntax declarations
|
|
||||||
that don't start with a string constant. -/
|
|
||||||
partial def getHeadTk (e : Expr) : Option String :=
|
|
||||||
match (Expr.withApp e λ e a => (e.constName?.getD Name.anonymous, a)) with
|
|
||||||
| (``ParserDescr.node, #[_, _, p]) => getHeadTk p
|
|
||||||
| (``ParserDescr.unary, #[.app _ (.lit (.strVal "withPosition")), p]) => getHeadTk p
|
|
||||||
| (``ParserDescr.unary, #[.app _ (.lit (.strVal "atomic")), p]) => getHeadTk p
|
|
||||||
| (``ParserDescr.binary, #[.app _ (.lit (.strVal "andthen")), p, _]) => getHeadTk p
|
|
||||||
| (``ParserDescr.nonReservedSymbol, #[.lit (.strVal tk), _]) => some tk
|
|
||||||
| (``ParserDescr.symbol, #[.lit (.strVal tk)]) => some tk
|
|
||||||
| (``Parser.withAntiquot, #[_, p]) => getHeadTk p
|
|
||||||
| (``Parser.leadingNode, #[_, _, p]) => getHeadTk p
|
|
||||||
| (``HAndThen.hAndThen, #[_, _, _, _, p, _]) => getHeadTk p
|
|
||||||
| (``Parser.nonReservedSymbol, #[.lit (.strVal tk), _]) => some tk
|
|
||||||
| (``Parser.symbol, #[.lit (.strVal tk)]) => some tk
|
|
||||||
| _ => none
|
|
||||||
|
|
||||||
/-- Modified from `#help` in `Mathlib.Tactic.HelpCmd` -/
|
|
||||||
def getTacticDocstring (env : Environment) (name: Name) : CommandElabM (Option String) := do
|
|
||||||
let name := name.toString (escape := false)
|
|
||||||
let mut decls : Lean.RBMap String (Array SyntaxNodeKind) compare := {}
|
|
||||||
|
|
||||||
let catName : Name := `tactic
|
|
||||||
let catStx : Ident := mkIdent catName -- TODO
|
|
||||||
let some cat := (Parser.parserExtension.getState env).categories.find? catName
|
|
||||||
| throwErrorAt catStx "{catStx} is not a syntax category"
|
|
||||||
liftTermElabM <| Term.addCategoryInfo catStx catName
|
|
||||||
for (k, _) in cat.kinds do
|
|
||||||
let mut used := false
|
|
||||||
if let some tk := do getHeadTk (← (← env.find? k).value?) then
|
|
||||||
let tk := tk.trim
|
|
||||||
if name ≠ tk then -- was `!name.isPrefixOf tk`
|
|
||||||
continue
|
|
||||||
used := true
|
|
||||||
decls := decls.insert tk ((decls.findD tk #[]).push k)
|
|
||||||
for (_name, ks) in decls do
|
|
||||||
for k in ks do
|
|
||||||
if let some doc ← findDocString? env k then
|
|
||||||
return doc
|
|
||||||
|
|
||||||
logWarning <| m!"Could not find a docstring for tactic {name}, consider adding one " ++
|
|
||||||
m!"using `TacticDoc {name} \"some doc\"`"
|
|
||||||
return none
|
|
||||||
|
|
||||||
/-- Retrieve the docstring associated to an inventory item. For Tactics, this
|
|
||||||
is not guaranteed to work. -/
|
|
||||||
def getDocstring (env : Environment) (name : Name) (type : InventoryType) :
|
|
||||||
CommandElabM (Option String) :=
|
|
||||||
match type with
|
|
||||||
-- for tactics it's a lookup following mathlib's `#help`. not guaranteed to be the correct one.
|
|
||||||
| .Tactic => getTacticDocstring env name
|
|
||||||
| .Lemma => findDocString? env name
|
|
||||||
-- TODO: for definitions not implemented yet, does it work?
|
|
||||||
| .Definition => findDocString? env name
|
|
||||||
|
|
||||||
|
|
||||||
partial def removeTransitiveAux (id : Name) (arrows : HashMap Name (HashSet Name))
|
partial def removeTransitiveAux (id : Name) (arrows : HashMap Name (HashSet Name))
|
||||||
(newArrows : HashMap Name (HashSet Name)) (decendants : HashMap Name (HashSet Name)) :
|
(newArrows : HashMap Name (HashSet Name)) (decendants : HashMap Name (HashSet Name)) :
|
||||||
HashMap Name (HashSet Name) × HashMap Name (HashSet Name) := Id.run do
|
HashMap Name (HashSet Name) × HashMap Name (HashSet Name) := Id.run do
|
||||||
@@ -805,8 +928,12 @@ elab "MakeGame" : command => do
|
|||||||
-- Items that should not be displayed in inventory
|
-- Items that should not be displayed in inventory
|
||||||
let mut hiddenItems : HashSet Name := {}
|
let mut hiddenItems : HashSet Name := {}
|
||||||
|
|
||||||
|
let allWorlds := game.worlds.nodes.toArray
|
||||||
|
let nrWorlds := allWorlds.size
|
||||||
|
let mut nrLevels := 0
|
||||||
|
|
||||||
-- Calculate which "items" are used/new in which world
|
-- Calculate which "items" are used/new in which world
|
||||||
for (worldId, world) in game.worlds.nodes.toArray do
|
for (worldId, world) in allWorlds do
|
||||||
let mut usedItems : HashSet Name := {}
|
let mut usedItems : HashSet Name := {}
|
||||||
let mut newItems : HashSet Name := {}
|
let mut newItems : HashSet Name := {}
|
||||||
for inventoryType in #[.Tactic, .Definition, .Lemma] do
|
for inventoryType in #[.Tactic, .Definition, .Lemma] do
|
||||||
@@ -845,9 +972,12 @@ elab "MakeGame" : command => do
|
|||||||
-- logInfo m!"{worldId} uses: {usedItems.toList}"
|
-- logInfo m!"{worldId} uses: {usedItems.toList}"
|
||||||
-- logInfo m!"{worldId} introduces: {newItems.toList}"
|
-- logInfo m!"{worldId} introduces: {newItems.toList}"
|
||||||
|
|
||||||
|
-- Moreover, count the number of levels in the game
|
||||||
|
nrLevels := nrLevels + world.levels.toArray.size
|
||||||
|
|
||||||
/- for each "item" this is a HashSet of `worldId`s that introduce this item -/
|
/- for each "item" this is a HashSet of `worldId`s that introduce this item -/
|
||||||
let mut worldsWithNewItem : HashMap Name (HashSet Name) := {}
|
let mut worldsWithNewItem : HashMap Name (HashSet Name) := {}
|
||||||
for (worldId, _world) in game.worlds.nodes.toArray do
|
for (worldId, _world) in allWorlds do
|
||||||
for newItem in newItemsInWorld.findD worldId {} do
|
for newItem in newItemsInWorld.findD worldId {} do
|
||||||
worldsWithNewItem := worldsWithNewItem.insert newItem $
|
worldsWithNewItem := worldsWithNewItem.insert newItem $
|
||||||
(worldsWithNewItem.findD newItem {}).insert worldId
|
(worldsWithNewItem.findD newItem {}).insert worldId
|
||||||
@@ -859,7 +989,7 @@ elab "MakeGame" : command => do
|
|||||||
let mut dependencyReasons : HashMap (Name × Name) (HashSet Name) := {}
|
let mut dependencyReasons : HashMap (Name × Name) (HashSet Name) := {}
|
||||||
|
|
||||||
-- Calculate world dependency graph `game.worlds`
|
-- Calculate world dependency graph `game.worlds`
|
||||||
for (dependentWorldId, _dependentWorld) in game.worlds.nodes.toArray do
|
for (dependentWorldId, _dependentWorld) in allWorlds do
|
||||||
let mut dependsOnWorlds : HashSet Name := {}
|
let mut dependsOnWorlds : HashSet Name := {}
|
||||||
-- Adding manual dependencies that were specified via the `Dependency` command.
|
-- Adding manual dependencies that were specified via the `Dependency` command.
|
||||||
for (sourceId, targetId) in game.worlds.edges do
|
for (sourceId, targetId) in game.worlds.edges do
|
||||||
@@ -910,14 +1040,25 @@ elab "MakeGame" : command => do
|
|||||||
logError m!"{w1} depends on {w2} because of {items.toList}."
|
logError m!"{w1} depends on {w2} because of {items.toList}."
|
||||||
else
|
else
|
||||||
worldDependsOnWorlds ← removeTransitive worldDependsOnWorlds
|
worldDependsOnWorlds ← removeTransitive worldDependsOnWorlds
|
||||||
|
|
||||||
|
-- need to delete all existing edges as they are already present in `worldDependsOnWorlds`.
|
||||||
|
modifyCurGame fun game =>
|
||||||
|
pure {game with worlds := {game.worlds with edges := Array.empty}}
|
||||||
|
|
||||||
for (dependentWorldId, worldIds) in worldDependsOnWorlds.toArray do
|
for (dependentWorldId, worldIds) in worldDependsOnWorlds.toArray do
|
||||||
modifyCurGame fun game =>
|
modifyCurGame fun game =>
|
||||||
pure {game with worlds := {game.worlds with
|
pure {game with worlds := {game.worlds with
|
||||||
edges := game.worlds.edges.append (worldIds.toArray.map fun wid => (wid, dependentWorldId))}}
|
edges := game.worlds.edges.append (worldIds.toArray.map fun wid => (wid, dependentWorldId))}}
|
||||||
|
|
||||||
|
-- Add the number of levels and worlds to the tile for the landing page
|
||||||
|
modifyCurGame fun game => pure {game with tile := {game.tile with
|
||||||
|
levels := nrLevels
|
||||||
|
worlds := nrWorlds }}
|
||||||
|
|
||||||
-- Apparently we need to reload `game` to get the changes to `game.worlds` we just made
|
-- Apparently we need to reload `game` to get the changes to `game.worlds` we just made
|
||||||
let game ← getCurGame
|
let game ← getCurGame
|
||||||
|
|
||||||
|
let mut allItemsByType : HashMap InventoryType (HashSet Name) := {}
|
||||||
-- Compute which inventory items are available in which level:
|
-- Compute which inventory items are available in which level:
|
||||||
for inventoryType in #[.Tactic, .Definition, .Lemma] do
|
for inventoryType in #[.Tactic, .Definition, .Lemma] do
|
||||||
|
|
||||||
@@ -1047,7 +1188,9 @@ elab "MakeGame" : command => do
|
|||||||
|
|
||||||
modifyLevel ⟨← getCurGameId, worldId, levelId⟩ fun level => do
|
modifyLevel ⟨← getCurGameId, worldId, levelId⟩ fun level => do
|
||||||
return level.setComputedInventory inventoryType itemsArray
|
return level.setComputedInventory inventoryType itemsArray
|
||||||
|
allItemsByType := allItemsByType.insert inventoryType allItems
|
||||||
|
|
||||||
|
saveGameData allItemsByType
|
||||||
|
|
||||||
/-! # Debugging tools -/
|
/-! # Debugging tools -/
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ structure InventoryTemplate where
|
|||||||
/-- Depends on the type:
|
/-- Depends on the type:
|
||||||
* Tactic: the tactic's name
|
* Tactic: the tactic's name
|
||||||
* Lemma: fully qualified lemma name
|
* Lemma: fully qualified lemma name
|
||||||
* Definition: no restrictions (preferrably the definions fully qualified name)
|
* Definition: no restrictions (preferably the definitions fully qualified name)
|
||||||
-/
|
-/
|
||||||
name: Name
|
name: Name
|
||||||
/-- Only for Lemmas. To sort them into tabs -/
|
/-- Only for Lemmas. To sort them into tabs -/
|
||||||
@@ -108,6 +108,12 @@ structure InventoryTile where
|
|||||||
hidden := false
|
hidden := false
|
||||||
deriving ToJson, FromJson, Repr, Inhabited
|
deriving ToJson, FromJson, Repr, Inhabited
|
||||||
|
|
||||||
|
def InventoryItem.toTile (item : InventoryItem) : InventoryTile := {
|
||||||
|
name := item.name,
|
||||||
|
displayName := item.displayName
|
||||||
|
category := item.category
|
||||||
|
}
|
||||||
|
|
||||||
/-- The extension that stores the doc templates. Note that you can only add, but never modify
|
/-- The extension that stores the doc templates. Note that you can only add, but never modify
|
||||||
entries! -/
|
entries! -/
|
||||||
initialize inventoryTemplateExt :
|
initialize inventoryTemplateExt :
|
||||||
@@ -135,7 +141,12 @@ def getInventoryItem? [Monad m] [MonadEnv m] (n : Name) (type : InventoryType) :
|
|||||||
m (Option InventoryItem) := do
|
m (Option InventoryItem) := do
|
||||||
return (inventoryExt.getState (← getEnv)).find? (fun x => x.name == n && x.type == type)
|
return (inventoryExt.getState (← getEnv)).find? (fun x => x.name == n && x.type == type)
|
||||||
|
|
||||||
|
structure InventoryOverview where
|
||||||
|
tactics : Array InventoryTile
|
||||||
|
lemmas : Array InventoryTile
|
||||||
|
definitions : Array InventoryTile
|
||||||
|
lemmaTab : Option String
|
||||||
|
deriving ToJson, FromJson
|
||||||
|
|
||||||
/-! ## Environment extensions for game specification -/
|
/-! ## Environment extensions for game specification -/
|
||||||
|
|
||||||
@@ -254,8 +265,68 @@ structure GameLevel where
|
|||||||
lemmas: InventoryInfo := default
|
lemmas: InventoryInfo := default
|
||||||
/-- A proof template that is printed in an empty editor. -/
|
/-- A proof template that is printed in an empty editor. -/
|
||||||
template: Option String := none
|
template: Option String := none
|
||||||
deriving Inhabited, Repr
|
/-- The image for this level. -/
|
||||||
|
image : String := default
|
||||||
|
deriving Inhabited, Repr
|
||||||
|
|
||||||
|
structure WorldOverview where
|
||||||
|
world: Name
|
||||||
|
tactics: Array InventoryTile := default
|
||||||
|
definitions: Array InventoryTile := default
|
||||||
|
lemmas: Array InventoryTile := default
|
||||||
|
deriving FromJson, ToJson, Inhabited, Repr
|
||||||
|
|
||||||
|
/-- Json-encodable version of `GameLevel`
|
||||||
|
Fields:
|
||||||
|
- description: Lemma in mathematical language.
|
||||||
|
- descriptionGoal: Lemma printed as Lean-Code.
|
||||||
|
-/
|
||||||
|
structure LevelInfo where
|
||||||
|
index : Nat
|
||||||
|
title : String
|
||||||
|
tactics : Array InventoryTile
|
||||||
|
lemmas : Array InventoryTile
|
||||||
|
definitions : Array InventoryTile
|
||||||
|
introduction : String
|
||||||
|
conclusion : String
|
||||||
|
descrText : Option String := none
|
||||||
|
descrFormat : String := ""
|
||||||
|
lemmaTab : Option String
|
||||||
|
displayName : Option String
|
||||||
|
statementName : Option String
|
||||||
|
template : Option String
|
||||||
|
image: Option String
|
||||||
|
deriving ToJson, FromJson
|
||||||
|
|
||||||
|
def GameLevel.toInfo (lvl : GameLevel) (env : Environment) : LevelInfo :=
|
||||||
|
{ index := lvl.index,
|
||||||
|
title := lvl.title,
|
||||||
|
tactics := lvl.tactics.tiles,
|
||||||
|
lemmas := lvl.lemmas.tiles,
|
||||||
|
definitions := lvl.definitions.tiles,
|
||||||
|
descrText := lvl.descrText,
|
||||||
|
descrFormat := lvl.descrFormat --toExpr <| format (lvl.goal.raw) --toString <| Syntax.formatStx (lvl.goal.raw) --Syntax.formatStx (lvl.goal.raw) , -- TODO
|
||||||
|
introduction := lvl.introduction
|
||||||
|
conclusion := lvl.conclusion
|
||||||
|
lemmaTab := match lvl.lemmaTab with
|
||||||
|
| some tab => tab
|
||||||
|
| none =>
|
||||||
|
-- Try to set the lemma tab to the category of the first added lemma
|
||||||
|
match lvl.lemmas.tiles.find? (·.new) with
|
||||||
|
| some tile => tile.category
|
||||||
|
| none => none
|
||||||
|
statementName := lvl.statementName.toString
|
||||||
|
displayName := match lvl.statementName with
|
||||||
|
| .anonymous => none
|
||||||
|
| name => match (inventoryExt.getState env).find?
|
||||||
|
(fun x => x.name == name && x.type == .Lemma) with
|
||||||
|
| some n => n.displayName
|
||||||
|
| none => name.toString
|
||||||
|
-- Note: we could call `.find!` because we check in `Statement` that the
|
||||||
|
-- lemma doc must exist.
|
||||||
|
template := lvl.template
|
||||||
|
image := lvl.image
|
||||||
|
}
|
||||||
|
|
||||||
/-! ## World -/
|
/-! ## World -/
|
||||||
|
|
||||||
@@ -271,17 +342,46 @@ structure World where
|
|||||||
conclusion : String := default
|
conclusion : String := default
|
||||||
/-- The levels of the world. -/
|
/-- The levels of the world. -/
|
||||||
levels: HashMap Nat GameLevel := default
|
levels: HashMap Nat GameLevel := default
|
||||||
|
/-- The introduction image of the world. -/
|
||||||
|
image: String := default
|
||||||
deriving Inhabited
|
deriving Inhabited
|
||||||
|
|
||||||
instance : ToJson World := ⟨
|
instance : ToJson World := ⟨
|
||||||
fun world => Json.mkObj [
|
fun world => Json.mkObj [
|
||||||
("name", toJson world.name),
|
("name", toJson world.name),
|
||||||
("title", world.title),
|
("title", world.title),
|
||||||
("introduction", world.introduction)]
|
("introduction", world.introduction),
|
||||||
|
("image", world.image)]
|
||||||
⟩
|
⟩
|
||||||
|
|
||||||
/-! ## Game -/
|
/-! ## Game -/
|
||||||
|
|
||||||
|
/-- A tile as they are displayed on the servers landing page. -/
|
||||||
|
structure GameTile where
|
||||||
|
/-- The title of the game -/
|
||||||
|
title: String
|
||||||
|
/-- One catch phrase about the game -/
|
||||||
|
short: String := default
|
||||||
|
/-- One paragraph description what the game is about -/
|
||||||
|
long: String := default
|
||||||
|
/-- List of languages the game supports
|
||||||
|
|
||||||
|
TODO: What's the expectected format
|
||||||
|
TODO: Must be a list with a single language currently
|
||||||
|
-/
|
||||||
|
languages: List String := default
|
||||||
|
/-- A list of games which this one builds upon -/
|
||||||
|
prerequisites: List String := default
|
||||||
|
/-- Number of worlds in the game -/
|
||||||
|
worlds: Nat := default
|
||||||
|
/-- Number of levels in the game -/
|
||||||
|
levels: Nat := default
|
||||||
|
/-- A cover image of the game
|
||||||
|
|
||||||
|
TODO: What's the format? -/
|
||||||
|
image: String := default
|
||||||
|
deriving Inhabited, ToJson
|
||||||
|
|
||||||
structure Game where
|
structure Game where
|
||||||
/-- Internal name of the game. -/
|
/-- Internal name of the game. -/
|
||||||
name : Name
|
name : Name
|
||||||
@@ -296,8 +396,19 @@ structure Game where
|
|||||||
/-- TODO: currently unused. -/
|
/-- TODO: currently unused. -/
|
||||||
authors : List String := default
|
authors : List String := default
|
||||||
worlds : Graph Name World := default
|
worlds : Graph Name World := default
|
||||||
|
/-- The tile displayed on the server's landing page. -/
|
||||||
|
tile : GameTile := default
|
||||||
|
/-- The path to the background image of the world. -/
|
||||||
|
image : String := default
|
||||||
deriving Inhabited, ToJson
|
deriving Inhabited, ToJson
|
||||||
|
|
||||||
|
def getGameJson (game : «Game») : Json := Id.run do
|
||||||
|
let gameJson : Json := toJson game
|
||||||
|
-- Add world sizes to Json object
|
||||||
|
let worldSize := game.worlds.nodes.toList.map (fun (n, w) => (n.toString, w.levels.size))
|
||||||
|
let gameJson := gameJson.mergeObj (Json.mkObj [("worldSize", Json.mkObj worldSize)])
|
||||||
|
return gameJson
|
||||||
|
|
||||||
/-! ## Game environment extension -/
|
/-! ## Game environment extension -/
|
||||||
|
|
||||||
def HashMap.merge [BEq α] [Hashable α] (old : HashMap α β) (new : HashMap α β) (merge : β → β → β) :
|
def HashMap.merge [BEq α] [Hashable α] (old : HashMap α β) (new : HashMap α β) (merge : β → β → β) :
|
||||||
@@ -386,8 +497,13 @@ def modifyCurWorld (fn : World → m World) [MonadError m] : m Unit := do
|
|||||||
return {game with worlds := {game.worlds with nodes := game.worlds.nodes.insert world.name (← fn world) }}
|
return {game with worlds := {game.worlds with nodes := game.worlds.nodes.insert world.name (← fn world) }}
|
||||||
|
|
||||||
def addLevel (level : GameLevel) [MonadError m] : m Unit := do
|
def addLevel (level : GameLevel) [MonadError m] : m Unit := do
|
||||||
modifyCurWorld fun world => do
|
let worldId ← getCurWorldId
|
||||||
return {world with levels := world.levels.insert level.index level}
|
match ← getLevel? ⟨← getCurGameId, worldId, level.index⟩ with
|
||||||
|
| some _existingLevel =>
|
||||||
|
throwError m!"Level {level.index} already exists for world {worldId}!"
|
||||||
|
| none =>
|
||||||
|
modifyCurWorld fun world => do
|
||||||
|
return {world with levels := world.levels.insert level.index level}
|
||||||
|
|
||||||
def getCurLevel [MonadError m] : m GameLevel := do
|
def getCurLevel [MonadError m] : m GameLevel := do
|
||||||
let some level := (← getCurWorld).levels.find? (← getCurLevelIdx)
|
let some level := (← getCurWorld).levels.find? (← getCurLevelIdx)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/- This file is mostly copied from `Lean/Server/FileWorker.lean`. -/
|
/- This file is mostly copied from `Lean/Server/FileWorker.lean`. -/
|
||||||
import Lean.Server.FileWorker
|
import Lean.Server.FileWorker
|
||||||
import GameServer.Game
|
import GameServer.Game
|
||||||
|
import GameServer.ImportModules
|
||||||
|
|
||||||
namespace MyModule
|
namespace MyModule
|
||||||
open Lean
|
open Lean
|
||||||
@@ -57,6 +58,17 @@ open JsonRpc
|
|||||||
|
|
||||||
section Elab
|
section Elab
|
||||||
|
|
||||||
|
def addErrorMessage (info : SourceInfo) (inputCtx : Parser.InputContext) (s : MessageData) :
|
||||||
|
Elab.Command.CommandElabM Unit := do
|
||||||
|
modify fun st => { st with
|
||||||
|
messages := st.messages.add {
|
||||||
|
fileName := inputCtx.fileName
|
||||||
|
severity := MessageSeverity.error
|
||||||
|
pos := inputCtx.fileMap.toPosition (info.getPos?.getD 0)
|
||||||
|
data := s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
-- TODO: use HashSet for allowed tactics?
|
-- TODO: use HashSet for allowed tactics?
|
||||||
/-- Find all tactics in syntax object that are forbidden according to a
|
/-- Find all tactics in syntax object that are forbidden according to a
|
||||||
set `allowed` of allowed tactics. -/
|
set `allowed` of allowed tactics. -/
|
||||||
@@ -65,7 +77,7 @@ partial def findForbiddenTactics (inputCtx : Parser.InputContext)
|
|||||||
Elab.Command.CommandElabM Unit := do
|
Elab.Command.CommandElabM Unit := do
|
||||||
match stx with
|
match stx with
|
||||||
| .missing => return ()
|
| .missing => return ()
|
||||||
| .node info kind args =>
|
| .node _info _kind args =>
|
||||||
for arg in args do
|
for arg in args do
|
||||||
findForbiddenTactics inputCtx levelParams arg
|
findForbiddenTactics inputCtx levelParams arg
|
||||||
| .atom info val =>
|
| .atom info val =>
|
||||||
@@ -89,13 +101,17 @@ partial def findForbiddenTactics (inputCtx : Parser.InputContext)
|
|||||||
| some _ => pure () -- tactic is in the inventory, allow it.
|
| some _ => pure () -- tactic is in the inventory, allow it.
|
||||||
else if tac.disabled then
|
else if tac.disabled then
|
||||||
addWarningMessage info s!"The tactic '{val}' is disabled in this level!"
|
addWarningMessage info s!"The tactic '{val}' is disabled in this level!"
|
||||||
| .ident info rawVal val preresolved =>
|
| .ident info _rawVal val _preresolved =>
|
||||||
let ns ←
|
let ns ←
|
||||||
try resolveGlobalConst (mkIdent val)
|
try resolveGlobalConst (mkIdent val)
|
||||||
catch | _ => pure [] -- catch "unknown constant" error
|
catch | _ => pure [] -- catch "unknown constant" error
|
||||||
for n in ns do
|
for n in ns do
|
||||||
let some (.thmInfo ..) := (← getEnv).find? n
|
let some (.thmInfo ..) := (← getEnv).find? n
|
||||||
| return () -- not a theroem -> ignore
|
| return () -- not a theorem -> ignore
|
||||||
|
-- Forbid the theorem we are proving currently
|
||||||
|
if n = levelParams.statementName then
|
||||||
|
addErrorMessage info inputCtx s!"Structural recursion: you can't use '{n}' to proof itself!"
|
||||||
|
|
||||||
let lemmasAndDefs := levelParams.lemmas ++ levelParams.definitions
|
let lemmasAndDefs := levelParams.lemmas ++ levelParams.definitions
|
||||||
match lemmasAndDefs.find? (fun l => l.name == n) with
|
match lemmasAndDefs.find? (fun l => l.name == n) with
|
||||||
| none => addWarningMessage info s!"You have not unlocked the lemma/definition '{n}' yet!"
|
| none => addWarningMessage info s!"You have not unlocked the lemma/definition '{n}' yet!"
|
||||||
@@ -105,14 +121,19 @@ partial def findForbiddenTactics (inputCtx : Parser.InputContext)
|
|||||||
else if lem.disabled then
|
else if lem.disabled then
|
||||||
addWarningMessage info s!"The lemma/definition '{n}' is disabled in this level!"
|
addWarningMessage info s!"The lemma/definition '{n}' is disabled in this level!"
|
||||||
where addWarningMessage (info : SourceInfo) (s : MessageData) :=
|
where addWarningMessage (info : SourceInfo) (s : MessageData) :=
|
||||||
modify fun st => { st with
|
let difficulty := levelParams.difficulty
|
||||||
messages := st.messages.add {
|
if difficulty > 0 then
|
||||||
fileName := inputCtx.fileName
|
modify fun st => { st with
|
||||||
severity := MessageSeverity.warning
|
messages := st.messages.add {
|
||||||
pos := inputCtx.fileMap.toPosition (info.getPos?.getD 0)
|
fileName := inputCtx.fileName
|
||||||
data := s
|
severity := if difficulty > 1 then MessageSeverity.error else MessageSeverity.warning
|
||||||
|
pos := inputCtx.fileMap.toPosition (info.getPos?.getD 0)
|
||||||
|
data := s
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
else pure ()
|
||||||
|
-- where addErrorMessage (info : SourceInfo) (s : MessageData) :=
|
||||||
|
-- pure ()
|
||||||
|
|
||||||
open Elab Meta Expr in
|
open Elab Meta Expr in
|
||||||
def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets : Bool)
|
def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets : Bool)
|
||||||
@@ -170,8 +191,7 @@ def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets
|
|||||||
parseResultRef.set (tacticStx, cmdParserState)
|
parseResultRef.set (tacticStx, cmdParserState)
|
||||||
|
|
||||||
-- Check for forbidden tactics
|
-- Check for forbidden tactics
|
||||||
if levelParams.checkEnabled then
|
findForbiddenTactics inputCtx levelParams tacticStx
|
||||||
findForbiddenTactics inputCtx levelParams tacticStx
|
|
||||||
|
|
||||||
-- Insert invisible `skip` command to make sure we always display the initial goal
|
-- Insert invisible `skip` command to make sure we always display the initial goal
|
||||||
let skip := Syntax.node (.original default 0 default endOfWhitespace) ``Lean.Parser.Tactic.skip #[]
|
let skip := Syntax.node (.original default 0 default endOfWhitespace) ``Lean.Parser.Tactic.skip #[]
|
||||||
@@ -273,7 +293,7 @@ where
|
|||||||
-- TODO(MH): check for interrupt with increased precision
|
-- TODO(MH): check for interrupt with increased precision
|
||||||
cancelTk.check
|
cancelTk.check
|
||||||
/- NOTE(MH): This relies on the client discarding old diagnostics upon receiving new ones
|
/- NOTE(MH): This relies on the client discarding old diagnostics upon receiving new ones
|
||||||
while prefering newer versions over old ones. The former is necessary because we do
|
while preferring newer versions over old ones. The former is necessary because we do
|
||||||
not explicitly clear older diagnostics, while the latter is necessary because we do
|
not explicitly clear older diagnostics, while the latter is necessary because we do
|
||||||
not guarantee that diagnostics are emitted in order. Specifically, it may happen that
|
not guarantee that diagnostics are emitted in order. Specifically, it may happen that
|
||||||
we interrupted this elaboration task right at this point and a newer elaboration task
|
we interrupted this elaboration task right at this point and a newer elaboration task
|
||||||
@@ -281,7 +301,7 @@ where
|
|||||||
the interrupt. Explicitly clearing diagnostics is difficult for a similar reason,
|
the interrupt. Explicitly clearing diagnostics is difficult for a similar reason,
|
||||||
because we cannot guarantee that no further diagnostics are emitted after clearing
|
because we cannot guarantee that no further diagnostics are emitted after clearing
|
||||||
them. -/
|
them. -/
|
||||||
-- NOTE(WN): this is *not* redundent even if there are no new diagnostics in this snapshot
|
-- NOTE(WN): this is *not* redundant even if there are no new diagnostics in this snapshot
|
||||||
-- because empty diagnostics clear existing error/information squiggles. Therefore we always
|
-- because empty diagnostics clear existing error/information squiggles. Therefore we always
|
||||||
-- want to publish in case there was previously a message at this position.
|
-- want to publish in case there was previously a message at this position.
|
||||||
publishDiagnostics m snap.diagnostics.toArray ctx.hOut
|
publishDiagnostics m snap.diagnostics.toArray ctx.hOut
|
||||||
@@ -393,7 +413,7 @@ section Initialization
|
|||||||
-- Set the search path
|
-- Set the search path
|
||||||
Lean.searchPathRef.set paths
|
Lean.searchPathRef.set paths
|
||||||
|
|
||||||
let env ← importModules [{ module := `Init : Import }, { module := levelParams.levelModule : Import }] {} 0
|
let env ← importModules' #[{ module := `Init : Import }, { module := levelParams.levelModule : Import }]
|
||||||
-- return (env, paths)
|
-- return (env, paths)
|
||||||
|
|
||||||
-- use empty header
|
-- use empty header
|
||||||
@@ -477,7 +497,7 @@ section NotificationHandling
|
|||||||
IO.eprintln s!"Got outdated version number: {newVersion} ≤ {oldDoc.meta.version}"
|
IO.eprintln s!"Got outdated version number: {newVersion} ≤ {oldDoc.meta.version}"
|
||||||
else if ¬ changes.isEmpty then
|
else if ¬ changes.isEmpty then
|
||||||
let newDocText := foldDocumentChanges changes oldDoc.meta.text
|
let newDocText := foldDocumentChanges changes oldDoc.meta.text
|
||||||
updateDocument ⟨docId.uri, newVersion, newDocText⟩
|
updateDocument ⟨docId.uri, newVersion, newDocText, .always⟩
|
||||||
|
|
||||||
end NotificationHandling
|
end NotificationHandling
|
||||||
|
|
||||||
@@ -533,7 +553,7 @@ section MainLoop
|
|||||||
|
|
||||||
set {s with levelParams := {s.levelParams with
|
set {s with levelParams := {s.levelParams with
|
||||||
inventory := p.inventory,
|
inventory := p.inventory,
|
||||||
checkEnabled := p.checkEnabled}}
|
difficulty := p.difficulty}}
|
||||||
mainLoop
|
mainLoop
|
||||||
| Message.notification method (some params) =>
|
| Message.notification method (some params) =>
|
||||||
handleNotification method (toJson params)
|
handleNotification method (toJson params)
|
||||||
@@ -554,7 +574,7 @@ def initAndRunWorker (i o e : FS.Stream) (opts : Options) : IO UInt32 := do
|
|||||||
This is because LSP always refers to characters by (line, column),
|
This is because LSP always refers to characters by (line, column),
|
||||||
so if we get the line number correct it shouldn't matter that there
|
so if we get the line number correct it shouldn't matter that there
|
||||||
is a CR there. -/
|
is a CR there. -/
|
||||||
let meta : DocumentMeta := ⟨doc.uri, doc.version, doc.text.toFileMap⟩
|
let meta : DocumentMeta := ⟨doc.uri, doc.version, doc.text.toFileMap, .always⟩
|
||||||
let e := e.withPrefix s!"[{param.textDocument.uri}] "
|
let e := e.withPrefix s!"[{param.textDocument.uri}] "
|
||||||
let _ ← IO.setStderr e
|
let _ ← IO.setStderr e
|
||||||
try
|
try
|
||||||
|
|||||||
+58
-153
@@ -7,7 +7,7 @@ structure GameServerState :=
|
|||||||
(game : Name)
|
(game : Name)
|
||||||
(gameDir : String)
|
(gameDir : String)
|
||||||
(inventory : Array String)
|
(inventory : Array String)
|
||||||
(checkEnabled : Bool)
|
(difficulty : Nat)
|
||||||
|
|
||||||
abbrev GameServerM := StateT GameServerState Server.Watchdog.ServerM
|
abbrev GameServerM := StateT GameServerState Server.Watchdog.ServerM
|
||||||
|
|
||||||
@@ -25,52 +25,6 @@ open Lsp
|
|||||||
open JsonRpc
|
open JsonRpc
|
||||||
open IO
|
open IO
|
||||||
|
|
||||||
def getGame (game : Name): GameServerM Json := do
|
|
||||||
let some game ← getGame? game
|
|
||||||
| throwServerError "Game not found"
|
|
||||||
let gameJson : Json := toJson game
|
|
||||||
-- Add world sizes to Json object
|
|
||||||
let worldSize := game.worlds.nodes.toList.map (fun (n, w) => (n.toString, w.levels.size))
|
|
||||||
let gameJson := gameJson.mergeObj (Json.mkObj [("worldSize", Json.mkObj worldSize)])
|
|
||||||
return gameJson
|
|
||||||
|
|
||||||
/--
|
|
||||||
Fields:
|
|
||||||
- description: Lemma in mathematical language.
|
|
||||||
- descriptionGoal: Lemma printed as Lean-Code.
|
|
||||||
-/
|
|
||||||
structure LevelInfo where
|
|
||||||
index : Nat
|
|
||||||
title : String
|
|
||||||
tactics : Array InventoryTile
|
|
||||||
lemmas : Array InventoryTile
|
|
||||||
definitions : Array InventoryTile
|
|
||||||
introduction : String
|
|
||||||
conclusion : String
|
|
||||||
descrText : Option String := none
|
|
||||||
descrFormat : String := ""
|
|
||||||
lemmaTab : Option String
|
|
||||||
statementName : Option String
|
|
||||||
template : Option String
|
|
||||||
deriving ToJson, FromJson
|
|
||||||
|
|
||||||
structure InventoryOverview where
|
|
||||||
tactics : Array InventoryTile
|
|
||||||
lemmas : Array InventoryTile
|
|
||||||
definitions : Array InventoryTile
|
|
||||||
lemmaTab : Option String
|
|
||||||
deriving ToJson, FromJson
|
|
||||||
|
|
||||||
structure LoadLevelParams where
|
|
||||||
world : Name
|
|
||||||
level : Nat
|
|
||||||
deriving ToJson, FromJson
|
|
||||||
|
|
||||||
-- structure LoadTemplateParams where
|
|
||||||
-- world : Name
|
|
||||||
-- level : Nat
|
|
||||||
-- deriving ToJson, FromJson
|
|
||||||
|
|
||||||
structure DidOpenLevelParams where
|
structure DidOpenLevelParams where
|
||||||
uri : String
|
uri : String
|
||||||
gameDir : String
|
gameDir : String
|
||||||
@@ -79,18 +33,20 @@ structure DidOpenLevelParams where
|
|||||||
lemmas : Array InventoryTile
|
lemmas : Array InventoryTile
|
||||||
definitions : Array InventoryTile
|
definitions : Array InventoryTile
|
||||||
inventory : Array String
|
inventory : Array String
|
||||||
/-- if true the server gives warnings for used tactics/lemmas that are not unlocked. -/
|
/--
|
||||||
checkEnabled : Bool
|
Check for tactics/theorems that are not unlocked.
|
||||||
|
0: no check
|
||||||
|
1: give warnings
|
||||||
|
2: give errors
|
||||||
|
-/
|
||||||
|
difficulty : Nat
|
||||||
|
/-- The name of the theorem to be proven in this level. -/
|
||||||
|
statementName : Name
|
||||||
deriving ToJson, FromJson
|
deriving ToJson, FromJson
|
||||||
|
|
||||||
structure LoadDocParams where
|
|
||||||
name : Name
|
|
||||||
type : InventoryType
|
|
||||||
deriving ToJson, FromJson
|
|
||||||
|
|
||||||
structure SetInventoryParams where
|
structure SetInventoryParams where
|
||||||
inventory : Array String
|
inventory : Array String
|
||||||
checkEnabled : Bool
|
difficulty : Nat
|
||||||
deriving ToJson, FromJson
|
deriving ToJson, FromJson
|
||||||
|
|
||||||
def handleDidOpenLevel (params : Json) : GameServerM Unit := do
|
def handleDidOpenLevel (params : Json) : GameServerM Unit := do
|
||||||
@@ -117,95 +73,20 @@ def handleDidOpenLevel (params : Json) : GameServerM Unit := do
|
|||||||
lemmas := lvl.lemmas.tiles
|
lemmas := lvl.lemmas.tiles
|
||||||
definitions := lvl.definitions.tiles
|
definitions := lvl.definitions.tiles
|
||||||
inventory := s.inventory
|
inventory := s.inventory
|
||||||
checkEnabled := s.checkEnabled
|
difficulty := s.difficulty
|
||||||
|
statementName := lvl.statementName
|
||||||
: DidOpenLevelParams
|
: DidOpenLevelParams
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
partial def handleServerEvent (ev : ServerEvent) : GameServerM Bool := do
|
partial def handleServerEvent (ev : ServerEvent) : GameServerM Bool := do
|
||||||
match ev with
|
match ev with
|
||||||
| ServerEvent.clientMsg msg =>
|
| ServerEvent.clientMsg msg =>
|
||||||
match msg with
|
match msg with
|
||||||
| Message.request id "info" _ =>
|
|
||||||
let s ← get
|
|
||||||
let c ← read
|
|
||||||
c.hOut.writeLspResponse ⟨id, (← getGame s.game)⟩
|
|
||||||
return true
|
|
||||||
| Message.request id "loadLevel" params =>
|
|
||||||
let p ← parseParams LoadLevelParams (toJson params)
|
|
||||||
let s ← get
|
|
||||||
let c ← read
|
|
||||||
let some lvl ← getLevel? {game := s.game, world := p.world, level := p.level}
|
|
||||||
| do
|
|
||||||
c.hOut.writeLspResponseError ⟨id, .invalidParams, s!"Level not found: world {p.world}, level {p.level}", none⟩
|
|
||||||
return true
|
|
||||||
|
|
||||||
let env ← getEnv
|
|
||||||
|
|
||||||
let levelInfo : LevelInfo :=
|
|
||||||
{ index := lvl.index,
|
|
||||||
title := lvl.title,
|
|
||||||
tactics := lvl.tactics.tiles,
|
|
||||||
lemmas := lvl.lemmas.tiles,
|
|
||||||
definitions := lvl.definitions.tiles,
|
|
||||||
descrText := lvl.descrText,
|
|
||||||
descrFormat := lvl.descrFormat --toExpr <| format (lvl.goal.raw) --toString <| Syntax.formatStx (lvl.goal.raw) --Syntax.formatStx (lvl.goal.raw) , -- TODO
|
|
||||||
introduction := lvl.introduction
|
|
||||||
conclusion := lvl.conclusion
|
|
||||||
lemmaTab := match lvl.lemmaTab with
|
|
||||||
| some tab => tab
|
|
||||||
| none =>
|
|
||||||
-- Try to set the lemma tab to the category of the first added lemma
|
|
||||||
match lvl.lemmas.tiles.find? (·.new) with
|
|
||||||
| some tile => tile.category
|
|
||||||
| none => none
|
|
||||||
statementName := match lvl.statementName with
|
|
||||||
| .anonymous => none
|
|
||||||
| name => match (inventoryExt.getState env).find?
|
|
||||||
(fun x => x.name == name && x.type == .Lemma) with
|
|
||||||
| some n => n.displayName
|
|
||||||
| none => name.toString
|
|
||||||
-- Note: we could call `.find!` because we check in `Statement` that the
|
|
||||||
-- lemma doc must exist.
|
|
||||||
template := lvl.template
|
|
||||||
}
|
|
||||||
c.hOut.writeLspResponse ⟨id, ToJson.toJson levelInfo⟩
|
|
||||||
return true
|
|
||||||
-- | Message.request id "loadTemplate" params =>
|
|
||||||
-- let p ← parseParams LoadTemplateParams (toJson params)
|
|
||||||
-- let s ← get
|
|
||||||
-- let c ← read
|
|
||||||
|
|
||||||
-- let some game ← getGame? s.game
|
|
||||||
-- | throwServerError "Game not found"
|
|
||||||
-- let some world := game.worlds.nodes.find? p.world
|
|
||||||
-- | throwServerError "World not found"
|
|
||||||
|
|
||||||
-- let mut templates : Array <| Option String := #[]
|
|
||||||
-- for (_, level) in world.levels.toArray do
|
|
||||||
-- templates := templates.push level.template
|
|
||||||
-- c.hOut.writeLspResponse ⟨id, ToJson.toJson templates⟩
|
|
||||||
-- return true
|
|
||||||
| Message.request id "loadDoc" params =>
|
|
||||||
let p ← parseParams LoadDocParams (toJson params)
|
|
||||||
let s ← get
|
|
||||||
let c ← read
|
|
||||||
let some doc ← getInventoryItem? p.name p.type
|
|
||||||
| do
|
|
||||||
c.hOut.writeLspResponseError ⟨id, .invalidParams,
|
|
||||||
s!"Documentation not found: {p.name}", none⟩
|
|
||||||
return true
|
|
||||||
-- TODO: not necessary at all?
|
|
||||||
-- Here we only need to convert the fields that were not `String` in the `InventoryDocEntry`
|
|
||||||
-- let doc : InventoryItem := { doc with
|
|
||||||
-- name := doc.name.toString }
|
|
||||||
c.hOut.writeLspResponse ⟨id, ToJson.toJson doc⟩
|
|
||||||
return true
|
|
||||||
| Message.notification "$/game/setInventory" params =>
|
| Message.notification "$/game/setInventory" params =>
|
||||||
let p := (← parseParams SetInventoryParams (toJson params))
|
let p := (← parseParams SetInventoryParams (toJson params))
|
||||||
let s ← get
|
let s ← get
|
||||||
set {s with inventory := p.inventory, checkEnabled := p.checkEnabled}
|
set {s with inventory := p.inventory, difficulty := p.difficulty}
|
||||||
let st ← read
|
let st ← read
|
||||||
let workers ← st.fileWorkersRef.get
|
let workers ← st.fileWorkersRef.get
|
||||||
for (_, fw) in workers do
|
for (_, fw) in workers do
|
||||||
@@ -216,26 +97,50 @@ partial def handleServerEvent (ev : ServerEvent) : GameServerM Bool := do
|
|||||||
let s ← get
|
let s ← get
|
||||||
let some game ← getGame? s.game
|
let some game ← getGame? s.game
|
||||||
| return false
|
| return false
|
||||||
-- All Levels have the same tiles, so we just load them from level 1 of an arbitrary world
|
|
||||||
-- and reset `new`, `disabled` and `unlocked`.
|
let mut overview : Array WorldOverview := #[]
|
||||||
-- Note: as we allow worlds without any levels (for developing), we might need
|
for ⟨worldId, world⟩ in game.worlds.nodes.toList do
|
||||||
-- to try until we find the first world with levels.
|
let mut tactics : Array InventoryTile := #[]
|
||||||
for ⟨worldId, _⟩ in game.worlds.nodes.toList do
|
let mut theorems : Array InventoryTile := #[]
|
||||||
let some lvl ← getLevel? {game := s.game, world := worldId, level := 1}
|
let mut definitions : Array InventoryTile := #[]
|
||||||
| do continue
|
for ⟨_levelId, lvl⟩ in world.levels.toList do
|
||||||
let inventory : InventoryOverview := {
|
tactics := tactics ++ lvl.tactics.tiles.filterMap (fun tile => match tile.new with
|
||||||
tactics := lvl.tactics.tiles.map
|
| true => some {tile with new := false, locked := true, disabled := false}
|
||||||
({ · with locked := true, disabled := false, new := false }),
|
| false => none)
|
||||||
lemmas := lvl.lemmas.tiles.map
|
theorems := theorems ++ lvl.lemmas.tiles.filterMap (fun tile => match tile.new with
|
||||||
({ · with locked := true, disabled := false, new := false }),
|
| true => some {tile with new := false, locked := true, disabled := false}
|
||||||
definitions := lvl.definitions.tiles.map
|
| false => none)
|
||||||
({ · with locked := true, disabled := false, new := false }),
|
definitions := definitions ++ lvl.definitions.tiles.filterMap (fun tile => match tile.new with
|
||||||
lemmaTab := none
|
| true => some {tile with new := false, locked := true, disabled := false}
|
||||||
}
|
| false => none)
|
||||||
let c ← read
|
overview := overview.push {
|
||||||
c.hOut.writeLspResponse ⟨id, ToJson.toJson inventory⟩
|
world := worldId,
|
||||||
return true
|
tactics := tactics,
|
||||||
return false
|
lemmas := theorems,
|
||||||
|
definitions := definitions }
|
||||||
|
let c ← read
|
||||||
|
c.hOut.writeLspResponse ⟨id, ToJson.toJson overview⟩
|
||||||
|
return true
|
||||||
|
-- -- All Levels have the same tiles, so we just load them from level 1 of an arbitrary world
|
||||||
|
-- -- and reset `new`, `disabled` and `unlocked`.
|
||||||
|
-- -- Note: as we allow worlds without any levels (for developing), we might need
|
||||||
|
-- -- to try until we find the first world with levels.
|
||||||
|
-- for ⟨worldId, _⟩ in game.worlds.nodes.toList do
|
||||||
|
-- let some lvl ← getLevel? {game := s.game, world := worldId, level := 1}
|
||||||
|
-- | do continue
|
||||||
|
-- let inventory : InventoryOverview := {
|
||||||
|
-- tactics := lvl.tactics.tiles.map
|
||||||
|
-- ({ · with locked := true, disabled := false, new := false }),
|
||||||
|
-- lemmas := lvl.lemmas.tiles.map
|
||||||
|
-- ({ · with locked := true, disabled := false, new := false }),
|
||||||
|
-- definitions := lvl.definitions.tiles.map
|
||||||
|
-- ({ · with locked := true, disabled := false, new := false }),
|
||||||
|
-- lemmaTab := none
|
||||||
|
-- }
|
||||||
|
-- let c ← read
|
||||||
|
-- c.hOut.writeLspResponse ⟨id, ToJson.toJson inventory⟩
|
||||||
|
-- return true
|
||||||
|
-- return false
|
||||||
| _ => return false
|
| _ => return false
|
||||||
| _ => return false
|
| _ => return false
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import Lean.Environment
|
||||||
|
import Std.Tactic.OpenPrivate
|
||||||
|
import Lean.Data.Lsp.Communication
|
||||||
|
|
||||||
|
open Lean
|
||||||
|
|
||||||
|
inductive LoadingKind := | finalizeExtensions | loadConstants
|
||||||
|
deriving ToJson
|
||||||
|
|
||||||
|
structure LoadingParams : Type where
|
||||||
|
counter : Nat
|
||||||
|
kind : LoadingKind
|
||||||
|
deriving ToJson
|
||||||
|
|
||||||
|
-- Code adapted from `Lean/Environment.lean`
|
||||||
|
|
||||||
|
partial def importModulesCore' (imports : Array Import) : ImportStateM Unit := do
|
||||||
|
for i in imports do
|
||||||
|
if i.runtimeOnly || (← get).moduleNameSet.contains i.module then
|
||||||
|
continue
|
||||||
|
modify fun s => { s with moduleNameSet := s.moduleNameSet.insert i.module }
|
||||||
|
let mFile ← findOLean i.module
|
||||||
|
unless (← mFile.pathExists) do
|
||||||
|
throw <| IO.userError s!"object file '{mFile}' of module {i.module} does not exist"
|
||||||
|
let (mod, region) ← readModuleData mFile
|
||||||
|
importModulesCore' mod.imports
|
||||||
|
modify fun s => { s with
|
||||||
|
moduleData := s.moduleData.push mod
|
||||||
|
regions := s.regions.push region
|
||||||
|
moduleNames := s.moduleNames.push i.module
|
||||||
|
}
|
||||||
|
|
||||||
|
open private mkInitialExtensionStates Environment.mk setImportedEntries finalizePersistentExtensions
|
||||||
|
ensureExtensionsArraySize from Lean.Environment
|
||||||
|
|
||||||
|
private partial def finalizePersistentExtensions' (env : Environment) (mods : Array ModuleData) (opts : Options) : IO Environment := do
|
||||||
|
loop 0 env
|
||||||
|
where
|
||||||
|
loop (i : Nat) (env : Environment) : IO Environment := do
|
||||||
|
(← IO.getStdout).writeLspNotification {
|
||||||
|
method := "$/game/loading",
|
||||||
|
param := {counter := i, kind := .finalizeExtensions : LoadingParams} }
|
||||||
|
-- Recall that the size of the array stored `persistentEnvExtensionRef` may increase when we import user-defined environment extensions.
|
||||||
|
let pExtDescrs ← persistentEnvExtensionsRef.get
|
||||||
|
if i < pExtDescrs.size then
|
||||||
|
let extDescr := pExtDescrs[i]!
|
||||||
|
let s := extDescr.toEnvExtension.getState env
|
||||||
|
let prevSize := (← persistentEnvExtensionsRef.get).size
|
||||||
|
let prevAttrSize ← getNumBuiltinAttributes
|
||||||
|
let newState ← extDescr.addImportedFn s.importedEntries { env := env, opts := opts }
|
||||||
|
let mut env := extDescr.toEnvExtension.setState env { s with state := newState }
|
||||||
|
env ← ensureExtensionsArraySize env
|
||||||
|
if (← persistentEnvExtensionsRef.get).size > prevSize || (← getNumBuiltinAttributes) > prevAttrSize then
|
||||||
|
-- This branch is executed when `pExtDescrs[i]` is the extension associated with the `init` attribute, and
|
||||||
|
-- a user-defined persistent extension is imported.
|
||||||
|
-- Thus, we invoke `setImportedEntries` to update the array `importedEntries` with the entries for the new extensions.
|
||||||
|
env ← setImportedEntries env mods prevSize
|
||||||
|
-- See comment at `updateEnvAttributesRef`
|
||||||
|
env ← updateEnvAttributes env
|
||||||
|
loop (i + 1) env
|
||||||
|
else
|
||||||
|
return env
|
||||||
|
|
||||||
|
def finalizeImport' (s : ImportState) (imports : Array Import) (opts : Options) (trustLevel : UInt32 := 0) : IO Environment := do
|
||||||
|
let numConsts := s.moduleData.foldl (init := 0) fun numConsts mod =>
|
||||||
|
numConsts + mod.constants.size + mod.extraConstNames.size
|
||||||
|
let mut const2ModIdx : HashMap Name ModuleIdx := mkHashMap (capacity := numConsts)
|
||||||
|
let mut constantMap : HashMap Name ConstantInfo := mkHashMap (capacity := numConsts)
|
||||||
|
for h:modIdx in [0:s.moduleData.size] do
|
||||||
|
if modIdx % 100 = 0 then
|
||||||
|
let percentage := modIdx * 100 / s.moduleData.size
|
||||||
|
(← IO.getStdout).writeLspNotification {
|
||||||
|
method := "$/game/loading",
|
||||||
|
param := {counter := percentage, kind := .loadConstants : LoadingParams} }
|
||||||
|
let mod := s.moduleData[modIdx]'h.upper
|
||||||
|
for cname in mod.constNames, cinfo in mod.constants do
|
||||||
|
match constantMap.insert' cname cinfo with
|
||||||
|
| (constantMap', replaced) =>
|
||||||
|
constantMap := constantMap'
|
||||||
|
if replaced then
|
||||||
|
throwAlreadyImported s const2ModIdx modIdx cname
|
||||||
|
const2ModIdx := const2ModIdx.insert cname modIdx
|
||||||
|
for cname in mod.extraConstNames do
|
||||||
|
const2ModIdx := const2ModIdx.insert cname modIdx
|
||||||
|
let constants : ConstMap := SMap.fromHashMap constantMap false
|
||||||
|
let exts ← mkInitialExtensionStates
|
||||||
|
let env : Environment := Environment.mk
|
||||||
|
(const2ModIdx := const2ModIdx)
|
||||||
|
(constants := constants)
|
||||||
|
(extraConstNames := {})
|
||||||
|
(extensions := exts)
|
||||||
|
(header := {
|
||||||
|
quotInit := !imports.isEmpty -- We assume `core.lean` initializes quotient module
|
||||||
|
trustLevel := trustLevel
|
||||||
|
imports := imports
|
||||||
|
regions := s.regions
|
||||||
|
moduleNames := s.moduleNames
|
||||||
|
moduleData := s.moduleData
|
||||||
|
})
|
||||||
|
|
||||||
|
let env ← setImportedEntries env s.moduleData
|
||||||
|
finalizePersistentExtensions' env s.moduleData opts
|
||||||
|
|
||||||
|
def importModules' (imports : Array Import) : IO Environment := do
|
||||||
|
withImporting do
|
||||||
|
let (_, s) ← importModulesCore' imports |>.run
|
||||||
|
let env ← finalizeImport' s imports {} 0
|
||||||
|
return env
|
||||||
@@ -78,7 +78,7 @@ partial def matchExpr (pattern : Expr) (e : Expr) (bij : FVarBijection := {}) :
|
|||||||
| _, _ => none
|
| _, _ => none
|
||||||
|
|
||||||
/-- Check if each fvar in `patterns` has a matching fvar in `fvars` -/
|
/-- Check if each fvar in `patterns` has a matching fvar in `fvars` -/
|
||||||
def matchDecls (patterns : Array Expr) (fvars : Array Expr) (strict := true) (initBij : FVarBijection := {}) : MetaM Bool := do
|
def matchDecls (patterns : Array Expr) (fvars : Array Expr) (strict := true) (initBij : FVarBijection := {}) : MetaM (Option FVarBijection) := do
|
||||||
-- We iterate through the array backwards hoping that this will find us faster results
|
-- We iterate through the array backwards hoping that this will find us faster results
|
||||||
-- TODO: implement backtracking
|
-- TODO: implement backtracking
|
||||||
let mut bij := initBij
|
let mut bij := initBij
|
||||||
@@ -97,11 +97,11 @@ def matchDecls (patterns : Array Expr) (fvars : Array Expr) (strict := true) (in
|
|||||||
-- usedFvars := usedFvars.set! (fvars.size - j - 1) true
|
-- usedFvars := usedFvars.set! (fvars.size - j - 1) true
|
||||||
bij := bij'.insert pattern.fvarId! fvar.fvarId!
|
bij := bij'.insert pattern.fvarId! fvar.fvarId!
|
||||||
break
|
break
|
||||||
if ! bij.forward.contains pattern.fvarId! then return false
|
if ! bij.forward.contains pattern.fvarId! then return none
|
||||||
|
|
||||||
if strict then
|
if !strict || fvars.all (fun fvar => bij.backward.contains fvar.fvarId!)
|
||||||
return fvars.all (fun fvar => bij.backward.contains fvar.fvarId!)
|
then return some bij
|
||||||
return true
|
else return none
|
||||||
|
|
||||||
unsafe def evalHintMessageUnsafe : Expr → MetaM (Array Expr → MessageData) :=
|
unsafe def evalHintMessageUnsafe : Expr → MetaM (Array Expr → MessageData) :=
|
||||||
evalExpr (Array Expr → MessageData)
|
evalExpr (Array Expr → MessageData)
|
||||||
@@ -122,10 +122,11 @@ def findHints (goal : MVarId) (doc : FileWorker.EditableDocument) (initParams :
|
|||||||
if let some fvarBij := matchExpr (← instantiateMVars $ hintGoal) (← instantiateMVars $ ← inferType $ mkMVar goal)
|
if let some fvarBij := matchExpr (← instantiateMVars $ hintGoal) (← instantiateMVars $ ← inferType $ mkMVar goal)
|
||||||
then
|
then
|
||||||
let lctx := (← goal.getDecl).lctx
|
let lctx := (← goal.getDecl).lctx
|
||||||
if ← matchDecls hintFVars lctx.getFVars (strict := hint.strict) (initBij := fvarBij)
|
if let some bij ← matchDecls hintFVars lctx.getFVars (strict := hint.strict) (initBij := fvarBij)
|
||||||
then
|
then
|
||||||
let text := (← evalHintMessage hint.text) hintFVars
|
let userFVars := hintFVars.map fun v => bij.forward.findD v.fvarId! v.fvarId!
|
||||||
let ctx := {env := ← getEnv, mctx := ← getMCtx, lctx := ← getLCtx, opts := {}}
|
let text := (← evalHintMessage hint.text) (userFVars.map Expr.fvar)
|
||||||
|
let ctx := {env := ← getEnv, mctx := ← getMCtx, lctx := lctx, opts := {}}
|
||||||
let text ← (MessageData.withContext ctx text).toString
|
let text ← (MessageData.withContext ctx text).toString
|
||||||
return some { text := text, hidden := hint.hidden }
|
return some { text := text, hidden := hint.hidden }
|
||||||
else return none
|
else return none
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import Lean
|
||||||
|
|
||||||
|
open Lean Meta Elab Tactic Command
|
||||||
|
|
||||||
|
syntax showOptArg := atomic(" (" (&"strict" <|> &"verbose") " := " withoutPosition(term) ")")
|
||||||
|
|
||||||
|
/--
|
||||||
|
Lists all available options that can be used with `set_option`.
|
||||||
|
|
||||||
|
Use `set_option (verbose := true)` to display more than just the option names.
|
||||||
|
The form `#show_option id` will show only options that begin with `id`.
|
||||||
|
|
||||||
|
Copied from `#help option` from mathlib.
|
||||||
|
-/
|
||||||
|
elab "#show_option" verbose:(ppSpace showOptArg)? id:(ppSpace Parser.rawIdent)? : command => do
|
||||||
|
let id := id.map (·.raw.getId.toString false)
|
||||||
|
let mut decls : Lean.RBMap _ _ compare := {}
|
||||||
|
for (name, decl) in show Lean.RBMap .. from ← getOptionDecls do
|
||||||
|
let name := name.toString false
|
||||||
|
if let some id := id then
|
||||||
|
if !id.isPrefixOf name then
|
||||||
|
continue
|
||||||
|
decls := decls.insert name decl
|
||||||
|
let mut msg := Format.nil
|
||||||
|
let opts ← getOptions
|
||||||
|
if decls.isEmpty then
|
||||||
|
match id with
|
||||||
|
| some id => throwError "no options start with {id}"
|
||||||
|
| none => throwError "no options found (!)"
|
||||||
|
for (name, decl) in decls do
|
||||||
|
let mut msg1 := match decl.defValue with
|
||||||
|
| .ofString val => s!"String := {repr val}"
|
||||||
|
| .ofBool val => s!"Bool := {repr val}"
|
||||||
|
| .ofName val => s!"Name := {repr val}"
|
||||||
|
| .ofNat val => s!"Nat := {repr val}"
|
||||||
|
| .ofInt val => s!"Int := {repr val}"
|
||||||
|
| .ofSyntax val => s!"Syntax := {repr val}"
|
||||||
|
if let some val := opts.find name then
|
||||||
|
msg1 := s!"{msg1} (currently: {val})"
|
||||||
|
msg := match verbose with
|
||||||
|
| some opt =>
|
||||||
|
match opt with
|
||||||
|
| `(showOptArg| (verbose := true)) =>
|
||||||
|
msg ++ .nest 2 (f!"option {name} : {msg1}" ++ .line ++ decl.descr) ++ .line ++ .line
|
||||||
|
| _ =>
|
||||||
|
msg ++ (f!"{name}" ++ .line )
|
||||||
|
| none =>
|
||||||
|
msg ++ (f!"{name}" ++ .line )
|
||||||
|
|
||||||
|
logInfo msg
|
||||||
@@ -91,7 +91,7 @@ def createEnv (gameDir : String) (module : String) : IO Environment := do
|
|||||||
-- Set the search path
|
-- Set the search path
|
||||||
Lean.searchPathRef.set paths
|
Lean.searchPathRef.set paths
|
||||||
|
|
||||||
let env ← importModules [{ module := `Init : Import }, { module := module : Import }] {} 0
|
let env ← importModules #[{ module := `Init : Import }, { module := module : Import }] {} 0
|
||||||
return env
|
return env
|
||||||
|
|
||||||
def initAndRunWatchdog (args : List String) (i o e : FS.Stream) : IO Unit := do
|
def initAndRunWatchdog (args : List String) (i o e : FS.Stream) : IO Unit := do
|
||||||
@@ -114,7 +114,7 @@ def initAndRunWatchdog (args : List String) (i o e : FS.Stream) : IO Unit := do
|
|||||||
game := gameName,
|
game := gameName,
|
||||||
gameDir := gameDir,
|
gameDir := gameDir,
|
||||||
inventory := #[]
|
inventory := #[]
|
||||||
checkEnabled := false
|
difficulty := 0
|
||||||
}
|
}
|
||||||
let initRequest ← i.readLspRequestAs "initialize" InitializeParams
|
let initRequest ← i.readLspRequestAs "initialize" InitializeParams
|
||||||
-- We misuse the `rootUri` field to the gameName
|
-- We misuse the `rootUri` field to the gameName
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import GameServer.FileWorker
|
import GameServer.FileWorker
|
||||||
import GameServer.Watchdog
|
import GameServer.Watchdog
|
||||||
|
import GameServer.Commands
|
||||||
|
|
||||||
|
-- TODO: The only reason we import `Commands` is so that it gets built to on `lake build`
|
||||||
|
-- should we have a different solution?
|
||||||
|
|
||||||
unsafe def main : List String → IO UInt32 := fun args => do
|
unsafe def main : List String → IO UInt32 := fun args => do
|
||||||
let e ← IO.getStderr
|
let e ← IO.getStderr
|
||||||
|
|||||||
@@ -1,164 +0,0 @@
|
|||||||
import { WebSocketServer } from 'ws';
|
|
||||||
import express from 'express'
|
|
||||||
import path from 'path'
|
|
||||||
import * as cp from 'child_process';
|
|
||||||
import * as url from 'url';
|
|
||||||
import * as rpc from 'vscode-ws-jsonrpc';
|
|
||||||
import * as jsonrpcserver from 'vscode-ws-jsonrpc/server';
|
|
||||||
import os from 'os';
|
|
||||||
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 = {
|
|
||||||
"g/hhu-adam/robo": {
|
|
||||||
// module: "Game", // The lean module's name. Defaults to "Game"
|
|
||||||
// name: "Adam", // For the `Game "Adam"` tag in the games. Defaults to "MyGame"
|
|
||||||
dir: "Robo",
|
|
||||||
queueLength: 5
|
|
||||||
},
|
|
||||||
"g/hhu-adam/nng4": {
|
|
||||||
// module: "Game",
|
|
||||||
// name: "NNG",
|
|
||||||
dir: "NNG4",
|
|
||||||
queueLength: 5
|
|
||||||
},
|
|
||||||
"g/local/game": {
|
|
||||||
dir: "game",
|
|
||||||
queueLength: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const __filename = url.fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
|
||||||
|
|
||||||
const app = express()
|
|
||||||
|
|
||||||
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
|
|
||||||
.use(express.static(path.join(__dirname, '../client/dist/')))
|
|
||||||
.use('/', router)
|
|
||||||
.listen(PORT, () => console.log(`Listening on ${PORT}`));
|
|
||||||
|
|
||||||
const wss = new WebSocketServer({ server })
|
|
||||||
|
|
||||||
var socketCounter = 0
|
|
||||||
|
|
||||||
const environment = process.env.NODE_ENV
|
|
||||||
const isDevelopment = environment === 'development'
|
|
||||||
|
|
||||||
/** We keep queues of started Lean Server processes to be ready when a user arrives */
|
|
||||||
const queue = {}
|
|
||||||
|
|
||||||
function startServerProcess(tag) {
|
|
||||||
if (! games[tag]?.dir) {
|
|
||||||
console.error(`Unknown game: ${tag}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let serverProcess
|
|
||||||
if (isDevelopment) {
|
|
||||||
let args = ["--server", path.join("../../../../", games[tag].dir)]
|
|
||||||
if (games[tag].module) {
|
|
||||||
args.push(games[tag].module)
|
|
||||||
if (games[tag].name) { args.push(games[tag].name) }
|
|
||||||
}
|
|
||||||
serverProcess = cp.spawn("./gameserver", args,
|
|
||||||
{ cwd: path.join(__dirname, "./build/bin/") })
|
|
||||||
} else {
|
|
||||||
serverProcess = cp.spawn("./bubblewrap.sh",
|
|
||||||
[games[tag].dir],
|
|
||||||
{ cwd: __dirname })
|
|
||||||
}
|
|
||||||
serverProcess.on('error', error =>
|
|
||||||
console.error(`Launching Lean Server failed: ${error}`)
|
|
||||||
);
|
|
||||||
if (serverProcess.stderr !== null) {
|
|
||||||
serverProcess.stderr.on('data', data =>
|
|
||||||
console.error(`Lean Server: ${data}`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return serverProcess
|
|
||||||
}
|
|
||||||
|
|
||||||
/** start Lean Server processes to refill the queue */
|
|
||||||
function fillQueue(tag) {
|
|
||||||
while (queue[tag].length < games[tag].queueLength) {
|
|
||||||
const serverProcess = startServerProcess(tag)
|
|
||||||
queue[tag].push(serverProcess)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isDevelopment) { // Don't use queue in development
|
|
||||||
for (let tag in games) {
|
|
||||||
queue[tag] = []
|
|
||||||
fillQueue(tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const urlRegEx = /^\/websocket\/g\/([\w.-]+)\/([\w.-]+)$/
|
|
||||||
|
|
||||||
wss.addListener("connection", function(ws, req) {
|
|
||||||
const reRes = urlRegEx.exec(req.url)
|
|
||||||
if (!reRes) { console.error(`Connection refused because of invalid URL: ${req.url}`); return; }
|
|
||||||
const owner = reRes[1]
|
|
||||||
const repo = reRes[2]
|
|
||||||
const tag = `g/${owner.toLowerCase()}/${repo.toLowerCase()}`
|
|
||||||
|
|
||||||
if (isDevelopment && process.env.DEV_CONTAINER) {
|
|
||||||
tag = `g/local/game`
|
|
||||||
}
|
|
||||||
|
|
||||||
let ps;
|
|
||||||
if (!queue[tag] || queue[tag].length == 0) {
|
|
||||||
ps = startServerProcess(tag)
|
|
||||||
} else {
|
|
||||||
ps = queue[tag].shift() // Pick the first Lean process; it's likely to be ready immediately
|
|
||||||
fillQueue(tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
socketCounter += 1;
|
|
||||||
const ip = anonymize(req.headers['x-forwarded-for'] || req.socket.remoteAddress)
|
|
||||||
console.log(`[${new Date()}] Socket opened - ${ip}`)
|
|
||||||
|
|
||||||
const socket = {
|
|
||||||
onMessage: (cb) => { ws.on("message", cb) },
|
|
||||||
onError: (cb) => { ws.on("error", cb) },
|
|
||||||
onClose: (cb) => { ws.on("close", cb) },
|
|
||||||
send: (data, cb) => { ws.send(data,cb) }
|
|
||||||
}
|
|
||||||
const reader = new rpc.WebSocketMessageReader(socket);
|
|
||||||
const writer = new rpc.WebSocketMessageWriter(socket);
|
|
||||||
const socketConnection = jsonrpcserver.createConnection(reader, writer, () => ws.close())
|
|
||||||
const serverConnection = jsonrpcserver.createProcessStreamConnection(ps);
|
|
||||||
socketConnection.forward(serverConnection, message => {
|
|
||||||
if (isDevelopment) {console.log(`CLIENT: ${JSON.stringify(message)}`)}
|
|
||||||
return message;
|
|
||||||
});
|
|
||||||
serverConnection.forward(socketConnection, message => {
|
|
||||||
if (isDevelopment) {console.log(`SERVER: ${JSON.stringify(message)}`)}
|
|
||||||
return message;
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`[${new Date()}] Number of open sockets - ${socketCounter}`)
|
|
||||||
console.log(`[${new Date()}] Free RAM - ${Math.round(os.freemem() / 1024 / 1024)} / ${Math.round(os.totalmem() / 1024 / 1024)} MB`)
|
|
||||||
|
|
||||||
ws.on('close', () => {
|
|
||||||
console.log(`[${new Date()}] Socket closed - ${ip}`)
|
|
||||||
socketCounter -= 1;
|
|
||||||
})
|
|
||||||
|
|
||||||
socketConnection.onClose(() => serverConnection.dispose());
|
|
||||||
serverConnection.onClose(() => socketConnection.dispose());
|
|
||||||
})
|
|
||||||
@@ -1 +1,14 @@
|
|||||||
{"version": 5, "packagesDir": "lake-packages", "packages": []}
|
{"version": 7,
|
||||||
|
"packagesDir": ".lake/packages",
|
||||||
|
"packages":
|
||||||
|
[{"url": "https://github.com/leanprover/std4.git",
|
||||||
|
"type": "git",
|
||||||
|
"subDir": null,
|
||||||
|
"rev": "2e4a3586a8f16713f16b2d2b3af3d8e65f3af087",
|
||||||
|
"name": "std",
|
||||||
|
"manifestFile": "lake-manifest.json",
|
||||||
|
"inputRev": "v4.3.0",
|
||||||
|
"inherited": false,
|
||||||
|
"configFile": "lakefile.lean"}],
|
||||||
|
"name": "GameServer",
|
||||||
|
"lakeDir": ".lake"}
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ open Lake DSL
|
|||||||
|
|
||||||
package GameServer
|
package GameServer
|
||||||
|
|
||||||
|
-- Using this assumes that each dependency has a tag of the form `v4.X.0`.
|
||||||
|
def leanVersion : String := s!"v{Lean.versionString}"
|
||||||
|
|
||||||
|
require std from git "https://github.com/leanprover/std4.git" @ leanVersion
|
||||||
|
|
||||||
lean_lib GameServer
|
lean_lib GameServer
|
||||||
|
|
||||||
@[default_target]
|
@[default_target]
|
||||||
@@ -10,3 +15,13 @@ lean_exe gameserver {
|
|||||||
root := `Main
|
root := `Main
|
||||||
supportInterpreter := true
|
supportInterpreter := true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/--
|
||||||
|
When a package depending on GameServer updates its dependencies,
|
||||||
|
build the `gameserver` executable.
|
||||||
|
-/
|
||||||
|
post_update pkg do
|
||||||
|
let rootPkg ← getRootPackage
|
||||||
|
if rootPkg.name = pkg.name then
|
||||||
|
return -- do not run in GameServer itself
|
||||||
|
discard <| runBuild gameserver.build >>= (·.await)
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
leanprover/lean4:v4.0.0
|
leanprover/lean4:v4.3.0
|
||||||
|
|||||||
@@ -13,13 +13,16 @@ Level 1
|
|||||||
Statement foo.bar : 5 ≤ 7 := by
|
Statement foo.bar : 5 ≤ 7 := by
|
||||||
simp
|
simp
|
||||||
|
|
||||||
-- Shows warning on `foo.bar₂`:
|
Game "Test"
|
||||||
|
World "TestW"
|
||||||
|
Level 2 -- should warn if set to `1`
|
||||||
|
|
||||||
|
-- Shows warning on `foo.bar₂`:
|
||||||
Statement foo.bar2 : 3 ≤ 7 := by
|
Statement foo.bar2 : 3 ≤ 7 := by
|
||||||
simp
|
simp
|
||||||
|
|
||||||
|
|
||||||
NewLemma foo.baz
|
NewLemma foo.bar
|
||||||
DisabledTactic tauto
|
DisabledTactic tauto
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
#/bin/bash
|
|
||||||
|
|
||||||
ARTIFACT_ID=$1
|
|
||||||
|
|
||||||
echo "Unpacking ZIP."
|
|
||||||
unzip -o tmp/artifact_${ARTIFACT_ID}.zip -d tmp/artifact_${ARTIFACT_ID}
|
|
||||||
echo "Unpacking TAR."
|
|
||||||
for f in tmp/artifact_${ARTIFACT_ID}/* #Should only be one file
|
|
||||||
do
|
|
||||||
echo "Unpacking $f"
|
|
||||||
mkdir tmp/artifact_${ARTIFACT_ID}_inner
|
|
||||||
tar -xvf $f -C tmp/artifact_${ARTIFACT_ID}_inner
|
|
||||||
done
|
|
||||||
+1
-1
@@ -12,5 +12,5 @@
|
|||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
},
|
},
|
||||||
"exclude": ["server"]
|
"exclude": ["server", "relay"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react-swc'
|
||||||
|
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
||||||
|
import svgr from "vite-plugin-svgr"
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
//root: 'client/src',
|
||||||
|
build: {
|
||||||
|
// Relative to the root
|
||||||
|
// Note: This has to match the path in `relay/index.mjs`
|
||||||
|
outDir: 'client/dist',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
svgr({
|
||||||
|
svgrOptions: {
|
||||||
|
// svgr options
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
viteStaticCopy({
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
src: 'node_modules/@leanprover/infoview/dist/*.production.min.js',
|
||||||
|
dest: '.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
],
|
||||||
|
publicDir: "client/public",
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: ['games']
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
proxy: {
|
||||||
|
'/websocket': {
|
||||||
|
target: 'ws://localhost:8080',
|
||||||
|
ws: true
|
||||||
|
},
|
||||||
|
'/import': {
|
||||||
|
target: 'http://localhost:8080',
|
||||||
|
},
|
||||||
|
'/data': {
|
||||||
|
target: 'http://localhost:8080',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
path: "path-browserify",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
const path = require("path");
|
|
||||||
const webpack = require('webpack');
|
|
||||||
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
|
|
||||||
const WebpackShellPluginNext = require('webpack-shell-plugin-next');
|
|
||||||
|
|
||||||
module.exports = env => {
|
|
||||||
|
|
||||||
const single_game = process.env.LEAN4GAME_SINGLE_GAME
|
|
||||||
|
|
||||||
|
|
||||||
const environment = process.env.NODE_ENV
|
|
||||||
const isDevelopment = environment === 'development'
|
|
||||||
|
|
||||||
const babelOptions = {
|
|
||||||
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'],
|
|
||||||
plugins: [
|
|
||||||
isDevelopment && require.resolve('react-refresh/babel'),
|
|
||||||
].filter(Boolean),
|
|
||||||
};
|
|
||||||
|
|
||||||
global.$RefreshReg$ = () => {};
|
|
||||||
global.$RefreshSig$ = () => () => {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
entry: [single_game ? "./client/src/index_local.tsx" : "./client/src/index.tsx"],
|
|
||||||
mode: isDevelopment ? 'development' : 'production',
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.(js|jsx)$/,
|
|
||||||
exclude: [/server/, /node_modules/],
|
|
||||||
use: [{
|
|
||||||
loader: require.resolve('babel-loader'),
|
|
||||||
options: babelOptions,
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.tsx?$/,
|
|
||||||
use: [{
|
|
||||||
loader: 'ts-loader',
|
|
||||||
options: { allowTsInNodeModules: true }
|
|
||||||
}],
|
|
||||||
// exclude: /node_modules(?!\/(lean4web|lean4|lean4-infoview))/,
|
|
||||||
// Allow .ts imports from node_modules/lean4web and node_modules/lean4
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.css$/,
|
|
||||||
use: ["style-loader", "css-loader"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(jpg|png)$/,
|
|
||||||
use: {
|
|
||||||
loader: 'file-loader',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: ["*", ".js", ".jsx", ".tsx", ".ts"],
|
|
||||||
fallback: {
|
|
||||||
"http": require.resolve("stream-http") ,
|
|
||||||
"path": require.resolve("path-browserify")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
path: path.resolve(__dirname, "client/dist/"),
|
|
||||||
filename: "bundle.js",
|
|
||||||
},
|
|
||||||
devServer: {
|
|
||||||
proxy: {
|
|
||||||
'/websocket': {
|
|
||||||
target: 'ws://localhost:8080',
|
|
||||||
ws: true
|
|
||||||
},
|
|
||||||
'/import': {
|
|
||||||
target: 'http://localhost:3000',
|
|
||||||
router: () => 'http://localhost:8080',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
static: path.join(__dirname, 'client/public/'),
|
|
||||||
port: 3000,
|
|
||||||
hot: true,
|
|
||||||
},
|
|
||||||
devtool: "source-map",
|
|
||||||
plugins: [
|
|
||||||
!isDevelopment && new WebpackShellPluginNext({
|
|
||||||
onBuildEnd:{
|
|
||||||
scripts: [
|
|
||||||
// It's hard to set up webpack to copy the index.html correctly,
|
|
||||||
// so we copy it explicitly after every build:
|
|
||||||
'cp client/public/index.html client/dist/',
|
|
||||||
// Similarly, I haven't been able to load `onigasm.wasm` properly:
|
|
||||||
'cp client/public/onigasm.wasm client/dist/',],
|
|
||||||
blocking: false,
|
|
||||||
parallel: true
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
isDevelopment && new ReactRefreshWebpackPlugin(),
|
|
||||||
].filter(Boolean),
|
|
||||||
|
|
||||||
// Webpack is not happy about the dynamically loaded widget code in the function
|
|
||||||
// `dynamicallyLoadComponent` in `infoview/userWidget.tsx`. If we want to support
|
|
||||||
// dynamically loaded widget code, we need to make sure that the files are available.
|
|
||||||
ignoreWarnings: [/Critical dependency: the request of a dependency is an expression/]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user