Compare commits
41
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c39fb6664 | ||
|
|
527f58e3a4 | ||
|
|
b239a5d3dc | ||
|
|
63cf5e8b72 | ||
|
|
25f166f57f | ||
|
|
c89e2e4020 | ||
|
|
f6738faf46 | ||
|
|
cb7224934c | ||
|
|
13d54ff0ff | ||
|
|
72e4011c62 | ||
|
|
4f5256fa88 | ||
|
|
c2b9175fe5 | ||
|
|
a1a6862b5a | ||
|
|
0a057913be | ||
|
|
bedb2ad5ec | ||
|
|
e02e73c1c0 | ||
|
|
c82a88867f | ||
|
|
8b43aed596 | ||
|
|
8c84d3fae7 | ||
|
|
d5697d052e | ||
|
|
0e652256f8 | ||
|
|
9492c1011a | ||
|
|
54bab2a016 | ||
|
|
3f8b180b04 | ||
|
|
23b1074aa2 | ||
|
|
0964a06f2f | ||
|
|
b17c8fc4cb | ||
|
|
0c4ae92856 | ||
|
|
088711b5d1 | ||
|
|
4c93b3a091 | ||
|
|
aa00e359c4 | ||
|
|
f6a2632d80 | ||
|
|
f2190d648f | ||
|
|
d7fd8709cb | ||
|
|
7a03c4fe0d | ||
|
|
333c9498f1 | ||
|
|
121b36b542 | ||
|
|
6bced7575b | ||
|
|
d18b48db2f | ||
|
|
97dc648452 | ||
|
|
472e2c66df |
@@ -1,5 +1,4 @@
|
||||
node_modules
|
||||
games/
|
||||
client/dist
|
||||
games/
|
||||
server/.lake
|
||||
|
||||
@@ -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 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
|
||||
|
||||
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)
|
||||
- [Old documentation](doc/DOCUMENTATION.md)
|
||||
- [Creating a Game](doc/create_game.md): **the main document to consult**.
|
||||
- [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
|
||||
|
||||
@@ -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
|
||||
(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).
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 398 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 308 KiB |
@@ -1,6 +1,7 @@
|
||||
import { GameHint } from "./infoview/rpc_api";
|
||||
import * as React from 'react';
|
||||
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}) {
|
||||
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}/>)}
|
||||
</>
|
||||
}
|
||||
|
||||
/** 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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ import { Button } from '../button';
|
||||
import { CircularProgress } from '@mui/material';
|
||||
import { GameHint } from './rpc_api';
|
||||
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
|
||||
* 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))
|
||||
|
||||
console.debug(`template: ${props.data.template}`)
|
||||
console.debug(`template: ${props.data?.template}`)
|
||||
|
||||
// React.useEffect (() => {
|
||||
// if (props.data.template) {
|
||||
@@ -367,9 +367,9 @@ export function TypewriterInterface({props}) {
|
||||
function deleteProof(line: number) {
|
||||
return (ev) => {
|
||||
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
|
||||
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)
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ function InventoryList({items, docType, openDoc, defaultTab=null, level=undefine
|
||||
{[...modifiedItems].sort(
|
||||
// For lemas, sort entries `available > disabled > locked`
|
||||
// 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) => {
|
||||
return <InventoryItem key={`${item.category}-${item.name}`}
|
||||
showDoc={() => {openDoc({name: item.name, type: docType})}}
|
||||
|
||||
@@ -7,12 +7,12 @@ import '@fontsource/roboto/500.css';
|
||||
import '@fontsource/roboto/700.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 Markdown from './markdown';
|
||||
import {PrivacyPolicyPopup} from './popup/privacy_policy'
|
||||
import { GameTile, useGetGameInfoQuery } from '../state/api'
|
||||
import path from 'path';
|
||||
|
||||
const flag = {
|
||||
'Dutch': '🇳🇱',
|
||||
@@ -20,6 +20,7 @@ const flag = {
|
||||
'French': '🇫🇷',
|
||||
'German': '🇩🇪',
|
||||
'Italian': '🇮🇹',
|
||||
'Spanish': '🇪🇸',
|
||||
}
|
||||
|
||||
function GithubIcon({url='https://github.com'}) {
|
||||
@@ -33,47 +34,42 @@ function GithubIcon({url='https://github.com'}) {
|
||||
</div>
|
||||
}
|
||||
|
||||
function GameTile({
|
||||
title,
|
||||
gameId,
|
||||
intro, // Catchy intro phrase.
|
||||
image=null,
|
||||
worlds='?',
|
||||
levels='?',
|
||||
prereq='–', // Optional list of games that this game builds on. Use markdown.
|
||||
description, // Longer description. Supports Markdown.
|
||||
language}) {
|
||||
function Tile({gameId, data}: {gameId: string, data: GameTile|undefined}) {
|
||||
|
||||
let navigate = useNavigate();
|
||||
const routeChange = () =>{
|
||||
navigate(gameId);
|
||||
}
|
||||
|
||||
if (typeof data === 'undefined') {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return <div className="game" onClick={routeChange}>
|
||||
<div className="wrapper">
|
||||
<div className="title">{title}</div>
|
||||
<div className="short-description">{intro}
|
||||
<div className="title">{data.title}</div>
|
||||
<div className="short-description">{data.short}
|
||||
</div>
|
||||
{ image ? <img className="image" src={image} alt="" /> : <div className="image"/> }
|
||||
<div className="long description"><Markdown>{description}</Markdown></div>
|
||||
{ data.image ? <img className="image" src={path.join("data", gameId, data.image)} alt="" /> : <div className="image"/> }
|
||||
<div className="long description"><Markdown>{data.long}</Markdown></div>
|
||||
</div>
|
||||
<table className="info">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td title="consider playing these games first.">Prerequisites</td>
|
||||
<td><Markdown>{prereq}</Markdown></td>
|
||||
<td><Markdown>{data.prerequisites.join(', ')}</Markdown></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Worlds</td>
|
||||
<td>{worlds}</td>
|
||||
<td>{data.worlds}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Levels</td>
|
||||
<td>{levels}</td>
|
||||
<td>{data.levels}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -89,6 +85,58 @@ function LandingPage() {
|
||||
const openImpressum = () => setImpressum(true);
|
||||
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">
|
||||
<header style={{backgroundImage: `url(${bgImage})`}}>
|
||||
@@ -105,60 +153,26 @@ function LandingPage() {
|
||||
</div>
|
||||
</header>
|
||||
<div className="game-list">
|
||||
|
||||
<GameTile
|
||||
title="Natural Number Game"
|
||||
gameId="g/hhu-adam/NNG4"
|
||||
intro="The classical introduction game for Lean."
|
||||
description="In this game you recreate the natural numbers $\mathbb{N}$ from the Peano axioms,
|
||||
learning the basics about theorem proving in Lean.
|
||||
|
||||
This is a good first introduction to Lean!"
|
||||
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"
|
||||
/>
|
||||
{allTiles.length == 0 ?
|
||||
<p>No Games loaded. Use <a>http://localhost:3000/#/g/local/FOLDER</a> to open a
|
||||
game directly from a local folder.
|
||||
</p>
|
||||
: allGames.map((id, i) => (
|
||||
<Tile
|
||||
key={id}
|
||||
gameId={`g/${id}`}
|
||||
data={allTiles[i]}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<section>
|
||||
<div className="wrapper">
|
||||
<h2>Development notes</h2>
|
||||
<p>
|
||||
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
|
||||
mathlib. We hope to address this limitation in the future.
|
||||
Our current estimate is about 70 simultaneous games.
|
||||
We hope to address and test this limitation better in the future.
|
||||
</p>
|
||||
<p>
|
||||
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>
|
||||
<p>
|
||||
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
|
||||
a template.
|
||||
the <a target="_blank" href="https://github.com/hhu-adam/GameSkeleton">GameSkeleton Github Repo</a> as
|
||||
a template and read <a target="_blank" href="https://github.com/leanprover-community/lean4game/">How to Create a Game</a>.
|
||||
</p>
|
||||
<p>
|
||||
There is an option to load and run your own games directly on the server,
|
||||
instructions are in the NNG repo. Since this is still in development we'd like to
|
||||
encourage you to contact us for support creating your own game. The documentation is
|
||||
not polished yet.
|
||||
You can directly load your games into the server and play it using
|
||||
the correct URL. The <a target="_blank" href="https://github.com/leanprover-community/lean4game/">instructions above</a> also
|
||||
explain the details for how to load your game to the server.
|
||||
|
||||
We'd like to encourage you to contact us if you have any questions.
|
||||
</p>
|
||||
<p>
|
||||
To add games to this main page, you should get in contact as
|
||||
games will need to be added manually.
|
||||
Featured games on this page are added manually.
|
||||
Please get in contact and we-ll happily add yours.
|
||||
</p>
|
||||
</div>
|
||||
</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>
|
||||
{impressum? <PrivacyPolicyPopup handleClose={closeImpressum} />: null}
|
||||
</footer>
|
||||
|
||||
{/* <PrivacyPolicy/> */}
|
||||
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import { ConnectionContext, connection, useLeanClient } from '../connection'
|
||||
import { useAppDispatch, useAppSelector } from '../hooks'
|
||||
import { useGetGameInfoQuery, useLoadInventoryOverviewQuery, useLoadLevelQuery } from '../state/api'
|
||||
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 { Button } from './button'
|
||||
import Markdown from './markdown'
|
||||
@@ -32,8 +32,9 @@ import { DeletedChatContext, InputModeContext, MobileContext, MonacoEditorContex
|
||||
ProofContext, ProofStep, SelectionContext, WorldLevelIdContext } from './infoview/context'
|
||||
import { DualEditor } from './infoview/main'
|
||||
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 path from 'path';
|
||||
|
||||
import '@fontsource/roboto/300.css'
|
||||
import '@fontsource/roboto/400.css'
|
||||
@@ -137,19 +138,24 @@ function ChatPanel({lastLevel}) {
|
||||
|
||||
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">
|
||||
<div ref={chatRef} className="chat">
|
||||
{introText?.filter(t => t.trim()).map(((t, i) =>
|
||||
// Show the level's intro text as hints, too
|
||||
<Hint key={`intro-p-${i}`}
|
||||
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
|
||||
// as the second-to-last step. Therefore we should not display them.
|
||||
if (!(i == proof.length - 1 && withErr)) {
|
||||
// TODO: Should not use index as key.
|
||||
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}/>
|
||||
}
|
||||
})}
|
||||
@@ -203,11 +209,16 @@ function PlayableLevel({impressum, setImpressum}) {
|
||||
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
||||
const {mobile} = React.useContext(MobileContext)
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const difficulty = useSelector(selectDifficulty(gameId))
|
||||
const initialCode = useAppSelector(selectCode(gameId, worldId, levelId))
|
||||
const initialSelections = useAppSelector(selectSelections(gameId, worldId, levelId))
|
||||
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 level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
||||
|
||||
@@ -220,12 +231,11 @@ function PlayableLevel({impressum, setImpressum}) {
|
||||
const [showHelp, setShowHelp] = useState<Set<number>>(new Set())
|
||||
// Only for mobile layout
|
||||
const [pageNumber, setPageNumber] = useState(0)
|
||||
const [typewriterMode, setTypewriterMode] = useState(true)
|
||||
|
||||
// set to true to prevent switching between typewriter and editor
|
||||
const [lockInputMode, setLockInputMode] = useState(false)
|
||||
const [typewriterInput, setTypewriterInput] = useState("")
|
||||
const lastLevel = levelId >= gameInfo.data?.worldSize[worldId]
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// impressum pop-up
|
||||
function toggleImpressum() {setImpressum(!impressum)}
|
||||
@@ -319,8 +329,6 @@ function PlayableLevel({impressum, setImpressum}) {
|
||||
console.debug(`not inserting template.`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setTypewriterMode(true)
|
||||
}
|
||||
}, [level, levelId, worldId, gameId, editor])
|
||||
|
||||
@@ -335,7 +343,7 @@ function PlayableLevel({impressum, setImpressum}) {
|
||||
}, [gameId, worldId, levelId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!typewriterMode) {
|
||||
if (!typewriterMode && editor) {
|
||||
// Delete last input attempt from command line
|
||||
editor.executeEdits("typewriter", [{
|
||||
range: editor.getSelection(),
|
||||
@@ -465,6 +473,11 @@ function Introduction({impressum, setImpressum}) {
|
||||
|
||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||
|
||||
const {worldId} = useContext(WorldLevelIdContext)
|
||||
|
||||
let image: string = gameInfo.data?.worlds.nodes[worldId].image
|
||||
|
||||
|
||||
const toggleImpressum = () => {
|
||||
setImpressum(!impressum)
|
||||
}
|
||||
@@ -478,7 +491,13 @@ function Introduction({impressum, setImpressum}) {
|
||||
:
|
||||
<Split minSize={0} snapOffset={200} sizes={[25, 50, 25]} className={`app-content level`}>
|
||||
<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} />
|
||||
</Split>
|
||||
}
|
||||
|
||||
@@ -336,6 +336,21 @@ td code {
|
||||
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 {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
+22
-7
@@ -3,14 +3,28 @@
|
||||
*/
|
||||
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 {
|
||||
title: null|string,
|
||||
introduction: 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},
|
||||
authors: null|string[],
|
||||
conclusion: null|string,
|
||||
tile: null|GameTile,
|
||||
image: null|string
|
||||
}
|
||||
|
||||
export interface InventoryTile {
|
||||
@@ -36,7 +50,8 @@ export interface LevelInfo {
|
||||
lemmaTab: null|string,
|
||||
statementName: null|string,
|
||||
displayName: null|string,
|
||||
template: null|string
|
||||
template: null|string,
|
||||
image: null|string
|
||||
}
|
||||
|
||||
/** 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
|
||||
export const apiSlice = createApi({
|
||||
reducerPath: 'gameApi',
|
||||
baseQuery: fetchBaseQuery({ baseUrl: window.location.origin + "/api" }),
|
||||
baseQuery: fetchBaseQuery({ baseUrl: window.location.origin + "/data" }),
|
||||
endpoints: (builder) => ({
|
||||
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}>({
|
||||
query: ({game, world, level}) => `${game}/level/${world}/${level}`,
|
||||
query: ({game, world, level}) => `${game}/level__${world}__${level}.json`,
|
||||
}),
|
||||
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"}>({
|
||||
query: ({game, type, name}) => `${game}/doc/${type}/${name}`,
|
||||
query: ({game, type, name}) => `${game}/doc__${type}__${name}.json`,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -26,7 +26,8 @@ export interface GameProgressState {
|
||||
inventory: string[],
|
||||
difficulty: number,
|
||||
openedIntro: boolean,
|
||||
data: WorldProgressState
|
||||
data: WorldProgressState,
|
||||
typewriterMode?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,6 +127,11 @@ export const progressSlice = createSlice({
|
||||
addGameProgress(state, action)
|
||||
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 const { changedSelection, codeEdited, levelCompleted, deleteProgress,
|
||||
deleteLevelProgress, loadProgress, helpEdited, changedInventory, changedOpenedIntro,
|
||||
changedDifficulty } = progressSlice.actions
|
||||
changedDifficulty, changeTypewriterMode} = progressSlice.actions
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
**NOTE! This document is deprecated! The current documentation is [How To Create A Game](create_game.md)**
|
||||
|
||||
# Creating a game.
|
||||
|
||||
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
|
||||
|
||||
@@ -295,8 +297,9 @@ cd lean4game
|
||||
npm install
|
||||
```
|
||||
|
||||
TODO: This is outdated!
|
||||
If you are developing a game other than `Robo` or `NNG4`, adapt the
|
||||
code at the beginning of `lean4game/server/index.mjs`:
|
||||
code at the beginning of `lean4game/relay/index.mjs`:
|
||||
```typescript
|
||||
const games = {
|
||||
"g/hhu-adam/robo": {
|
||||
|
||||
+22
-15
@@ -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`.
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
Hint (strict := true) "some hidden hint"
|
||||
```
|
||||
Read [More about Hints](doc/hints.md) for how they work and what the options are.
|
||||
|
||||
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":
|
||||
```
|
||||
Hint (hidden := true) "some hidden hint"
|
||||
```
|
||||
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).
|
||||
|
||||
Lastly, you should put variable names in hints inside brackets:
|
||||
|
||||
```
|
||||
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.
|
||||
NOTE: At present, only the images for a world are displayed. They appear in the introduction of the world.
|
||||
|
||||
## 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)
|
||||
|
||||
There are a few more options you can add in `Game.lean` before the `MakeGame` command, which describe the tile that is visible on the server's landing page:
|
||||
|
||||
```lean
|
||||
Languages "English"
|
||||
CaptionShort "Game Template"
|
||||
CaptionLong "You should use this game as a template for your own game and add your own levels."
|
||||
Prerequisites "NNG"
|
||||
CoverImage "images/cover.png"
|
||||
```
|
||||
* `Languages`: Currently only a single language (capital English name). The tile will show a corresponding flag.
|
||||
* `CaptionShort`: One catch phrase. Appears above the image.
|
||||
* `CaptionLong`: 2-4 sentences to describe the game.
|
||||
* `Prerequisites` a list of other games you should play before this one, e.g. `Prerequisites "NNG" "STG"`. The game names are free-text.
|
||||
* `CoverImage`: You can create a folder `images/` and put images there for the game to use. The maximal ratio is ca. 500x200 (W x H) but it might be cropped horizontally on narrow screens.
|
||||
|
||||
## Further Notes
|
||||
|
||||
Here are some random further things you should consider designing a new game:
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
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).
|
||||
|
||||
@@ -86,11 +86,10 @@ Download dependencies and build the game:
|
||||
```bash
|
||||
cd GameSkeleton
|
||||
lake update -R
|
||||
lake exe cache get # if your game depends on mathlib
|
||||
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
|
||||
cd ..
|
||||
git clone https://github.com/leanprover-community/lean4game.git
|
||||
|
||||
+3
-4
@@ -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:
|
||||
|
||||
* Dev Container: Rebuild the VSCode Devcontainer.
|
||||
* Local Setup: run
|
||||
* Local Setup: in your game's folder run the following:
|
||||
```
|
||||
lake update -R
|
||||
lake build
|
||||
```
|
||||
in your game folder.
|
||||
|
||||
* 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 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`
|
||||
* 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
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
module.exports = {
|
||||
apps : [{
|
||||
name : "lean4game",
|
||||
script : "server/index.mjs",
|
||||
script : "relay/index.mjs",
|
||||
env: {
|
||||
LEAN4GAME_GITHUB_USER: "",
|
||||
LEAN4GAME_GITHUB_TOKEN: "",
|
||||
|
||||
Generated
+3
-3
@@ -16122,9 +16122,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
|
||||
"integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==",
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz",
|
||||
"integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.18.10",
|
||||
"postcss": "^8.4.27",
|
||||
|
||||
+2
-2
@@ -63,13 +63,13 @@
|
||||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"build": "npm run build_server && npm run build_client",
|
||||
"preview": "vite preview",
|
||||
"build_server": "cd server && lake 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": {
|
||||
"extends": [
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
ELAN_HOME=$(lake env printenv ELAN_HOME)
|
||||
|
||||
|
||||
# $1 : the game directory
|
||||
# $2 : the lean4game folder
|
||||
# $3 : the gameserver executable
|
||||
|
||||
(exec bwrap\
|
||||
--bind $2 /lean4game \
|
||||
@@ -24,6 +26,6 @@ ELAN_HOME=$(lake env printenv ELAN_HOME)
|
||||
--unshare-uts \
|
||||
--unshare-cgroup \
|
||||
--die-with-parent \
|
||||
--chdir "/lean4game/server/.lake/build/bin/" \
|
||||
--chdir "/game/.lake/packages/GameServer/server/.lake/build/bin/" \
|
||||
./gameserver --server /game
|
||||
)
|
||||
@@ -79,17 +79,17 @@ async function doImport (owner, repo, id) {
|
||||
artifactId = artifact.id
|
||||
const url = artifact.archive_download_url
|
||||
// Make sure the download folder exists
|
||||
if (!fs.existsSync(`${__dirname}/../games`)){
|
||||
fs.mkdirSync(`${__dirname}/../games`);
|
||||
if (!fs.existsSync(path.join(__dirname, "..", "games"))){
|
||||
fs.mkdirSync(path.join(__dirname, "..", "games"));
|
||||
}
|
||||
if (!fs.existsSync(`${__dirname}/../games/tmp`)){
|
||||
fs.mkdirSync(`${__dirname}/../games/tmp`);
|
||||
if (!fs.existsSync(path.join(__dirname, "..", "games", "tmp"))){
|
||||
fs.mkdirSync(path.join(__dirname, "..", "games", "tmp"));
|
||||
}
|
||||
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`
|
||||
|
||||
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`);
|
||||
@@ -108,12 +108,11 @@ async function doImport (owner, repo, id) {
|
||||
} catch (e) {
|
||||
progress[id].output += `Error: ${e.toString()}\n${e.stack}`
|
||||
} finally {
|
||||
// if (artifactId) {
|
||||
// // fs.rmSync(`tmp/artifact_${artifactId}.zip`, {force: true, recursive: true});
|
||||
// // fs.rmSync(`tmp/artifact_${artifactId}`, {force: true, recursive: true});
|
||||
// // fs.rmSync(`tmp/artifact_${artifactId}_inner`, {force: true, recursive: true});
|
||||
// // fs.rmSync(`tmp/archive_${artifactId}.tar`, {force: true, recursive: true});
|
||||
// }
|
||||
// clean-up temp. files
|
||||
if (artifactId) {
|
||||
fs.rmSync(path.join(__dirname, "..", "games", "tmp", `${owner}_${repo}_${artifactId}.zip`), {force: true, recursive: false});
|
||||
fs.rmSync(path.join(__dirname, "..", "games", "tmp", `${owner}_${repo}_${artifactId}`), {force: true, recursive: true});
|
||||
}
|
||||
progress[id].done = true
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 10000))
|
||||
@@ -34,34 +34,15 @@ var router = express.Router();
|
||||
router.get('/import/status/:owner/:repo', importStatus)
|
||||
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
|
||||
.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)
|
||||
.listen(PORT, () => console.log(`Listening on ${PORT}`));
|
||||
|
||||
@@ -84,13 +65,13 @@ function getGameDir(owner, repo) {
|
||||
if (owner == 'local') {
|
||||
if(!isDevelopment) {
|
||||
console.error(`No local games in production mode.`)
|
||||
return
|
||||
return ""
|
||||
}
|
||||
} else {
|
||||
if(!fs.existsSync(path.join(__dirname, '..', 'games'))) {
|
||||
console.error(`Did not find the following folder: ${path.join(__dirname, '..', 'games')}`)
|
||||
console.error('Did you already import any games?')
|
||||
return
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +81,7 @@ function getGameDir(owner, repo) {
|
||||
|
||||
if(!fs.existsSync(game_dir)) {
|
||||
console.error(`Game '${game_dir}' does not exist!`)
|
||||
return
|
||||
return ""
|
||||
}
|
||||
|
||||
return game_dir;
|
||||
@@ -114,13 +95,22 @@ function startServerProcess(owner, repo) {
|
||||
let serverProcess
|
||||
if (isDevelopment) {
|
||||
let args = ["--server", game_dir]
|
||||
serverProcess = cp.spawn("./gameserver", args,
|
||||
{ cwd: path.join(__dirname, "./.lake/build/bin/") })
|
||||
let binDir = path.join(game_dir, ".lake", "packages", "GameServer", "server", ".lake", "build", "bin")
|
||||
// Note: `cwd` is important to be the `bin` directory as `Watchdog` calls `./gameserver` again
|
||||
if (fs.existsSync(binDir)) {
|
||||
// Try to use the game's own copy of `gameserver`.
|
||||
serverProcess = cp.spawn("./gameserver", args, { cwd: binDir })
|
||||
} else {
|
||||
// If the game is built with `-Klean4game.local` there is no copy in the lake packages.
|
||||
serverProcess = cp.spawn("./gameserver", args,
|
||||
{ cwd: path.join(__dirname, "..", "server", ".lake", "build", "bin") })
|
||||
}
|
||||
} else {
|
||||
serverProcess = cp.spawn("./bubblewrap.sh",
|
||||
[game_dir, path.join(__dirname, '..')],
|
||||
[ game_dir, path.join(__dirname, '..')],
|
||||
{ cwd: __dirname })
|
||||
}
|
||||
|
||||
serverProcess.on('error', error =>
|
||||
console.error(`Launching Lean Server failed: ${error}`)
|
||||
)
|
||||
@@ -6,7 +6,7 @@ REPO=$3
|
||||
|
||||
# mkdir -p games
|
||||
cd games
|
||||
pwd
|
||||
|
||||
# mkdir -p tmp
|
||||
mkdir -p ${OWNER}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
build/
|
||||
games/
|
||||
.lake
|
||||
+107
-363
@@ -1,23 +1,12 @@
|
||||
import GameServer.EnvExtensions
|
||||
import GameServer.Helpers
|
||||
import GameServer.Inventory
|
||||
import GameServer.Options
|
||||
import GameServer.SaveData
|
||||
|
||||
open Lean Meta Elab Command
|
||||
|
||||
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 -/
|
||||
|
||||
/-- 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
|
||||
| .Level => modifyCurLevel fun level => pure {level 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. -/
|
||||
elab "Introduction" t:str : command => do
|
||||
let intro := t.getString
|
||||
match ← getCurLayer with
|
||||
| .Level => modifyCurLevel fun level => pure {level with introduction := t.getString}
|
||||
| .World => modifyCurWorld fun world => pure {world with introduction := t.getString}
|
||||
| .Game => modifyCurGame fun game => pure {game with introduction := t.getString}
|
||||
| .Level => modifyCurLevel fun level => pure {level with introduction := intro}
|
||||
| .World => modifyCurWorld fun world => pure {world with introduction := intro}
|
||||
| .Game => modifyCurGame fun game => pure {game with introduction := intro}
|
||||
|
||||
/-- Define the info of the current game. Used for e.g. credits -/
|
||||
elab "Info" t:str : command => do
|
||||
let info:= t.getString
|
||||
match ← getCurLayer with
|
||||
| .Level => pure ()
|
||||
| .World => pure ()
|
||||
| .Game => modifyCurGame fun game => pure {game with info := t.getString}
|
||||
| .Level =>
|
||||
logError "Can't use `Info` in a level!"
|
||||
pure ()
|
||||
| .World =>
|
||||
logError "Can't use `Info` in a world"
|
||||
pure ()
|
||||
| .Game => modifyCurGame fun game => pure {game with info := 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
|
||||
building a level. -/
|
||||
elab "Conclusion" t:str : command => do
|
||||
let conclusion := t.getString
|
||||
match ← getCurLayer with
|
||||
| .Level => modifyCurLevel fun level => pure {level with conclusion := t.getString}
|
||||
| .World => modifyCurWorld fun world => pure {world with conclusion := t.getString}
|
||||
| .Game => modifyCurGame fun game => pure {game with conclusion := t.getString}
|
||||
| .Level => modifyCurLevel fun level => pure {level with conclusion := conclusion}
|
||||
| .World => modifyCurWorld fun world => pure {world with conclusion := conclusion}
|
||||
| .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
|
||||
|
||||
@@ -80,113 +130,6 @@ in the first level and get enabled during the game.
|
||||
|
||||
/-! ## 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:
|
||||
|
||||
```
|
||||
@@ -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 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 · {
|
||||
type := .Tactic
|
||||
name := name.getId
|
||||
displayName := name.getId.toString
|
||||
content := content.getString })
|
||||
content := doc })
|
||||
|
||||
/-- 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
|
||||
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 · {
|
||||
type := .Lemma
|
||||
name := name.getId
|
||||
category := category.getString
|
||||
displayName := displayName.getString
|
||||
content := content.getString })
|
||||
content := doc })
|
||||
-- TODO: Catch the following behaviour.
|
||||
-- 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`
|
||||
@@ -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
|
||||
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 · {
|
||||
type := .Definition
|
||||
name := name.getId,
|
||||
displayName := displayName.getString,
|
||||
content := template.getString })
|
||||
content := doc })
|
||||
|
||||
/-! ## 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. -/
|
||||
elab "NewTactic" args:ident* : command => do
|
||||
for name in ↑args do
|
||||
@@ -349,54 +278,12 @@ elab "LemmaTab" category:str : command =>
|
||||
|
||||
/-! # 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. -/
|
||||
elab doc:docComment ? attrs:Parser.Term.attributes ?
|
||||
"Statement" statementName:ident ? sig:declSig val:declVal : command => do
|
||||
let lvlIdx ← getCurLevelIdx
|
||||
|
||||
let docContent : Option String := match doc with
|
||||
| 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]}"
|
||||
let docContent ← parseDocComment doc
|
||||
|
||||
-- Save the messages before evaluation of the proof.
|
||||
let initMsgs ← modifyGet fun st => (st.messages, { st with messages := {} })
|
||||
@@ -497,23 +384,6 @@ elab doc:docComment ? attrs:Parser.Term.attributes ?
|
||||
|
||||
/-! # 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
|
||||
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
|
||||
return {level with template := s!"{template}"}
|
||||
|
||||
|
||||
-- TODO: Notes for testing if a declaration has the simp attribute
|
||||
|
||||
-- -- Test: From zulip
|
||||
@@ -644,136 +515,6 @@ elab "Template" tacs:tacticSeq : tactic => do
|
||||
|
||||
/-! # 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
|
||||
automatically computed but can also be defined with the syntax
|
||||
`Dependency World₁ → World₂ → World₃`. -/
|
||||
@@ -848,8 +589,12 @@ elab "MakeGame" : command => do
|
||||
-- Items that should not be displayed in inventory
|
||||
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
|
||||
for (worldId, world) in game.worlds.nodes.toArray do
|
||||
for (worldId, world) in allWorlds do
|
||||
let mut usedItems : HashSet Name := {}
|
||||
let mut newItems : HashSet Name := {}
|
||||
for inventoryType in #[.Tactic, .Definition, .Lemma] do
|
||||
@@ -888,9 +633,12 @@ elab "MakeGame" : command => do
|
||||
-- logInfo m!"{worldId} uses: {usedItems.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 -/
|
||||
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
|
||||
worldsWithNewItem := worldsWithNewItem.insert newItem $
|
||||
(worldsWithNewItem.findD newItem {}).insert worldId
|
||||
@@ -902,7 +650,7 @@ elab "MakeGame" : command => do
|
||||
let mut dependencyReasons : HashMap (Name × Name) (HashSet Name) := {}
|
||||
|
||||
-- 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 := {}
|
||||
-- Adding manual dependencies that were specified via the `Dependency` command.
|
||||
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}."
|
||||
else
|
||||
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
|
||||
modifyCurGame fun game =>
|
||||
pure {game with worlds := {game.worlds with
|
||||
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
|
||||
let game ← getCurGame
|
||||
|
||||
@@ -1094,17 +852,3 @@ elab "MakeGame" : command => do
|
||||
allItemsByType := allItemsByType.insert inventoryType allItems
|
||||
|
||||
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
|
||||
|
||||
@@ -148,6 +148,12 @@ structure InventoryOverview where
|
||||
lemmaTab : Option String
|
||||
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 -/
|
||||
|
||||
/-- Register a (non-persistent) environment extension to hold the current level -/
|
||||
@@ -265,7 +271,9 @@ structure GameLevel where
|
||||
lemmas: InventoryInfo := default
|
||||
/-- A proof template that is printed in an empty editor. -/
|
||||
template: Option String := none
|
||||
deriving Inhabited, Repr
|
||||
/-- The image for this level. -/
|
||||
image : String := default
|
||||
deriving Inhabited, Repr
|
||||
|
||||
/-- Json-encodable version of `GameLevel`
|
||||
Fields:
|
||||
@@ -286,6 +294,7 @@ structure LevelInfo where
|
||||
displayName : Option String
|
||||
statementName : Option String
|
||||
template : Option String
|
||||
image: Option String
|
||||
deriving ToJson, FromJson
|
||||
|
||||
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
|
||||
-- lemma doc must exist.
|
||||
template := lvl.template
|
||||
image := lvl.image
|
||||
}
|
||||
|
||||
/-! ## World -/
|
||||
@@ -331,17 +341,46 @@ structure World where
|
||||
conclusion : String := default
|
||||
/-- The levels of the world. -/
|
||||
levels: HashMap Nat GameLevel := default
|
||||
/-- The introduction image of the world. -/
|
||||
image: String := default
|
||||
deriving Inhabited
|
||||
|
||||
instance : ToJson World := ⟨
|
||||
fun world => Json.mkObj [
|
||||
("name", toJson world.name),
|
||||
("title", world.title),
|
||||
("introduction", world.introduction)]
|
||||
("introduction", world.introduction),
|
||||
("image", world.image)]
|
||||
⟩
|
||||
|
||||
/-! ## 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
|
||||
/-- Internal name of the game. -/
|
||||
name : Name
|
||||
@@ -356,6 +395,10 @@ structure Game where
|
||||
/-- TODO: currently unused. -/
|
||||
authors : List String := 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
|
||||
|
||||
def getGameJson (game : «Game») : Json := Id.run do
|
||||
|
||||
@@ -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 []
|
||||
@@ -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}}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -126,7 +126,7 @@ def findHints (goal : MVarId) (doc : FileWorker.EditableDocument) (initParams :
|
||||
then
|
||||
let userFVars := hintFVars.map fun v => bij.forward.findD v.fvarId! v.fvarId!
|
||||
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
|
||||
return some { text := text, hidden := hint.hidden }
|
||||
else return none
|
||||
|
||||
@@ -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))
|
||||
@@ -4,10 +4,10 @@
|
||||
[{"url": "https://github.com/leanprover/std4.git",
|
||||
"type": "git",
|
||||
"subDir": null,
|
||||
"rev": "a652e09bd81bcb43ea132d64ecc16580b0c7fa50",
|
||||
"rev": "2e4a3586a8f16713f16b2d2b3af3d8e65f3af087",
|
||||
"name": "std",
|
||||
"manifestFile": "lake-manifest.json",
|
||||
"inputRev": "v4.3.0-rc2",
|
||||
"inputRev": "v4.3.0",
|
||||
"inherited": false,
|
||||
"configFile": "lakefile.lean"}],
|
||||
"name": "GameServer",
|
||||
|
||||
+11
-1
@@ -12,6 +12,16 @@ lean_lib GameServer
|
||||
|
||||
@[default_target]
|
||||
lean_exe gameserver {
|
||||
root := `Main
|
||||
root := `GameServer
|
||||
supportInterpreter := true
|
||||
}
|
||||
|
||||
/--
|
||||
When a package depending on GameServer updates its dependencies,
|
||||
build the `gameserver` executable.
|
||||
-/
|
||||
post_update pkg do
|
||||
let rootPkg ← getRootPackage
|
||||
if rootPkg.name = pkg.name then
|
||||
return -- do not run in GameServer itself
|
||||
discard <| runBuild gameserver.build >>= (·.await)
|
||||
|
||||
@@ -1 +1 @@
|
||||
leanprover/lean4:v4.3.0-rc2
|
||||
leanprover/lean4:v4.3.0
|
||||
|
||||
+1
-1
@@ -12,5 +12,5 @@
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
},
|
||||
"exclude": ["server"]
|
||||
"exclude": ["server", "relay"]
|
||||
}
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ export default defineConfig({
|
||||
//root: 'client/src',
|
||||
build: {
|
||||
// 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',
|
||||
},
|
||||
plugins: [
|
||||
@@ -41,7 +41,7 @@ export default defineConfig({
|
||||
'/import': {
|
||||
target: 'http://localhost:8080',
|
||||
},
|
||||
'/api': {
|
||||
'/data': {
|
||||
target: 'http://localhost:8080',
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user