Compare commits

..
Author SHA1 Message Date
joneugster 8c39fb6664 cleanup; including using doc comment syntax for documentation like TacticDoc 2023-12-11 19:38:58 +01:00
Jon Eugster 527f58e3a4 separate lean server from socket server 2023-12-09 22:42:40 +01:00
Jon Eugster b239a5d3dc Merge branch 'main' into dev 2023-12-09 22:09:58 +01:00
Jon Eugster 63cf5e8b72 update docs 2023-12-09 22:09:05 +01:00
Jon Eugster 25f166f57f do not filter hidden hints #142 2023-12-09 22:06:12 +01:00
Jon Eugster c89e2e4020 remove consequtive identical hints #142 2023-12-09 13:27:03 +01:00
Jon Eugster f6738faf46 golf 2023-12-09 11:14:55 +01:00
Jon Eugster cb7224934c fix cwd for gameserver 2023-12-08 21:06:31 +01:00
Jon Eugster 13d54ff0ff fix gameserver path 2023-12-08 20:38:35 +01:00
Jon Eugster 72e4011c62 update vite 2023-12-08 18:35:29 +01:00
Jon Eugster 4f5256fa88 fix bubblewrap script 2023-12-08 18:32:45 +01:00
Jon Eugster c2b9175fe5 use the gameserver of each game individually 2023-12-08 18:14:35 +01:00
Jon Eugster a1a6862b5a add tmp option to test images 2023-12-08 10:18:00 +01:00
Jon Eugster 0a057913be update landing page 2023-12-08 03:16:26 +01:00
Jon Eugster bedb2ad5ec Update README.md 2023-12-08 02:22:10 +01:00
Jon Eugster e02e73c1c0 Update DOCUMENTATION.md 2023-12-08 02:17:30 +01:00
Jon Eugster c82a88867f Update create_game.md 2023-12-08 02:12:09 +01:00
Jon Eugster 8b43aed596 Update publish_game.md 2023-12-08 01:57:47 +01:00
Jon Eugster 8c84d3fae7 Merge branch 'dev' 2023-12-08 01:54:02 +01:00
Jon Eugster d5697d052e images folder might not exist 2023-12-08 00:58:17 +01:00
Jon Eugster 0e652256f8 Revert "update lean4web dependency"
This reverts commit 6bced7575b.
2023-12-08 00:51:28 +01:00
Jon Eugster 9492c1011a Revert "use pre-cleanup commit of lean4web"
This reverts commit 3f8b180b04.
2023-12-08 00:51:17 +01:00
Jon Eugster 54bab2a016 add support for images 2023-12-08 00:39:45 +01:00
Alexander Bentkamp 3f8b180b04 use pre-cleanup commit of lean4web 2023-12-07 21:38:51 +01:00
Jon Eugster 23b1074aa2 cleanup tmp files after import 2023-12-07 18:09:12 +01:00
Jon Eugster 0964a06f2f Merge pull request #159 from Wzixiao/cache-typewriter-mode
Add the function of caching typewriterMode
2023-12-07 16:00:19 +01:00
ran b17c8fc4cb remove \n in progress.ts 211 line 2023-12-07 21:45:47 +08:00
ran 0c4ae92856 Optimize the typewriterMode code logic about the game 2023-12-07 21:43:50 +08:00
ran 088711b5d1 Use 'progress' to construct the processing flow of typewriterMode 2023-12-07 21:38:15 +08:00
Jon Eugster 4c93b3a091 Update running_locally.md 2023-12-07 14:10:03 +01:00
ran aa00e359c4 Add the function of caching typewriterMode 2023-12-07 15:06:58 +08:00
joneugster f6a2632d80 fix error message on importing non-existing game tile 2023-12-07 02:29:56 +01:00
joneugster f2190d648f fix manual dependencies being added twice to svg 2023-12-07 02:28:57 +01:00
joneugster d7fd8709cb fix inventory sorting #158 2023-12-07 02:27:24 +01:00
joneugster 7a03c4fe0d move landing page tiles to the games 2023-12-06 18:21:37 +01:00
Jon Eugster 333c9498f1 Update update_game.md 2023-12-01 14:50:27 +01:00
joneugster 121b36b542 bump to v4.3.0 2023-12-01 12:19:58 +01:00
Alexander Bentkamp 6bced7575b update lean4web dependency 2023-12-01 11:32:17 +01:00
Alexander Bentkamp d18b48db2f make json files directly accessible 2023-11-30 20:55:03 +01:00
Alexander Bentkamp 97dc648452 use goal lctx for hints
Closes  #135
2023-11-30 20:38:25 +01:00
joneugster 472e2c66df remove old-nng tile 2023-11-30 20:29:47 +01:00
39 changed files with 974 additions and 568 deletions
-1
View File
@@ -1,5 +1,4 @@
node_modules node_modules
games/
client/dist client/dist
games/ games/
server/.lake server/.lake
+16 -11
View File
@@ -10,20 +10,25 @@ Please follow the tutorial [Creating a Game](doc/create_game.md). In particular,
* Step 7: [How to Update an existing Game](doc/update_game.md) * Step 7: [How to Update an existing Game](doc/update_game.md)
* Step 8: [How to Publishing a Game](doc/publish_game.md) * Step 8: [How to Publishing a Game](doc/publish_game.md)
### Publishing a Game
We encourage anybody to have games hosted on our [Lean Game Server](https://adam.math.hhu.de) for anybody to play. For that you simply need to contact us with the link to your game repo. We are also happy to add work-in-progress games and games in any language.
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).
## Documentation ## Documentation
The documentation for the game engine itself is still missing, but there is [Creating a Game](doc/create_game.md) explaining the API to create a game. The documentation is very much work in progress but the linked documentation here
should be up-to-date:
Some documentation: ### Game creation API
- [NPM Scripts](doc/npm_scripts.md) - [Creating a Game](doc/create_game.md): **the main document to consult**.
- [Old documentation](doc/DOCUMENTATION.md) - [More about Hints](doc/hints.md): describes the `Hint` and `Branch` tactic.
### Frontend API
* [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 ## Contributing
@@ -37,5 +42,5 @@ Providing the use access to a Lean instance running on the server is a severe se
The project is based on ideas from the [Lean Game Maker](https://github.com/mpedramfar/Lean-game-maker) and the [Natural Number Game 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/) (NNG)](https://www.ma.imperial.ac.uk/~buzzard/xena/natural_number_game/)
of Kevin Buzzard and Mohammad Pedramfar. by Kevin Buzzard and Mohammad Pedramfar.
The project is based on Patrick Massot's prototype: [NNG4](https://github.com/PatrickMassot/NNG4). The project is based on Patrick Massot's prototype: [NNG4](https://github.com/PatrickMassot/NNG4).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 KiB

+22
View File
@@ -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))
}
})
}
+4 -4
View File
@@ -34,7 +34,7 @@ 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.
@@ -156,7 +156,7 @@ export function Main(props: { world: string, level: number, data: LevelInfo}) {
const completed = useAppSelector(selectCompleted(gameId, props.world, props.level)) const completed = useAppSelector(selectCompleted(gameId, props.world, props.level))
console.debug(`template: ${props.data.template}`) console.debug(`template: ${props.data?.template}`)
// React.useEffect (() => { // React.useEffect (() => {
// if (props.data.template) { // if (props.data.template) {
@@ -367,9 +367,9 @@ export function TypewriterInterface({props}) {
function deleteProof(line: number) { function deleteProof(line: number) {
return (ev) => { return (ev) => {
let deletedChat: Array<GameHint> = [] let deletedChat: Array<GameHint> = []
proof.slice(line).map((step, i) => { filterHints(proof).slice(line).map((hintsAtStep, i) => {
// Only add these hidden hints to the deletion stack which were visible // Only add these hidden hints to the deletion stack which were visible
deletedChat = [...deletedChat, ...step.hints.filter(hint => (!hint.hidden || showHelp.has(line + i)))] deletedChat = [...deletedChat, ...hintsAtStep.filter(hint => (!hint.hidden || showHelp.has(line + i)))]
}) })
setDeletedChat(deletedChat) setDeletedChat(deletedChat)
+1 -1
View File
@@ -86,7 +86,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})}}
+91 -79
View File
@@ -7,12 +7,12 @@ import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css'; import '@fontsource/roboto/700.css';
import '../css/landing_page.css' import '../css/landing_page.css'
import coverRobo from '../assets/covers/formaloversum.png'
import coverNNG from '../assets/covers/nng.png'
import bgImage from '../assets/bg.jpg' import bgImage from '../assets/bg.jpg'
import Markdown from './markdown'; import Markdown from './markdown';
import {PrivacyPolicyPopup} from './popup/privacy_policy' import {PrivacyPolicyPopup} from './popup/privacy_policy'
import { GameTile, useGetGameInfoQuery } from '../state/api'
import path from 'path';
const flag = { const flag = {
'Dutch': '🇳🇱', 'Dutch': '🇳🇱',
@@ -20,6 +20,7 @@ const flag = {
'French': '🇫🇷', 'French': '🇫🇷',
'German': '🇩🇪', 'German': '🇩🇪',
'Italian': '🇮🇹', 'Italian': '🇮🇹',
'Spanish': '🇪🇸',
} }
function GithubIcon({url='https://github.com'}) { function GithubIcon({url='https://github.com'}) {
@@ -33,47 +34,42 @@ 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='&ndash;', // 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> <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> </tbody>
</table> </table>
@@ -89,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})`}}>
@@ -105,60 +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="8" />
levels="67" ))
image={coverNNG} }
language="English"
/>
<GameTile
title="Set Theory Game"
gameId="g/djvelleman/STG4"
intro="A game about set theory"
description=""
worlds="5"
levels="30"
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"
/>
<GameTile
title="NNG (OLD)"
gameId="g/hhu-adam/nng4-old"
intro="The old version of the NNG copied from lean3."
description="This version is not maintained and might break at any point. You should play the new version instead"
worlds="9"
levels="72"
language="English"
/>
</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
@@ -172,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 directly 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>
@@ -201,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>
} }
+29 -10
View File
@@ -22,7 +22,7 @@ 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'
@@ -32,8 +32,9 @@ import { DeletedChatContext, InputModeContext, MobileContext, MonacoEditorContex
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 { PrivacyPolicyPopup } from './popup/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'
@@ -137,19 +138,24 @@ function ChatPanel({lastLevel}) {
let introText: Array<string> = level?.data?.introduction.split(/\n(\s*\n)+/) 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">
{introText?.filter(t => t.trim()).map(((t, i) => {introText?.filter(t => t.trim()).map(((t, i) =>
// Show the level's intro text as hints, too
<Hint key={`intro-p-${i}`} <Hint key={`intro-p-${i}`}
hint={{text: t, hidden: false}} step={0} selected={selectedStep} toggleSelection={toggleSelection(0)} /> 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}/>
} }
})} })}
@@ -203,11 +209,16 @@ function PlayableLevel({impressum, setImpressum}) {
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})
@@ -220,12 +231,11 @@ function PlayableLevel({impressum, setImpressum}) {
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 [typewriterMode, setTypewriterMode] = useState(true)
// set to true to prevent switching between typewriter and editor // set to true to prevent switching between typewriter and editor
const [lockInputMode, setLockInputMode] = useState(false) const [lockInputMode, setLockInputMode] = useState(false)
const [typewriterInput, setTypewriterInput] = useState("") 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
function toggleImpressum() {setImpressum(!impressum)} function toggleImpressum() {setImpressum(!impressum)}
@@ -319,8 +329,6 @@ function PlayableLevel({impressum, setImpressum}) {
console.debug(`not inserting template.`) console.debug(`not inserting template.`)
} }
} }
} else {
setTypewriterMode(true)
} }
}, [level, levelId, worldId, gameId, editor]) }, [level, levelId, worldId, gameId, editor])
@@ -335,7 +343,7 @@ function PlayableLevel({impressum, setImpressum}) {
}, [gameId, worldId, levelId]) }, [gameId, worldId, levelId])
useEffect(() => { useEffect(() => {
if (!typewriterMode) { if (!typewriterMode && editor) {
// Delete last input attempt from command line // Delete last input attempt from command line
editor.executeEdits("typewriter", [{ editor.executeEdits("typewriter", [{
range: editor.getSelection(), range: editor.getSelection(),
@@ -465,6 +473,11 @@ function Introduction({impressum, setImpressum}) {
const gameInfo = useGetGameInfoQuery({game: gameId}) const gameInfo = useGetGameInfoQuery({game: gameId})
const {worldId} = useContext(WorldLevelIdContext)
let image: string = gameInfo.data?.worlds.nodes[worldId].image
const toggleImpressum = () => { const toggleImpressum = () => {
setImpressum(!impressum) setImpressum(!impressum)
} }
@@ -478,7 +491,13 @@ function Introduction({impressum, setImpressum}) {
: :
<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} /> <IntroductionPanel gameInfo={gameInfo} />
<div className="world-image-container empty"></div> <div className="world-image-container empty">
{image &&
// TODO: Temporary for testing
<img className={worldId=="Proposition" ? "cover" : "contain"} src={path.join("data", gameId, image)} alt="" />
}
</div>
<InventoryPanel levelInfo={inventory?.data} /> <InventoryPanel levelInfo={inventory?.data} />
</Split> </Split>
} }
+15
View File
@@ -336,6 +336,21 @@ td code {
background-color: #eee; background-color: #eee;
} }
.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 { .typewriter-interface .proof {
background-color: #fff; background-color: #fff;
} }
+22 -7
View File
@@ -3,14 +3,28 @@
*/ */
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
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 +50,8 @@ export interface LevelInfo {
lemmaTab: null|string, lemmaTab: null|string,
statementName: null|string, statementName: null|string,
displayName: null|string, displayName: null|string,
template: 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 */
@@ -59,19 +74,19 @@ interface Doc {
// 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: fetchBaseQuery({ baseUrl: window.location.origin + "/api" }), 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}) => `${game}/game`, 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}) => `${game}/level/${world}/${level}`, query: ({game, world, level}) => `${game}/level__${world}__${level}.json`,
}), }),
loadInventoryOverview: builder.query<InventoryOverview, {game: string}>({ loadInventoryOverview: builder.query<InventoryOverview, {game: string}>({
query: ({game}) => `${game}/inventory`, 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, type, name}) => `${game}/doc/${type}/${name}`, query: ({game, type, name}) => `${game}/doc__${type}__${name}.json`,
}), }),
}), }),
}) })
+15 -2
View File
@@ -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
+5 -2
View File
@@ -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
@@ -295,8 +297,9 @@ 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 ```typescript
const games = { const games = {
"g/hhu-adam/robo": { "g/hhu-adam/robo": {
+22 -15
View File
@@ -223,25 +223,17 @@ One thing to keep in mind is that the game will look at the main proof to figure
Most important for game development are probably the `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 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. If you specify 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.
Hint (strict := true) "some hidden hint"
```
a hint only matches iff the assumptions match exactly one-to-one. (Otherwise, it does not care if there are additional assumptions in context) ### 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.
Further, you can choose to hide hints and only have them displayed when the player presses "More Help": 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).
Hint (hidden := true) "some hidden hint"
```
Lastly, you should put variable names in hints inside brackets: NOTE: At present, only the images for a world are displayed. They appear in the introduction of the world.
```
Hint "now use `rw [{h}]` to use your assumption {h}."
```
That way, the game will replace it with the actual name the assumption has in the player's proof state.
## 7. Update your game ## 7. Update your game
@@ -251,6 +243,21 @@ In principle, it is as simple as modifying `lean-toolchain` to update your game
To publish your game on the official server, see [Publishing a game](doc/publish_game.md) 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 ## Further Notes
Here are some random further things you should consider designing a new game: Here are some random further things you should consider designing a new game:
+87
View File
@@ -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.
+2
View File
@@ -28,3 +28,5 @@ Now you can immediately play the game at `adam.math.hhu.de/#/g/{USER}/{REPOSITOR
Adding games to the main page happens manually by the server maintainers. Tell us if you want us 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! 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).
+1 -2
View File
@@ -86,11 +86,10 @@ Download dependencies and build the game:
```bash ```bash
cd GameSkeleton cd GameSkeleton
lake update -R lake update -R
lake exe cache get # if your game depends on mathlib
lake build lake build
``` ```
Clone the game repository into a directory next to the game: Clone the server repository into a directory next to the game:
```bash ```bash
cd .. cd ..
git clone https://github.com/leanprover-community/lean4game.git git clone https://github.com/leanprover-community/lean4game.git
+3 -4
View File
@@ -10,15 +10,14 @@ Before you continue, make sure there [exists a `v4.X.0`-tag in this repo](https:
Then, depending on the setup you use, do one of the following: Then, depending on the setup you use, do one of the following:
* Dev Container: Rebuild the VSCode Devcontainer. * Dev Container: Rebuild the VSCode Devcontainer.
* Local Setup: run * Local Setup: in your game's folder run the following:
``` ```
lake update -R lake update -R
lake build lake build
``` ```
in your game folder.
* Additionally, if you have a local copy of the server `lean4game`, * Additionally, if you have a local copy of the server `lean4game`,
you should update this one to the matching version, too: you should update this one to the matching version. Run the following in the folder `lean4game/`:
``` ```
git fetch git fetch
git checkout {VERSION_TAG} git checkout {VERSION_TAG}
@@ -27,7 +26,7 @@ Then, depending on the setup you use, do one of the following:
where `{VERSION_TAG}` is the tag from above of the form `v4.X.0` where `{VERSION_TAG}` is the tag from above of the form `v4.X.0`
* Gitpod/Codespaces: Create a fresh one * Gitpod/Codespaces: Create a fresh one
This will your game (and the mathlib version you might be using) to the new lean version. This will update your game (and the mathlib version you might be using) to the new lean version.
## Newest developing setup ## Newest developing setup
+1 -1
View File
@@ -2,7 +2,7 @@
module.exports = { module.exports = {
apps : [{ apps : [{
name : "lean4game", name : "lean4game",
script : "server/index.mjs", script : "relay/index.mjs",
env: { env: {
LEAN4GAME_GITHUB_USER: "", LEAN4GAME_GITHUB_USER: "",
LEAN4GAME_GITHUB_TOKEN: "", LEAN4GAME_GITHUB_TOKEN: "",
+3 -3
View File
@@ -16122,9 +16122,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "4.5.0", "version": "4.5.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz",
"integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", "integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==",
"dependencies": { "dependencies": {
"esbuild": "^0.18.10", "esbuild": "^0.18.10",
"postcss": "^8.4.27", "postcss": "^8.4.27",
+2 -2
View File
@@ -63,13 +63,13 @@
}, },
"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 vite --host", "start_client": "cross-env NODE_ENV=development vite --host",
"build": "npm run build_server && npm run build_client", "build": "npm run build_server && npm run build_client",
"preview": "vite preview", "preview": "vite preview",
"build_server": "cd server && lake build", "build_server": "cd server && lake build",
"build_client": "cross-env NODE_ENV=production vite build", "build_client": "cross-env NODE_ENV=production vite build",
"production": "cross-env NODE_ENV=production node server/index.mjs" "production": "cross-env NODE_ENV=production node relay/index.mjs"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
+4 -2
View File
@@ -2,7 +2,9 @@
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\
--bind $2 /lean4game \ --bind $2 /lean4game \
@@ -24,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/.lake/build/bin/" \ --chdir "/game/.lake/packages/GameServer/server/.lake/build/bin/" \
./gameserver --server /game ./gameserver --server /game
) )
+11 -12
View File
@@ -79,17 +79,17 @@ async function doImport (owner, repo, id) {
artifactId = artifact.id artifactId = artifact.id
const url = artifact.archive_download_url const url = artifact.archive_download_url
// Make sure the download folder exists // Make sure the download folder exists
if (!fs.existsSync(`${__dirname}/../games`)){ if (!fs.existsSync(path.join(__dirname, "..", "games"))){
fs.mkdirSync(`${__dirname}/../games`); fs.mkdirSync(path.join(__dirname, "..", "games"));
} }
if (!fs.existsSync(`${__dirname}/../games/tmp`)){ if (!fs.existsSync(path.join(__dirname, "..", "games", "tmp"))){
fs.mkdirSync(`${__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, `${__dirname}/../games/tmp/${owner.toLowerCase()}_${repo.toLowerCase()}_${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, owner.toLowerCase(), repo.toLowerCase()], `${__dirname}/..`) await runProcess(id, "/bin/bash", [path.join(__dirname, "unpack.sh"), artifactId, owner.toLowerCase(), repo.toLowerCase()], path.join(__dirname, ".."))
// let manifest = fs.readFileSync(`tmp/artifact_${artifactId}_inner/manifest.json`); // let manifest = fs.readFileSync(`tmp/artifact_${artifactId}_inner/manifest.json`);
@@ -108,12 +108,11 @@ async function doImport (owner, repo, id) {
} catch (e) { } catch (e) {
progress[id].output += `Error: ${e.toString()}\n${e.stack}` progress[id].output += `Error: ${e.toString()}\n${e.stack}`
} finally { } finally {
// if (artifactId) { // clean-up temp. files
// // fs.rmSync(`tmp/artifact_${artifactId}.zip`, {force: true, recursive: true}); if (artifactId) {
// // fs.rmSync(`tmp/artifact_${artifactId}`, {force: true, recursive: true}); fs.rmSync(path.join(__dirname, "..", "games", "tmp", `${owner}_${repo}_${artifactId}.zip`), {force: true, recursive: false});
// // fs.rmSync(`tmp/artifact_${artifactId}_inner`, {force: true, recursive: true}); fs.rmSync(path.join(__dirname, "..", "games", "tmp", `${owner}_${repo}_${artifactId}`), {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)) await new Promise(resolve => setTimeout(resolve, 10000))
+23 -33
View File
@@ -34,34 +34,15 @@ var router = express.Router();
router.get('/import/status/:owner/:repo', importStatus) router.get('/import/status/:owner/:repo', importStatus)
router.get('/import/trigger/:owner/:repo', importTrigger) router.get('/import/trigger/:owner/:repo', importTrigger)
function loadJson(req, filename) {
const owner = req.params.owner;
const repo = req.params.repo
return JSON.parse(fs.readFileSync(path.join(getGameDir(owner,repo),".lake","gamedata",filename)))
}
router.get("/api/g/:owner/:repo/game", (req, res) => {
res.send(loadJson(req, `game.json`));
});
router.get("/api/g/:owner/:repo/inventory", (req, res) => {
res.send(loadJson(req, `inventory.json`));
});
router.get("/api/g/:owner/:repo/level/:world/:level", (req, res) => {
const world = req.params.world;
const level = req.params.level;
res.send(loadJson(req, `level__${world}__${level}.json`));
});
router.get("/api/g/:owner/:repo/doc/:type/:name", (req, res) => {
const type = req.params.type;
const name = req.params.name;
res.send(loadJson(req, `doc__${type}__${name}.json`));
});
const server = app const server = app
.use(express.static(path.join(__dirname, '../client/dist/'))) .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) .use('/', router)
.listen(PORT, () => console.log(`Listening on ${PORT}`)); .listen(PORT, () => console.log(`Listening on ${PORT}`));
@@ -84,13 +65,13 @@ function getGameDir(owner, repo) {
if (owner == 'local') { if (owner == 'local') {
if(!isDevelopment) { if(!isDevelopment) {
console.error(`No local games in production mode.`) console.error(`No local games in production mode.`)
return return ""
} }
} else { } else {
if(!fs.existsSync(path.join(__dirname, '..', 'games'))) { if(!fs.existsSync(path.join(__dirname, '..', 'games'))) {
console.error(`Did not find the following folder: ${path.join(__dirname, '..', 'games')}`) console.error(`Did not find the following folder: ${path.join(__dirname, '..', 'games')}`)
console.error('Did you already import any games?') console.error('Did you already import any games?')
return return ""
} }
} }
@@ -100,7 +81,7 @@ function getGameDir(owner, repo) {
if(!fs.existsSync(game_dir)) { if(!fs.existsSync(game_dir)) {
console.error(`Game '${game_dir}' does not exist!`) console.error(`Game '${game_dir}' does not exist!`)
return return ""
} }
return game_dir; return game_dir;
@@ -114,13 +95,22 @@ function startServerProcess(owner, repo) {
let serverProcess let serverProcess
if (isDevelopment) { if (isDevelopment) {
let args = ["--server", game_dir] let args = ["--server", game_dir]
serverProcess = cp.spawn("./gameserver", args, let binDir = path.join(game_dir, ".lake", "packages", "GameServer", "server", ".lake", "build", "bin")
{ cwd: path.join(__dirname, "./.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 { } else {
serverProcess = cp.spawn("./bubblewrap.sh", serverProcess = cp.spawn("./bubblewrap.sh",
[game_dir, path.join(__dirname, '..')], [ game_dir, path.join(__dirname, '..')],
{ cwd: __dirname }) { cwd: __dirname })
} }
serverProcess.on('error', error => serverProcess.on('error', error =>
console.error(`Launching Lean Server failed: ${error}`) console.error(`Launching Lean Server failed: ${error}`)
) )
+1 -1
View File
@@ -6,7 +6,7 @@ REPO=$3
# mkdir -p games # mkdir -p games
cd games cd games
pwd
# mkdir -p tmp # mkdir -p tmp
mkdir -p ${OWNER} mkdir -p ${OWNER}
-3
View File
@@ -1,3 +0,0 @@
build/
games/
.lake
+107 -363
View File
@@ -1,23 +1,12 @@
import GameServer.EnvExtensions import GameServer.Helpers
import GameServer.Inventory
import GameServer.Options
import GameServer.SaveData
open Lean Meta Elab Command open Lean Meta Elab Command
set_option autoImplicit false set_option autoImplicit false
/-- Let `MakeGame` print the reasons why the worlds depend on each other. -/
register_option lean4game.showDependencyReasons : Bool := {
defValue := false
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"` -/
@@ -46,31 +35,92 @@ 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
let intro := t.getString
match ← getCurLayer with match ← getCurLayer with
| .Level => modifyCurLevel fun level => pure {level with introduction := t.getString} | .Level => modifyCurLevel fun level => pure {level with introduction := intro}
| .World => modifyCurWorld fun world => pure {world with introduction := t.getString} | .World => modifyCurWorld fun world => pure {world with introduction := intro}
| .Game => modifyCurGame fun game => pure {game with introduction := t.getString} | .Game => modifyCurGame fun game => pure {game with introduction := intro}
/-- 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
let info:= t.getString
match ← getCurLayer with match ← getCurLayer with
| .Level => pure () | .Level =>
| .World => pure () logError "Can't use `Info` in a level!"
| .Game => modifyCurGame fun game => pure {game with info := t.getString} pure ()
| .World =>
logError "Can't use `Info` in a world"
pure ()
| .Game => modifyCurGame fun game => pure {game with info := info}
/-- 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. -/
elab "Conclusion" t:str : command => do elab "Conclusion" t:str : command => do
let conclusion := t.getString
match ← getCurLayer with match ← getCurLayer with
| .Level => modifyCurLevel fun level => pure {level with conclusion := t.getString} | .Level => modifyCurLevel fun level => pure {level with conclusion := conclusion}
| .World => modifyCurWorld fun world => pure {world with conclusion := t.getString} | .World => modifyCurWorld fun world => pure {world with conclusion := conclusion}
| .Game => modifyCurGame fun game => pure {game with conclusion := t.getString} | .Game => modifyCurGame fun game => pure {game with conclusion := conclusion}
/-- 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
let caption := t.getString
modifyCurGame fun game => pure {game with
tile := {game.tile with short := caption}}
/-- More detailed description what the game is about (2-4 sentences). -/
elab "CaptionLong" t:str : command => do
let caption := t.getString
modifyCurGame fun game => pure {game with
tile := {game.tile with long := caption}}
/-- 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
@@ -80,113 +130,6 @@ 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
a warning otherwise. If `template` is provided, it will add such an entry instead of yielding a
warning.
`ref` is the syntax piece. If `name` is not provided, it will use `ident.getId`.
I used this workaround, because I needed a new name (with correct namespace etc)
to be used, and I don't know how to create a new ident with same position but different name.
-/
def checkInventoryDoc (type : InventoryType) (ref : Ident) (name : Name := ref.getId)
(template : Option String := none) : CommandElabM Unit := do
-- note: `name` is an `Ident` (instead of `Name`) for the log messages.
let env ← getEnv
let n := name
-- Find a key with matching `(type, name)`.
match (inventoryTemplateExt.getState env).findIdx?
(fun x => x.name == n && x.type == type) with
-- Nothing to do if the entry exists
| some _ => pure ()
| none =>
match template with
-- Warn about missing documentation
| 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
modifyEnv (inventoryTemplateExt.addEntry · {
type := type
name := name
category := if type == .Lemma then s!"{n.getPrefix}" else ""
content := docstring})
-- Add the default documentation
| some s =>
modifyEnv (inventoryTemplateExt.addEntry · {
type := type
name := name
category := if type == .Lemma then s!"{n.getPrefix}" else ""
content := s })
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!"`{type}Doc {name}` somewhere above this statement.")
/-- Documentation entry of a tactic. Example: /-- Documentation entry of a tactic. Example:
``` ```
@@ -196,12 +139,13 @@ TacticDoc rw "`rw` stands for rewrite, etc. "
* The identifier is the tactics name. Some need to be escaped like `«have»`. * The identifier is the tactics name. Some need to be escaped like `«have»`.
* The description is a string supporting Markdown. * The description is a string supporting Markdown.
-/ -/
elab "TacticDoc" name:ident content:str : command => elab doc:docComment ? "TacticDoc" name:ident content:str ? : command => do
let doc ← parseDocCommentLegacy doc content
modifyEnv (inventoryTemplateExt.addEntry · { modifyEnv (inventoryTemplateExt.addEntry · {
type := .Tactic type := .Tactic
name := name.getId name := name.getId
displayName := name.getId.toString displayName := name.getId.toString
content := content.getString }) content := doc })
/-- Documentation entry of a lemma. Example: /-- Documentation entry of a lemma. Example:
@@ -218,13 +162,15 @@ LemmaDoc Nat.succ_pos as "succ_pos" in "Nat" "says `0 < n.succ`, etc."
Use `[[mathlib_doc]]` in the string to insert a link to the mathlib doc page. This requires 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 doc:docComment ? "LemmaDoc" name:ident "as" displayName:str "in" category:str content:str ? :
command => do
let doc ← parseDocCommentLegacy doc content
modifyEnv (inventoryTemplateExt.addEntry · { modifyEnv (inventoryTemplateExt.addEntry · {
type := .Lemma type := .Lemma
name := name.getId name := name.getId
category := category.getString category := category.getString
displayName := displayName.getString displayName := displayName.getString
content := content.getString }) content := doc })
-- TODO: Catch the following behaviour. -- TODO: Catch the following behaviour.
-- 1. if `LemmaDoc` appears in the same file as `Statement`, it will silently use -- 1. if `LemmaDoc` appears in the same file as `Statement`, it will silently use
-- it but display the info that it wasn't found in `Statement` -- it but display the info that it wasn't found in `Statement`
@@ -246,33 +192,16 @@ DefinitionDoc Function.Bijective as "Bijective" "defined as `Injective f ∧ Sur
Use `[[mathlib_doc]]` in the string to insert a link to the mathlib doc page. This requires 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 doc:docComment ? "DefinitionDoc" name:ident "as" displayName:str template:str ? : command => do
let doc ← parseDocCommentLegacy doc template
modifyEnv (inventoryTemplateExt.addEntry · { modifyEnv (inventoryTemplateExt.addEntry · {
type := .Definition type := .Definition
name := name.getId, name := name.getId,
displayName := displayName.getString, displayName := displayName.getString,
content := template.getString }) content := doc })
/-! ## Add inventory items -/ /-! ## Add inventory items -/
def getStatement (name : Name) : CommandElabM MessageData := do
return ← addMessageContextPartial (.ofPPFormat { pp := fun
| some ctx => ctx.runMetaM <| PrettyPrinter.ppSignature name
| none => return "that's a bug." })
-- Note: We use `String` because we can't send `MessageData` as json, but
-- `MessageData` might be better for interactive highlighting.
/-- 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
try
return ← (← getStatement name).toString
catch
| _ => throwError m!"Could not find {name} in context."
-- TODO: I think it would be nicer to unresolve Namespaces as much as possible.
/-- 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 for name in ↑args do
@@ -349,54 +278,12 @@ elab "LemmaTab" category:str : command =>
/-! # Exercise Statement -/ /-! # Exercise Statement -/
/-- A `attr := ...` option for `Statement`. Add attributes to the defined theorem. -/
syntax statementAttr := "(" &"attr" ":=" Parser.Term.attrInstance,* ")"
-- TODO
-- TODO: Reuse the following code for checking available tactics in user code:
structure UsedInventory where
(tactics : HashSet Name := {})
(definitions : HashSet Name := {})
(lemmas : HashSet Name := {})
partial def collectUsedInventory (stx : Syntax) (acc : UsedInventory := {}) : CommandElabM UsedInventory := do
match stx with
| .missing => return acc
| .node _info kind args =>
if kind == `GameServer.Tactic.Hint || kind == `GameServer.Tactic.Branch then return acc
return ← args.foldlM (fun acc arg => collectUsedInventory arg acc) acc
| .atom _info val =>
-- ignore syntax elements that do not start with a letter
-- and ignore some standard keywords
let allowed := ["with", "fun", "at", "only", "by"]
if 0 < val.length ∧ val.data[0]!.isAlpha ∧ not (allowed.contains val) then
let val := val.dropRightWhile (fun c => c == '!' || c == '?') -- treat `simp?` and `simp!` like `simp`
return {acc with tactics := acc.tactics.insert val}
else
return acc
| .ident _info _rawVal val _preresolved =>
let ns ←
try resolveGlobalConst (mkIdent val)
catch | _ => pure [] -- catch "unknown constant" error
return ← ns.foldlM (fun acc n => do
if let some (.thmInfo ..) := (← getEnv).find? n then
return {acc with lemmas := acc.lemmas.insertMany ns}
else
return {acc with definitions := acc.definitions.insertMany ns}
) acc
-- #check expandOptDocComment?
/-- Define the statement of the current level. -/ /-- Define the statement of the current level. -/
elab doc:docComment ? attrs:Parser.Term.attributes ? elab doc:docComment ? attrs:Parser.Term.attributes ?
"Statement" statementName:ident ? sig:declSig val:declVal : command => do "Statement" statementName:ident ? sig:declSig val:declVal : command => do
let lvlIdx ← getCurLevelIdx let lvlIdx ← getCurLevelIdx
let docContent : Option String := match doc with let docContent ← parseDocComment doc
| none => none
| some s => match s.raw[1] with
| .atom _ val => val.dropRight 2 |>.trim -- some (val.extract 0 (val.endPos - ⟨2⟩))
| _ => none --panic "not implemented error message" --throwErrorAt s "unexpected doc string{indentD s.raw[1]}"
-- Save the messages before evaluation of the proof. -- Save the messages before evaluation of the proof.
let initMsgs ← modifyGet fun st => (st.messages, { st with messages := {} }) let initMsgs ← modifyGet fun st => (st.messages, { st with messages := {} })
@@ -497,23 +384,6 @@ elab doc:docComment ? attrs:Parser.Term.attributes ?
/-! # Hints -/ /-! # Hints -/
syntax hintArg := atomic(" (" (&"strict" <|> &"hidden") " := " withoutPosition(term) ")")
/-- Remove any spaces at the beginning of a new line -/
partial def removeIndentation (s : String) : String :=
let rec loop (i : String.Pos) (acc : String) (removeSpaces := false) : String :=
let c := s.get i
let i := s.next i
if s.atEnd i then
acc.push c
else if removeSpaces && c == ' ' then
loop i acc (removeSpaces := true)
else if c == '\n' then
loop i (acc.push c) (removeSpaces := true)
else
loop i (acc.push c)
loop ⟨0⟩ ""
/-- A tactic that can be used inside `Statement`s to indicate in which proof states players should /-- A tactic that can be used inside `Statement`s to indicate in which proof states players should
see hints. The tactic does not affect the goal state. see hints. The tactic does not affect the goal state.
-/ -/
@@ -626,6 +496,7 @@ elab "Template" tacs:tacticSeq : tactic => do
modifyLevel (←getCurLevelId) fun level => do modifyLevel (←getCurLevelId) fun level => do
return {level with template := s!"{template}"} return {level with template := s!"{template}"}
-- 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
@@ -644,136 +515,6 @@ 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
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
| .Tactic => level.tactics
| .Definition => level.definitions
| .Lemma => level.lemmas
def GameLevel.setComputedInventory (level : GameLevel) :
InventoryType → Array InventoryTile → GameLevel
| .Tactic, v => {level with tactics := {level.tactics with tiles := v}}
| .Definition, v => {level with definitions := {level.definitions with tiles := v}}
| .Lemma, v => {level with lemmas := {level.lemmas with tiles := v}}
partial def removeTransitiveAux (id : Name) (arrows : 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
match (newArrows.find? id, decendants.find? id) with
| (some _, some _) => return (newArrows, decendants)
| _ =>
let mut newArr := newArrows
let mut desc := decendants
desc := desc.insert id {} -- mark as worked in case of loops
newArr := newArr.insert id {} -- mark as worked in case of loops
let children := arrows.findD id {}
let mut trimmedChildren := children
let mut theseDescs := children
for child in children do
(newArr, desc) := removeTransitiveAux child arrows newArr desc
let childDescs := desc.findD child {}
theseDescs := theseDescs.insertMany childDescs
for d in childDescs do
trimmedChildren := trimmedChildren.erase d
desc := desc.insert id theseDescs
newArr := newArr.insert id trimmedChildren
return (newArr, desc)
def removeTransitive (arrows : HashMap Name (HashSet Name)) : CommandElabM (HashMap Name (HashSet Name)) := do
let mut newArr := {}
let mut desc := {}
for id in arrows.toArray.map Prod.fst do
(newArr, desc) := removeTransitiveAux id arrows newArr desc
if (desc.findD id {}).contains id then
logError <| m!"Loop at {id}. " ++
m!"This should not happen and probably means that `findLoops` has a bug."
-- DEBUG:
-- for ⟨x, hx⟩ in desc.toList do
-- m := m ++ m!"{x}: {hx.toList}\n"
-- logError m
return newArr
/-- The recursive part of `findLoops`. Finds loops that appear as successors of `node`.
For performance reason it returns a HashSet of visited
nodes as well. This is filled with all nodes ever looked at as they cannot be
part of a loop anymore. -/
partial def findLoopsAux (arrows : HashMap Name (HashSet Name)) (node : Name)
(path : Array Name := #[]) (visited : HashSet Name := {}) :
Array Name × HashSet Name := Id.run do
let mut visited := visited
match path.getIdx? node with
| some i =>
-- Found a loop: `node` is already the iᵗʰ element of the path
return (path.extract i path.size, visited.insert node)
| none =>
for successor in arrows.findD node {} do
-- If we already visited the successor, it cannot be part of a loop anymore
if visited.contains successor then
continue
-- Find any loop involving `successor`
let (loop, _) := findLoopsAux arrows successor (path.push node) visited
visited := visited.insert successor
-- No loop found in the dependants of `successor`
if loop.isEmpty then
continue
-- Found a loop, return it
return (loop, visited)
return (#[], visited.insert node)
/-- Find a loop in the graph and return it. Returns `[]` if there are no loops. -/
partial def findLoops (arrows : HashMap Name (HashSet Name)) : List Name := Id.run do
let mut visited : HashSet Name := {}
for node in arrows.toArray.map (·.1) do
-- Skip a node if it was already visited
if visited.contains node then
continue
-- `findLoopsAux` returns a loop or `[]` together with a set of nodes it visited on its
-- search starting from `node`
let (loop, moreVisited) := (findLoopsAux arrows node (visited := visited))
visited := moreVisited
if !loop.isEmpty then
return loop.toList
return []
/-- The worlds of a game are joint by dependencies. These are /-- The worlds of a game are joint by dependencies. These are
automatically computed but can also be defined with the syntax automatically computed but can also be defined with the syntax
`Dependency World₁ → World₂ → World₃`. -/ `Dependency World₁ → World₂ → World₃`. -/
@@ -848,8 +589,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
@@ -888,9 +633,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
@@ -902,7 +650,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
@@ -953,11 +701,21 @@ 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
@@ -1094,17 +852,3 @@ elab "MakeGame" : command => do
allItemsByType := allItemsByType.insert inventoryType allItems allItemsByType := allItemsByType.insert inventoryType allItems
saveGameData allItemsByType saveGameData allItemsByType
/-! # Debugging tools -/
-- /-- Print current game for debugging purposes. -/
-- elab "PrintCurGame" : command => do
-- logInfo (toJson (← getCurGame))
/-- Print current level for debugging purposes. -/
elab "PrintCurLevel" : command => do
logInfo (repr (← getCurLevel))
/-- Print levels for debugging purposes. -/
elab "PrintLevels" : command => do
logInfo $ repr $ (← getCurWorld).levels.toArray
+45 -2
View File
@@ -148,6 +148,12 @@ structure InventoryOverview where
lemmaTab : Option String lemmaTab : Option String
deriving ToJson, FromJson deriving ToJson, FromJson
-- TODO: Reuse the following code for checking available tactics in user code:
structure UsedInventory where
(tactics : HashSet Name := {})
(definitions : HashSet Name := {})
(lemmas : HashSet Name := {})
/-! ## Environment extensions for game specification -/ /-! ## Environment extensions for game specification -/
/-- Register a (non-persistent) environment extension to hold the current level -/ /-- Register a (non-persistent) environment extension to hold the current level -/
@@ -265,7 +271,9 @@ 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
/-- Json-encodable version of `GameLevel` /-- Json-encodable version of `GameLevel`
Fields: Fields:
@@ -286,6 +294,7 @@ structure LevelInfo where
displayName : Option String displayName : Option String
statementName : Option String statementName : Option String
template : Option String template : Option String
image: Option String
deriving ToJson, FromJson deriving ToJson, FromJson
def GameLevel.toInfo (lvl : GameLevel) (env : Environment) : LevelInfo := def GameLevel.toInfo (lvl : GameLevel) (env : Environment) : LevelInfo :=
@@ -315,6 +324,7 @@ def GameLevel.toInfo (lvl : GameLevel) (env : Environment) : LevelInfo :=
-- Note: we could call `.find!` because we check in `Statement` that the -- Note: we could call `.find!` because we check in `Statement` that the
-- lemma doc must exist. -- lemma doc must exist.
template := lvl.template template := lvl.template
image := lvl.image
} }
/-! ## World -/ /-! ## World -/
@@ -331,17 +341,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
@@ -356,6 +395,10 @@ 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 def getGameJson (game : «Game») : Json := Id.run do
+190
View File
@@ -0,0 +1,190 @@
import Lean
/-! This document contains various things which cluttered `Commands.lean`. -/
open Lean Meta Elab Command
syntax hintArg := atomic(" (" (&"strict" <|> &"hidden") " := " withoutPosition(term) ")")
/-! ## Doc Comment Parsing -/
/-- Read a doc comment and get its content. Return `""` if no doc comment available. -/
def parseDocComment! (doc: Option (TSyntax `Lean.Parser.Command.docComment)) :
CommandElabM String := do
match doc with
| none =>
logWarning "Add a text to this command with `/-- yada yada -/ MyCommand`!"
pure ""
| some s => match s.raw[1] with
| .atom _ val => pure <| val.dropRight 2 |>.trim -- some (val.extract 0 (val.endPos - ⟨2⟩))
| _ => pure "" --panic "not implemented error message" --throwErrorAt s "unexpected doc string{indentD s.raw[1]}"
/-- Read a doc comment and get its content. Return `none` if no doc comment available. -/
def parseDocComment (doc: Option (TSyntax `Lean.Parser.Command.docComment)) :
CommandElabM <| Option String := do
match doc with
| none => pure none
| some _ => parseDocComment! doc
/-- TODO: This is only used to provide some backwards compatibility and you can
replace `parseDocCommentLegacy` with `parseDocComment` in the future. -/
def parseDocCommentLegacy (doc: Option (TSyntax `Lean.Parser.Command.docComment))
(t : Option (TSyntax `str)) : CommandElabM <| String := do
match doc with
| none =>
match t with
| none =>
pure <| ← parseDocComment! doc
| some t =>
logWarningAt t "You should use the new Syntax:
/-- yada yada -/
YourCommand
instead of
YourCommand \"yada yada\"
"
pure t.getString
| some _ =>
match t with
| none =>
pure <| ← parseDocComment! doc
| some t =>
logErrorAt t "You must not provide both, a docstring and a string following the command!
Only use
/-- yada yada -/
YourCommand
and remove the string following it!"
pure <| ← parseDocComment! doc
/-! ## Statement string -/
def getStatement (name : Name) : CommandElabM MessageData := do
return ← addMessageContextPartial (.ofPPFormat { pp := fun
| some ctx => ctx.runMetaM <| PrettyPrinter.ppSignature name
| none => return "that's a bug." })
-- Note: We use `String` because we can't send `MessageData` as json, but
-- `MessageData` might be better for interactive highlighting.
/-- 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
try
return ← (← getStatement name).toString
catch
| _ => throwError m!"Could not find {name} in context."
-- TODO: I think it would be nicer to unresolve Namespaces as much as possible.
/-- A `attr := ...` option for `Statement`. Add attributes to the defined theorem. -/
syntax statementAttr := "(" &"attr" ":=" Parser.Term.attrInstance,* ")"
-- TODO
/-- Remove any spaces at the beginning of a new line -/
partial def removeIndentation (s : String) : String :=
let rec loop (i : String.Pos) (acc : String) (removeSpaces := false) : String :=
let c := s.get i
let i := s.next i
if s.atEnd i then
acc.push c
else if removeSpaces && c == ' ' then
loop i acc (removeSpaces := true)
else if c == '\n' then
loop i (acc.push c) (removeSpaces := true)
else
loop i (acc.push c)
loop ⟨0⟩ ""
/-! ## Loops in Graph-like construct
TODO: Why are we not using graphs here but our own construct `HashMap Name (HashSet Name)`?
-/
partial def removeTransitiveAux (id : Name) (arrows : 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
match (newArrows.find? id, decendants.find? id) with
| (some _, some _) => return (newArrows, decendants)
| _ =>
let mut newArr := newArrows
let mut desc := decendants
desc := desc.insert id {} -- mark as worked in case of loops
newArr := newArr.insert id {} -- mark as worked in case of loops
let children := arrows.findD id {}
let mut trimmedChildren := children
let mut theseDescs := children
for child in children do
(newArr, desc) := removeTransitiveAux child arrows newArr desc
let childDescs := desc.findD child {}
theseDescs := theseDescs.insertMany childDescs
for d in childDescs do
trimmedChildren := trimmedChildren.erase d
desc := desc.insert id theseDescs
newArr := newArr.insert id trimmedChildren
return (newArr, desc)
def removeTransitive (arrows : HashMap Name (HashSet Name)) : CommandElabM (HashMap Name (HashSet Name)) := do
let mut newArr := {}
let mut desc := {}
for id in arrows.toArray.map Prod.fst do
(newArr, desc) := removeTransitiveAux id arrows newArr desc
if (desc.findD id {}).contains id then
logError <| m!"Loop at {id}. " ++
m!"This should not happen and probably means that `findLoops` has a bug."
-- DEBUG:
-- for ⟨x, hx⟩ in desc.toList do
-- m := m ++ m!"{x}: {hx.toList}\n"
-- logError m
return newArr
/-- The recursive part of `findLoops`. Finds loops that appear as successors of `node`.
For performance reason it returns a HashSet of visited
nodes as well. This is filled with all nodes ever looked at as they cannot be
part of a loop anymore. -/
partial def findLoopsAux (arrows : HashMap Name (HashSet Name)) (node : Name)
(path : Array Name := #[]) (visited : HashSet Name := {}) :
Array Name × HashSet Name := Id.run do
let mut visited := visited
match path.getIdx? node with
| some i =>
-- Found a loop: `node` is already the iᵗʰ element of the path
return (path.extract i path.size, visited.insert node)
| none =>
for successor in arrows.findD node {} do
-- If we already visited the successor, it cannot be part of a loop anymore
if visited.contains successor then
continue
-- Find any loop involving `successor`
let (loop, _) := findLoopsAux arrows successor (path.push node) visited
visited := visited.insert successor
-- No loop found in the dependants of `successor`
if loop.isEmpty then
continue
-- Found a loop, return it
return (loop, visited)
return (#[], visited.insert node)
/-- Find a loop in the graph and return it. Returns `[]` if there are no loops. -/
partial def findLoops (arrows : HashMap Name (HashSet Name)) : List Name := Id.run do
let mut visited : HashSet Name := {}
for node in arrows.toArray.map (·.1) do
-- Skip a node if it was already visited
if visited.contains node then
continue
-- `findLoopsAux` returns a loop or `[]` together with a set of nodes it visited on its
-- search starting from `node`
let (loop, moreVisited) := (findLoopsAux arrows node (visited := visited))
visited := moreVisited
if !loop.isEmpty then
return loop.toList
return []
+152
View File
@@ -0,0 +1,152 @@
import Lean
import GameServer.EnvExtensions
open Lean Elab Command
/-- 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
/-! ## Doc entries -/
/-- 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
a warning otherwise. If `template` is provided, it will add such an entry instead of yielding a
warning.
`ref` is the syntax piece. If `name` is not provided, it will use `ident.getId`.
I used this workaround, because I needed a new name (with correct namespace etc)
to be used, and I don't know how to create a new ident with same position but different name.
-/
def checkInventoryDoc (type : InventoryType) (ref : Ident) (name : Name := ref.getId)
(template : Option String := none) : CommandElabM Unit := do
-- note: `name` is an `Ident` (instead of `Name`) for the log messages.
let env ← getEnv
let n := name
-- Find a key with matching `(type, name)`.
match (inventoryTemplateExt.getState env).findIdx?
(fun x => x.name == n && x.type == type) with
-- Nothing to do if the entry exists
| some _ => pure ()
| none =>
match template with
-- Warn about missing documentation
| 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
modifyEnv (inventoryTemplateExt.addEntry · {
type := type
name := name
category := if type == .Lemma then s!"{n.getPrefix}" else ""
content := docstring})
-- Add the default documentation
| some s =>
modifyEnv (inventoryTemplateExt.addEntry · {
type := type
name := name
category := if type == .Lemma then s!"{n.getPrefix}" else ""
content := s })
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!"`{type}Doc {name}` somewhere above this statement.")
partial def collectUsedInventory (stx : Syntax) (acc : UsedInventory := {}) : CommandElabM UsedInventory := do
match stx with
| .missing => return acc
| .node _info kind args =>
if kind == `GameServer.Tactic.Hint || kind == `GameServer.Tactic.Branch then return acc
return ← args.foldlM (fun acc arg => collectUsedInventory arg acc) acc
| .atom _info val =>
-- ignore syntax elements that do not start with a letter
-- and ignore some standard keywords
let allowed := ["with", "fun", "at", "only", "by"]
if 0 < val.length ∧ val.data[0]!.isAlpha ∧ not (allowed.contains val) then
let val := val.dropRightWhile (fun c => c == '!' || c == '?') -- treat `simp?` and `simp!` like `simp`
return {acc with tactics := acc.tactics.insert val}
else
return acc
| .ident _info _rawVal val _preresolved =>
let ns ←
try resolveGlobalConst (mkIdent val)
catch | _ => pure [] -- catch "unknown constant" error
return ← ns.foldlM (fun acc n => do
if let some (.thmInfo ..) := (← getEnv).find? n then
return {acc with lemmas := acc.lemmas.insertMany ns}
else
return {acc with definitions := acc.definitions.insertMany ns}
) acc
-- #check expandOptDocComment?
def GameLevel.getInventory (level : GameLevel) : InventoryType → InventoryInfo
| .Tactic => level.tactics
| .Definition => level.definitions
| .Lemma => level.lemmas
def GameLevel.setComputedInventory (level : GameLevel) :
InventoryType → Array InventoryTile → GameLevel
| .Tactic, v => {level with tactics := {level.tactics with tiles := v}}
| .Definition, v => {level with definitions := {level.definitions with tiles := v}}
| .Lemma, v => {level with lemmas := {level.lemmas with tiles := v}}
+17
View File
@@ -0,0 +1,17 @@
import Lean
/-! This document contains custom options available in the game. -/
/-- Let `MakeGame` print the reasons why the worlds depend on each other. -/
register_option lean4game.showDependencyReasons : Bool := {
defValue := false
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."
}
+1 -1
View File
@@ -126,7 +126,7 @@ def findHints (goal : MVarId) (doc : FileWorker.EditableDocument) (initParams :
then then
let userFVars := hintFVars.map fun v => bij.forward.findD v.fvarId! v.fvarId! let userFVars := hintFVars.map fun v => bij.forward.findD v.fvarId! v.fvarId!
let text := (← evalHintMessage hint.text) (userFVars.map Expr.fvar) let text := (← evalHintMessage hint.text) (userFVars.map Expr.fvar)
let ctx := {env := ← getEnv, mctx := ← getMCtx, lctx := ← getLCtx, opts := {}} 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
+65
View File
@@ -0,0 +1,65 @@
import GameServer.EnvExtensions
open Lean Meta Elab Command
/-! ## Copy images -/
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: I'm not sure this should be happening here...
#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))
+2 -2
View File
@@ -4,10 +4,10 @@
[{"url": "https://github.com/leanprover/std4.git", [{"url": "https://github.com/leanprover/std4.git",
"type": "git", "type": "git",
"subDir": null, "subDir": null,
"rev": "a652e09bd81bcb43ea132d64ecc16580b0c7fa50", "rev": "2e4a3586a8f16713f16b2d2b3af3d8e65f3af087",
"name": "std", "name": "std",
"manifestFile": "lake-manifest.json", "manifestFile": "lake-manifest.json",
"inputRev": "v4.3.0-rc2", "inputRev": "v4.3.0",
"inherited": false, "inherited": false,
"configFile": "lakefile.lean"}], "configFile": "lakefile.lean"}],
"name": "GameServer", "name": "GameServer",
+11 -1
View File
@@ -12,6 +12,16 @@ lean_lib GameServer
@[default_target] @[default_target]
lean_exe gameserver { lean_exe gameserver {
root := `Main root := `GameServer
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
View File
@@ -1 +1 @@
leanprover/lean4:v4.3.0-rc2 leanprover/lean4:v4.3.0
+1 -1
View File
@@ -12,5 +12,5 @@
"experimentalDecorators": true, "experimentalDecorators": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
}, },
"exclude": ["server"] "exclude": ["server", "relay"]
} }
+2 -2
View File
@@ -8,7 +8,7 @@ export default defineConfig({
//root: 'client/src', //root: 'client/src',
build: { build: {
// Relative to the root // Relative to the root
// Note: This has to match the path in `server/index.mjs` // Note: This has to match the path in `relay/index.mjs`
outDir: 'client/dist', outDir: 'client/dist',
}, },
plugins: [ plugins: [
@@ -41,7 +41,7 @@ export default defineConfig({
'/import': { '/import': {
target: 'http://localhost:8080', target: 'http://localhost:8080',
}, },
'/api': { '/data': {
target: 'http://localhost:8080', target: 'http://localhost:8080',
}, },
} }