Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb214f426b | ||
|
|
4266c090db |
@@ -1,6 +1,6 @@
|
|||||||
# Lean 4 Game
|
# Lean 4 Game
|
||||||
|
|
||||||
This is the source code for a Lean game platform hosted at [adam.math.hhu.de](https://adam.math.hhu.de).
|
This is the source code for a Lean 4 game platform hosted at [adam.math.hhu.de](https://adam.math.hhu.de).
|
||||||
|
|
||||||
## Creating a Game
|
## Creating a Game
|
||||||
|
|
||||||
@@ -28,9 +28,7 @@ should be up-to-date:
|
|||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
|
|
||||||
not fully written yet.
|
not written yet
|
||||||
|
|
||||||
* [Server](doc/DOCUMENTATION.md): describes the server part (i.e. the content of `server/` und `relay/`).
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
@@ -42,8 +40,7 @@ Providing the use access to a Lean instance running on the server is a severe se
|
|||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
The project has pimarily been developed by Alexander Bentkamp and Jon Eugster.
|
The project is based on ideas from the [Lean Game Maker](https://github.com/mpedramfar/Lean-game-maker) and the [Natural Number Game
|
||||||
|
|
||||||
It is based on ideas from the [Lean Game Maker](https://github.com/mpedramfar/Lean-game-maker) and the [Natural Number Game
|
|
||||||
(NNG)](https://www.ma.imperial.ac.uk/~buzzard/xena/natural_number_game/)
|
(NNG)](https://www.ma.imperial.ac.uk/~buzzard/xena/natural_number_game/)
|
||||||
by Kevin Buzzard and Mohammad Pedramfar, and on Patrick Massot's prototype: [NNG4](https://github.com/PatrickMassot/NNG4).
|
by Kevin Buzzard and Mohammad Pedramfar.
|
||||||
|
The project is based on Patrick Massot's prototype: [NNG4](https://github.com/PatrickMassot/NNG4).
|
||||||
|
|||||||
+8
-20
@@ -9,37 +9,25 @@ import '@fontsource/roboto/700.css';
|
|||||||
import './css/reset.css';
|
import './css/reset.css';
|
||||||
import './css/app.css';
|
import './css/app.css';
|
||||||
import { MobileContext } from './components/infoview/context';
|
import { MobileContext } from './components/infoview/context';
|
||||||
import { useMobile } from './hooks';
|
import { useWindowDimensions } from './window_width';
|
||||||
import { AUTO_SWITCH_THRESHOLD, getWindowDimensions} from './state/preferences';
|
import { connection } from './connection';
|
||||||
|
|
||||||
export const GameIdContext = React.createContext<string>(undefined);
|
export const GameIdContext = React.createContext<string>(undefined);
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { mobile, setMobile, lockMobile, setLockMobile } = useMobile();
|
|
||||||
|
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const gameId = "g/" + params.owner + "/" + params.repo
|
const gameId = "g/" + params.owner + "/" + params.repo
|
||||||
|
const {width, height} = useWindowDimensions()
|
||||||
|
const [mobile, setMobile] = React.useState(width < 800)
|
||||||
|
|
||||||
const automaticallyAdjustLayout = () => {
|
React.useEffect(() => {
|
||||||
const {width} = getWindowDimensions()
|
connection.startLeanClient(gameId);
|
||||||
setMobile(width < AUTO_SWITCH_THRESHOLD)
|
}, [gameId])
|
||||||
}
|
|
||||||
|
|
||||||
React.useEffect(()=>{
|
|
||||||
if (!lockMobile){
|
|
||||||
void automaticallyAdjustLayout()
|
|
||||||
window.addEventListener('resize', automaticallyAdjustLayout)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', automaticallyAdjustLayout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [lockMobile])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<GameIdContext.Provider value={gameId}>
|
<GameIdContext.Provider value={gameId}>
|
||||||
<MobileContext.Provider value={{mobile, setMobile, lockMobile, setLockMobile}}>
|
<MobileContext.Provider value={{mobile, setMobile}}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</MobileContext.Provider>
|
</MobileContext.Provider>
|
||||||
</GameIdContext.Provider>
|
</GameIdContext.Provider>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import * as React from 'react'
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faDownload, faUpload, faEraser, faBook, faBookOpen, faGlobe, faHome,
|
import { faDownload, faUpload, faEraser, faBook, faBookOpen, faGlobe, faHome,
|
||||||
faArrowRight, faArrowLeft, faXmark, faBars, faCode,
|
faArrowRight, faArrowLeft, faXmark, faBars, faCode,
|
||||||
faCircleInfo, faTerminal, faMobileScreenButton, faDesktop, faGear } from '@fortawesome/free-solid-svg-icons'
|
faCircleInfo, faTerminal } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { GameIdContext } from "../app"
|
import { GameIdContext } from "../app"
|
||||||
import { InputModeContext, MobileContext, WorldLevelIdContext } from "./infoview/context"
|
import { InputModeContext, MobileContext, WorldLevelIdContext } from "./infoview/context"
|
||||||
import { GameInfo, useGetGameInfoQuery } from '../state/api'
|
import { GameInfo, useGetGameInfoQuery } from '../state/api'
|
||||||
@@ -150,23 +150,22 @@ function InventoryButton({pageNumber, setPageNumber}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** the navigation bar on the welcome page */
|
/** the navigation bar on the welcome page */
|
||||||
export function WelcomeAppBar({pageNumber, setPageNumber, gameInfo, toggleImpressum, toggleEraseMenu, toggleUploadMenu, toggleInfo, togglePreferencesPopup} : {
|
export function WelcomeAppBar({pageNumber, setPageNumber, gameInfo, toggleImpressum, toggleEraseMenu, toggleUploadMenu, toggleInfo} : {
|
||||||
pageNumber: number,
|
pageNumber: number,
|
||||||
setPageNumber: any,
|
setPageNumber: any,
|
||||||
gameInfo: GameInfo,
|
gameInfo: GameInfo,
|
||||||
toggleImpressum: any,
|
toggleImpressum: any,
|
||||||
toggleEraseMenu: any,
|
toggleEraseMenu: any,
|
||||||
toggleUploadMenu: any,
|
toggleUploadMenu: any,
|
||||||
toggleInfo: any,
|
toggleInfo: any
|
||||||
togglePreferencesPopup: () => void;
|
|
||||||
}) {
|
}) {
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const gameProgress = useAppSelector(selectProgress(gameId))
|
const gameProgress = useAppSelector(selectProgress(gameId))
|
||||||
const {mobile, setMobile} = React.useContext(MobileContext)
|
const {mobile} = React.useContext(MobileContext)
|
||||||
const [navOpen, setNavOpen] = React.useState(false)
|
const [navOpen, setNavOpen] = React.useState(false)
|
||||||
|
|
||||||
return <div className="app-bar">
|
return <div className="app-bar">
|
||||||
<div className='app-bar-left'>
|
<div>
|
||||||
<Button inverted="false" title="back to games selection" to="/">
|
<Button inverted="false" title="back to games selection" to="/">
|
||||||
<FontAwesomeIcon icon={faArrowLeft} /> <FontAwesomeIcon icon={faGlobe} />
|
<FontAwesomeIcon icon={faArrowLeft} /> <FontAwesomeIcon icon={faGlobe} />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -195,9 +194,6 @@ export function WelcomeAppBar({pageNumber, setPageNumber, gameInfo, toggleImpres
|
|||||||
<Button title="Impressum, privacy policy" inverted="true" to="" onClick={() => {toggleImpressum(); setNavOpen(false)}}>
|
<Button title="Impressum, privacy policy" inverted="true" to="" onClick={() => {toggleImpressum(); setNavOpen(false)}}>
|
||||||
<FontAwesomeIcon icon={faCircleInfo} /> Impressum
|
<FontAwesomeIcon icon={faCircleInfo} /> Impressum
|
||||||
</Button>
|
</Button>
|
||||||
<Button title="Preferences" inverted="true" to="" onClick={() => {togglePreferencesPopup(); setNavOpen(false)}}>
|
|
||||||
<FontAwesomeIcon icon={faGear} /> Preferences
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -241,7 +237,7 @@ export function LevelAppBar({isLoading, levelTitle, toggleImpressum, pageNumber=
|
|||||||
</> :
|
</> :
|
||||||
<>
|
<>
|
||||||
{/* DESKTOP VERSION */}
|
{/* DESKTOP VERSION */}
|
||||||
<div className='app-bar-left'>
|
<div>
|
||||||
<HomeButton isDropdown={false} />
|
<HomeButton isDropdown={false} />
|
||||||
<span className="app-bar-title">{worldTitle && `World: ${worldTitle}`}</span>
|
<span className="app-bar-title">{worldTitle && `World: ${worldTitle}`}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -62,18 +62,12 @@ export const ProofStateContext = React.createContext<{
|
|||||||
setProofState: () => {},
|
setProofState: () => {},
|
||||||
})
|
})
|
||||||
|
|
||||||
export interface IMobileContext {
|
export const MobileContext = React.createContext<{
|
||||||
mobile : boolean,
|
mobile : boolean,
|
||||||
setMobile: React.Dispatch<React.SetStateAction<Boolean>>,
|
setMobile: React.Dispatch<React.SetStateAction<Boolean>>,
|
||||||
lockMobile: boolean,
|
}>({
|
||||||
setLockMobile: React.Dispatch<React.SetStateAction<Boolean>>,
|
mobile : false,
|
||||||
}
|
|
||||||
|
|
||||||
export const MobileContext = React.createContext<IMobileContext>({
|
|
||||||
mobile: false,
|
|
||||||
setMobile: () => {},
|
setMobile: () => {},
|
||||||
lockMobile: false,
|
|
||||||
setLockMobile: () => {}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const WorldLevelIdContext = React.createContext<{
|
export const WorldLevelIdContext = React.createContext<{
|
||||||
|
|||||||
@@ -493,20 +493,18 @@ export function TypewriterInterface({props}) {
|
|||||||
<Markdown>{props.data?.introduction}</Markdown>
|
<Markdown>{props.data?.introduction}</Markdown>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{mobile &&
|
{mobile && <>
|
||||||
<Hints key={`hints-${i}`}
|
<Hints key={`hints-${i}`}
|
||||||
hints={step.hints} showHidden={showHelp.has(i)} step={i}
|
hints={step.hints} showHidden={showHelp.has(i)} step={i}
|
||||||
selected={selectedStep} toggleSelection={toggleSelectStep(i)}/>
|
selected={selectedStep} toggleSelection={toggleSelectStep(i)}/>
|
||||||
|
{i == proof.length - 1 && hasHiddenHints(proof.length - 1) && !showHelp.has(k - withErr) &&
|
||||||
|
<Button className="btn btn-help" to="" onClick={activateHiddenHints}>
|
||||||
|
Show more help!
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
<GoalsTabs proofStep={step} last={i == proof.length - (lastStepErrors ? 2 : 1)} onClick={toggleSelectStep(i)} onGoalChange={i == proof.length - 1 - withErr ? (n) => setDisableInput(n > 0) : (n) => {}}/>
|
<GoalsTabs proofStep={step} last={i == proof.length - (lastStepErrors ? 2 : 1)} onClick={toggleSelectStep(i)} onGoalChange={i == proof.length - 1 - withErr ? (n) => setDisableInput(n > 0) : (n) => {}}/>
|
||||||
|
|
||||||
{mobile && i == proof.length - 1 &&
|
|
||||||
hasHiddenHints(proof.length - 1) && !showHelp.has(k - withErr) &&
|
|
||||||
<Button className="btn btn-help" to="" onClick={activateHiddenHints}>
|
|
||||||
Show more help!
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
|
|
||||||
{/* Show a message that there are no goals left */}
|
{/* Show a message that there are no goals left */}
|
||||||
{!step.goals.length && (
|
{!step.goals.length && (
|
||||||
<div className="message information">
|
<div className="message information">
|
||||||
@@ -523,7 +521,7 @@ export function TypewriterInterface({props}) {
|
|||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
{mobile && completed &&
|
{mobile && completed &&
|
||||||
<div className="button-row mobile">
|
<div className="button-row">
|
||||||
{props.level >= props.worldSize ?
|
{props.level >= props.worldSize ?
|
||||||
<Button to={`/${gameId}`}>
|
<Button to={`/${gameId}`}>
|
||||||
<FontAwesomeIcon icon={faHome} /> Leave World
|
<FontAwesomeIcon icon={faHome} /> Leave World
|
||||||
|
|||||||
@@ -2,21 +2,18 @@ import * as React from 'react';
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import '../css/inventory.css'
|
import '../css/inventory.css'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faLock, faBan, faCheck } from '@fortawesome/free-solid-svg-icons'
|
import { faLock, faBan } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { faClipboard } from '@fortawesome/free-regular-svg-icons'
|
|
||||||
import { GameIdContext } from '../app';
|
import { GameIdContext } from '../app';
|
||||||
import Markdown from './markdown';
|
import Markdown from './markdown';
|
||||||
import { useLoadDocQuery, InventoryTile, LevelInfo, InventoryOverview, useLoadInventoryOverviewQuery } from '../state/api';
|
import { useLoadDocQuery, InventoryTile, LevelInfo, InventoryOverview, useLoadInventoryOverviewQuery, WorldOverview } from '../state/api';
|
||||||
import { selectDifficulty, selectInventory } from '../state/progress';
|
import { selectDifficulty, selectInventory } from '../state/progress';
|
||||||
import { store } from '../state/store';
|
import { store } from '../state/store';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
export function Inventory({levelInfo, openDoc, lemmaTab, setLemmaTab, enableAll=false} :
|
export function Inventory({levelInfo, openDoc, enableAll=false} :
|
||||||
{
|
{
|
||||||
levelInfo: LevelInfo|InventoryOverview,
|
levelInfo: LevelInfo|InventoryOverview,
|
||||||
openDoc: (props: {name: string, type: string}) => void,
|
openDoc: (props: {name: string, type: string}) => void,
|
||||||
lemmaTab: any,
|
|
||||||
setLemmaTab: any,
|
|
||||||
enableAll?: boolean,
|
enableAll?: boolean,
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
@@ -34,20 +31,92 @@ export function Inventory({levelInfo, openDoc, lemmaTab, setLemmaTab, enableAll=
|
|||||||
}
|
}
|
||||||
<h2>Theorems</h2>
|
<h2>Theorems</h2>
|
||||||
{levelInfo?.lemmas &&
|
{levelInfo?.lemmas &&
|
||||||
<InventoryList items={levelInfo?.lemmas} docType="Lemma" openDoc={openDoc} level={levelInfo} enableAll={enableAll} tab={lemmaTab} setTab={setLemmaTab}/>
|
<InventoryList items={levelInfo?.lemmas} docType="Lemma" openDoc={openDoc} defaultTab={levelInfo?.lemmaTab} level={levelInfo} enableAll={enableAll}/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function InventoryList({items, docType, openDoc, tab=null, setTab=undefined, level=undefined, enableAll=false} :
|
export function OverviewInventory({data, openDoc, enableAll=false, showOverview=true} :
|
||||||
|
{
|
||||||
|
data: WorldOverview[],
|
||||||
|
openDoc: (props: {name: string, type: string}) => void,
|
||||||
|
enableAll?: boolean,
|
||||||
|
showOverview?: boolean
|
||||||
|
}) {
|
||||||
|
|
||||||
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
const difficulty = useSelector(selectDifficulty(gameId))
|
||||||
|
const [tab, setTab] = useState<string>()
|
||||||
|
let inv: string[] = selectInventory(gameId)(store.getState())
|
||||||
|
|
||||||
|
let levelInfo : InventoryOverview = {
|
||||||
|
tactics : data ? data.map(world => (world.tactics.map(tile => inv.includes(tile.name) ? {...tile, locked: false} : tile))).flat() : [],
|
||||||
|
lemmas : data ? data.map(world => (world.lemmas.map(tile => inv.includes(tile.name) ? {...tile, locked: false} : tile))).flat() : [],
|
||||||
|
definitions : data ? data.map(world => (world.definitions.map(tile => inv.includes(tile.name) ? {...tile, locked: false} : tile))).flat() : [],
|
||||||
|
lemmaTab: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Inventory levelInfo={levelInfo} openDoc={openDoc} enableAll={enableAll}/>
|
||||||
|
{showOverview &&
|
||||||
|
<div className="inventory">
|
||||||
|
{data && <>
|
||||||
|
<h2>Overviews</h2>
|
||||||
|
<div className="tab-bar">
|
||||||
|
{data.map(world => {
|
||||||
|
return <div key={`category-${world.world}`} className={`tab ${world.world == tab ? "active": ""}`} onClick={() => { setTab((tab == world.world) ? null : world.world) }}>{world.world}</div>
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="inventory-list">
|
||||||
|
{/* TODO: a bit hacky, do we need to redesign inventory completely and provide it in a better order? */}
|
||||||
|
{data.map(world => {
|
||||||
|
if (world.world == tab) {
|
||||||
|
return [
|
||||||
|
...world.tactics.map(item => {
|
||||||
|
return <InventoryItem
|
||||||
|
key={`${world.world}-${item.name}`}
|
||||||
|
showDoc={() => {openDoc({name: item.name, type: 'Tactic'})}}
|
||||||
|
name={item.name} displayName={item.displayName}
|
||||||
|
locked={difficulty > 0 ? !inv.includes(item.name) : false}
|
||||||
|
disabled={item.disabled} newly={false} enableAll={enableAll} />
|
||||||
|
}),
|
||||||
|
...world.lemmas.map(item => {
|
||||||
|
return <InventoryItem
|
||||||
|
key={`${world.world}-${item.name}`}
|
||||||
|
showDoc={() => {openDoc({name: item.name, type: 'Lemma'})}}
|
||||||
|
name={item.name} displayName={item.displayName}
|
||||||
|
locked={difficulty > 0 ? item.locked : false}
|
||||||
|
disabled={item.disabled} newly={false} enableAll={enableAll} />
|
||||||
|
}),
|
||||||
|
...world.definitions.map(item => {
|
||||||
|
return <InventoryItem
|
||||||
|
key={`${world.world}-${item.name}`}
|
||||||
|
showDoc={() => {openDoc({name: item.name, type: 'Definition'})}}
|
||||||
|
name={item.name} displayName={item.displayName}
|
||||||
|
locked={difficulty > 0 ? item.locked : false}
|
||||||
|
disabled={item.disabled} newly={false} enableAll={enableAll} />
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function InventoryList({items, docType, openDoc, defaultTab=null, level=undefined, enableAll=false} :
|
||||||
{
|
{
|
||||||
items: InventoryTile[],
|
items: InventoryTile[],
|
||||||
docType: string,
|
docType: string,
|
||||||
openDoc(props: {name: string, type: string}): void,
|
openDoc(props: {name: string, type: string}): void,
|
||||||
tab?: any,
|
defaultTab? : string,
|
||||||
setTab?: any,
|
level? : LevelInfo|InventoryOverview,
|
||||||
level?: LevelInfo|InventoryOverview,
|
|
||||||
enableAll?: boolean,
|
enableAll?: boolean,
|
||||||
}) {
|
}) {
|
||||||
// TODO: `level` is only used in the `useEffect` below to check if a new level has
|
// TODO: `level` is only used in the `useEffect` below to check if a new level has
|
||||||
@@ -63,6 +132,8 @@ function InventoryList({items, docType, openDoc, tab=null, setTab=undefined, lev
|
|||||||
}
|
}
|
||||||
const categories = Array.from(categorySet).sort()
|
const categories = Array.from(categorySet).sort()
|
||||||
|
|
||||||
|
const [tab, setTab] = useState(defaultTab)
|
||||||
|
|
||||||
// Add inventory items from local store as unlocked.
|
// Add inventory items from local store as unlocked.
|
||||||
// Items are unlocked if they are in the local store, or if the server says they should be
|
// Items are unlocked if they are in the local store, or if the server says they should be
|
||||||
// given the dependency graph. (OR-connection) (TODO: maybe add different logic for different
|
// given the dependency graph. (OR-connection) (TODO: maybe add different logic for different
|
||||||
@@ -70,6 +141,13 @@ function InventoryList({items, docType, openDoc, tab=null, setTab=undefined, lev
|
|||||||
let inv: string[] = selectInventory(gameId)(store.getState())
|
let inv: string[] = selectInventory(gameId)(store.getState())
|
||||||
let modifiedItems : InventoryTile[] = items.map(tile => inv.includes(tile.name) ? {...tile, locked: false} : tile)
|
let modifiedItems : InventoryTile[] = items.map(tile => inv.includes(tile.name) ? {...tile, locked: false} : tile)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// If the level specifies `LemmaTab "Nat"`, we switch to this tab on loading.
|
||||||
|
// `defaultTab` is `null` or `undefined` otherwise, in which case we don't want to switch.
|
||||||
|
if (defaultTab) {
|
||||||
|
setTab(defaultTab)
|
||||||
|
}}, [level])
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
{categories.length > 1 &&
|
{categories.length > 1 &&
|
||||||
<div className="tab-bar">
|
<div className="tab-bar">
|
||||||
@@ -84,26 +162,21 @@ function InventoryList({items, docType, openDoc, tab=null, setTab=undefined, lev
|
|||||||
(x, y) => +(docType == "Lemma") * (+x.locked - +y.locked || +x.disabled - +y.disabled) || x.displayName.localeCompare(y.displayName)
|
(x, y) => +(docType == "Lemma") * (+x.locked - +y.locked || +x.disabled - +y.disabled) || x.displayName.localeCompare(y.displayName)
|
||||||
).filter(item => !item.hidden && ((tab ?? categories[0]) == item.category)).map((item, i) => {
|
).filter(item => !item.hidden && ((tab ?? categories[0]) == item.category)).map((item, i) => {
|
||||||
return <InventoryItem key={`${item.category}-${item.name}`}
|
return <InventoryItem key={`${item.category}-${item.name}`}
|
||||||
item={item}
|
|
||||||
showDoc={() => {openDoc({name: item.name, type: docType})}}
|
showDoc={() => {openDoc({name: item.name, type: docType})}}
|
||||||
name={item.name} displayName={item.displayName} locked={difficulty > 0 ? item.locked : false}
|
name={item.name} displayName={item.displayName} locked={difficulty > 0 ? item.locked : false}
|
||||||
disabled={item.disabled} newly={item.new} enableAll={enableAll} />
|
disabled={item.disabled} newly={item.new} enableAll={enableAll}/>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
function InventoryItem({item, name, displayName, locked, disabled, newly, showDoc, enableAll=false}) {
|
function InventoryItem({name, displayName, locked, disabled, newly, showDoc, enableAll=false}) {
|
||||||
const icon = locked ? <FontAwesomeIcon icon={faLock} /> :
|
const icon = locked ? <FontAwesomeIcon icon={faLock} /> :
|
||||||
disabled ? <FontAwesomeIcon icon={faBan} /> : item.st
|
disabled ? <FontAwesomeIcon icon={faBan} /> : ""
|
||||||
const className = locked ? "locked" : disabled ? "disabled" : newly ? "new" : ""
|
const className = locked ? "locked" : disabled ? "disabled" : newly ? "new" : ""
|
||||||
// Note: This is somewhat a hack as the statement of lemmas comes currently in the form
|
|
||||||
// `Namespace.statement_name (x y : Nat) : some type`
|
|
||||||
const title = locked ? "Not unlocked yet" :
|
const title = locked ? "Not unlocked yet" :
|
||||||
disabled ? "Not available in this level" : (item.altTitle ? item.altTitle.substring(item.altTitle.indexOf(' ') + 1) : '')
|
disabled ? "Not available in this level" : ""
|
||||||
|
|
||||||
const [copied, setCopied] = useState(false)
|
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (enableAll || !locked) {
|
if (enableAll || !locked) {
|
||||||
@@ -111,21 +184,7 @@ function InventoryItem({item, name, displayName, locked, disabled, newly, showDo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyItemName = (ev) => {
|
return <div className={`item ${className}${enableAll ? ' enabled' : ''}`} onClick={handleClick} title={title}>{icon} {displayName}</div>
|
||||||
navigator.clipboard.writeText(displayName)
|
|
||||||
setCopied(true)
|
|
||||||
setInterval(() => {
|
|
||||||
setCopied(false)
|
|
||||||
}, 3000);
|
|
||||||
ev.stopPropagation()
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className={`item ${className}${enableAll ? ' enabled' : ''}`} onClick={handleClick} title={title}>
|
|
||||||
{icon} {displayName}
|
|
||||||
<div className="copy-button" onClick={copyItemName}>
|
|
||||||
{copied ? <FontAwesomeIcon icon={faCheck} /> : <FontAwesomeIcon icon={faClipboard} />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Documentation({name, type, handleClose}) {
|
export function Documentation({name, type, handleClose}) {
|
||||||
@@ -145,25 +204,34 @@ export function Documentation({name, type, handleClose}) {
|
|||||||
export function InventoryPanel({levelInfo, visible = true}) {
|
export function InventoryPanel({levelInfo, visible = true}) {
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
|
||||||
const [lemmaTab, setLemmaTab] = useState(levelInfo?.lemmaTab)
|
// The inventory is overlayed by the doc entry of a clicked item
|
||||||
|
const [inventoryDoc, setInventoryDoc] = useState<{name: string, type: string}>(null)
|
||||||
|
// Set `inventoryDoc` to `null` to close the doc
|
||||||
|
function closeInventoryDoc() {setInventoryDoc(null)}
|
||||||
|
|
||||||
|
return <div className={`column inventory-panel ${visible ? '' : 'hidden'}`}>
|
||||||
|
{inventoryDoc ?
|
||||||
|
<Documentation name={inventoryDoc.name} type={inventoryDoc.type} handleClose={closeInventoryDoc}/>
|
||||||
|
:
|
||||||
|
<Inventory levelInfo={levelInfo} openDoc={setInventoryDoc} enableAll={true}/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The panel (on the welcome page) showing the user's inventory with tactics, definitions, and lemmas */
|
||||||
|
export function InventoryOverviewPanel({data, visible = true, showOverview=true} : {data : WorldOverview[], visible?: boolean, showOverview?: boolean}) {
|
||||||
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
|
||||||
// The inventory is overlayed by the doc entry of a clicked item
|
// The inventory is overlayed by the doc entry of a clicked item
|
||||||
const [inventoryDoc, setInventoryDoc] = useState<{name: string, type: string}>(null)
|
const [inventoryDoc, setInventoryDoc] = useState<{name: string, type: string}>(null)
|
||||||
// Set `inventoryDoc` to `null` to close the doc
|
// Set `inventoryDoc` to `null` to close the doc
|
||||||
function closeInventoryDoc() {setInventoryDoc(null)}
|
function closeInventoryDoc() {setInventoryDoc(null)}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// If the level specifies `LemmaTab "Nat"`, we switch to this tab on loading.
|
|
||||||
// `defaultTab` is `null` or `undefined` otherwise, in which case we don't want to switch.
|
|
||||||
if (levelInfo?.lemmaTab) {
|
|
||||||
setLemmaTab(levelInfo?.lemmaTab)
|
|
||||||
}}, [levelInfo])
|
|
||||||
|
|
||||||
return <div className={`column inventory-panel ${visible ? '' : 'hidden'}`}>
|
return <div className={`column inventory-panel ${visible ? '' : 'hidden'}`}>
|
||||||
{inventoryDoc ?
|
{inventoryDoc ?
|
||||||
<Documentation name={inventoryDoc.name} type={inventoryDoc.type} handleClose={closeInventoryDoc}/>
|
<Documentation name={inventoryDoc.name} type={inventoryDoc.type} handleClose={closeInventoryDoc}/>
|
||||||
:
|
:
|
||||||
<Inventory levelInfo={levelInfo} openDoc={setInventoryDoc} enableAll={true} lemmaTab={lemmaTab} setLemmaTab={setLemmaTab}/>
|
<OverviewInventory data={data} openDoc={setInventoryDoc} enableAll={true} showOverview={showOverview}/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { EditorConnection, EditorEvents } from '../../../node_modules/lean4-info
|
|||||||
import { EventEmitter } from '../../../node_modules/lean4-infoview/src/infoview/event'
|
import { EventEmitter } from '../../../node_modules/lean4-infoview/src/infoview/event'
|
||||||
|
|
||||||
import { GameIdContext } from '../app'
|
import { GameIdContext } from '../app'
|
||||||
|
import { ConnectionContext, connection, useLeanClient } from '../connection'
|
||||||
import { useAppDispatch, useAppSelector } from '../hooks'
|
import { useAppDispatch, useAppSelector } from '../hooks'
|
||||||
import { useGetGameInfoQuery, useLoadInventoryOverviewQuery, useLoadLevelQuery } from '../state/api'
|
import { useGetGameInfoQuery, useLoadInventoryOverviewQuery, useLoadLevelQuery } from '../state/api'
|
||||||
import { changedSelection, codeEdited, selectCode, selectSelections, selectCompleted, helpEdited,
|
import { changedSelection, codeEdited, selectCode, selectSelections, selectCompleted, helpEdited,
|
||||||
@@ -25,7 +26,7 @@ import { changedSelection, codeEdited, selectCode, selectSelections, selectCompl
|
|||||||
import { store } from '../state/store'
|
import { store } from '../state/store'
|
||||||
import { Button } from './button'
|
import { Button } from './button'
|
||||||
import Markdown from './markdown'
|
import Markdown from './markdown'
|
||||||
import {InventoryPanel} from './inventory'
|
import {InventoryOverviewPanel, InventoryPanel} from './inventory'
|
||||||
import { hasInteractiveErrors } from './infoview/typewriter'
|
import { hasInteractiveErrors } from './infoview/typewriter'
|
||||||
import { DeletedChatContext, InputModeContext, MobileContext, MonacoEditorContext,
|
import { DeletedChatContext, InputModeContext, MobileContext, MonacoEditorContext,
|
||||||
ProofContext, ProofStep, SelectionContext, WorldLevelIdContext } from './infoview/context'
|
ProofContext, ProofStep, SelectionContext, WorldLevelIdContext } from './infoview/context'
|
||||||
@@ -43,15 +44,6 @@ import 'lean4web/client/src/editor/infoview.css'
|
|||||||
import 'lean4web/client/src/editor/vscode.css'
|
import 'lean4web/client/src/editor/vscode.css'
|
||||||
import '../css/level.css'
|
import '../css/level.css'
|
||||||
import { LevelAppBar } from './app_bar'
|
import { LevelAppBar } from './app_bar'
|
||||||
import { LeanClient } from 'lean4web/client/src/editor/leanclient'
|
|
||||||
import { DisposingWebSocketMessageReader } from 'lean4web/client/src/reader'
|
|
||||||
import { WebSocketMessageWriter, toSocket } from 'vscode-ws-jsonrpc'
|
|
||||||
import { IConnectionProvider } from 'monaco-languageclient'
|
|
||||||
import { monacoSetup } from 'lean4web/client/src/monacoSetup'
|
|
||||||
import { onigasmH } from 'onigasm/lib/onigasmH'
|
|
||||||
|
|
||||||
|
|
||||||
monacoSetup()
|
|
||||||
|
|
||||||
function Level() {
|
function Level() {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
@@ -219,8 +211,10 @@ function PlayableLevel({impressum, setImpressum}) {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
const difficulty = useSelector(selectDifficulty(gameId))
|
||||||
const initialCode = useAppSelector(selectCode(gameId, worldId, levelId))
|
const initialCode = useAppSelector(selectCode(gameId, worldId, levelId))
|
||||||
const initialSelections = useAppSelector(selectSelections(gameId, worldId, levelId))
|
const initialSelections = useAppSelector(selectSelections(gameId, worldId, levelId))
|
||||||
|
const inventory: Array<String> = useSelector(selectInventory(gameId))
|
||||||
|
|
||||||
const typewriterMode = useSelector(selectTypewriterMode(gameId))
|
const typewriterMode = useSelector(selectTypewriterMode(gameId))
|
||||||
const setTypewriterMode = (newTypewriterMode: boolean) => dispatch(changeTypewriterMode({game: gameId, typewriterMode: newTypewriterMode}))
|
const setTypewriterMode = (newTypewriterMode: boolean) => dispatch(changeTypewriterMode({game: gameId, typewriterMode: newTypewriterMode}))
|
||||||
@@ -299,6 +293,12 @@ function PlayableLevel({impressum, setImpressum}) {
|
|||||||
// a hint at the beginning of the proof...
|
// a hint at the beginning of the proof...
|
||||||
const [selectedStep, setSelectedStep] = useState<number>()
|
const [selectedStep, setSelectedStep] = useState<number>()
|
||||||
|
|
||||||
|
// if the user inventory changes, notify the server
|
||||||
|
useEffect(() => {
|
||||||
|
let leanClient = connection.getLeanClient(gameId)
|
||||||
|
leanClient.sendNotification('$/game/setInventory', {inventory: inventory, difficulty: difficulty})
|
||||||
|
}, [inventory])
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
// Lock editor mode
|
// Lock editor mode
|
||||||
if (level?.data?.template) {
|
if (level?.data?.template) {
|
||||||
@@ -372,7 +372,7 @@ function PlayableLevel({impressum, setImpressum}) {
|
|||||||
|
|
||||||
// Effect when command line mode gets enabled
|
// Effect when command line mode gets enabled
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (onigasmH && editor && typewriterMode) {
|
if (editor && typewriterMode) {
|
||||||
let code = editor.getModel().getLinesContent().filter(line => line.trim())
|
let code = editor.getModel().getLinesContent().filter(line => line.trim())
|
||||||
editor.executeEdits("typewriter", [{
|
editor.executeEdits("typewriter", [{
|
||||||
range: editor.getModel().getFullModelRange(),
|
range: editor.getModel().getFullModelRange(),
|
||||||
@@ -395,7 +395,7 @@ function PlayableLevel({impressum, setImpressum}) {
|
|||||||
// editor.setSelection(monaco.Selection.fromPositions(endPos, endPos))
|
// editor.setSelection(monaco.Selection.fromPositions(endPos, endPos))
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
}, [editor, typewriterMode, onigasmH == null])
|
}, [editor, typewriterMode])
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<div style={level.isLoading ? null : {display: "none"}} className="app-content loading"><CircularProgress /></div>
|
<div style={level.isLoading ? null : {display: "none"}} className="app-content loading"><CircularProgress /></div>
|
||||||
@@ -441,7 +441,6 @@ function PlayableLevel({impressum, setImpressum}) {
|
|||||||
function IntroductionPanel({gameInfo}) {
|
function IntroductionPanel({gameInfo}) {
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {worldId} = useContext(WorldLevelIdContext)
|
const {worldId} = useContext(WorldLevelIdContext)
|
||||||
const {mobile} = React.useContext(MobileContext)
|
|
||||||
|
|
||||||
let text: Array<string> = gameInfo.data?.worlds.nodes[worldId].introduction.split(/\n(\s*\n)+/)
|
let text: Array<string> = gameInfo.data?.worlds.nodes[worldId].introduction.split(/\n(\s*\n)+/)
|
||||||
|
|
||||||
@@ -452,7 +451,7 @@ function IntroductionPanel({gameInfo}) {
|
|||||||
hint={{text: t, hidden: false}} step={0} selected={null} toggleSelection={undefined} />
|
hint={{text: t, hidden: false}} step={0} selected={null} toggleSelection={undefined} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className={`button-row${mobile ? ' mobile' : ''}`}>
|
<div className="button-row">
|
||||||
{gameInfo.data?.worldSize[worldId] == 0 ?
|
{gameInfo.data?.worldSize[worldId] == 0 ?
|
||||||
<Button to={`/${gameId}`}><FontAwesomeIcon icon={faHome} /></Button> :
|
<Button to={`/${gameId}`}><FontAwesomeIcon icon={faHome} /></Button> :
|
||||||
<Button to={`/${gameId}/world/${worldId}/level/1`}>
|
<Button to={`/${gameId}/world/${worldId}/level/1`}>
|
||||||
@@ -499,7 +498,7 @@ function Introduction({impressum, setImpressum}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<InventoryPanel levelInfo={inventory?.data} />
|
<InventoryOverviewPanel data={inventory?.data} />
|
||||||
</Split>
|
</Split>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,32 +530,21 @@ function Introduction({impressum, setImpressum}) {
|
|||||||
|
|
||||||
function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChangeContent, onDidChangeSelection) {
|
function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChangeContent, onDidChangeSelection) {
|
||||||
|
|
||||||
|
const connection = React.useContext(ConnectionContext)
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
||||||
|
|
||||||
|
|
||||||
const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor|null>(null)
|
const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor|null>(null)
|
||||||
const [infoProvider, setInfoProvider] = useState<null|InfoProvider>(null)
|
const [infoProvider, setInfoProvider] = useState<null|InfoProvider>(null)
|
||||||
|
const [infoviewApi, setInfoviewApi] = useState<null|InfoviewApi>(null)
|
||||||
const [editorConnection, setEditorConnection] = useState<null|EditorConnection>(null)
|
const [editorConnection, setEditorConnection] = useState<null|EditorConnection>(null)
|
||||||
|
|
||||||
const uriStr = `file:///${worldId}/${levelId}`
|
// Create Editor
|
||||||
const uri = monaco.Uri.parse(uriStr)
|
|
||||||
|
|
||||||
const inventory: Array<String> = useSelector(selectInventory(gameId))
|
|
||||||
const difficulty: number = useSelector(selectDifficulty(gameId))
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const model = monaco.editor.createModel(initialCode ?? '', 'lean4', uri)
|
|
||||||
if (onDidChangeContent) {
|
|
||||||
model.onDidChangeContent(() => onDidChangeContent(model.getValue()))
|
|
||||||
}
|
|
||||||
|
|
||||||
const editor = monaco.editor.create(codeviewRef.current!, {
|
const editor = monaco.editor.create(codeviewRef.current!, {
|
||||||
model,
|
|
||||||
glyphMargin: true,
|
glyphMargin: true,
|
||||||
quickSuggestions: false,
|
quickSuggestions: false,
|
||||||
lineDecorationsWidth: 5,
|
|
||||||
folding: false,
|
|
||||||
lineNumbers: 'on',
|
|
||||||
lightbulb: {
|
lightbulb: {
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
@@ -568,61 +556,11 @@ function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChange
|
|||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
lineNumbersMinChars: 3,
|
lineNumbersMinChars: 3,
|
||||||
tabSize: 2,
|
|
||||||
'semanticHighlighting.enabled': true,
|
'semanticHighlighting.enabled': true,
|
||||||
theme: 'vs-code-theme-converted'
|
theme: 'vs-code-theme-converted'
|
||||||
})
|
})
|
||||||
if (onDidChangeSelection) {
|
|
||||||
editor.onDidChangeCursorSelection(() => onDidChangeSelection(editor.getSelections()))
|
|
||||||
}
|
|
||||||
if (initialSelections) {
|
|
||||||
console.debug("Initial Selection: ", initialSelections)
|
|
||||||
// BUG: Somehow I get an `invalid arguments` bug here
|
|
||||||
// editor.setSelections(initialSelections)
|
|
||||||
}
|
|
||||||
setEditor(editor)
|
|
||||||
const abbrevRewriter = new AbbreviationRewriter(new AbbreviationProvider(), model, editor)
|
|
||||||
|
|
||||||
const socketUrl = ((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + '/websocket/' + gameId
|
const infoProvider = new InfoProvider(connection.getLeanClient(gameId))
|
||||||
|
|
||||||
const connectionProvider : IConnectionProvider = {
|
|
||||||
get: async () => {
|
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
console.log(`connecting ${socketUrl}`)
|
|
||||||
const websocket = new WebSocket(socketUrl)
|
|
||||||
websocket.addEventListener('error', (ev) => {
|
|
||||||
reject(ev)
|
|
||||||
})
|
|
||||||
websocket.addEventListener('message', (msg) => {
|
|
||||||
// console.log(msg.data)
|
|
||||||
})
|
|
||||||
websocket.addEventListener('open', () => {
|
|
||||||
const socket = toSocket(websocket)
|
|
||||||
const reader = new DisposingWebSocketMessageReader(socket)
|
|
||||||
const writer = new WebSocketMessageWriter(socket)
|
|
||||||
resolve({
|
|
||||||
reader,
|
|
||||||
writer
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Following `vscode-lean4/webview/index.ts`
|
|
||||||
const client = new LeanClient(connectionProvider, showRestartMessage, {inventory, difficulty})
|
|
||||||
const infoProvider = new InfoProvider(client)
|
|
||||||
// const div: HTMLElement = infoviewRef.current!
|
|
||||||
const imports = {
|
|
||||||
'@leanprover/infoview': `${window.location.origin}/index.production.min.js`,
|
|
||||||
'react': `${window.location.origin}/react.production.min.js`,
|
|
||||||
'react/jsx-runtime': `${window.location.origin}/react-jsx-runtime.production.min.js`,
|
|
||||||
'react-dom': `${window.location.origin}/react-dom.production.min.js`,
|
|
||||||
'react-popper': `${window.location.origin}/react-popper.production.min.js`
|
|
||||||
}
|
|
||||||
// loadRenderInfoview(imports, [infoProvider.getApi(), div], setInfoviewApi)
|
|
||||||
setInfoProvider(infoProvider)
|
|
||||||
client.restart()
|
|
||||||
|
|
||||||
const editorApi = infoProvider.getApi()
|
const editorApi = infoProvider.getApi()
|
||||||
|
|
||||||
@@ -667,27 +605,54 @@ function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChange
|
|||||||
|
|
||||||
setEditor(editor)
|
setEditor(editor)
|
||||||
setInfoProvider(infoProvider)
|
setInfoProvider(infoProvider)
|
||||||
|
setInfoviewApi(infoviewApi)
|
||||||
|
|
||||||
infoProvider.openPreview(editor, infoviewApi)
|
return () => { infoProvider.dispose(); editor.dispose() }
|
||||||
const taskgutter = new LeanTaskGutter(infoProvider.client, editor)
|
}, [])
|
||||||
|
|
||||||
// TODO:
|
const {leanClient, leanClientStarted} = useLeanClient(gameId)
|
||||||
// setRestart(() => restart)
|
const uriStr = `file:///${worldId}/${levelId}`
|
||||||
|
const uri = monaco.Uri.parse(uriStr)
|
||||||
|
|
||||||
return () => {
|
// Create model when level changes
|
||||||
editor.dispose();
|
useEffect(() => {
|
||||||
model.dispose();
|
if (editor && leanClientStarted) {
|
||||||
abbrevRewriter.dispose();
|
|
||||||
taskgutter.dispose();
|
let model = monaco.editor.getModel(uri)
|
||||||
infoProvider.dispose();
|
if (!model) {
|
||||||
client.dispose();
|
model = monaco.editor.createModel(initialCode, 'lean4', uri)
|
||||||
|
}
|
||||||
|
model.onDidChangeContent(() => onDidChangeContent(model.getValue()))
|
||||||
|
editor.onDidChangeCursorSelection(() => onDidChangeSelection(editor.getSelections()))
|
||||||
|
editor.setModel(model)
|
||||||
|
if (initialSelections) {
|
||||||
|
console.debug("Initial Selection: ", initialSelections)
|
||||||
|
// BUG: Somehow I get an `invalid arguments` bug here
|
||||||
|
// editor.setSelections(initialSelections)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
editorConnection.api.sendClientNotification(uriStr, "textDocument/didClose", {textDocument: {uri: uriStr}})
|
||||||
|
model.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [gameId, worldId, levelId])
|
}, [editor, levelId, connection, leanClientStarted])
|
||||||
|
|
||||||
const showRestartMessage = () => {
|
|
||||||
// setRestartMessage(true)
|
useEffect(() => {
|
||||||
console.log("TODO: SHOW RESTART MESSAGE")
|
if (editor && leanClientStarted) {
|
||||||
}
|
|
||||||
|
let model = monaco.editor.getModel(uri)
|
||||||
|
infoviewApi.serverRestarted(leanClient.initializeResult)
|
||||||
|
|
||||||
|
infoProvider.openPreview(editor, infoviewApi)
|
||||||
|
|
||||||
|
const taskGutter = new LeanTaskGutter(infoProvider.client, editor)
|
||||||
|
const abbrevRewriter = new AbbreviationRewriter(new AbbreviationProvider(), model, editor)
|
||||||
|
|
||||||
|
return () => { abbrevRewriter.dispose(); taskGutter.dispose(); }
|
||||||
|
}
|
||||||
|
}, [editor, connection, leanClientStarted])
|
||||||
|
|
||||||
return {editor, infoProvider, editorConnection}
|
return {editor, infoProvider, editorConnection}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
import * as React from 'react'
|
|
||||||
import { Input, Typography } from '@mui/material'
|
|
||||||
import Markdown from '../markdown'
|
|
||||||
import Switch from '@mui/material/Switch';
|
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
|
||||||
|
|
||||||
import { IMobileContext } from "../infoview/context"
|
|
||||||
|
|
||||||
interface PreferencesPopupProps extends IMobileContext{
|
|
||||||
handleClose: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PreferencesPopup({ mobile, setMobile, lockMobile, setLockMobile, handleClose }: PreferencesPopupProps) {
|
|
||||||
return <div className="modal-wrapper">
|
|
||||||
<div className="modal-backdrop" onClick={handleClose} />
|
|
||||||
<div className="modal">
|
|
||||||
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
|
||||||
<Typography variant="body1" component="div" className="settings">
|
|
||||||
<div className='preferences-category'>
|
|
||||||
<div className='category-title'>
|
|
||||||
<h3>Mobile layout</h3>
|
|
||||||
</div>
|
|
||||||
<div className='preferences-item'>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
checked={mobile}
|
|
||||||
onChange={() => setMobile(!mobile)}
|
|
||||||
name="checked"
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Enable"
|
|
||||||
labelPlacement="start"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='preferences-item'>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
checked={!lockMobile}
|
|
||||||
onChange={() => setLockMobile(!lockMobile)}
|
|
||||||
name="checked"
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Auto"
|
|
||||||
labelPlacement="start"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@@ -11,13 +11,12 @@ import { changedOpenedIntro, selectOpenedIntro } from '../state/progress'
|
|||||||
import { useGetGameInfoQuery, useLoadInventoryOverviewQuery } from '../state/api'
|
import { useGetGameInfoQuery, useLoadInventoryOverviewQuery } from '../state/api'
|
||||||
import { Button } from './button'
|
import { Button } from './button'
|
||||||
import { MobileContext } from './infoview/context'
|
import { MobileContext } from './infoview/context'
|
||||||
import { InventoryPanel } from './inventory'
|
import { InventoryOverviewPanel, InventoryPanel } from './inventory'
|
||||||
import { ErasePopup } from './popup/erase'
|
import { ErasePopup } from './popup/erase'
|
||||||
import { InfoPopup } from './popup/game_info'
|
import { InfoPopup } from './popup/game_info'
|
||||||
import { PrivacyPolicyPopup } from './popup/privacy_policy'
|
import { PrivacyPolicyPopup } from './popup/privacy_policy'
|
||||||
import { RulesHelpPopup } from './popup/rules_help'
|
import { RulesHelpPopup } from './popup/rules_help'
|
||||||
import { UploadPopup } from './popup/upload'
|
import { UploadPopup } from './popup/upload'
|
||||||
import { PreferencesPopup} from "./popup/preferences"
|
|
||||||
import { WorldTreePanel } from './world_tree'
|
import { WorldTreePanel } from './world_tree'
|
||||||
|
|
||||||
import '../css/welcome.css'
|
import '../css/welcome.css'
|
||||||
@@ -64,7 +63,7 @@ function IntroductionPanel({introduction, setPageNumber}: {introduction: string,
|
|||||||
/** main page of the game showing among others the tree of worlds/levels */
|
/** main page of the game showing among others the tree of worlds/levels */
|
||||||
function Welcome() {
|
function Welcome() {
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {mobile, setMobile, lockMobile, setLockMobile} = React.useContext(MobileContext)
|
const {mobile} = React.useContext(MobileContext)
|
||||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||||
const inventory = useLoadInventoryOverviewQuery({game: gameId})
|
const inventory = useLoadInventoryOverviewQuery({game: gameId})
|
||||||
|
|
||||||
@@ -78,20 +77,15 @@ function Welcome() {
|
|||||||
const [info, setInfo] = React.useState(false)
|
const [info, setInfo] = React.useState(false)
|
||||||
const [rulesHelp, setRulesHelp] = React.useState(false)
|
const [rulesHelp, setRulesHelp] = React.useState(false)
|
||||||
const [uploadMenu, setUploadMenu] = React.useState(false)
|
const [uploadMenu, setUploadMenu] = React.useState(false)
|
||||||
const [preferencesPopup, setPreferencesPopup] = React.useState(false)
|
|
||||||
|
|
||||||
function closeEraseMenu() {setEraseMenu(false)}
|
function closeEraseMenu() {setEraseMenu(false)}
|
||||||
function closeImpressum() {setImpressum(false)}
|
function closeImpressum() {setImpressum(false)}
|
||||||
function closeInfo() {setInfo(false)}
|
function closeInfo() {setInfo(false)}
|
||||||
function closeRulesHelp() {setRulesHelp(false)}
|
function closeRulesHelp() {setRulesHelp(false)}
|
||||||
function closeUploadMenu() {setUploadMenu(false)}
|
function closeUploadMenu() {setUploadMenu(false)}
|
||||||
function closePreferencesPopup() {setPreferencesPopup(false)}
|
|
||||||
function toggleEraseMenu() {setEraseMenu(!eraseMenu)}
|
function toggleEraseMenu() {setEraseMenu(!eraseMenu)}
|
||||||
function toggleImpressum() {setImpressum(!impressum)}
|
function toggleImpressum() {setImpressum(!impressum)}
|
||||||
function toggleInfo() {setInfo(!info)}
|
function toggleInfo() {setInfo(!info)}
|
||||||
function toggleUploadMenu() {setUploadMenu(!uploadMenu)}
|
function toggleUploadMenu() {setUploadMenu(!uploadMenu)}
|
||||||
function togglePreferencesPopup() {setPreferencesPopup(!preferencesPopup)}
|
|
||||||
|
|
||||||
|
|
||||||
// set the window title
|
// set the window title
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -107,7 +101,7 @@ function Welcome() {
|
|||||||
: <>
|
: <>
|
||||||
<WelcomeAppBar pageNumber={pageNumber} setPageNumber={setPageNumber} gameInfo={gameInfo.data} toggleImpressum={toggleImpressum}
|
<WelcomeAppBar pageNumber={pageNumber} setPageNumber={setPageNumber} gameInfo={gameInfo.data} toggleImpressum={toggleImpressum}
|
||||||
toggleEraseMenu={toggleEraseMenu} toggleUploadMenu={toggleUploadMenu}
|
toggleEraseMenu={toggleEraseMenu} toggleUploadMenu={toggleUploadMenu}
|
||||||
toggleInfo={toggleInfo} togglePreferencesPopup={togglePreferencesPopup}/>
|
toggleInfo={toggleInfo} />
|
||||||
<div className="app-content">
|
<div className="app-content">
|
||||||
{ mobile ?
|
{ mobile ?
|
||||||
<div className="welcome mobile">
|
<div className="welcome mobile">
|
||||||
@@ -117,7 +111,7 @@ function Welcome() {
|
|||||||
<WorldTreePanel worlds={gameInfo.data?.worlds} worldSize={gameInfo.data?.worldSize}
|
<WorldTreePanel worlds={gameInfo.data?.worlds} worldSize={gameInfo.data?.worldSize}
|
||||||
rulesHelp={rulesHelp} setRulesHelp={setRulesHelp} />
|
rulesHelp={rulesHelp} setRulesHelp={setRulesHelp} />
|
||||||
:
|
:
|
||||||
<InventoryPanel levelInfo={inventory?.data} />
|
<InventoryOverviewPanel data={inventory?.data} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
@@ -125,7 +119,7 @@ function Welcome() {
|
|||||||
<IntroductionPanel introduction={gameInfo.data?.introduction} setPageNumber={setPageNumber} />
|
<IntroductionPanel introduction={gameInfo.data?.introduction} setPageNumber={setPageNumber} />
|
||||||
<WorldTreePanel worlds={gameInfo.data?.worlds} worldSize={gameInfo.data?.worldSize}
|
<WorldTreePanel worlds={gameInfo.data?.worlds} worldSize={gameInfo.data?.worldSize}
|
||||||
rulesHelp={rulesHelp} setRulesHelp={setRulesHelp} />
|
rulesHelp={rulesHelp} setRulesHelp={setRulesHelp} />
|
||||||
<InventoryPanel levelInfo={inventory?.data} />
|
<InventoryOverviewPanel data={inventory?.data} />
|
||||||
</Split>
|
</Split>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -134,7 +128,6 @@ function Welcome() {
|
|||||||
{eraseMenu? <ErasePopup handleClose={closeEraseMenu}/> : null}
|
{eraseMenu? <ErasePopup handleClose={closeEraseMenu}/> : null}
|
||||||
{uploadMenu? <UploadPopup handleClose={closeUploadMenu}/> : null}
|
{uploadMenu? <UploadPopup handleClose={closeUploadMenu}/> : null}
|
||||||
{info ? <InfoPopup info={gameInfo.data?.info} handleClose={closeInfo}/> : null}
|
{info ? <InfoPopup info={gameInfo.data?.info} handleClose={closeInfo}/> : null}
|
||||||
{preferencesPopup ? <PreferencesPopup mobile={mobile} setMobile={setMobile} lockMobile={lockMobile} setLockMobile={setLockMobile} handleClose={closePreferencesPopup}/> : null}
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|||||||
import { faXmark, faCircleQuestion } from '@fortawesome/free-solid-svg-icons'
|
import { faXmark, faCircleQuestion } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
import { GameIdContext } from '../app'
|
import { GameIdContext } from '../app'
|
||||||
import { useAppDispatch, useMobile } from '../hooks'
|
import { useAppDispatch } from '../hooks'
|
||||||
import { selectDifficulty, changedDifficulty, selectCompleted } from '../state/progress'
|
import { selectDifficulty, changedDifficulty, selectCompleted } from '../state/progress'
|
||||||
import { store } from '../state/store'
|
import { store } from '../state/store'
|
||||||
|
|
||||||
@@ -197,15 +197,13 @@ export function WorldSelectionMenu({rulesHelp, setRulesHelp}) {
|
|||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const difficulty = useSelector(selectDifficulty(gameId))
|
const difficulty = useSelector(selectDifficulty(gameId))
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const { mobile } = useMobile()
|
|
||||||
|
|
||||||
|
|
||||||
function label(x : number) {
|
function label(x : number) {
|
||||||
return x == 0 ? 'none' : x == 1 ? 'relaxed' : 'regular'
|
return x == 0 ? 'none' : x == 1 ? 'relaxed' : 'regular'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return <nav className={`world-selection-menu${mobile ? '' : ' desktop'}`}>
|
return <nav className="world-selection-menu">
|
||||||
<div className="slider-wrap">
|
<div className="slider-wrap">
|
||||||
<span className="difficulty-label">Rules
|
<span className="difficulty-label">Rules
|
||||||
<FontAwesomeIcon icon={rulesHelp ? faXmark : faCircleQuestion} className='helpButton' onClick={() => (setRulesHelp(!rulesHelp))}/>
|
<FontAwesomeIcon icon={rulesHelp ? faXmark : faCircleQuestion} className='helpButton' onClick={() => (setRulesHelp(!rulesHelp))}/>
|
||||||
@@ -215,7 +213,7 @@ export function WorldSelectionMenu({rulesHelp, setRulesHelp}) {
|
|||||||
title="Game Rules"
|
title="Game Rules"
|
||||||
min={0} max={2}
|
min={0} max={2}
|
||||||
aria-label="Game Rules"
|
aria-label="Game Rules"
|
||||||
value={difficulty}
|
defaultValue={difficulty}
|
||||||
marks={[
|
marks={[
|
||||||
{value: 0, label: label(0)},
|
{value: 0, label: label(0)},
|
||||||
{value: 1, label: label(1)},
|
{value: 1, label: label(1)},
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* @fileOverview todo
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
|
||||||
|
import { LeanClient } from 'lean4web/client/src/editor/leanclient';
|
||||||
|
|
||||||
|
export class Connection {
|
||||||
|
private game: string = undefined // We only keep a connection to a single game at a time
|
||||||
|
private leanClient: LeanClient = null
|
||||||
|
|
||||||
|
getLeanClient(game): LeanClient {
|
||||||
|
if (this.game !== game) {
|
||||||
|
if (this.leanClient) {
|
||||||
|
this.leanClient.stop() // Stop previous Lean client
|
||||||
|
}
|
||||||
|
this.game = game
|
||||||
|
// Start a new Lean client for the new `gameId`.
|
||||||
|
const socketUrl = ((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + '/websocket/' + game
|
||||||
|
const uri = monaco.Uri.parse('file:///')
|
||||||
|
this.leanClient = new LeanClient(socketUrl, undefined, uri, () => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.leanClient
|
||||||
|
}
|
||||||
|
|
||||||
|
/** If not already started, starts the Lean client. resolves the returned promise as soon as a
|
||||||
|
* Lean client is running.
|
||||||
|
*/
|
||||||
|
startLeanClient = (game) => {
|
||||||
|
return new Promise<LeanClient>((resolve) => {
|
||||||
|
const leanClient = this.getLeanClient(game)
|
||||||
|
if (leanClient.isRunning()) {
|
||||||
|
resolve(leanClient)
|
||||||
|
} else {
|
||||||
|
if (!leanClient.isStarted()) {
|
||||||
|
leanClient.start()
|
||||||
|
}
|
||||||
|
leanClient.restarted(() => {
|
||||||
|
// This keep alive message is not recognized by the server,
|
||||||
|
// but it makes sure that the websocket connection does not
|
||||||
|
// time out after 60 seconds.
|
||||||
|
setInterval(() => {leanClient.sendNotification('$/keepAlive', {}) }, 5000)
|
||||||
|
resolve(leanClient)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const connection = new Connection()
|
||||||
|
|
||||||
|
export const ConnectionContext = React.createContext(null);
|
||||||
|
|
||||||
|
export const useLeanClient = (gameId) => {
|
||||||
|
const leanClient = connection.getLeanClient(gameId)
|
||||||
|
const [leanClientStarted, setLeanClientStarted] = React.useState(leanClient.isStarted())
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const t1 = leanClient.restarted(() => { console.log("START"); setLeanClientStarted(true) })
|
||||||
|
const t2 = leanClient.stopped(() => { console.log("STOP"); setLeanClientStarted(false) })
|
||||||
|
|
||||||
|
return () => {t1.dispose(); t2.dispose()}
|
||||||
|
}, [leanClient, setLeanClientStarted])
|
||||||
|
|
||||||
|
return {leanClientStarted, leanClient}
|
||||||
|
}
|
||||||
@@ -105,18 +105,11 @@ em {
|
|||||||
position: relative;
|
position: relative;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
|
||||||
padding: 1.1em;
|
padding: 1.1em;
|
||||||
filter: drop-shadow(0 0 5px rgba(0,0,0,0.5));
|
filter: drop-shadow(0 0 5px rgba(0,0,0,0.5));
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-bar > .app-bar-left{
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: .5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-bar-title, .app-bar-subtitle {
|
.app-bar-title, .app-bar-subtitle {
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|||||||
@@ -26,11 +26,7 @@
|
|||||||
.inventory .item {
|
.inventory .item {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: solid 1px #777;
|
border: solid 1px #777;
|
||||||
padding-left: .5rem;
|
padding: .1em .5em;
|
||||||
padding-right: 1.0rem;
|
|
||||||
padding-top: .1rem;
|
|
||||||
padding-bottom: .1rem;
|
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.inventory .item.locked {
|
.inventory .item.locked {
|
||||||
@@ -76,21 +72,3 @@
|
|||||||
color: black;
|
color: black;
|
||||||
border-bottom: 0.3em solid #999;
|
border-bottom: 0.3em solid #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.inventory .item .copy-button {
|
|
||||||
min-width: 3px;
|
|
||||||
min-height: 3px;
|
|
||||||
display: inline-block;
|
|
||||||
color: #ccc;
|
|
||||||
font-size: 0.6em;
|
|
||||||
padding-right: .2rem;
|
|
||||||
vertical-align: top;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 100%;
|
|
||||||
width: 1rem;
|
|
||||||
align-items: end;
|
|
||||||
text-align: end;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -232,20 +232,6 @@ td code {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
} */
|
} */
|
||||||
|
|
||||||
.button-row.mobile {
|
|
||||||
margin: .5rem;
|
|
||||||
padding-top: .2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-row.mobile .btn {
|
|
||||||
padding: .5em;
|
|
||||||
border-radius: .2em;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.typewriter-interface {
|
.typewriter-interface {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
@@ -331,6 +317,11 @@ td code {
|
|||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#home-btn {
|
||||||
|
margin-right: .5em;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.menu.dropdown .svg-inline--fa {
|
.menu.dropdown .svg-inline--fa {
|
||||||
width: 1.8rem;
|
width: 1.8rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ svg .disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.world-selection-menu {
|
.world-selection-menu {
|
||||||
|
position: absolute;
|
||||||
right: 1em;
|
right: 1em;
|
||||||
top: 1em;
|
top: 1em;
|
||||||
/* margin: 1em; */
|
/* margin: 1em; */
|
||||||
@@ -59,10 +60,6 @@ svg .disabled {
|
|||||||
filter: drop-shadow(4px 4px 5px rgba(0,0,0,0.5));
|
filter: drop-shadow(4px 4px 5px rgba(0,0,0,0.5));
|
||||||
}
|
}
|
||||||
|
|
||||||
.world-selection-menu.desktop {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.world-selection-menu .btn, .welcome .btn {
|
.world-selection-menu .btn, .welcome .btn {
|
||||||
min-width: 5em;
|
min-width: 5em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -1,30 +1,6 @@
|
|||||||
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
|
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
|
||||||
import type { RootState, AppDispatch } from './state/store'
|
import type { RootState, AppDispatch } from './state/store'
|
||||||
|
|
||||||
import { setMobile as setMobileState, setLockMobile as setLockMobileState} from "./state/preferences"
|
|
||||||
|
|
||||||
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
||||||
export const useAppDispatch: () => AppDispatch = useDispatch
|
export const useAppDispatch: () => AppDispatch = useDispatch
|
||||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
|
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
|
||||||
|
|
||||||
export const useMobile = () => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const mobile = useAppSelector((state) => state.preferences.mobile);
|
|
||||||
const lockMobile = useAppSelector((state) => state.preferences.lockMobile);
|
|
||||||
|
|
||||||
const setMobile = (val: boolean) => {
|
|
||||||
dispatch(setMobileState(val));
|
|
||||||
};
|
|
||||||
|
|
||||||
const setLockMobile = (val: boolean) => {
|
|
||||||
dispatch(setLockMobileState(val));
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
mobile,
|
|
||||||
setMobile,
|
|
||||||
lockMobile,
|
|
||||||
setLockMobile,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import App from './app'
|
import App from './app'
|
||||||
|
import { ConnectionContext, connection } from './connection'
|
||||||
import { store } from './state/store'
|
import { store } from './state/store'
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import type { RouteObject } from "react-router"
|
import type { RouteObject } from "react-router"
|
||||||
@@ -9,8 +10,11 @@ import ErrorPage from './components/error_page'
|
|||||||
import Welcome from './components/welcome'
|
import Welcome from './components/welcome'
|
||||||
import LandingPage from './components/landing_page'
|
import LandingPage from './components/landing_page'
|
||||||
import Level from './components/level'
|
import Level from './components/level'
|
||||||
|
import { monacoSetup } from 'lean4web/client/src/monacoSetup'
|
||||||
|
|
||||||
|
|
||||||
|
monacoSetup()
|
||||||
|
|
||||||
|
|
||||||
// If `VITE_LEAN4GAME_SINGLE` is set to true, then `/` should be redirected to
|
// If `VITE_LEAN4GAME_SINGLE` is set to true, then `/` should be redirected to
|
||||||
// `/g/local/game`. This is used for the devcontainer setup
|
// `/g/local/game`. This is used for the devcontainer setup
|
||||||
@@ -57,7 +61,9 @@ const root = createRoot(container!);
|
|||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<RouterProvider router={router} />
|
<ConnectionContext.Provider value={connection}>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</ConnectionContext.Provider>
|
||||||
</Provider>
|
</Provider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export interface InventoryTile {
|
|||||||
locked: boolean,
|
locked: boolean,
|
||||||
new: boolean,
|
new: boolean,
|
||||||
hidden: boolean
|
hidden: boolean
|
||||||
altTitle: string,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LevelInfo {
|
export interface LevelInfo {
|
||||||
@@ -72,6 +71,13 @@ interface Doc {
|
|||||||
category: string,
|
category: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WorldOverview {
|
||||||
|
world: string
|
||||||
|
tactics: InventoryTile[]
|
||||||
|
lemmas: InventoryTile[]
|
||||||
|
definitions: InventoryTile[]
|
||||||
|
}
|
||||||
|
|
||||||
// Define a service using a base URL and expected endpoints
|
// Define a service using a base URL and expected endpoints
|
||||||
export const apiSlice = createApi({
|
export const apiSlice = createApi({
|
||||||
reducerPath: 'gameApi',
|
reducerPath: 'gameApi',
|
||||||
@@ -83,7 +89,7 @@ export const apiSlice = createApi({
|
|||||||
loadLevel: builder.query<LevelInfo, {game: string, world: string, level: number}>({
|
loadLevel: builder.query<LevelInfo, {game: string, world: string, level: number}>({
|
||||||
query: ({game, world, level}) => `${game}/level__${world}__${level}.json`,
|
query: ({game, world, level}) => `${game}/level__${world}__${level}.json`,
|
||||||
}),
|
}),
|
||||||
loadInventoryOverview: builder.query<InventoryOverview, {game: string}>({
|
loadInventoryOverview: builder.query<WorldOverview[], {game: string}>({
|
||||||
query: ({game}) => `${game}/inventory.json`,
|
query: ({game}) => `${game}/inventory.json`,
|
||||||
}),
|
}),
|
||||||
loadDoc: builder.query<Doc, {game: string, name: string, type: "lemma"|"tactic"}>({
|
loadDoc: builder.query<Doc, {game: string, name: string, type: "lemma"|"tactic"}>({
|
||||||
|
|||||||
@@ -36,24 +36,3 @@ export async function saveState(state: any) {
|
|||||||
// Ignore
|
// Ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const PREFERENCES_KEY = "preferences"
|
|
||||||
|
|
||||||
/** Load from browser storage */
|
|
||||||
export function loadPreferences() {
|
|
||||||
try {
|
|
||||||
const serializedState = localStorage.getItem(PREFERENCES_KEY);
|
|
||||||
return JSON.parse(serializedState)
|
|
||||||
} catch (e) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function savePreferences(state: any) {
|
|
||||||
try {
|
|
||||||
const serializedState = JSON.stringify(state)
|
|
||||||
localStorage.setItem(PREFERENCES_KEY, serializedState);
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
import { createSlice } from "@reduxjs/toolkit";
|
|
||||||
|
|
||||||
import { loadPreferences } from "./local_storage";
|
|
||||||
|
|
||||||
interface PreferencesState {
|
|
||||||
mobile: boolean;
|
|
||||||
lockMobile: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getWindowDimensions() {
|
|
||||||
const {innerWidth: width, innerHeight: height } = window
|
|
||||||
return {width, height}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { width } = getWindowDimensions()
|
|
||||||
|
|
||||||
export const AUTO_SWITCH_THRESHOLD = 800
|
|
||||||
|
|
||||||
const initialState: PreferencesState = loadPreferences() ?? {
|
|
||||||
mobile: width < AUTO_SWITCH_THRESHOLD,
|
|
||||||
lockMobile: false
|
|
||||||
}
|
|
||||||
|
|
||||||
export const preferencesSlice = createSlice({
|
|
||||||
name: "preferences",
|
|
||||||
initialState,
|
|
||||||
reducers: {
|
|
||||||
setMobile: (state, action) => {
|
|
||||||
state.mobile = action.payload;
|
|
||||||
},
|
|
||||||
setLockMobile: (state, action) => {
|
|
||||||
state.lockMobile = action.payload;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { setMobile, setLockMobile } = preferencesSlice.actions;
|
|
||||||
@@ -7,19 +7,21 @@ import { debounce } from "debounce";
|
|||||||
import { connection } from '../connection'
|
import { connection } from '../connection'
|
||||||
import { apiSlice } from './api'
|
import { apiSlice } from './api'
|
||||||
import { progressSlice } from './progress'
|
import { progressSlice } from './progress'
|
||||||
import { preferencesSlice } from "./preferences"
|
import { saveState } from "./local_storage";
|
||||||
import { saveState, savePreferences } from "./local_storage";
|
|
||||||
|
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
[apiSlice.reducerPath]: apiSlice.reducer,
|
[apiSlice.reducerPath]: apiSlice.reducer,
|
||||||
[progressSlice.name]: progressSlice.reducer,
|
[progressSlice.name]: progressSlice.reducer,
|
||||||
[preferencesSlice.name]: preferencesSlice.reducer,
|
|
||||||
},
|
},
|
||||||
// Make connection available in thunks:
|
// Make connection available in thunks:
|
||||||
middleware: getDefaultMiddleware =>
|
middleware: getDefaultMiddleware =>
|
||||||
getDefaultMiddleware().concat(apiSlice.middleware),
|
getDefaultMiddleware({
|
||||||
|
thunk: {
|
||||||
|
extraArgument: { connection }
|
||||||
|
}
|
||||||
|
}).concat(apiSlice.middleware),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,7 +31,6 @@ export const store = configureStore({
|
|||||||
store.subscribe(
|
store.subscribe(
|
||||||
debounce(() => {
|
debounce(() => {
|
||||||
saveState(store.getState()[progressSlice.name]);
|
saveState(store.getState()[progressSlice.name]);
|
||||||
savePreferences(store.getState()[preferencesSlice.name]);
|
|
||||||
}, 800)
|
}, 800)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import {useState, useEffect} from 'react'
|
||||||
|
|
||||||
|
function getWindowDimensions() {
|
||||||
|
const {innerWidth: width, innerHeight: height } = window
|
||||||
|
return {width, height}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWindowDimensions() {
|
||||||
|
const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions())
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleResize() {
|
||||||
|
setWindowDimensions(getWindowDimensions())
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
return () => window.removeEventListener('resize', handleResize)
|
||||||
|
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return windowDimensions
|
||||||
|
}
|
||||||
+321
-53
@@ -1,67 +1,335 @@
|
|||||||
# Server
|
**NOTE! This document is deprecated! The current documentation is [How To Create A Game](create_game.md)**
|
||||||
|
|
||||||
The server is made out of two parts, named "relay" and "server".
|
# Creating a game.
|
||||||
|
|
||||||
The former, "relay", is the server which
|
Ideally one takes the [GameSkeleton template](https://github.com/hhu-adam/GameSkeleton) to create a new game.
|
||||||
sets up a socket connection to the client, starts the lean servers to work on files and
|
|
||||||
relays messages between the lean server and the client. `index.mjs` is the file that needs to
|
|
||||||
be run, which is done for example using `pm2` or by calling `npm run start_server` or
|
|
||||||
`npm run production`, see more later.
|
|
||||||
|
|
||||||
The latter, "server", is the lean server which has two jobs. For one it produces the "gameserver"
|
## Game Structure
|
||||||
executable which is the lean server that handles the files the player plays on. The second job
|
|
||||||
is to provide the lean commands which are used when creating a game. These are located in
|
|
||||||
`Commands.lean`.
|
|
||||||
|
|
||||||
|
A game consist of worlds which have multiple levels each. In the following we describe how to create a level file and how to combine these into a game.
|
||||||
|
|
||||||
## Integration into Games
|
### Level
|
||||||
|
|
||||||
Games need the "server" as a lake-dependency, which is done in the game's lakefile.
|
A level file is a lean file that imports at least `import GameServer.Commands` and starts with the following Lean commands.
|
||||||
|
|
||||||
A game imports `GameServer.Commands` which provides to all the API required to
|
|
||||||
create a game.
|
|
||||||
|
|
||||||
In particular the lean command `MakeGame` compiles the entire game. Static information is
|
|
||||||
stored as JSON files in `.lake/gamedata` for faster loading, while other data is only
|
|
||||||
saved to lean env-extensions which the lean server has access to after loading the lean file.
|
|
||||||
|
|
||||||
For games to be run successfully, it is important that the "gameserver" executable inside
|
|
||||||
the game's `.lake` folder is actually built.
|
|
||||||
Currently this happens through a lake-post-update-hook when calling `lake update -R` (in the game's folder), but if this fails, you can always build it manually by calling `lake build gameserver`.
|
|
||||||
(both commands are to be executed in the game's directory!)
|
|
||||||
|
|
||||||
## Modifying the server
|
|
||||||
|
|
||||||
### Starting the server
|
|
||||||
|
|
||||||
When using the [manual installation](running_locally.md#manual-installation) you can run the server
|
|
||||||
using
|
|
||||||
|
|
||||||
|
```lean
|
||||||
|
Game "NNG"
|
||||||
|
World "Addition"
|
||||||
|
Level 1
|
||||||
|
Title "The rfl tactic"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note that the levels inside a world must have consecutive numbering starting with `1`. The `Game`
|
||||||
|
and `World` strings can be anything, see below.
|
||||||
|
|
||||||
|
#### Statement
|
||||||
|
|
||||||
|
The core of a level is the `Statement`, which is the exercise that should be proven.
|
||||||
|
|
||||||
|
```lean
|
||||||
|
/-- For all natural numbers $n$, we have $0 + n = n$. -/
|
||||||
|
@[simp]
|
||||||
|
Statement MyNat.zero_add
|
||||||
|
(n : ℕ) : 0 + n = n := by
|
||||||
|
Hint "You can start a proof by `induction n`."
|
||||||
|
induction n with n hn
|
||||||
|
· Hint "This is the base case."
|
||||||
|
rw [add_zero]
|
||||||
|
rfl
|
||||||
|
· Hint "This is the induction hypothesis"
|
||||||
|
rw [add_succ]
|
||||||
|
Branch
|
||||||
|
simp
|
||||||
|
Hint "A branch is an alternative tactic sequence. Does not need to finish the proof."
|
||||||
|
rw [hn]
|
||||||
|
rfl
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Proof
|
||||||
|
|
||||||
|
The proof must always be a tactic proof, i.e. `:= by` is a mandatory part of the syntax.
|
||||||
|
|
||||||
|
There are a few extra tactics that help you structuring the proof:
|
||||||
|
|
||||||
|
- `Hint`: You can use `Hint "text"` to display text if the goal state in-game matches
|
||||||
|
the one where `Hint` is placed. For more options about hints, see below.
|
||||||
|
- `Branch`: In the proof you can add a `Branch` that runs an alternative tactic sequence, which
|
||||||
|
helps setting `Hints` in different places. The `Branch` does not affect the main
|
||||||
|
proof and does not need to finish any goals.
|
||||||
|
- `Template`/`Hole`: Used to provide a sample proof template. Anything inside `Template`
|
||||||
|
will be copied into the editor with all `Hole`s replaced with `sorry`. Note that
|
||||||
|
having a `Template` will force the user to use Editor-mode for this level.
|
||||||
|
|
||||||
|
##### Statement Name (optional)
|
||||||
|
|
||||||
|
If you specify a name (`MyNat.zero_add`), this lemma will be available in future levels.
|
||||||
|
(Note that a future level must also import this level,
|
||||||
|
so that Lean knows about the added statement).
|
||||||
|
|
||||||
|
The name must be *fully qualified*. (TODO: is that still true? Did we implement namespaces?)
|
||||||
|
|
||||||
|
##### Doc Comment (optional)
|
||||||
|
|
||||||
|
There are three places where the documentation comment appears:
|
||||||
|
|
||||||
|
1. as doc comment when hovering over the theorem
|
||||||
|
2. as exercise description at the top of the level: ``Theorem `zero_add`: yada yada.``
|
||||||
|
3. in the inventory. This can be overwritten by using
|
||||||
|
`LemmaDoc MyNat.zero_add "different yada yada"` as one might want to add a more detailed
|
||||||
|
description there including examples etc.
|
||||||
|
|
||||||
|
Both latter points support Markdown (including katex).
|
||||||
|
|
||||||
|
##### Attributes (optional)
|
||||||
|
|
||||||
|
the `@[ attributes ]` prefix should work just like you know it from the `theorem` keyword.
|
||||||
|
|
||||||
|
#### Introduction/Conclusion
|
||||||
|
|
||||||
|
Optionally, you can add an `Introduction "some text"` and `Conclusion "some text"` to your level.
|
||||||
|
The introduction will be shown at the beginning, the conclusion is displayed once the level
|
||||||
|
is solved.
|
||||||
|
|
||||||
|
#### Theorems/Tactics/Definitions
|
||||||
|
|
||||||
|
Only enabled theorems/tactics/definitions (called "items" here) are available in a level.
|
||||||
|
|
||||||
|
To add a new item in a level, you can add
|
||||||
|
|
||||||
|
```lean
|
||||||
|
NewTactic rfl simp
|
||||||
|
NewLemma MyNat.add_zero MyNat.add_succ
|
||||||
|
NewDefinition Nat Pow Mul
|
||||||
|
```
|
||||||
|
|
||||||
|
Once added, items will be available in all future levels/worlds,
|
||||||
|
unless you disable them for a particular level with
|
||||||
|
|
||||||
|
```lean
|
||||||
|
DisabledTactic tauto
|
||||||
|
DisabledLemma MyNat.add_zero
|
||||||
|
```
|
||||||
|
|
||||||
|
or specify explicitly which items should be available with
|
||||||
|
|
||||||
|
```lean
|
||||||
|
OnlyTactic rw rfl apply
|
||||||
|
OnlyLemma MyNat.add_zero
|
||||||
|
```
|
||||||
|
|
||||||
|
Lastly, all items need documentation entries (which are imported in the level),
|
||||||
|
see more about that below. There is also explains the `LemmaTab` keyword.
|
||||||
|
|
||||||
|
### World
|
||||||
|
|
||||||
|
Multiple levels are combined into a world and the worlds are then added to the game. It is recommended that all levels of a world are inside one folder (e.g. `NNG/Levels/Addition/`) and
|
||||||
|
then there is one world file (`NNG/Levels/Addition.lean`) which contains the following
|
||||||
|
|
||||||
|
```lean
|
||||||
|
import NNG.Levels.Addition.Level_1
|
||||||
|
import NNG.Levels.Addition.Level_2
|
||||||
|
|
||||||
|
Game "NNG"
|
||||||
|
World "Addition"
|
||||||
|
Title "Addition World"
|
||||||
|
|
||||||
|
Introduction "some text"
|
||||||
|
```
|
||||||
|
|
||||||
|
The `Title` is the world's display title. The `Introduction` is displayed before loading level 1.
|
||||||
|
Note that all levels of a world should be imported by the world file.
|
||||||
|
|
||||||
|
BUG: A level **must not** be imported in a different world's level. Instead, you have to import an entire world there: `import NNG.Levels.Addition`
|
||||||
|
|
||||||
|
### Game
|
||||||
|
|
||||||
|
The Game itself (i.e. the main file of you lake project, `NNG.lean`) should import all worlds and have the following layout, concluding with `MakeGame`:
|
||||||
|
|
||||||
|
```lean
|
||||||
|
import NNG.Levels.Addition
|
||||||
|
import NNG.Levels.Multiplication
|
||||||
|
import NNG.Levels.Power
|
||||||
|
|
||||||
|
Game "NNG"
|
||||||
|
Title "Natural Number Game"
|
||||||
|
Introduction "some text"
|
||||||
|
|
||||||
|
MakeGame
|
||||||
|
```
|
||||||
|
|
||||||
|
The game will automatically compute the order of the worlds depending on the sample proofs of the Levels (ignoring anything inside a `Branch`). You can add additional dependencies manually by adding `Dependency PowerWorld → ImpossibleWorld` before `MakeGame`.
|
||||||
|
The order of worlds influences which tactics and lemmas will be unlocked in a given level.
|
||||||
|
|
||||||
|
`MakeGame` will display warnings about things in the game that need to be fixed, like missing
|
||||||
|
documentation or if a tactic is never introduced.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
Each tactic, theorem, or definition (all called items here) that is introduced in the game
|
||||||
|
needs a documentation entry. These are statements of the following form:
|
||||||
|
|
||||||
|
```lean
|
||||||
|
LemmaDoc MyNat.add_squared as "add_squared" in "Pow"
|
||||||
|
"(missing)"
|
||||||
|
|
||||||
|
TacticDoc constructor
|
||||||
|
"(missing)"
|
||||||
|
|
||||||
|
DefinitionDoc One as "1"
|
||||||
|
"(missing)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
* The lemma name must be **fully qualified**. The string display name can be arbitrary.
|
||||||
|
* Tactics must have their proper name. use `TacticDoc «have» ""` if it does not work
|
||||||
|
without french quotes.
|
||||||
|
* Definition names can be arbitrary. E.g. I used `DefinitionDoc Symbol.Fun as "fun x ↦ x" "(missing)"` once.
|
||||||
|
|
||||||
|
Moreover, the lemmas are in sorted in tabs (the `in "Pow`) part. In each level file, you
|
||||||
|
can define which tab is open when the level is loaded by adding `LemmaTab "Pow"`.
|
||||||
|
|
||||||
|
There will be features added to get automatic information from mathlib!
|
||||||
|
|
||||||
|
## Escaping
|
||||||
|
(TODO: Move)
|
||||||
|
|
||||||
|
|
||||||
|
Inside the doc comment you don't need to escape the backslashes:
|
||||||
|
|
||||||
|
```lean
|
||||||
|
/-- $\operatorname{succ}(n)$. notation for naturals is `\N`. -/
|
||||||
|
Statement ...
|
||||||
|
```
|
||||||
|
|
||||||
|
However, inside interpolated strings (e.g. in `Hint`, `Introduction` and `Conclusion`)
|
||||||
|
you do need to escape backslashes
|
||||||
|
with `\\` and `{` with `\{`:
|
||||||
|
|
||||||
|
```lean
|
||||||
|
Hint "This code has some $\\operatorname\{succ}(n)$ math. The value of `h` is {h}.
|
||||||
|
Notation for naturals is `\\N`."
|
||||||
|
```
|
||||||
|
|
||||||
|
## Game design
|
||||||
|
Here are some things you should consider designing a new game:
|
||||||
|
|
||||||
|
* A world with more than 16 levels will be displayed with the levels spiraling outwards,
|
||||||
|
it might be desirable to stay below that bound. Above 22 levels the spiral start getting out
|
||||||
|
of control.
|
||||||
|
|
||||||
|
# Running Games Locally
|
||||||
|
|
||||||
|
The installation instructions are not yet tested on Mac/Windows. Comments very welcome!
|
||||||
|
|
||||||
|
## VSCode Dev Containers
|
||||||
|
|
||||||
|
1. **Install Docker and Dev Containers** *(once)*:<br/>
|
||||||
|
See [official instructions](https://code.visualstudio.com/docs/devcontainers/containers#_getting-started).
|
||||||
|
Explicitly this means:
|
||||||
|
* Install docker engine if you have not yet: [Instructions](https://docs.docker.com/engine/install/).
|
||||||
|
I followed the "Server" instructions for linux.
|
||||||
|
* Note that on Linux you need to add your user to the `docker` group
|
||||||
|
([see instructions](https://docs.docker.com/engine/install/linux-postinstall/)) and probably reboot.
|
||||||
|
* Open the games folder in VSCode: `cd NNG4 && code .` or "Open Folder" within VSCode
|
||||||
|
* a message appears prompting you to install the "Dev Containers" extension (by Microsoft).
|
||||||
|
|
||||||
|
2. **Open Project in Dev Container** *(everytime)*:<br/>
|
||||||
|
Once you have the Dev Containers Extension installed, (re)open the project folder of your game in VSCode.
|
||||||
|
A message appears asking you to "Reopen in Container".
|
||||||
|
|
||||||
|
* The first start will take a while, ca. 2-10 minutes. After the first
|
||||||
|
start this should be very quickly.
|
||||||
|
* Once built, it should open a tab "Simple Browser" inside VSCode displaying
|
||||||
|
the game. (Alternatively, open http://localhost:3000 in your browser).
|
||||||
|
|
||||||
|
3. **Editing Files** *(everytime)*:<br/>
|
||||||
|
After editing some files in VSCode, open VSCode's terminal (View > Terminal) and run `lake build`.
|
||||||
|
Now you can reload your browser to see the changes.
|
||||||
|
|
||||||
|
### Errors
|
||||||
|
|
||||||
|
* If you don't get the pop-up, you might have disabled them and you can reenable it by
|
||||||
|
running the `remote-containers.showReopenInContainerNotificationReset` command in vscode.
|
||||||
|
* If the starting the container fails, in particular with a message `Error: network xyz not found`,
|
||||||
|
you might have deleted stuff from docker via your shell. Try deleting the container and image
|
||||||
|
explicitly in VSCode (left side, "Docker" icon). Then reopen vscode and let it rebuild the
|
||||||
|
container. (this will again take some time)
|
||||||
|
|
||||||
|
|
||||||
|
## Without Dev Containers
|
||||||
|
Install `nvm`:
|
||||||
|
```bash
|
||||||
|
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.2/install.sh | bash
|
||||||
|
```
|
||||||
|
then reopen bash and test with `command -v nvm` if it is available (Should print "nvm").
|
||||||
|
|
||||||
|
Now install node:
|
||||||
|
```bash
|
||||||
|
nvm install node
|
||||||
|
```
|
||||||
|
|
||||||
|
Clone the game (e.g. `NNG4` here):
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/hhu-adam/NNG4.git
|
||||||
|
# or: git clone git@github.com:hhu-adam/NNG4.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Download dependencies and build the game:
|
||||||
|
```bash
|
||||||
|
cd NNG4
|
||||||
|
lake update
|
||||||
|
lake exe cache get # if your game depends on mathlib
|
||||||
|
lake build
|
||||||
|
```
|
||||||
|
|
||||||
|
Clone the game repository into a directory next to the game:
|
||||||
|
```bash
|
||||||
|
cd ..
|
||||||
|
git clone https://github.com/leanprover-community/lean4game.git
|
||||||
|
# or: git clone git@github.com:leanprover-community/lean4game.git
|
||||||
|
```
|
||||||
|
The folders `NNG4` and `lean4game` must be in the same directory!
|
||||||
|
|
||||||
|
In `lean4game`, install dependencies:
|
||||||
|
```bash
|
||||||
|
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/relay/index.mjs`:
|
||||||
|
```typescript
|
||||||
|
const games = {
|
||||||
|
"g/hhu-adam/robo": {
|
||||||
|
dir: "../../../../Robo",
|
||||||
|
queueLength: 5
|
||||||
|
},
|
||||||
|
"g/hhu-adam/nng4": {
|
||||||
|
dir: "../../../../NNG4",
|
||||||
|
queueLength: 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the game:
|
||||||
|
```bash
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
This way any changes to files in `client/` or `relay/` will cause the server to restart automatically.
|
This takes a little time. Eventually, the server is available on http://localhost:3000/
|
||||||
|
and the game is available on http://localhost:3000/#/g/hhu-adam/NNG4.
|
||||||
|
|
||||||
Alternative, you can run `npm run build` followed by the commands
|
### Modifying the GameServer
|
||||||
|
|
||||||
|
When modifying the game engine itself (in particular the content in `lean4game/server`) you can test it live with this
|
||||||
|
setup by setting `export NODE_ENV=development` inside your local game before building it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd NNG4
|
||||||
|
export NODE_ENV=development
|
||||||
|
lake update
|
||||||
|
lake build
|
||||||
```
|
```
|
||||||
npm run start_client
|
This causes lake to search locally for the `GameServer` lake package instead of using the version from github.
|
||||||
npm run production
|
|
||||||
```
|
|
||||||
|
|
||||||
(in two separate terminals) to test the production modus of the server. This way it will only
|
|
||||||
change once you build and restart the server.
|
|
||||||
|
|
||||||
### Modifying the lean server
|
|
||||||
|
|
||||||
To test a modified lean server (i.e. content of `server/`), you can use the local dev setup and call
|
|
||||||
`lake update -R -Klean4game.local` in your game followed by `lake build`.
|
|
||||||
This will cause lake to look for the
|
|
||||||
local lean server as a dependency instead of the version it downloaded from git.
|
|
||||||
|
|
||||||
You can play a local game at https://localhost:3000/#/g/local/{FolderName} where you replace `{FolderName}` with the game folder name.
|
|
||||||
|
|
||||||
After modifications in `server/`, you will need to call `lake build gameserver` (called in `server/` or in your game's folder) to rebuild
|
|
||||||
the gameserver executable and
|
|
||||||
`lake build` (called in the game's folder) to rebuild the game.
|
|
||||||
|
|||||||
+1
-6
@@ -129,9 +129,6 @@ NewLemma Nat.zero_mul
|
|||||||
NewDefinition Pow
|
NewDefinition Pow
|
||||||
```
|
```
|
||||||
|
|
||||||
**Important:** All commands in this section 6a) expect the `Name` they take as input
|
|
||||||
to be **fully qualified**. For example `NewLemma Nat.zero_mul` and not `NewLemma zero_mul`.
|
|
||||||
|
|
||||||
#### Doc entries
|
#### Doc entries
|
||||||
|
|
||||||
You'll see a warning about a missing Lemma documentation. You can fix it by adding doc-entries like the following somewhere above it.
|
You'll see a warning about a missing Lemma documentation. You can fix it by adding doc-entries like the following somewhere above it.
|
||||||
@@ -185,8 +182,6 @@ The statement is the exercise of the level. the basics work the same as they wou
|
|||||||
|
|
||||||
You can give your exercise a name: `Statement my_first_exercise (n : Nat) ...`. If you do so, it will be added to the inventory and be available in future levels.
|
You can give your exercise a name: `Statement my_first_exercise (n : Nat) ...`. If you do so, it will be added to the inventory and be available in future levels.
|
||||||
|
|
||||||
You can but a `Statement` inside namespaces like you would with `theorem`.
|
|
||||||
|
|
||||||
#### Doc String / Exercise statement
|
#### Doc String / Exercise statement
|
||||||
|
|
||||||
Add a docstring that contains the exercise statement in natural language. If you do this, it will appear at the top of the exercise. It supports Latex.
|
Add a docstring that contains the exercise statement in natural language. If you do this, it will appear at the top of the exercise. It supports Latex.
|
||||||
@@ -235,7 +230,7 @@ Read [More about Hints](doc/hints.md) for how they work and what the options are
|
|||||||
### 6. e) Extra: Images
|
### 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.
|
You can add images on any layer of the game (i.e. game/world/level). These will be displayed in your game.
|
||||||
|
|
||||||
The images need to be placed in `images/` and you need to add a command like `Image "images/path/to/myWorldImage.png"`
|
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).
|
in one of the files you created in 2), 3), or 4) (i.e. game/world/level).
|
||||||
|
|
||||||
NOTE: At present, only the images for a world are displayed. They appear in the introduction of the world.
|
NOTE: At present, only the images for a world are displayed. They appear in the introduction of the world.
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ Statement .... := by
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that hints are only **context-aware but not history-aware**. In particular they only look at the assumptions and the current goal. Player's might encounter hints in a different order - or not at all - if they decide to go for a unique proof idea. The `Branch` tactic helps placing hints outside the sample solution's proof.
|
|
||||||
|
|
||||||
## 1. When do hints show?
|
## 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
|
A hint will be displayed if the player's goal matches the one where the hint was placed in the
|
||||||
|
|||||||
+3
-3
@@ -9,8 +9,8 @@ Before you continue, make sure there [exists a `v4.X.0`-tag in this repo](https:
|
|||||||
|
|
||||||
Then, depending on the setup you use, do one of the following:
|
Then, depending on the setup you use, do one of the following:
|
||||||
|
|
||||||
* **Dev Container**: Rebuild the VSCode Devcontainer (without Cache!).
|
* Dev Container: Rebuild the VSCode Devcontainer.
|
||||||
* **Local Setup**: in your game's folder run the following:
|
* Local Setup: in your game's folder run the following:
|
||||||
```
|
```
|
||||||
lake update -R
|
lake update -R
|
||||||
lake build
|
lake build
|
||||||
@@ -24,7 +24,7 @@ Then, depending on the setup you use, do one of the following:
|
|||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
where `{VERSION_TAG}` is the tag from above of the form `v4.X.0`
|
where `{VERSION_TAG}` is the tag from above of the form `v4.X.0`
|
||||||
* **Gitpod/Codespaces**: Create a fresh one
|
* Gitpod/Codespaces: Create a fresh one
|
||||||
|
|
||||||
This will update 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.
|
||||||
|
|
||||||
|
|||||||
Generated
+465
-34
@@ -13,10 +13,6 @@
|
|||||||
"@emotion/styled": "^11.10.5",
|
"@emotion/styled": "^11.10.5",
|
||||||
"@fontsource/roboto": "^4.5.8",
|
"@fontsource/roboto": "^4.5.8",
|
||||||
"@fontsource/roboto-mono": "^4.5.8",
|
"@fontsource/roboto-mono": "^4.5.8",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.5.1",
|
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
|
||||||
"@leanprover/infoview": "^0.4.3",
|
"@leanprover/infoview": "^0.4.3",
|
||||||
"@mui/icons-material": "^5.11.0",
|
"@mui/icons-material": "^5.11.0",
|
||||||
"@mui/material": "^5.11.1",
|
"@mui/material": "^5.11.1",
|
||||||
@@ -31,7 +27,7 @@
|
|||||||
"debounce": "^1.2.1",
|
"debounce": "^1.2.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"lean4-infoview": "https://gitpkg.now.sh/leanprover/vscode-lean4/lean4-infoview?de0062c",
|
"lean4-infoview": "https://gitpkg.now.sh/leanprover/vscode-lean4/lean4-infoview?de0062c",
|
||||||
"lean4web": "github:hhu-adam/lean4web#b91645a7b88814675ba9f99817436d0a2ce3a0ec",
|
"lean4web": "github:hhu-adam/lean4web",
|
||||||
"octokit": "^2.0.14",
|
"octokit": "^2.0.14",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@@ -2225,6 +2221,231 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz",
|
||||||
"integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww=="
|
"integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/linux-x64": {
|
"node_modules/@esbuild/linux-x64": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
|
||||||
@@ -2240,6 +2461,96 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@floating-ui/core": {
|
"node_modules/@floating-ui/core": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz",
|
||||||
@@ -2285,45 +2596,33 @@
|
|||||||
"integrity": "sha512-KrJdmkqz6DszT2wV/bbhXef4r0hV3B0vw2mAqei8A2kRnvq+gcJLmmIeQ94vu9VEXrUQzos5M9lH1TAAXpRphw=="
|
"integrity": "sha512-KrJdmkqz6DszT2wV/bbhXef4r0hV3B0vw2mAqei8A2kRnvq+gcJLmmIeQ94vu9VEXrUQzos5M9lH1TAAXpRphw=="
|
||||||
},
|
},
|
||||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||||
"version": "6.5.1",
|
"version": "6.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz",
|
||||||
"integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==",
|
"integrity": "sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fortawesome/fontawesome-svg-core": {
|
"node_modules/@fortawesome/fontawesome-svg-core": {
|
||||||
"version": "6.5.1",
|
"version": "6.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.2.tgz",
|
||||||
"integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==",
|
"integrity": "sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-common-types": "6.5.1"
|
"@fortawesome/fontawesome-common-types": "6.4.2"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@fortawesome/free-regular-svg-icons": {
|
|
||||||
"version": "6.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.1.tgz",
|
|
||||||
"integrity": "sha512-m6ShXn+wvqEU69wSP84coxLbNl7sGVZb+Ca+XZq6k30SzuP3X4TfPqtycgUh9ASwlNh5OfQCd8pDIWxl+O+LlQ==",
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@fortawesome/fontawesome-common-types": "6.5.1"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fortawesome/free-solid-svg-icons": {
|
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||||
"version": "6.5.1",
|
"version": "6.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.2.tgz",
|
||||||
"integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==",
|
"integrity": "sha512-sYwXurXUEQS32fZz9hVCUUv/xu49PEJEyUOsA51l6PU/qVgfbTb2glsTEaJngVVT8VqBATRIdh7XVgV1JF1LkA==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-common-types": "6.5.1"
|
"@fortawesome/fontawesome-common-types": "6.4.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -2539,9 +2838,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@leanprover/infoview": {
|
"node_modules/@leanprover/infoview": {
|
||||||
"version": "0.4.4",
|
"version": "0.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/@leanprover/infoview/-/infoview-0.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/@leanprover/infoview/-/infoview-0.4.3.tgz",
|
||||||
"integrity": "sha512-OxHffFaHcEudLyBEWpicOl7TfXuTYxW5Sz1RkHdUINWJpQsQn60YDF5fNRKmSb0d/fm7p+LVeBvM273jvfR5wQ==",
|
"integrity": "sha512-SufdOr2myHAbZNUmobfQdAhsEC5H9ddi3KS0z1v/8riWSMm+yJk3u4LxVuzCmmSmV2QxFqtFzn5z+HQqj1Vo7g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@leanprover/infoview-api": "~0.2.1",
|
"@leanprover/infoview-api": "~0.2.1",
|
||||||
"@vscode/codicons": "^0.0.32",
|
"@vscode/codicons": "^0.0.32",
|
||||||
@@ -4778,6 +5077,81 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@swc/core-darwin-arm64": {
|
||||||
|
"version": "1.3.95",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.95.tgz",
|
||||||
|
"integrity": "sha512-VAuBAP3MNetO/yBIBzvorUXq7lUBwhfpJxYViSxyluMwtoQDhE/XWN598TWMwMl1ZuImb56d7eUsuFdjgY7pJw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-darwin-x64": {
|
||||||
|
"version": "1.3.95",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.95.tgz",
|
||||||
|
"integrity": "sha512-20vF2rvUsN98zGLZc+dsEdHvLoCuiYq/1B+TDeE4oolgTFDmI1jKO+m44PzWjYtKGU9QR95sZ6r/uec0QC5O4Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-linux-arm-gnueabihf": {
|
||||||
|
"version": "1.3.95",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.95.tgz",
|
||||||
|
"integrity": "sha512-oEudEM8PST1MRNGs+zu0cx5i9uP8TsLE4/L9HHrS07Ck0RJ3DCj3O2fU832nmLe2QxnAGPwBpSO9FntLfOiWEQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-linux-arm64-gnu": {
|
||||||
|
"version": "1.3.95",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.95.tgz",
|
||||||
|
"integrity": "sha512-pIhFI+cuC1aYg+0NAPxwT/VRb32f2ia8oGxUjQR6aJg65gLkUYQzdwuUmpMtFR2WVf7WVFYxUnjo4UyMuyh3ng==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-linux-arm64-musl": {
|
||||||
|
"version": "1.3.95",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.95.tgz",
|
||||||
|
"integrity": "sha512-ZpbTr+QZDT4OPJfjPAmScqdKKaT+wGurvMU5AhxLaf85DuL8HwUwwlL0n1oLieLc47DwIJEMuKQkYhXMqmJHlg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@swc/core-linux-x64-gnu": {
|
"node_modules/@swc/core-linux-x64-gnu": {
|
||||||
"version": "1.3.95",
|
"version": "1.3.95",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.95.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.95.tgz",
|
||||||
@@ -4808,6 +5182,51 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@swc/core-win32-arm64-msvc": {
|
||||||
|
"version": "1.3.95",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.95.tgz",
|
||||||
|
"integrity": "sha512-YaP4x/aZbUyNdqCBpC2zL8b8n58MEpOUpmOIZK6G1SxGi+2ENht7gs7+iXpWPc0sy7X3YPKmSWMAuui0h8lgAA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-win32-ia32-msvc": {
|
||||||
|
"version": "1.3.95",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.95.tgz",
|
||||||
|
"integrity": "sha512-w0u3HI916zT4BC/57gOd+AwAEjXeUlQbGJ9H4p/gzs1zkSHtoDQghVUNy3n/ZKp9KFod/95cA8mbVF9t1+6epQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-win32-x64-msvc": {
|
||||||
|
"version": "1.3.95",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.95.tgz",
|
||||||
|
"integrity": "sha512-5RGnMt0S6gg4Gc6QtPUJ3Qs9Un4sKqccEzgH/tj7V/DVTJwKdnBKxFZfgQ34OR2Zpz7zGOn889xwsFVXspVWNA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@swc/counter": {
|
"node_modules/@swc/counter": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.2.tgz",
|
||||||
@@ -8116,6 +8535,19 @@
|
|||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
|
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/function-bind": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||||
@@ -10081,8 +10513,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/lean4web": {
|
"node_modules/lean4web": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "git+ssh://git@github.com/hhu-adam/lean4web.git#b91645a7b88814675ba9f99817436d0a2ce3a0ec",
|
"resolved": "git+ssh://git@github.com/hhu-adam/lean4web.git#6fc9c11179934cce7ca1f78140c57b6931186b42",
|
||||||
"integrity": "sha512-s9qYeXuMNBGDPKC5IuFTjV/j8tQCRkZr+poYKEWljrM95rsz4JHIvjdEt891fE9JhPDLSN+iXNRXwIOCT6FlMg==",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.11.1",
|
"@emotion/react": "^11.11.1",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
@@ -10091,7 +10522,7 @@
|
|||||||
"@fortawesome/fontawesome-svg-core": "^6.2.0",
|
"@fortawesome/fontawesome-svg-core": "^6.2.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.2.0",
|
"@fortawesome/free-solid-svg-icons": "^6.2.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@leanprover/infoview": "^0.4.4",
|
"@leanprover/infoview": "^0.4.3",
|
||||||
"@mui/material": "^5.13.7",
|
"@mui/material": "^5.13.7",
|
||||||
"@vitejs/plugin-react-swc": "^3.4.0",
|
"@vitejs/plugin-react-swc": "^3.4.0",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
|||||||
+1
-5
@@ -10,10 +10,6 @@
|
|||||||
"@emotion/styled": "^11.10.5",
|
"@emotion/styled": "^11.10.5",
|
||||||
"@fontsource/roboto": "^4.5.8",
|
"@fontsource/roboto": "^4.5.8",
|
||||||
"@fontsource/roboto-mono": "^4.5.8",
|
"@fontsource/roboto-mono": "^4.5.8",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.5.1",
|
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
|
||||||
"@leanprover/infoview": "^0.4.3",
|
"@leanprover/infoview": "^0.4.3",
|
||||||
"@mui/icons-material": "^5.11.0",
|
"@mui/icons-material": "^5.11.0",
|
||||||
"@mui/material": "^5.11.1",
|
"@mui/material": "^5.11.1",
|
||||||
@@ -28,7 +24,7 @@
|
|||||||
"debounce": "^1.2.1",
|
"debounce": "^1.2.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"lean4-infoview": "https://gitpkg.now.sh/leanprover/vscode-lean4/lean4-infoview?de0062c",
|
"lean4-infoview": "https://gitpkg.now.sh/leanprover/vscode-lean4/lean4-infoview?de0062c",
|
||||||
"lean4web": "github:hhu-adam/lean4web#b91645a7b88814675ba9f99817436d0a2ce3a0ec",
|
"lean4web": "github:hhu-adam/lean4web",
|
||||||
"octokit": "^2.0.14",
|
"octokit": "^2.0.14",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#/bin/bash
|
#/bin/bash
|
||||||
|
|
||||||
# Note: This fails if there is no default toolchain installed
|
|
||||||
ELAN_HOME=$(lake env printenv ELAN_HOME)
|
ELAN_HOME=$(lake env printenv ELAN_HOME)
|
||||||
|
|
||||||
# $1 : the game directory
|
# $1 : the game directory
|
||||||
|
|||||||
+400
-119
@@ -1,12 +1,23 @@
|
|||||||
import GameServer.Helpers
|
import GameServer.EnvExtensions
|
||||||
import GameServer.Inventory
|
|
||||||
import GameServer.Options
|
|
||||||
import GameServer.SaveData
|
|
||||||
|
|
||||||
open Lean Meta Elab Command
|
open Lean Meta Elab Command
|
||||||
|
|
||||||
set_option autoImplicit false
|
set_option autoImplicit false
|
||||||
|
|
||||||
|
/-- Let `MakeGame` print the reasons why the worlds depend on each other. -/
|
||||||
|
register_option lean4game.showDependencyReasons : Bool := {
|
||||||
|
defValue := false
|
||||||
|
descr := "show reasons for calculated world dependencies."
|
||||||
|
}
|
||||||
|
|
||||||
|
/-- Let `MakeGame` print the reasons why the worlds depend on each other.
|
||||||
|
|
||||||
|
Note: currently unused in favour of setting `set_option trace.debug true`. -/
|
||||||
|
register_option lean4game.verbose : Bool := {
|
||||||
|
defValue := false
|
||||||
|
descr := "display more info messages to help developing the game."
|
||||||
|
}
|
||||||
|
|
||||||
/-! # Game metadata -/
|
/-! # Game metadata -/
|
||||||
|
|
||||||
/-- Switch to the specified `Game` (and create it if non-existent). Example: `Game "NNG"` -/
|
/-- Switch to the specified `Game` (and create it if non-existent). Example: `Game "NNG"` -/
|
||||||
@@ -41,15 +52,13 @@ elab "Title" t:str : command => do
|
|||||||
|
|
||||||
/-- Define the introduction of the current game/world/level. -/
|
/-- Define the introduction of the current game/world/level. -/
|
||||||
elab "Introduction" t:str : command => do
|
elab "Introduction" t:str : command => do
|
||||||
let intro := t.getString
|
|
||||||
match ← getCurLayer with
|
match ← getCurLayer with
|
||||||
| .Level => modifyCurLevel fun level => pure {level with introduction := intro}
|
| .Level => modifyCurLevel fun level => pure {level with introduction := t.getString}
|
||||||
| .World => modifyCurWorld fun world => pure {world with introduction := intro}
|
| .World => modifyCurWorld fun world => pure {world with introduction := t.getString}
|
||||||
| .Game => modifyCurGame fun game => pure {game with introduction := intro}
|
| .Game => modifyCurGame fun game => pure {game with introduction := t.getString}
|
||||||
|
|
||||||
/-- Define the info of the current game. Used for e.g. credits -/
|
/-- Define the info of the current game. Used for e.g. credits -/
|
||||||
elab "Info" t:str : command => do
|
elab "Info" t:str : command => do
|
||||||
let info:= t.getString
|
|
||||||
match ← getCurLayer with
|
match ← getCurLayer with
|
||||||
| .Level =>
|
| .Level =>
|
||||||
logError "Can't use `Info` in a level!"
|
logError "Can't use `Info` in a level!"
|
||||||
@@ -57,7 +66,7 @@ elab "Info" t:str : command => do
|
|||||||
| .World =>
|
| .World =>
|
||||||
logError "Can't use `Info` in a world"
|
logError "Can't use `Info` in a world"
|
||||||
pure ()
|
pure ()
|
||||||
| .Game => modifyCurGame fun game => pure {game with info := info}
|
| .Game => modifyCurGame fun game => pure {game with info := t.getString}
|
||||||
|
|
||||||
/-- Provide the location of the image for the current game/world/level.
|
/-- Provide the location of the image for the current game/world/level.
|
||||||
Paths are relative to the lean project's root. -/
|
Paths are relative to the lean project's root. -/
|
||||||
@@ -81,11 +90,10 @@ elab "Image" t:str : command => do
|
|||||||
/-- Define the conclusion of the current game or current level if some
|
/-- Define the conclusion of the current game or current level if some
|
||||||
building a level. -/
|
building a level. -/
|
||||||
elab "Conclusion" t:str : command => do
|
elab "Conclusion" t:str : command => do
|
||||||
let conclusion := t.getString
|
|
||||||
match ← getCurLayer with
|
match ← getCurLayer with
|
||||||
| .Level => modifyCurLevel fun level => pure {level with conclusion := conclusion}
|
| .Level => modifyCurLevel fun level => pure {level with conclusion := t.getString}
|
||||||
| .World => modifyCurWorld fun world => pure {world with conclusion := conclusion}
|
| .World => modifyCurWorld fun world => pure {world with conclusion := t.getString}
|
||||||
| .Game => modifyCurGame fun game => pure {game with conclusion := conclusion}
|
| .Game => modifyCurGame fun game => pure {game with conclusion := t.getString}
|
||||||
|
|
||||||
/-- A list of games that should be played before this one. Example `Prerequisites "NNG" "STG"`. -/
|
/-- A list of games that should be played before this one. Example `Prerequisites "NNG" "STG"`. -/
|
||||||
elab "Prerequisites" t:str* : command => do
|
elab "Prerequisites" t:str* : command => do
|
||||||
@@ -94,15 +102,13 @@ elab "Prerequisites" t:str* : command => do
|
|||||||
|
|
||||||
/-- Short caption for the game (1 sentence) -/
|
/-- Short caption for the game (1 sentence) -/
|
||||||
elab "CaptionShort" t:str : command => do
|
elab "CaptionShort" t:str : command => do
|
||||||
let caption := t.getString
|
|
||||||
modifyCurGame fun game => pure {game with
|
modifyCurGame fun game => pure {game with
|
||||||
tile := {game.tile with short := caption}}
|
tile := {game.tile with short := t.getString}}
|
||||||
|
|
||||||
/-- More detailed description what the game is about (2-4 sentences). -/
|
/-- More detailed description what the game is about (2-4 sentences). -/
|
||||||
elab "CaptionLong" t:str : command => do
|
elab "CaptionLong" t:str : command => do
|
||||||
let caption := t.getString
|
|
||||||
modifyCurGame fun game => pure {game with
|
modifyCurGame fun game => pure {game with
|
||||||
tile := {game.tile with long := caption}}
|
tile := {game.tile with long := t.getString}}
|
||||||
|
|
||||||
/-- A list of Languages the game is translated to. For example `Languages "German" "English"`.
|
/-- 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.
|
NOTE: For the time being, only a single language is supported.
|
||||||
@@ -124,12 +130,119 @@ elab "CoverImage" t:str : command => do
|
|||||||
|
|
||||||
/-! # Inventory
|
/-! # Inventory
|
||||||
|
|
||||||
The inventory contains docs for tactics, theorems, and definitions. These are all locked
|
The inventory contains docs for tactics, lemmas, and definitions. These are all locked
|
||||||
in the first level and get enabled during the game.
|
in the first level and get enabled during the game.
|
||||||
-/
|
-/
|
||||||
|
|
||||||
/-! ## Doc entries -/
|
/-! ## Doc entries -/
|
||||||
|
|
||||||
|
/-- Copied from `Mathlib.Tactic.HelpCmd`.
|
||||||
|
|
||||||
|
Gets the initial string token in a parser description. For example, for a declaration like
|
||||||
|
`syntax "bla" "baz" term : tactic`, it returns `some "bla"`. Returns `none` for syntax declarations
|
||||||
|
that don't start with a string constant. -/
|
||||||
|
partial def getHeadTk (e : Expr) : Option String :=
|
||||||
|
match (Expr.withApp e λ e a => (e.constName?.getD Name.anonymous, a)) with
|
||||||
|
| (``ParserDescr.node, #[_, _, p]) => getHeadTk p
|
||||||
|
| (``ParserDescr.unary, #[.app _ (.lit (.strVal "withPosition")), p]) => getHeadTk p
|
||||||
|
| (``ParserDescr.unary, #[.app _ (.lit (.strVal "atomic")), p]) => getHeadTk p
|
||||||
|
| (``ParserDescr.binary, #[.app _ (.lit (.strVal "andthen")), p, _]) => getHeadTk p
|
||||||
|
| (``ParserDescr.nonReservedSymbol, #[.lit (.strVal tk), _]) => some tk
|
||||||
|
| (``ParserDescr.symbol, #[.lit (.strVal tk)]) => some tk
|
||||||
|
| (``Parser.withAntiquot, #[_, p]) => getHeadTk p
|
||||||
|
| (``Parser.leadingNode, #[_, _, p]) => getHeadTk p
|
||||||
|
| (``HAndThen.hAndThen, #[_, _, _, _, p, _]) => getHeadTk p
|
||||||
|
| (``Parser.nonReservedSymbol, #[.lit (.strVal tk), _]) => some tk
|
||||||
|
| (``Parser.symbol, #[.lit (.strVal tk)]) => some tk
|
||||||
|
| _ => none
|
||||||
|
|
||||||
|
/-- Modified from `#help` in `Mathlib.Tactic.HelpCmd` -/
|
||||||
|
def getTacticDocstring (env : Environment) (name: Name) : CommandElabM (Option String) := do
|
||||||
|
let name := name.toString (escape := false)
|
||||||
|
let mut decls : Lean.RBMap String (Array SyntaxNodeKind) compare := {}
|
||||||
|
|
||||||
|
let catName : Name := `tactic
|
||||||
|
let catStx : Ident := mkIdent catName -- TODO
|
||||||
|
let some cat := (Parser.parserExtension.getState env).categories.find? catName
|
||||||
|
| throwErrorAt catStx "{catStx} is not a syntax category"
|
||||||
|
liftTermElabM <| Term.addCategoryInfo catStx catName
|
||||||
|
for (k, _) in cat.kinds do
|
||||||
|
let mut used := false
|
||||||
|
if let some tk := do getHeadTk (← (← env.find? k).value?) then
|
||||||
|
let tk := tk.trim
|
||||||
|
if name ≠ tk then -- was `!name.isPrefixOf tk`
|
||||||
|
continue
|
||||||
|
used := true
|
||||||
|
decls := decls.insert tk ((decls.findD tk #[]).push k)
|
||||||
|
for (_name, ks) in decls do
|
||||||
|
for k in ks do
|
||||||
|
if let some doc ← findDocString? env k then
|
||||||
|
return doc
|
||||||
|
|
||||||
|
logWarning <| m!"Could not find a docstring for tactic {name}, consider adding one " ++
|
||||||
|
m!"using `TacticDoc {name} \"some doc\"`"
|
||||||
|
return none
|
||||||
|
|
||||||
|
/-- Retrieve the docstring associated to an inventory item. For Tactics, this
|
||||||
|
is not guaranteed to work. -/
|
||||||
|
def getDocstring (env : Environment) (name : Name) (type : InventoryType) :
|
||||||
|
CommandElabM (Option String) :=
|
||||||
|
match type with
|
||||||
|
-- for tactics it's a lookup following mathlib's `#help`. not guaranteed to be the correct one.
|
||||||
|
| .Tactic => getTacticDocstring env name
|
||||||
|
| .Lemma => findDocString? env name
|
||||||
|
-- TODO: for definitions not implemented yet, does it work?
|
||||||
|
| .Definition => findDocString? env name
|
||||||
|
|
||||||
|
/-- Checks if `inventoryTemplateExt` contains an entry with `(type, name)` and yields
|
||||||
|
a warning otherwise. If `template` is provided, it will add such an entry instead of yielding a
|
||||||
|
warning.
|
||||||
|
|
||||||
|
`ref` is the syntax piece. If `name` is not provided, it will use `ident.getId`.
|
||||||
|
I used this workaround, because I needed a new name (with correct namespace etc)
|
||||||
|
to be used, and I don't know how to create a new ident with same position but different name.
|
||||||
|
-/
|
||||||
|
def checkInventoryDoc (type : InventoryType) (ref : Ident) (name : Name := ref.getId)
|
||||||
|
(template : Option String := none) : CommandElabM Unit := do
|
||||||
|
-- note: `name` is an `Ident` (instead of `Name`) for the log messages.
|
||||||
|
let env ← getEnv
|
||||||
|
let n := name
|
||||||
|
-- Find a key with matching `(type, name)`.
|
||||||
|
match (inventoryTemplateExt.getState env).findIdx?
|
||||||
|
(fun x => x.name == n && x.type == type) with
|
||||||
|
-- Nothing to do if the entry exists
|
||||||
|
| some _ => pure ()
|
||||||
|
| none =>
|
||||||
|
match template with
|
||||||
|
-- Warn about missing documentation
|
||||||
|
| none =>
|
||||||
|
let docstring ← match (← getDocstring env name type) with
|
||||||
|
| some ds =>
|
||||||
|
logInfoAt ref (m!"Missing {type} Documentation. Using existing docstring. " ++
|
||||||
|
m!"Add {name}\nAdd `{type}Doc {name}` somewhere above this statement.")
|
||||||
|
pure s!"*(lean docstring)*\\\n{ds}"
|
||||||
|
| none =>
|
||||||
|
logWarningAt ref (m!"Missing {type} Documentation: {name}\nAdd `{type}Doc {name}` " ++
|
||||||
|
m!"somewhere above this statement.")
|
||||||
|
pure "(missing)"
|
||||||
|
|
||||||
|
-- We just add a dummy entry
|
||||||
|
modifyEnv (inventoryTemplateExt.addEntry · {
|
||||||
|
type := type
|
||||||
|
name := name
|
||||||
|
category := if type == .Lemma then s!"{n.getPrefix}" else ""
|
||||||
|
content := docstring})
|
||||||
|
-- Add the default documentation
|
||||||
|
| some s =>
|
||||||
|
modifyEnv (inventoryTemplateExt.addEntry · {
|
||||||
|
type := type
|
||||||
|
name := name
|
||||||
|
category := if type == .Lemma then s!"{n.getPrefix}" else ""
|
||||||
|
content := s })
|
||||||
|
logInfoAt ref (m!"Missing {type} Documentation: {name}, used default (e.g. provided " ++
|
||||||
|
m!"docstring) instead. If you want to write a different description, add " ++
|
||||||
|
m!"`{type}Doc {name}` somewhere above this statement.")
|
||||||
|
|
||||||
/-- Documentation entry of a tactic. Example:
|
/-- Documentation entry of a tactic. Example:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -139,40 +252,37 @@ TacticDoc rw "`rw` stands for rewrite, etc. "
|
|||||||
* The identifier is the tactics name. Some need to be escaped like `«have»`.
|
* The identifier is the tactics name. Some need to be escaped like `«have»`.
|
||||||
* The description is a string supporting Markdown.
|
* The description is a string supporting Markdown.
|
||||||
-/
|
-/
|
||||||
elab doc:docComment ? "TacticDoc" name:ident content:str ? : command => do
|
elab "TacticDoc" name:ident content:str : command =>
|
||||||
let doc ← parseDocCommentLegacy doc content
|
|
||||||
modifyEnv (inventoryTemplateExt.addEntry · {
|
modifyEnv (inventoryTemplateExt.addEntry · {
|
||||||
type := .Tactic
|
type := .Tactic
|
||||||
name := name.getId
|
name := name.getId
|
||||||
displayName := name.getId.toString
|
displayName := name.getId.toString
|
||||||
content := doc })
|
content := content.getString })
|
||||||
|
|
||||||
/-- Documentation entry of a theorem. Example:
|
/-- Documentation entry of a lemma. Example:
|
||||||
|
|
||||||
```
|
```
|
||||||
TheoremDoc Nat.succ_pos as "succ_pos" in "Nat" "says `0 < n.succ`, etc."
|
LemmaDoc Nat.succ_pos as "succ_pos" in "Nat" "says `0 < n.succ`, etc."
|
||||||
```
|
```
|
||||||
|
|
||||||
* The first identifier is used in the commands `[New/Only/Disabled]Theorem`.
|
* The first identifier is used in the commands `[New/Only/Disabled]Lemma`.
|
||||||
It is preferably the true name of the theorem. However, this is not required.
|
It is preferably the true name of the lemma. However, this is not required.
|
||||||
* The string following `as` is the displayed name (in the Inventory).
|
* The string following `as` is the displayed name (in the Inventory).
|
||||||
* The identifier after `in` is the category to group theorems by (in the Inventory).
|
* The identifier after `in` is the category to group lemmas by (in the Inventory).
|
||||||
* The description is a string supporting Markdown.
|
* The description is a string supporting Markdown.
|
||||||
|
|
||||||
Use `[[mathlib_doc]]` in the string to insert a link to the mathlib doc page. This requires
|
Use `[[mathlib_doc]]` in the string to insert a link to the mathlib doc page. This requires
|
||||||
The theorem/definition to have the same fully qualified name as in mathlib.
|
The lemma/definition to have the same fully qualified name as in mathlib.
|
||||||
-/
|
-/
|
||||||
elab doc:docComment ? "TheoremDoc" name:ident "as" displayName:str "in" category:str content:str ? :
|
elab "LemmaDoc" name:ident "as" displayName:str "in" category:str content:str : command =>
|
||||||
command => do
|
|
||||||
let doc ← parseDocCommentLegacy doc content
|
|
||||||
modifyEnv (inventoryTemplateExt.addEntry · {
|
modifyEnv (inventoryTemplateExt.addEntry · {
|
||||||
type := .Lemma
|
type := .Lemma
|
||||||
name := name.getId
|
name := name.getId
|
||||||
category := category.getString
|
category := category.getString
|
||||||
displayName := displayName.getString
|
displayName := displayName.getString
|
||||||
content := doc })
|
content := content.getString })
|
||||||
-- TODO: Catch the following behaviour.
|
-- TODO: Catch the following behaviour.
|
||||||
-- 1. if `TheoremDoc` appears in the same file as `Statement`, it will silently use
|
-- 1. if `LemmaDoc` appears in the same file as `Statement`, it will silently use
|
||||||
-- it but display the info that it wasn't found in `Statement`
|
-- it but display the info that it wasn't found in `Statement`
|
||||||
-- 2. if it appears in a later file, however, it will silently not do anything and keep
|
-- 2. if it appears in a later file, however, it will silently not do anything and keep
|
||||||
-- the first one.
|
-- the first one.
|
||||||
@@ -190,25 +300,37 @@ DefinitionDoc Function.Bijective as "Bijective" "defined as `Injective f ∧ Sur
|
|||||||
* The description is a string supporting Markdown.
|
* The description is a string supporting Markdown.
|
||||||
|
|
||||||
Use `[[mathlib_doc]]` in the string to insert a link to the mathlib doc page. This requires
|
Use `[[mathlib_doc]]` in the string to insert a link to the mathlib doc page. This requires
|
||||||
The theorem/definition to have the same fully qualified name as in mathlib.
|
The lemma/definition to have the same fully qualified name as in mathlib.
|
||||||
-/
|
-/
|
||||||
elab doc:docComment ? "DefinitionDoc" name:ident "as" displayName:str template:str ? : command => do
|
elab "DefinitionDoc" name:ident "as" displayName:str template:str : command =>
|
||||||
let doc ← parseDocCommentLegacy doc template
|
|
||||||
modifyEnv (inventoryTemplateExt.addEntry · {
|
modifyEnv (inventoryTemplateExt.addEntry · {
|
||||||
type := .Definition
|
type := .Definition
|
||||||
name := name.getId,
|
name := name.getId,
|
||||||
displayName := displayName.getString,
|
displayName := displayName.getString,
|
||||||
content := doc })
|
content := template.getString })
|
||||||
|
|
||||||
/-! ## Add inventory items -/
|
/-! ## Add inventory items -/
|
||||||
|
|
||||||
def checkCommandNotDuplicated (items : Array Name) (cmd := "Command") : CommandElabM Unit := do
|
def getStatement (name : Name) : CommandElabM MessageData := do
|
||||||
if ¬ items.isEmpty then
|
return ← addMessageContextPartial (.ofPPFormat { pp := fun
|
||||||
logWarning s!"You should only use one `{cmd}` per level, but it takes multiple arguments: `{cmd} obj₁ obj₂ obj₃`!"
|
| some ctx => ctx.runMetaM <| PrettyPrinter.ppSignature name
|
||||||
|
| none => return "that's a bug." })
|
||||||
|
|
||||||
|
-- Note: We use `String` because we can't send `MessageData` as json, but
|
||||||
|
-- `MessageData` might be better for interactive highlighting.
|
||||||
|
/-- Get a string of the form `my_lemma (n : ℕ) : n + n = 2 * n`.
|
||||||
|
|
||||||
|
Note: A statement like `theorem abc : ∀ x : Nat, x ≥ 0` would be turned into
|
||||||
|
`theorem abc (x : Nat) : x ≥ 0` by `PrettyPrinter.ppSignature`. -/
|
||||||
|
def getStatementString (name : Name) : CommandElabM String := do
|
||||||
|
try
|
||||||
|
return ← (← getStatement name).toString
|
||||||
|
catch
|
||||||
|
| _ => throwError m!"Could not find {name} in context."
|
||||||
|
-- TODO: I think it would be nicer to unresolve Namespaces as much as possible.
|
||||||
|
|
||||||
/-- Declare tactics that are introduced by this level. -/
|
/-- Declare tactics that are introduced by this level. -/
|
||||||
elab "NewTactic" args:ident* : command => do
|
elab "NewTactic" args:ident* : command => do
|
||||||
checkCommandNotDuplicated ((←getCurLevel).tactics.new) "NewTactic"
|
|
||||||
for name in ↑args do
|
for name in ↑args do
|
||||||
checkInventoryDoc .Tactic name -- TODO: Add (template := "[docstring]")
|
checkInventoryDoc .Tactic name -- TODO: Add (template := "[docstring]")
|
||||||
modifyCurLevel fun level => pure {level with
|
modifyCurLevel fun level => pure {level with
|
||||||
@@ -216,16 +338,14 @@ elab "NewTactic" args:ident* : command => do
|
|||||||
|
|
||||||
/-- Declare tactics that are introduced by this level but do not show up in inventory. -/
|
/-- Declare tactics that are introduced by this level but do not show up in inventory. -/
|
||||||
elab "NewHiddenTactic" args:ident* : command => do
|
elab "NewHiddenTactic" args:ident* : command => do
|
||||||
checkCommandNotDuplicated ((←getCurLevel).tactics.hidden) "NewHiddenTactic"
|
|
||||||
for name in ↑args do
|
for name in ↑args do
|
||||||
checkInventoryDoc .Tactic name (template := "")
|
checkInventoryDoc .Tactic name (template := "")
|
||||||
modifyCurLevel fun level => pure {level with
|
modifyCurLevel fun level => pure {level with
|
||||||
tactics := {level.tactics with new := level.tactics.new ++ args.map (·.getId),
|
tactics := {level.tactics with new := level.tactics.new ++ args.map (·.getId),
|
||||||
hidden := level.tactics.hidden ++ args.map (·.getId)}}
|
hidden := level.tactics.hidden ++ args.map (·.getId)}}
|
||||||
|
|
||||||
/-- Declare theorems that are introduced by this level. -/
|
/-- Declare lemmas that are introduced by this level. -/
|
||||||
elab "NewTheorem" args:ident* : command => do
|
elab "NewLemma" args:ident* : command => do
|
||||||
checkCommandNotDuplicated ((←getCurLevel).lemmas.new) "NewTheorem"
|
|
||||||
for name in ↑args do
|
for name in ↑args do
|
||||||
try let _decl ← getConstInfo name.getId catch
|
try let _decl ← getConstInfo name.getId catch
|
||||||
| _ => logErrorAt name m!"unknown identifier '{name}'."
|
| _ => logErrorAt name m!"unknown identifier '{name}'."
|
||||||
@@ -235,7 +355,6 @@ elab "NewTheorem" args:ident* : command => do
|
|||||||
|
|
||||||
/-- Declare definitions that are introduced by this level. -/
|
/-- Declare definitions that are introduced by this level. -/
|
||||||
elab "NewDefinition" args:ident* : command => do
|
elab "NewDefinition" args:ident* : command => do
|
||||||
checkCommandNotDuplicated ((←getCurLevel).definitions.new) "NewDefinition"
|
|
||||||
for name in ↑args do checkInventoryDoc .Definition name -- TODO: Add (template := "[mathlib]")
|
for name in ↑args do checkInventoryDoc .Definition name -- TODO: Add (template := "[mathlib]")
|
||||||
modifyCurLevel fun level => pure {level with
|
modifyCurLevel fun level => pure {level with
|
||||||
definitions := {level.definitions with new := args.map (·.getId)}}
|
definitions := {level.definitions with new := args.map (·.getId)}}
|
||||||
@@ -243,36 +362,31 @@ elab "NewDefinition" args:ident* : command => do
|
|||||||
/-- Declare tactics that are temporarily disabled in this level.
|
/-- Declare tactics that are temporarily disabled in this level.
|
||||||
This is ignored if `OnlyTactic` is set. -/
|
This is ignored if `OnlyTactic` is set. -/
|
||||||
elab "DisabledTactic" args:ident* : command => do
|
elab "DisabledTactic" args:ident* : command => do
|
||||||
checkCommandNotDuplicated ((←getCurLevel).tactics.disabled) "DisabledTactic"
|
|
||||||
for name in ↑args do checkInventoryDoc .Tactic name
|
for name in ↑args do checkInventoryDoc .Tactic name
|
||||||
modifyCurLevel fun level => pure {level with
|
modifyCurLevel fun level => pure {level with
|
||||||
tactics := {level.tactics with disabled := args.map (·.getId)}}
|
tactics := {level.tactics with disabled := args.map (·.getId)}}
|
||||||
|
|
||||||
/-- Declare theorems that are temporarily disabled in this level.
|
/-- Declare lemmas that are temporarily disabled in this level.
|
||||||
This is ignored if `OnlyTheorem` is set. -/
|
This is ignored if `OnlyLemma` is set. -/
|
||||||
elab "DisabledTheorem" args:ident* : command => do
|
elab "DisabledLemma" args:ident* : command => do
|
||||||
checkCommandNotDuplicated ((←getCurLevel).lemmas.disabled) "DisabledTheorem"
|
|
||||||
for name in ↑args do checkInventoryDoc .Lemma name
|
for name in ↑args do checkInventoryDoc .Lemma name
|
||||||
modifyCurLevel fun level => pure {level with
|
modifyCurLevel fun level => pure {level with
|
||||||
lemmas := {level.lemmas with disabled := args.map (·.getId)}}
|
lemmas := {level.lemmas with disabled := args.map (·.getId)}}
|
||||||
|
|
||||||
/-- Declare definitions that are temporarily disabled in this level -/
|
/-- Declare definitions that are temporarily disabled in this level -/
|
||||||
elab "DisabledDefinition" args:ident* : command => do
|
elab "DisabledDefinition" args:ident* : command => do
|
||||||
checkCommandNotDuplicated ((←getCurLevel).definitions.disabled) "DisabledDefinition"
|
|
||||||
for name in ↑args do checkInventoryDoc .Definition name
|
for name in ↑args do checkInventoryDoc .Definition name
|
||||||
modifyCurLevel fun level => pure {level with
|
modifyCurLevel fun level => pure {level with
|
||||||
definitions := {level.definitions with disabled := args.map (·.getId)}}
|
definitions := {level.definitions with disabled := args.map (·.getId)}}
|
||||||
|
|
||||||
/-- Temporarily disable all tactics except the ones declared here -/
|
/-- Temporarily disable all tactics except the ones declared here -/
|
||||||
elab "OnlyTactic" args:ident* : command => do
|
elab "OnlyTactic" args:ident* : command => do
|
||||||
checkCommandNotDuplicated ((←getCurLevel).tactics.only) "OnlyTactic"
|
|
||||||
for name in ↑args do checkInventoryDoc .Tactic name
|
for name in ↑args do checkInventoryDoc .Tactic name
|
||||||
modifyCurLevel fun level => pure {level with
|
modifyCurLevel fun level => pure {level with
|
||||||
tactics := {level.tactics with only := args.map (·.getId)}}
|
tactics := {level.tactics with only := args.map (·.getId)}}
|
||||||
|
|
||||||
/-- Temporarily disable all theorems except the ones declared here -/
|
/-- Temporarily disable all lemmas except the ones declared here -/
|
||||||
elab "OnlyTheorem" args:ident* : command => do
|
elab "OnlyLemma" args:ident* : command => do
|
||||||
checkCommandNotDuplicated ((←getCurLevel).lemmas.only) "OnlyTheorem"
|
|
||||||
for name in ↑args do checkInventoryDoc .Lemma name
|
for name in ↑args do checkInventoryDoc .Lemma name
|
||||||
modifyCurLevel fun level => pure {level with
|
modifyCurLevel fun level => pure {level with
|
||||||
lemmas := {level.lemmas with only := args.map (·.getId)}}
|
lemmas := {level.lemmas with only := args.map (·.getId)}}
|
||||||
@@ -280,66 +394,65 @@ elab "OnlyTheorem" args:ident* : command => do
|
|||||||
/-- Temporarily disable all definitions except the ones declared here.
|
/-- Temporarily disable all definitions except the ones declared here.
|
||||||
This is ignored if `OnlyDefinition` is set. -/
|
This is ignored if `OnlyDefinition` is set. -/
|
||||||
elab "OnlyDefinition" args:ident* : command => do
|
elab "OnlyDefinition" args:ident* : command => do
|
||||||
checkCommandNotDuplicated ((←getCurLevel).definitions.only) "OnlyDefinition"
|
|
||||||
for name in ↑args do checkInventoryDoc .Definition name
|
for name in ↑args do checkInventoryDoc .Definition name
|
||||||
modifyCurLevel fun level => pure {level with
|
modifyCurLevel fun level => pure {level with
|
||||||
definitions := {level.definitions with only := args.map (·.getId)}}
|
definitions := {level.definitions with only := args.map (·.getId)}}
|
||||||
|
|
||||||
/-- Define which tab of Lemmas is opened by default. Usage: `TheoremTab "Nat"`.
|
/-- Define which tab of Lemmas is opened by default. Usage: `LemmaTab "Nat"`.
|
||||||
If omitted, the current tab will remain open. -/
|
If omitted, the current tab will remain open. -/
|
||||||
elab "TheoremTab" category:str : command =>
|
elab "LemmaTab" category:str : command =>
|
||||||
modifyCurLevel fun level => pure {level with lemmaTab := category.getString}
|
|
||||||
|
|
||||||
|
|
||||||
/-! DEPRECATED -/
|
|
||||||
|
|
||||||
elab doc:docComment ? "LemmaDoc" name:ident "as" displayName:str "in" category:str content:str ? :
|
|
||||||
command => do
|
|
||||||
logWarning "Deprecated. Has been renamed to `TheoremDoc`"
|
|
||||||
let doc ← parseDocCommentLegacy doc content
|
|
||||||
modifyEnv (inventoryTemplateExt.addEntry · {
|
|
||||||
type := .Lemma
|
|
||||||
name := name.getId
|
|
||||||
category := category.getString
|
|
||||||
displayName := displayName.getString
|
|
||||||
content := doc })
|
|
||||||
|
|
||||||
elab "NewLemma" args:ident* : command => do
|
|
||||||
logWarning "Deprecated. Has been renamed to `NewTheorem`"
|
|
||||||
checkCommandNotDuplicated ((←getCurLevel).lemmas.new) "NewLemma"
|
|
||||||
for name in ↑args do
|
|
||||||
try let _decl ← getConstInfo name.getId catch
|
|
||||||
| _ => logErrorAt name m!"unknown identifier '{name}'."
|
|
||||||
checkInventoryDoc .Lemma name -- TODO: Add (template := "[mathlib]")
|
|
||||||
modifyCurLevel fun level => pure {level with
|
|
||||||
lemmas := {level.lemmas with new := args.map (·.getId)}}
|
|
||||||
|
|
||||||
elab "DisabledLemma" args:ident* : command => do
|
|
||||||
logWarning "Deprecated. Has been renamed to `DisabledTheorem`"
|
|
||||||
checkCommandNotDuplicated ((←getCurLevel).lemmas.disabled) "DisabledLemma"
|
|
||||||
for name in ↑args do checkInventoryDoc .Lemma name
|
|
||||||
modifyCurLevel fun level => pure {level with
|
|
||||||
lemmas := {level.lemmas with disabled := args.map (·.getId)}}
|
|
||||||
|
|
||||||
elab "OnlyLemma" args:ident* : command => do
|
|
||||||
logWarning "Deprecated. Has been renamed to `OnlyTheorem`"
|
|
||||||
checkCommandNotDuplicated ((←getCurLevel).lemmas.only) "OnlyLemma"
|
|
||||||
for name in ↑args do checkInventoryDoc .Lemma name
|
|
||||||
modifyCurLevel fun level => pure {level with
|
|
||||||
lemmas := {level.lemmas with only := args.map (·.getId)}}
|
|
||||||
|
|
||||||
elab "LemmaTab" category:str : command => do
|
|
||||||
logWarning "Deprecated. Has been renamed to `TheoremTab`"
|
|
||||||
modifyCurLevel fun level => pure {level with lemmaTab := category.getString}
|
modifyCurLevel fun level => pure {level with lemmaTab := category.getString}
|
||||||
|
|
||||||
/-! # Exercise Statement -/
|
/-! # Exercise Statement -/
|
||||||
|
|
||||||
|
/-- A `attr := ...` option for `Statement`. Add attributes to the defined theorem. -/
|
||||||
|
syntax statementAttr := "(" &"attr" ":=" Parser.Term.attrInstance,* ")"
|
||||||
|
-- TODO
|
||||||
|
|
||||||
|
-- TODO: Reuse the following code for checking available tactics in user code:
|
||||||
|
structure UsedInventory where
|
||||||
|
(tactics : HashSet Name := {})
|
||||||
|
(definitions : HashSet Name := {})
|
||||||
|
(lemmas : HashSet Name := {})
|
||||||
|
|
||||||
|
partial def collectUsedInventory (stx : Syntax) (acc : UsedInventory := {}) : CommandElabM UsedInventory := do
|
||||||
|
match stx with
|
||||||
|
| .missing => return acc
|
||||||
|
| .node _info kind args =>
|
||||||
|
if kind == `GameServer.Tactic.Hint || kind == `GameServer.Tactic.Branch then return acc
|
||||||
|
return ← args.foldlM (fun acc arg => collectUsedInventory arg acc) acc
|
||||||
|
| .atom _info val =>
|
||||||
|
-- ignore syntax elements that do not start with a letter
|
||||||
|
-- and ignore some standard keywords
|
||||||
|
let allowed := ["with", "fun", "at", "only", "by"]
|
||||||
|
if 0 < val.length ∧ val.data[0]!.isAlpha ∧ not (allowed.contains val) then
|
||||||
|
let val := val.dropRightWhile (fun c => c == '!' || c == '?') -- treat `simp?` and `simp!` like `simp`
|
||||||
|
return {acc with tactics := acc.tactics.insert val}
|
||||||
|
else
|
||||||
|
return acc
|
||||||
|
| .ident _info _rawVal val _preresolved =>
|
||||||
|
let ns ←
|
||||||
|
try resolveGlobalConst (mkIdent val)
|
||||||
|
catch | _ => pure [] -- catch "unknown constant" error
|
||||||
|
return ← ns.foldlM (fun acc n => do
|
||||||
|
if let some (.thmInfo ..) := (← getEnv).find? n then
|
||||||
|
return {acc with lemmas := acc.lemmas.insertMany ns}
|
||||||
|
else
|
||||||
|
return {acc with definitions := acc.definitions.insertMany ns}
|
||||||
|
) acc
|
||||||
|
|
||||||
|
-- #check expandOptDocComment?
|
||||||
|
|
||||||
/-- Define the statement of the current level. -/
|
/-- Define the statement of the current level. -/
|
||||||
elab doc:docComment ? attrs:Parser.Term.attributes ?
|
elab doc:docComment ? attrs:Parser.Term.attributes ?
|
||||||
"Statement" statementName:ident ? sig:declSig val:declVal : command => do
|
"Statement" statementName:ident ? sig:declSig val:declVal : command => do
|
||||||
let lvlIdx ← getCurLevelIdx
|
let lvlIdx ← getCurLevelIdx
|
||||||
|
|
||||||
let docContent ← parseDocComment doc
|
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]}"
|
||||||
|
|
||||||
-- Save the messages before evaluation of the proof.
|
-- Save the messages before evaluation of the proof.
|
||||||
let initMsgs ← modifyGet fun st => (st.messages, { st with messages := {} })
|
let initMsgs ← modifyGet fun st => (st.messages, { st with messages := {} })
|
||||||
@@ -440,6 +553,23 @@ elab doc:docComment ? attrs:Parser.Term.attributes ?
|
|||||||
|
|
||||||
/-! # Hints -/
|
/-! # Hints -/
|
||||||
|
|
||||||
|
syntax hintArg := atomic(" (" (&"strict" <|> &"hidden") " := " withoutPosition(term) ")")
|
||||||
|
|
||||||
|
/-- Remove any spaces at the beginning of a new line -/
|
||||||
|
partial def removeIndentation (s : String) : String :=
|
||||||
|
let rec loop (i : String.Pos) (acc : String) (removeSpaces := false) : String :=
|
||||||
|
let c := s.get i
|
||||||
|
let i := s.next i
|
||||||
|
if s.atEnd i then
|
||||||
|
acc.push c
|
||||||
|
else if removeSpaces && c == ' ' then
|
||||||
|
loop i acc (removeSpaces := true)
|
||||||
|
else if c == '\n' then
|
||||||
|
loop i (acc.push c) (removeSpaces := true)
|
||||||
|
else
|
||||||
|
loop i (acc.push c)
|
||||||
|
loop ⟨0⟩ ""
|
||||||
|
|
||||||
/-- A tactic that can be used inside `Statement`s to indicate in which proof states players should
|
/-- A tactic that can be used inside `Statement`s to indicate in which proof states players should
|
||||||
see hints. The tactic does not affect the goal state.
|
see hints. The tactic does not affect the goal state.
|
||||||
-/
|
-/
|
||||||
@@ -553,6 +683,26 @@ elab "Template" tacs:tacticSeq : tactic => do
|
|||||||
return {level with template := s!"{template}"}
|
return {level with template := s!"{template}"}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
open IO.FS System FilePath in
|
||||||
|
/-- Copies the folder `images/` to `.lake/gamedata/images/` -/
|
||||||
|
def copyImages : IO Unit := do
|
||||||
|
let target : FilePath := ".lake" / "gamedata"
|
||||||
|
if ← FilePath.pathExists "images" then
|
||||||
|
for file in ← walkDir "images" do
|
||||||
|
let outFile := target.join file
|
||||||
|
-- create the directories
|
||||||
|
if ← file.isDir then
|
||||||
|
createDirAll outFile
|
||||||
|
else
|
||||||
|
if let some parent := outFile.parent then
|
||||||
|
createDirAll parent
|
||||||
|
-- copy file
|
||||||
|
let content ← readBinFile file
|
||||||
|
writeBinFile outFile content
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- TODO: Notes for testing if a declaration has the simp attribute
|
-- TODO: Notes for testing if a declaration has the simp attribute
|
||||||
|
|
||||||
-- -- Test: From zulip
|
-- -- Test: From zulip
|
||||||
@@ -571,6 +721,139 @@ elab "Template" tacs:tacticSeq : tactic => do
|
|||||||
|
|
||||||
/-! # Make Game -/
|
/-! # Make Game -/
|
||||||
|
|
||||||
|
#eval IO.FS.createDirAll ".lake/gamedata/"
|
||||||
|
|
||||||
|
-- TODO: register all of this as ToJson instance?
|
||||||
|
def saveGameData (allItemsByType : HashMap InventoryType (HashSet Name)) : CommandElabM Unit := do
|
||||||
|
let game ← getCurGame
|
||||||
|
let env ← getEnv
|
||||||
|
let path : System.FilePath := s!"{← IO.currentDir}" / ".lake" / "gamedata"
|
||||||
|
|
||||||
|
if ← path.isDir then
|
||||||
|
IO.FS.removeDirAll path
|
||||||
|
IO.FS.createDirAll path
|
||||||
|
|
||||||
|
-- copy the images folder
|
||||||
|
copyImages
|
||||||
|
|
||||||
|
for (worldId, world) in game.worlds.nodes.toArray do
|
||||||
|
for (levelId, level) in world.levels.toArray do
|
||||||
|
IO.FS.writeFile (path / s!"level__{worldId}__{levelId}.json") (toString (toJson (level.toInfo env)))
|
||||||
|
|
||||||
|
IO.FS.writeFile (path / s!"game.json") (toString (getGameJson game))
|
||||||
|
|
||||||
|
for inventoryType in [InventoryType.Lemma, .Tactic, .Definition] do
|
||||||
|
for name in allItemsByType.findD inventoryType {} do
|
||||||
|
let some item ← getInventoryItem? name inventoryType
|
||||||
|
| throwError "Expected item to exist: {name}"
|
||||||
|
IO.FS.writeFile (path / s!"doc__{inventoryType}__{name}.json") (toString (toJson item))
|
||||||
|
|
||||||
|
let getTiles (type : InventoryType) : CommandElabM (Array InventoryTile) := do
|
||||||
|
(allItemsByType.findD type {}).toArray.mapM (fun name => do
|
||||||
|
let some item ← getInventoryItem? name type
|
||||||
|
| throwError "Expected item to exist: {name}"
|
||||||
|
return item.toTile)
|
||||||
|
let inventory : InventoryOverview := {
|
||||||
|
lemmas := ← getTiles .Lemma
|
||||||
|
tactics := ← getTiles .Tactic
|
||||||
|
definitions := ← getTiles .Definition
|
||||||
|
lemmaTab := none
|
||||||
|
}
|
||||||
|
IO.FS.writeFile (path / s!"inventory.json") (toString (toJson inventory))
|
||||||
|
|
||||||
|
|
||||||
|
def GameLevel.getInventory (level : GameLevel) : InventoryType → InventoryInfo
|
||||||
|
| .Tactic => level.tactics
|
||||||
|
| .Definition => level.definitions
|
||||||
|
| .Lemma => level.lemmas
|
||||||
|
|
||||||
|
def GameLevel.setComputedInventory (level : GameLevel) :
|
||||||
|
InventoryType → Array InventoryTile → GameLevel
|
||||||
|
| .Tactic, v => {level with tactics := {level.tactics with tiles := v}}
|
||||||
|
| .Definition, v => {level with definitions := {level.definitions with tiles := v}}
|
||||||
|
| .Lemma, v => {level with lemmas := {level.lemmas with tiles := v}}
|
||||||
|
|
||||||
|
partial def removeTransitiveAux (id : Name) (arrows : HashMap Name (HashSet Name))
|
||||||
|
(newArrows : HashMap Name (HashSet Name)) (decendants : HashMap Name (HashSet Name)) :
|
||||||
|
HashMap Name (HashSet Name) × HashMap Name (HashSet Name) := Id.run do
|
||||||
|
match (newArrows.find? id, decendants.find? id) with
|
||||||
|
| (some _, some _) => return (newArrows, decendants)
|
||||||
|
| _ =>
|
||||||
|
let mut newArr := newArrows
|
||||||
|
let mut desc := decendants
|
||||||
|
desc := desc.insert id {} -- mark as worked in case of loops
|
||||||
|
newArr := newArr.insert id {} -- mark as worked in case of loops
|
||||||
|
let children := arrows.findD id {}
|
||||||
|
let mut trimmedChildren := children
|
||||||
|
let mut theseDescs := children
|
||||||
|
for child in children do
|
||||||
|
(newArr, desc) := removeTransitiveAux child arrows newArr desc
|
||||||
|
let childDescs := desc.findD child {}
|
||||||
|
theseDescs := theseDescs.insertMany childDescs
|
||||||
|
for d in childDescs do
|
||||||
|
trimmedChildren := trimmedChildren.erase d
|
||||||
|
desc := desc.insert id theseDescs
|
||||||
|
newArr := newArr.insert id trimmedChildren
|
||||||
|
return (newArr, desc)
|
||||||
|
|
||||||
|
def removeTransitive (arrows : HashMap Name (HashSet Name)) : CommandElabM (HashMap Name (HashSet Name)) := do
|
||||||
|
let mut newArr := {}
|
||||||
|
let mut desc := {}
|
||||||
|
for id in arrows.toArray.map Prod.fst do
|
||||||
|
(newArr, desc) := removeTransitiveAux id arrows newArr desc
|
||||||
|
if (desc.findD id {}).contains id then
|
||||||
|
logError <| m!"Loop at {id}. " ++
|
||||||
|
m!"This should not happen and probably means that `findLoops` has a bug."
|
||||||
|
-- DEBUG:
|
||||||
|
-- for ⟨x, hx⟩ in desc.toList do
|
||||||
|
-- m := m ++ m!"{x}: {hx.toList}\n"
|
||||||
|
-- logError m
|
||||||
|
|
||||||
|
return newArr
|
||||||
|
|
||||||
|
/-- The recursive part of `findLoops`. Finds loops that appear as successors of `node`.
|
||||||
|
|
||||||
|
For performance reason it returns a HashSet of visited
|
||||||
|
nodes as well. This is filled with all nodes ever looked at as they cannot be
|
||||||
|
part of a loop anymore. -/
|
||||||
|
partial def findLoopsAux (arrows : HashMap Name (HashSet Name)) (node : Name)
|
||||||
|
(path : Array Name := #[]) (visited : HashSet Name := {}) :
|
||||||
|
Array Name × HashSet Name := Id.run do
|
||||||
|
let mut visited := visited
|
||||||
|
match path.getIdx? node with
|
||||||
|
| some i =>
|
||||||
|
-- Found a loop: `node` is already the iᵗʰ element of the path
|
||||||
|
return (path.extract i path.size, visited.insert node)
|
||||||
|
| none =>
|
||||||
|
for successor in arrows.findD node {} do
|
||||||
|
-- If we already visited the successor, it cannot be part of a loop anymore
|
||||||
|
if visited.contains successor then
|
||||||
|
continue
|
||||||
|
-- Find any loop involving `successor`
|
||||||
|
let (loop, _) := findLoopsAux arrows successor (path.push node) visited
|
||||||
|
visited := visited.insert successor
|
||||||
|
-- No loop found in the dependants of `successor`
|
||||||
|
if loop.isEmpty then
|
||||||
|
continue
|
||||||
|
-- Found a loop, return it
|
||||||
|
return (loop, visited)
|
||||||
|
return (#[], visited.insert node)
|
||||||
|
|
||||||
|
/-- Find a loop in the graph and return it. Returns `[]` if there are no loops. -/
|
||||||
|
partial def findLoops (arrows : HashMap Name (HashSet Name)) : List Name := Id.run do
|
||||||
|
let mut visited : HashSet Name := {}
|
||||||
|
for node in arrows.toArray.map (·.1) do
|
||||||
|
-- Skip a node if it was already visited
|
||||||
|
if visited.contains node then
|
||||||
|
continue
|
||||||
|
-- `findLoopsAux` returns a loop or `[]` together with a set of nodes it visited on its
|
||||||
|
-- search starting from `node`
|
||||||
|
let (loop, moreVisited) := (findLoopsAux arrows node (visited := visited))
|
||||||
|
visited := moreVisited
|
||||||
|
if !loop.isEmpty then
|
||||||
|
return loop.toList
|
||||||
|
return []
|
||||||
|
|
||||||
/-- The worlds of a game are joint by dependencies. These are
|
/-- The worlds of a game are joint by dependencies. These are
|
||||||
automatically computed but can also be defined with the syntax
|
automatically computed but can also be defined with the syntax
|
||||||
`Dependency World₁ → World₂ → World₃`. -/
|
`Dependency World₁ → World₂ → World₃`. -/
|
||||||
@@ -837,7 +1120,6 @@ elab "MakeGame" : command => do
|
|||||||
name := item
|
name := item
|
||||||
displayName := data.displayName
|
displayName := data.displayName
|
||||||
category := data.category
|
category := data.category
|
||||||
altTitle := data.statement
|
|
||||||
hidden := hiddenItems.contains item })
|
hidden := hiddenItems.contains item })
|
||||||
|
|
||||||
|
|
||||||
@@ -857,7 +1139,6 @@ elab "MakeGame" : command => do
|
|||||||
displayName := data.displayName
|
displayName := data.displayName
|
||||||
category := data.category
|
category := data.category
|
||||||
locked := false
|
locked := false
|
||||||
altTitle := data.statement
|
|
||||||
hidden := hiddenItems.contains item }
|
hidden := hiddenItems.contains item }
|
||||||
itemsInWorld := itemsInWorld.insert worldId items
|
itemsInWorld := itemsInWorld.insert worldId items
|
||||||
|
|
||||||
@@ -877,8 +1158,7 @@ elab "MakeGame" : command => do
|
|||||||
displayName := data.displayName
|
displayName := data.displayName
|
||||||
category := data.category
|
category := data.category
|
||||||
locked := false
|
locked := false
|
||||||
altTitle := data.statement
|
hidden := levelInfo.hidden.contains item }
|
||||||
hidden := hiddenItems.contains item }
|
|
||||||
|
|
||||||
-- add the exercise statement from the previous level
|
-- add the exercise statement from the previous level
|
||||||
-- if it was named
|
-- if it was named
|
||||||
@@ -891,7 +1171,6 @@ elab "MakeGame" : command => do
|
|||||||
name := name
|
name := name
|
||||||
displayName := data.displayName
|
displayName := data.displayName
|
||||||
category := data.category
|
category := data.category
|
||||||
altTitle := data.statement
|
|
||||||
locked := false }
|
locked := false }
|
||||||
|
|
||||||
-- add marks for `disabled` and `new` lemmas here, so that they only apply to
|
-- add marks for `disabled` and `new` lemmas here, so that they only apply to
|
||||||
@@ -911,16 +1190,18 @@ elab "MakeGame" : command => do
|
|||||||
return level.setComputedInventory inventoryType itemsArray
|
return level.setComputedInventory inventoryType itemsArray
|
||||||
allItemsByType := allItemsByType.insert inventoryType allItems
|
allItemsByType := allItemsByType.insert inventoryType allItems
|
||||||
|
|
||||||
let getTiles (type : InventoryType) : CommandElabM (Array InventoryTile) := do
|
saveGameData allItemsByType
|
||||||
(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).map (fun tile => {tile with hidden := hiddenItems.contains tile.name})
|
|
||||||
tactics := (← getTiles .Tactic).map (fun tile => {tile with hidden := hiddenItems.contains tile.name})
|
|
||||||
definitions := (← getTiles .Definition).map (fun tile => {tile with hidden := hiddenItems.contains tile.name})
|
|
||||||
lemmaTab := none
|
|
||||||
}
|
|
||||||
|
|
||||||
saveGameData allItemsByType inventory
|
/-! # 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
|
||||||
|
|||||||
@@ -106,8 +106,6 @@ structure InventoryTile where
|
|||||||
new := false
|
new := false
|
||||||
/-- hide the item in the inventory display -/
|
/-- hide the item in the inventory display -/
|
||||||
hidden := false
|
hidden := false
|
||||||
/-- hover text -/
|
|
||||||
altTitle : String := default
|
|
||||||
deriving ToJson, FromJson, Repr, Inhabited
|
deriving ToJson, FromJson, Repr, Inhabited
|
||||||
|
|
||||||
def InventoryItem.toTile (item : InventoryItem) : InventoryTile := {
|
def InventoryItem.toTile (item : InventoryItem) : InventoryTile := {
|
||||||
@@ -150,12 +148,6 @@ structure InventoryOverview where
|
|||||||
lemmaTab : Option String
|
lemmaTab : Option String
|
||||||
deriving ToJson, FromJson
|
deriving ToJson, FromJson
|
||||||
|
|
||||||
-- TODO: Reuse the following code for checking available tactics in user code:
|
|
||||||
structure UsedInventory where
|
|
||||||
(tactics : HashSet Name := {})
|
|
||||||
(definitions : HashSet Name := {})
|
|
||||||
(lemmas : HashSet Name := {})
|
|
||||||
|
|
||||||
/-! ## Environment extensions for game specification -/
|
/-! ## Environment extensions for game specification -/
|
||||||
|
|
||||||
/-- Register a (non-persistent) environment extension to hold the current level -/
|
/-- Register a (non-persistent) environment extension to hold the current level -/
|
||||||
@@ -277,6 +269,13 @@ structure GameLevel where
|
|||||||
image : String := default
|
image : String := default
|
||||||
deriving Inhabited, Repr
|
deriving Inhabited, Repr
|
||||||
|
|
||||||
|
structure WorldOverview where
|
||||||
|
world: Name
|
||||||
|
tactics: Array InventoryTile := default
|
||||||
|
definitions: Array InventoryTile := default
|
||||||
|
lemmas: Array InventoryTile := default
|
||||||
|
deriving FromJson, ToJson, Inhabited, Repr
|
||||||
|
|
||||||
/-- Json-encodable version of `GameLevel`
|
/-- Json-encodable version of `GameLevel`
|
||||||
Fields:
|
Fields:
|
||||||
- description: Lemma in mathematical language.
|
- description: Lemma in mathematical language.
|
||||||
@@ -293,7 +292,6 @@ structure LevelInfo where
|
|||||||
descrText : Option String := none
|
descrText : Option String := none
|
||||||
descrFormat : String := ""
|
descrFormat : String := ""
|
||||||
lemmaTab : Option String
|
lemmaTab : Option String
|
||||||
module : Name
|
|
||||||
displayName : Option String
|
displayName : Option String
|
||||||
statementName : Option String
|
statementName : Option String
|
||||||
template : Option String
|
template : Option String
|
||||||
@@ -318,7 +316,6 @@ def GameLevel.toInfo (lvl : GameLevel) (env : Environment) : LevelInfo :=
|
|||||||
| some tile => tile.category
|
| some tile => tile.category
|
||||||
| none => none
|
| none => none
|
||||||
statementName := lvl.statementName.toString
|
statementName := lvl.statementName.toString
|
||||||
module := lvl.module
|
|
||||||
displayName := match lvl.statementName with
|
displayName := match lvl.statementName with
|
||||||
| .anonymous => none
|
| .anonymous => none
|
||||||
| name => match (inventoryExt.getState env).find?
|
| name => match (inventoryExt.getState env).find?
|
||||||
@@ -383,7 +380,7 @@ structure GameTile where
|
|||||||
|
|
||||||
TODO: What's the format? -/
|
TODO: What's the format? -/
|
||||||
image: String := default
|
image: String := default
|
||||||
deriving Inhabited, ToJson, FromJson
|
deriving Inhabited, ToJson
|
||||||
|
|
||||||
structure Game where
|
structure Game where
|
||||||
/-- Internal name of the game. -/
|
/-- Internal name of the game. -/
|
||||||
@@ -403,7 +400,7 @@ structure Game where
|
|||||||
tile : GameTile := default
|
tile : GameTile := default
|
||||||
/-- The path to the background image of the world. -/
|
/-- The path to the background image of the world. -/
|
||||||
image : String := default
|
image : String := default
|
||||||
deriving Inhabited, ToJson, FromJson
|
deriving Inhabited, ToJson
|
||||||
|
|
||||||
def getGameJson (game : «Game») : Json := Id.run do
|
def getGameJson (game : «Game») : Json := Id.run do
|
||||||
let gameJson : Json := toJson game
|
let gameJson : Json := toJson game
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
/- This file is adapted from `Lean/Server/FileWorker.lean`. -/
|
/- This file is mostly copied from `Lean/Server/FileWorker.lean`. -/
|
||||||
import Lean.Server.FileWorker
|
import Lean.Server.FileWorker
|
||||||
import GameServer.Game
|
import GameServer.Game
|
||||||
import GameServer.ImportModules
|
import GameServer.ImportModules
|
||||||
import GameServer.SaveData
|
|
||||||
|
|
||||||
namespace MyModule
|
namespace MyModule
|
||||||
open Lean
|
open Lean
|
||||||
@@ -18,7 +17,7 @@ private def mkEOI (pos : String.Pos) : Syntax :=
|
|||||||
mkNode ``Command.eoi #[atom]
|
mkNode ``Command.eoi #[atom]
|
||||||
|
|
||||||
partial def parseTactic (inputCtx : InputContext) (pmctx : ParserModuleContext)
|
partial def parseTactic (inputCtx : InputContext) (pmctx : ParserModuleContext)
|
||||||
(mps : ModuleParserState) (messages : MessageLog) :
|
(mps : ModuleParserState) (messages : MessageLog) (couldBeEndSnap : Bool) :
|
||||||
Syntax × ModuleParserState × MessageLog × String.Pos := Id.run do
|
Syntax × ModuleParserState × MessageLog × String.Pos := Id.run do
|
||||||
let mut pos := mps.pos
|
let mut pos := mps.pos
|
||||||
let mut recovering := mps.recovering
|
let mut recovering := mps.recovering
|
||||||
@@ -57,20 +56,6 @@ open IO
|
|||||||
open Snapshots
|
open Snapshots
|
||||||
open JsonRpc
|
open JsonRpc
|
||||||
|
|
||||||
structure GameWorkerState :=
|
|
||||||
inventory : Array String
|
|
||||||
/--
|
|
||||||
Check for tactics/theorems that are not unlocked.
|
|
||||||
0: no check
|
|
||||||
1: give warnings
|
|
||||||
2: give errors
|
|
||||||
-/
|
|
||||||
difficulty : Nat
|
|
||||||
levelInfo : LevelInfo
|
|
||||||
deriving ToJson, FromJson
|
|
||||||
|
|
||||||
abbrev GameWorkerM := StateT GameWorkerState Server.FileWorker.WorkerM
|
|
||||||
|
|
||||||
section Elab
|
section Elab
|
||||||
|
|
||||||
def addErrorMessage (info : SourceInfo) (inputCtx : Parser.InputContext) (s : MessageData) :
|
def addErrorMessage (info : SourceInfo) (inputCtx : Parser.InputContext) (s : MessageData) :
|
||||||
@@ -88,30 +73,29 @@ def addErrorMessage (info : SourceInfo) (inputCtx : Parser.InputContext) (s : Me
|
|||||||
/-- Find all tactics in syntax object that are forbidden according to a
|
/-- Find all tactics in syntax object that are forbidden according to a
|
||||||
set `allowed` of allowed tactics. -/
|
set `allowed` of allowed tactics. -/
|
||||||
partial def findForbiddenTactics (inputCtx : Parser.InputContext)
|
partial def findForbiddenTactics (inputCtx : Parser.InputContext)
|
||||||
(gameWorkerState : GameWorkerState) (stx : Syntax) :
|
(levelParams : Game.DidOpenLevelParams) (stx : Syntax) :
|
||||||
Elab.Command.CommandElabM Unit := do
|
Elab.Command.CommandElabM Unit := do
|
||||||
let levelInfo := gameWorkerState.levelInfo
|
|
||||||
match stx with
|
match stx with
|
||||||
| .missing => return ()
|
| .missing => return ()
|
||||||
| .node _info _kind args =>
|
| .node _info _kind args =>
|
||||||
for arg in args do
|
for arg in args do
|
||||||
findForbiddenTactics inputCtx gameWorkerState arg
|
findForbiddenTactics inputCtx levelParams arg
|
||||||
| .atom info val =>
|
| .atom info val =>
|
||||||
-- ignore syntax elements that do not start with a letter
|
-- ignore syntax elements that do not start with a letter
|
||||||
-- and ignore "with" keyword
|
-- and ignore "with" keyword
|
||||||
let allowed := ["with", "fun", "at", "only", "by", "to"]
|
let allowed := ["with", "fun", "at", "only", "by", "to"]
|
||||||
if 0 < val.length ∧ val.data[0]!.isAlpha ∧ not (allowed.contains val) then
|
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`
|
let val := val.dropRightWhile (fun c => c == '!' || c == '?') -- treat `simp?` and `simp!` like `simp`
|
||||||
match levelInfo.tactics.find? (·.name.toString == val) with
|
match levelParams.tactics.find? (·.name.toString == val) with
|
||||||
| none =>
|
| none =>
|
||||||
-- Note: This case means that the tactic will never be introduced in the game.
|
-- Note: This case means that the tactic will never be introduced in the game.
|
||||||
match gameWorkerState.inventory.find? (· == val) with
|
match levelParams.inventory.find? (· == val) with
|
||||||
| none =>
|
| none =>
|
||||||
addWarningMessage info s!"You have not unlocked the tactic '{val}' yet!"
|
addWarningMessage info s!"You have not unlocked the tactic '{val}' yet!"
|
||||||
| some _ => pure () -- tactic is in the inventory, allow it.
|
| some _ => pure () -- tactic is in the inventory, allow it.
|
||||||
| some tac =>
|
| some tac =>
|
||||||
if tac.locked then
|
if tac.locked then
|
||||||
match gameWorkerState.inventory.find? (· == val) with
|
match levelParams.inventory.find? (· == val) with
|
||||||
| none =>
|
| none =>
|
||||||
addWarningMessage info s!"You have not unlocked the tactic '{val}' yet!"
|
addWarningMessage info s!"You have not unlocked the tactic '{val}' yet!"
|
||||||
| some _ => pure () -- tactic is in the inventory, allow it.
|
| some _ => pure () -- tactic is in the inventory, allow it.
|
||||||
@@ -125,10 +109,10 @@ partial def findForbiddenTactics (inputCtx : Parser.InputContext)
|
|||||||
let some (.thmInfo ..) := (← getEnv).find? n
|
let some (.thmInfo ..) := (← getEnv).find? n
|
||||||
| return () -- not a theorem -> ignore
|
| return () -- not a theorem -> ignore
|
||||||
-- Forbid the theorem we are proving currently
|
-- Forbid the theorem we are proving currently
|
||||||
if some n = levelInfo.statementName then
|
if n = levelParams.statementName then
|
||||||
addErrorMessage info inputCtx s!"Structural recursion: you can't use '{n}' to proof itself!"
|
addErrorMessage info inputCtx s!"Structural recursion: you can't use '{n}' to proof itself!"
|
||||||
|
|
||||||
let lemmasAndDefs := levelInfo.lemmas ++ levelInfo.definitions
|
let lemmasAndDefs := levelParams.lemmas ++ levelParams.definitions
|
||||||
match lemmasAndDefs.find? (fun l => l.name == n) with
|
match lemmasAndDefs.find? (fun l => l.name == n) with
|
||||||
| none => addWarningMessage info s!"You have not unlocked the lemma/definition '{n}' yet!"
|
| none => addWarningMessage info s!"You have not unlocked the lemma/definition '{n}' yet!"
|
||||||
| some lem =>
|
| some lem =>
|
||||||
@@ -137,7 +121,7 @@ partial def findForbiddenTactics (inputCtx : Parser.InputContext)
|
|||||||
else if lem.disabled then
|
else if lem.disabled then
|
||||||
addWarningMessage info s!"The lemma/definition '{n}' is disabled in this level!"
|
addWarningMessage info s!"The lemma/definition '{n}' is disabled in this level!"
|
||||||
where addWarningMessage (info : SourceInfo) (s : MessageData) :=
|
where addWarningMessage (info : SourceInfo) (s : MessageData) :=
|
||||||
let difficulty := gameWorkerState.difficulty
|
let difficulty := levelParams.difficulty
|
||||||
if difficulty > 0 then
|
if difficulty > 0 then
|
||||||
modify fun st => { st with
|
modify fun st => { st with
|
||||||
messages := st.messages.add {
|
messages := st.messages.add {
|
||||||
@@ -153,7 +137,7 @@ where addWarningMessage (info : SourceInfo) (s : MessageData) :=
|
|||||||
|
|
||||||
open Elab Meta Expr in
|
open Elab Meta Expr in
|
||||||
def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets : Bool)
|
def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets : Bool)
|
||||||
(couldBeEndSnap : Bool) (gameWorkerState : GameWorkerState)
|
(couldBeEndSnap : Bool) (levelParams : Game.DidOpenLevelParams)
|
||||||
(initParams : Lsp.InitializeParams) : IO Snapshot := do
|
(initParams : Lsp.InitializeParams) : IO Snapshot := do
|
||||||
-- Recognize end snap
|
-- Recognize end snap
|
||||||
if inputCtx.input.atEnd snap.mpState.pos ∧ couldBeEndSnap then
|
if inputCtx.input.atEnd snap.mpState.pos ∧ couldBeEndSnap then
|
||||||
@@ -184,7 +168,7 @@ def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets
|
|||||||
Elab.Command.catchExceptions
|
Elab.Command.catchExceptions
|
||||||
(getResetInfoTrees *> do
|
(getResetInfoTrees *> do
|
||||||
let some level ← GameServer.getLevelByFileName? initParams inputCtx.fileName
|
let some level ← GameServer.getLevelByFileName? initParams inputCtx.fileName
|
||||||
| panic! s!"Level not found: {inputCtx.fileName} / {GameServer.levelIdFromFileName? initParams inputCtx.fileName}"
|
| throwError "Level not found: {inputCtx.fileName}"
|
||||||
let scope := level.scope
|
let scope := level.scope
|
||||||
|
|
||||||
-- use open namespaces and options as in the level file
|
-- use open namespaces and options as in the level file
|
||||||
@@ -202,12 +186,12 @@ def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets
|
|||||||
currNamespace := scope.currNamespace,
|
currNamespace := scope.currNamespace,
|
||||||
openDecls := scope.openDecls }
|
openDecls := scope.openDecls }
|
||||||
let (tacticStx, cmdParserState, msgLog, endOfWhitespace) :=
|
let (tacticStx, cmdParserState, msgLog, endOfWhitespace) :=
|
||||||
MyModule.parseTactic inputCtx pmctx snap.mpState snap.msgLog
|
MyModule.parseTactic inputCtx pmctx snap.mpState snap.msgLog couldBeEndSnap
|
||||||
modify (fun s => { s with messages := msgLog })
|
modify (fun s => { s with messages := msgLog })
|
||||||
parseResultRef.set (tacticStx, cmdParserState)
|
parseResultRef.set (tacticStx, cmdParserState)
|
||||||
|
|
||||||
-- Check for forbidden tactics
|
-- Check for forbidden tactics
|
||||||
findForbiddenTactics inputCtx gameWorkerState tacticStx
|
findForbiddenTactics inputCtx levelParams tacticStx
|
||||||
|
|
||||||
-- Insert invisible `skip` command to make sure we always display the initial goal
|
-- Insert invisible `skip` command to make sure we always display the initial goal
|
||||||
let skip := Syntax.node (.original default 0 default endOfWhitespace) ``Lean.Parser.Tactic.skip #[]
|
let skip := Syntax.node (.original default 0 default endOfWhitespace) ``Lean.Parser.Tactic.skip #[]
|
||||||
@@ -235,7 +219,6 @@ def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets
|
|||||||
}
|
}
|
||||||
|
|
||||||
let (tacticStx, cmdParserState) ← parseResultRef.get
|
let (tacticStx, cmdParserState) ← parseResultRef.get
|
||||||
if tacticStx.isMissing then throwServerError "Tactic execution went wrong. No stx found."
|
|
||||||
|
|
||||||
let postCmdSnap : Snapshot := {
|
let postCmdSnap : Snapshot := {
|
||||||
beginPos := tacticStx.getPos?.getD 0
|
beginPos := tacticStx.getPos?.getD 0
|
||||||
@@ -287,7 +270,7 @@ where
|
|||||||
|
|
||||||
/-- Elaborates the next command after `parentSnap` and emits diagnostics into `hOut`. -/
|
/-- Elaborates the next command after `parentSnap` and emits diagnostics into `hOut`. -/
|
||||||
private def nextSnap (ctx : WorkerContext) (m : DocumentMeta) (cancelTk : CancelToken)
|
private def nextSnap (ctx : WorkerContext) (m : DocumentMeta) (cancelTk : CancelToken)
|
||||||
(gameWorkerState : GameWorkerState) (initParams : Lsp.InitializeParams)
|
(levelParams : Game.DidOpenLevelParams) (initParams : Lsp.InitializeParams)
|
||||||
: AsyncElabM (Option Snapshot) := do
|
: AsyncElabM (Option Snapshot) := do
|
||||||
cancelTk.check
|
cancelTk.check
|
||||||
let s ← get
|
let s ← get
|
||||||
@@ -305,7 +288,7 @@ where
|
|||||||
-- we can see the current goal even on an empty document
|
-- we can see the current goal even on an empty document
|
||||||
let couldBeEndSnap := s.snaps.size > 1
|
let couldBeEndSnap := s.snaps.size > 1
|
||||||
let snap ← compileProof m.mkInputContext lastSnap ctx.clientHasWidgets couldBeEndSnap
|
let snap ← compileProof m.mkInputContext lastSnap ctx.clientHasWidgets couldBeEndSnap
|
||||||
gameWorkerState initParams
|
levelParams initParams
|
||||||
set { s with snaps := s.snaps.push snap }
|
set { s with snaps := s.snaps.push snap }
|
||||||
-- TODO(MH): check for interrupt with increased precision
|
-- TODO(MH): check for interrupt with increased precision
|
||||||
cancelTk.check
|
cancelTk.check
|
||||||
@@ -327,7 +310,7 @@ where
|
|||||||
|
|
||||||
/-- Elaborates all commands after the last snap (at least the header snap is assumed to exist), emitting the diagnostics into `hOut`. -/
|
/-- Elaborates all commands after the last snap (at least the header snap is assumed to exist), emitting the diagnostics into `hOut`. -/
|
||||||
def unfoldSnaps (m : DocumentMeta) (snaps : Array Snapshot) (cancelTk : CancelToken)
|
def unfoldSnaps (m : DocumentMeta) (snaps : Array Snapshot) (cancelTk : CancelToken)
|
||||||
(startAfterMs : UInt32) (gameWorkerState : GameWorkerState)
|
(startAfterMs : UInt32) (levelParams : Game.DidOpenLevelParams)
|
||||||
: ReaderT WorkerContext IO (AsyncList ElabTaskError Snapshot) := do
|
: ReaderT WorkerContext IO (AsyncList ElabTaskError Snapshot) := do
|
||||||
let ctx ← read
|
let ctx ← read
|
||||||
let some headerSnap := snaps[0]? | panic! "empty snapshots"
|
let some headerSnap := snaps[0]? | panic! "empty snapshots"
|
||||||
@@ -343,15 +326,21 @@ where
|
|||||||
publishIleanInfoUpdate m ctx.hOut snaps
|
publishIleanInfoUpdate m ctx.hOut snaps
|
||||||
return AsyncList.ofList snaps.toList ++ AsyncList.delayed (← EIO.asTask (ε := ElabTaskError) (prio := .dedicated) do
|
return AsyncList.ofList snaps.toList ++ AsyncList.delayed (← EIO.asTask (ε := ElabTaskError) (prio := .dedicated) do
|
||||||
IO.sleep startAfterMs
|
IO.sleep startAfterMs
|
||||||
AsyncList.unfoldAsync (nextSnap ctx m cancelTk gameWorkerState ctx.initParams) { snaps })
|
AsyncList.unfoldAsync (nextSnap ctx m cancelTk levelParams ctx.initParams) { snaps })
|
||||||
|
|
||||||
end Elab
|
end Elab
|
||||||
|
|
||||||
|
structure GameWorkerState :=
|
||||||
|
(levelParams : Game.DidOpenLevelParams)
|
||||||
|
|
||||||
|
abbrev GameWorkerM := StateT GameWorkerState Server.FileWorker.WorkerM
|
||||||
|
|
||||||
section Updates
|
section Updates
|
||||||
|
|
||||||
/-- Given the new document, updates editable doc state. -/
|
/-- Given the new document, updates editable doc state. -/
|
||||||
def updateDocument (newMeta : DocumentMeta) : GameWorkerM Unit := do
|
def updateDocument (newMeta : DocumentMeta) : GameWorkerM Unit := do
|
||||||
let s ← get
|
let s ← get
|
||||||
|
let levelParams := s.levelParams
|
||||||
let ctx ← read
|
let ctx ← read
|
||||||
let oldDoc := (← StateT.lift get).doc
|
let oldDoc := (← StateT.lift get).doc
|
||||||
oldDoc.cancelTk.set
|
oldDoc.cancelTk.set
|
||||||
@@ -393,7 +382,7 @@ section Updates
|
|||||||
validSnaps := validSnaps.dropLast
|
validSnaps := validSnaps.dropLast
|
||||||
-- wait for a bit, giving the initial `cancelTk.check` in `nextCmdSnap` time to trigger
|
-- wait for a bit, giving the initial `cancelTk.check` in `nextCmdSnap` time to trigger
|
||||||
-- before kicking off any expensive elaboration (TODO: make expensive elaboration cancelable)
|
-- before kicking off any expensive elaboration (TODO: make expensive elaboration cancelable)
|
||||||
unfoldSnaps newMeta validSnaps.toArray cancelTk s ctx
|
unfoldSnaps newMeta validSnaps.toArray cancelTk levelParams ctx
|
||||||
(startAfterMs := ctx.initParams.editDelay.toUInt32)
|
(startAfterMs := ctx.initParams.editDelay.toUInt32)
|
||||||
StateT.lift <| modify fun st => { st with
|
StateT.lift <| modify fun st => { st with
|
||||||
doc := { meta := newMeta, cmdSnaps := AsyncList.delayed newSnaps, cancelTk }}
|
doc := { meta := newMeta, cmdSnaps := AsyncList.delayed newSnaps, cancelTk }}
|
||||||
@@ -408,23 +397,24 @@ section Initialization
|
|||||||
fileMap := default
|
fileMap := default
|
||||||
|
|
||||||
def compileHeader (m : DocumentMeta) (hOut : FS.Stream) (opts : Options) (hasWidgets : Bool)
|
def compileHeader (m : DocumentMeta) (hOut : FS.Stream) (opts : Options) (hasWidgets : Bool)
|
||||||
(gameDir : String) (module : Name):
|
(levelParams : Game.DidOpenLevelParams) (initParams : InitializeParams) :
|
||||||
IO (Syntax × Task (Except Error (Snapshot × SearchPath))) := do
|
IO (Syntax × Task (Except Error (Snapshot × SearchPath))) := do
|
||||||
-- Determine search paths of the game project by running `lake env printenv LEAN_PATH`.
|
-- Determine search paths of the game project by running `lake env printenv LEAN_PATH`.
|
||||||
let out ← IO.Process.output
|
let out ← IO.Process.output
|
||||||
{ cwd := gameDir, cmd := "lake", args := #["env","printenv","LEAN_PATH"] }
|
{ cwd := levelParams.gameDir, cmd := "lake", args := #["env","printenv","LEAN_PATH"] }
|
||||||
if out.exitCode != 0 then
|
if out.exitCode != 0 then
|
||||||
throwServerError s!"Error while running Lake: {out.stderr}"
|
throwServerError s!"Error while running Lake: {out.stderr}"
|
||||||
|
|
||||||
-- Make the paths relative to the current directory
|
-- Make the paths relative to the current directory
|
||||||
let paths : List System.FilePath := System.SearchPath.parse out.stdout.trim
|
let paths : List System.FilePath := System.SearchPath.parse out.stdout.trim
|
||||||
let currentDir ← IO.currentDir
|
let currentDir ← IO.currentDir
|
||||||
let paths := paths.map fun p => currentDir / (gameDir : System.FilePath) / p
|
let paths := paths.map fun p => currentDir / (levelParams.gameDir : System.FilePath) / p
|
||||||
|
|
||||||
-- Set the search path
|
-- Set the search path
|
||||||
Lean.searchPathRef.set paths
|
Lean.searchPathRef.set paths
|
||||||
|
|
||||||
let env ← importModules' #[{ module := `Init : Import }, { module := module : Import }]
|
let env ← importModules' #[{ module := `Init : Import }, { module := levelParams.levelModule : Import }]
|
||||||
|
-- return (env, paths)
|
||||||
|
|
||||||
-- use empty header
|
-- use empty header
|
||||||
let (headerStx, headerParserState, msgLog) ← Parser.parseHeader
|
let (headerStx, headerParserState, msgLog) ← Parser.parseHeader
|
||||||
@@ -468,11 +458,10 @@ section Initialization
|
|||||||
return (headerSnap, srcSearchPath)
|
return (headerSnap, srcSearchPath)
|
||||||
|
|
||||||
def initializeWorker (meta : DocumentMeta) (i o e : FS.Stream) (initParams : InitializeParams) (opts : Options)
|
def initializeWorker (meta : DocumentMeta) (i o e : FS.Stream) (initParams : InitializeParams) (opts : Options)
|
||||||
(gameDir : String) (gameWorkerState : GameWorkerState) : IO (WorkerContext × WorkerState) := do
|
(levelParams : Game.DidOpenLevelParams) : IO (WorkerContext × WorkerState) := do
|
||||||
let clientHasWidgets := initParams.initializationOptions?.bind (·.hasWidgets?) |>.getD false
|
let clientHasWidgets := initParams.initializationOptions?.bind (·.hasWidgets?) |>.getD false
|
||||||
|
|
||||||
let (headerStx, headerTask) ← compileHeader meta o opts (hasWidgets := clientHasWidgets)
|
let (headerStx, headerTask) ← compileHeader meta o opts (hasWidgets := clientHasWidgets)
|
||||||
gameDir gameWorkerState.levelInfo.module
|
levelParams initParams
|
||||||
let cancelTk ← CancelToken.new
|
let cancelTk ← CancelToken.new
|
||||||
let ctx :=
|
let ctx :=
|
||||||
{ hIn := i
|
{ hIn := i
|
||||||
@@ -483,14 +472,12 @@ section Initialization
|
|||||||
clientHasWidgets
|
clientHasWidgets
|
||||||
}
|
}
|
||||||
let cmdSnaps ← EIO.mapTask (t := headerTask) (match · with
|
let cmdSnaps ← EIO.mapTask (t := headerTask) (match · with
|
||||||
| Except.ok (s, _) => unfoldSnaps meta #[s] cancelTk gameWorkerState ctx (startAfterMs := 0)
|
| Except.ok (s, _) => unfoldSnaps meta #[s] cancelTk levelParams ctx (startAfterMs := 0)
|
||||||
| Except.error e => throw (e : ElabTaskError))
|
| Except.error e => throw (e : ElabTaskError))
|
||||||
let doc : EditableDocument := { meta, cmdSnaps := AsyncList.delayed cmdSnaps, cancelTk }
|
let doc : EditableDocument := { meta, cmdSnaps := AsyncList.delayed cmdSnaps, cancelTk }
|
||||||
return (ctx,
|
return (ctx,
|
||||||
{ doc := doc
|
{ doc := doc
|
||||||
initHeaderStx := headerStx
|
initHeaderStx := headerStx
|
||||||
currHeaderStx := headerStx
|
|
||||||
importCachingTask? := none
|
|
||||||
pendingRequests := RBMap.empty
|
pendingRequests := RBMap.empty
|
||||||
rpcSessions := RBMap.empty
|
rpcSessions := RBMap.empty
|
||||||
})
|
})
|
||||||
@@ -522,7 +509,6 @@ section MessageHandling
|
|||||||
match method with
|
match method with
|
||||||
| "textDocument/didChange" => handle DidChangeTextDocumentParams (handleDidChange)
|
| "textDocument/didChange" => handle DidChangeTextDocumentParams (handleDidChange)
|
||||||
| "$/cancelRequest" => handle CancelParams (handleCancelRequest ·)
|
| "$/cancelRequest" => handle CancelParams (handleCancelRequest ·)
|
||||||
| "$/setTrace" => pure ()
|
|
||||||
| "$/lean/rpc/release" => handle RpcReleaseParams (handleRpcRelease ·)
|
| "$/lean/rpc/release" => handle RpcReleaseParams (handleRpcRelease ·)
|
||||||
| "$/lean/rpc/keepAlive" => handle RpcKeepAliveParams (handleRpcKeepAlive ·)
|
| "$/lean/rpc/keepAlive" => handle RpcKeepAliveParams (handleRpcKeepAlive ·)
|
||||||
| _ => throwServerError s!"Got unsupported notification method: {method}"
|
| _ => throwServerError s!"Got unsupported notification method: {method}"
|
||||||
@@ -561,32 +547,26 @@ section MainLoop
|
|||||||
let doc := st.doc
|
let doc := st.doc
|
||||||
doc.cancelTk.set
|
doc.cancelTk.set
|
||||||
return ()
|
return ()
|
||||||
| Message.request id "shutdown" none =>
|
| Message.notification "$/game/setInventory" params =>
|
||||||
ctx.hOut.writeLspResponse ⟨id, Json.null⟩
|
let p := (← parseParams Game.SetInventoryParams (toJson params))
|
||||||
|
let s ← get
|
||||||
|
|
||||||
|
set {s with levelParams := {s.levelParams with
|
||||||
|
inventory := p.inventory,
|
||||||
|
difficulty := p.difficulty}}
|
||||||
mainLoop
|
mainLoop
|
||||||
| Message.notification method (some params) =>
|
| Message.notification method (some params) =>
|
||||||
handleNotification method (toJson params)
|
handleNotification method (toJson params)
|
||||||
mainLoop
|
mainLoop
|
||||||
| _ => throwServerError s!"Got invalid JSON-RPC message: {toJson msg}"
|
| _ => throwServerError "Got invalid JSON-RPC message"
|
||||||
end MainLoop
|
end MainLoop
|
||||||
|
|
||||||
def initAndRunWorker (i o e : FS.Stream) (opts : Options) (gameDir : String) : IO UInt32 := do
|
def initAndRunWorker (i o e : FS.Stream) (opts : Options) : IO UInt32 := do
|
||||||
let i ← maybeTee "fwIn.txt" false i
|
let i ← maybeTee "fwIn.txt" false i
|
||||||
let o ← maybeTee "fwOut.txt" true o
|
let o ← maybeTee "fwOut.txt" true o
|
||||||
let initRequest ← i.readLspRequestAs "initialize" Game.InitializeParams
|
let initParams ← i.readLspRequestAs "initialize" InitializeParams
|
||||||
o.writeLspResponse {
|
|
||||||
id := initRequest.id
|
|
||||||
result := {
|
|
||||||
capabilities := Watchdog.mkLeanServerCapabilities
|
|
||||||
serverInfo? := some {
|
|
||||||
name := "Lean 4 Game Server"
|
|
||||||
version? := "0.1.1"
|
|
||||||
}
|
|
||||||
: InitializeResult
|
|
||||||
}
|
|
||||||
}
|
|
||||||
discard $ i.readLspNotificationAs "initialized" InitializedParams
|
|
||||||
let ⟨_, param⟩ ← i.readLspNotificationAs "textDocument/didOpen" DidOpenTextDocumentParams
|
let ⟨_, param⟩ ← i.readLspNotificationAs "textDocument/didOpen" DidOpenTextDocumentParams
|
||||||
|
let ⟨_, levelParams⟩ ← i.readLspNotificationAs "$/game/didOpenLevel" Game.DidOpenLevelParams
|
||||||
|
|
||||||
let doc := param.textDocument
|
let doc := param.textDocument
|
||||||
/- NOTE(WN): `toFileMap` marks line beginnings as immediately following
|
/- NOTE(WN): `toFileMap` marks line beginnings as immediately following
|
||||||
@@ -598,24 +578,9 @@ def initAndRunWorker (i o e : FS.Stream) (opts : Options) (gameDir : String) : I
|
|||||||
let e := e.withPrefix s!"[{param.textDocument.uri}] "
|
let e := e.withPrefix s!"[{param.textDocument.uri}] "
|
||||||
let _ ← IO.setStderr e
|
let _ ← IO.setStderr e
|
||||||
try
|
try
|
||||||
let game ← loadGameData gameDir
|
let (ctx, st) ← initializeWorker meta i o e initParams.param opts levelParams
|
||||||
-- TODO: We misuse the `rootUri` field to the gameName
|
|
||||||
let rootUri? : Option String := some (toString game.name)
|
|
||||||
let initParams := {initRequest.param.toLeanInternal with rootUri?}
|
|
||||||
let some (levelId : LevelId) := GameServer.levelIdFromFileName?
|
|
||||||
initParams meta.mkInputContext.fileName
|
|
||||||
| throwServerError s!"Could not determine level ID: {meta.mkInputContext.fileName}"
|
|
||||||
let levelInfo ← loadLevelData gameDir levelId.world levelId.level
|
|
||||||
let some initializationOptions := initRequest.param.initializationOptions?
|
|
||||||
| throwServerError "no initialization options found"
|
|
||||||
let gameWorkerState : GameWorkerState:= {
|
|
||||||
inventory := initializationOptions.inventory
|
|
||||||
difficulty := initializationOptions.difficulty
|
|
||||||
levelInfo
|
|
||||||
}
|
|
||||||
let (ctx, st) ← initializeWorker meta i o e initParams opts gameDir gameWorkerState
|
|
||||||
let _ ← StateRefT'.run (s := st) <| ReaderT.run (r := ctx) <|
|
let _ ← StateRefT'.run (s := st) <| ReaderT.run (r := ctx) <|
|
||||||
StateT.run (s := gameWorkerState) <| (mainLoop)
|
StateT.run (s := {levelParams := levelParams}) <| (mainLoop)
|
||||||
return (0 : UInt32)
|
return (0 : UInt32)
|
||||||
catch e =>
|
catch e =>
|
||||||
IO.eprintln e
|
IO.eprintln e
|
||||||
@@ -625,13 +590,12 @@ def initAndRunWorker (i o e : FS.Stream) (opts : Options) (gameDir : String) : I
|
|||||||
message := e.toString }] o
|
message := e.toString }] o
|
||||||
return (1 : UInt32)
|
return (1 : UInt32)
|
||||||
|
|
||||||
def workerMain (opts : Options) (args : List String): IO UInt32 := do
|
def workerMain (opts : Options) : IO UInt32 := do
|
||||||
let i ← IO.getStdin
|
let i ← IO.getStdin
|
||||||
let o ← IO.getStdout
|
let o ← IO.getStdout
|
||||||
let e ← IO.getStderr
|
let e ← IO.getStderr
|
||||||
try
|
try
|
||||||
let some gameDir := args[1]? | throwServerError "Expected second argument: gameDir"
|
let exitCode ← initAndRunWorker i o e opts
|
||||||
let exitCode ← initAndRunWorker i o e opts gameDir
|
|
||||||
-- HACK: all `Task`s are currently "foreground", i.e. we join on them on main thread exit, but we definitely don't
|
-- HACK: all `Task`s are currently "foreground", i.e. we join on them on main thread exit, but we definitely don't
|
||||||
-- want to do that in the case of the worker processes, which can produce non-terminating tasks evaluating user code
|
-- want to do that in the case of the worker processes, which can produce non-terminating tasks evaluating user code
|
||||||
o.flush
|
o.flush
|
||||||
|
|||||||
+113
-46
@@ -25,56 +25,123 @@ open Lsp
|
|||||||
open JsonRpc
|
open JsonRpc
|
||||||
open IO
|
open IO
|
||||||
|
|
||||||
/- Game-specific version of `InitializeParams` that allows for extra options: -/
|
structure DidOpenLevelParams where
|
||||||
|
uri : String
|
||||||
structure InitializationOptions extends Lean.Lsp.InitializationOptions :=
|
gameDir : String
|
||||||
difficulty : Nat
|
levelModule : Name
|
||||||
|
tactics : Array InventoryTile
|
||||||
|
lemmas : Array InventoryTile
|
||||||
|
definitions : Array InventoryTile
|
||||||
inventory : Array String
|
inventory : Array String
|
||||||
|
/--
|
||||||
|
Check for tactics/theorems that are not unlocked.
|
||||||
|
0: no check
|
||||||
|
1: give warnings
|
||||||
|
2: give errors
|
||||||
|
-/
|
||||||
|
difficulty : Nat
|
||||||
|
/-- The name of the theorem to be proven in this level. -/
|
||||||
|
statementName : Name
|
||||||
deriving ToJson, FromJson
|
deriving ToJson, FromJson
|
||||||
|
|
||||||
structure InitializeParams where
|
structure SetInventoryParams where
|
||||||
processId? : Option Int := none
|
inventory : Array String
|
||||||
clientInfo? : Option ClientInfo := none
|
difficulty : Nat
|
||||||
/- We don't support the deprecated rootPath
|
deriving ToJson, FromJson
|
||||||
(rootPath? : Option String) -/
|
|
||||||
rootUri? : Option String := none
|
|
||||||
initializationOptions? : Option InitializationOptions := none
|
|
||||||
capabilities : ClientCapabilities
|
|
||||||
/-- If omitted, we default to off. -/
|
|
||||||
trace : Trace := Trace.off
|
|
||||||
workspaceFolders? : Option (Array WorkspaceFolder) := none
|
|
||||||
deriving ToJson
|
|
||||||
|
|
||||||
instance : FromJson InitializeParams where
|
def handleDidOpenLevel (params : Json) : GameServerM Unit := do
|
||||||
fromJson? j := do
|
let p ← parseParams _ params
|
||||||
let processId? := j.getObjValAs? Int "processId"
|
let m := p.textDocument
|
||||||
let clientInfo? := j.getObjValAs? ClientInfo "clientInfo"
|
-- Execute the regular handling of the `didOpen` event
|
||||||
let rootUri? := j.getObjValAs? String "rootUri"
|
handleDidOpen p
|
||||||
let initializationOptions? := j.getObjValAs? InitializationOptions "initializationOptions"
|
let fw ← findFileWorker! m.uri
|
||||||
let capabilities ← j.getObjValAs? ClientCapabilities "capabilities"
|
-- let s ← get
|
||||||
let trace := (j.getObjValAs? Trace "trace").toOption.getD Trace.off
|
let c ← read
|
||||||
let workspaceFolders? := j.getObjValAs? (Array WorkspaceFolder) "workspaceFolders"
|
let some lvl ← GameServer.getLevelByFileName? c.initParams ((System.Uri.fileUriToPath? m.uri).getD m.uri |>.toString)
|
||||||
return ⟨
|
| do
|
||||||
processId?.toOption,
|
c.hLog.putStr s!"Level not found: {m.uri} {c.initParams.rootUri?}"
|
||||||
clientInfo?.toOption,
|
c.hLog.flush
|
||||||
rootUri?.toOption,
|
-- Send an extra notification to the file worker to inform it about the level data
|
||||||
initializationOptions?.toOption,
|
let s ← get
|
||||||
capabilities,
|
fw.stdin.writeLspNotification {
|
||||||
trace,
|
method := "$/game/didOpenLevel"
|
||||||
workspaceFolders?.toOption⟩
|
param := {
|
||||||
|
uri := m.uri
|
||||||
def InitializeParams.toLeanInternal (p : InitializeParams) : Lean.Lsp.InitializeParams :=
|
gameDir := s.gameDir
|
||||||
{
|
levelModule := lvl.module
|
||||||
processId? := p.processId?
|
tactics := lvl.tactics.tiles
|
||||||
clientInfo? := p.clientInfo?
|
lemmas := lvl.lemmas.tiles
|
||||||
rootUri? := p.rootUri?
|
definitions := lvl.definitions.tiles
|
||||||
initializationOptions? := p.initializationOptions?.map fun o => {
|
inventory := s.inventory
|
||||||
editDelay? := o.editDelay?
|
difficulty := s.difficulty
|
||||||
hasWidgets? := o.hasWidgets?
|
statementName := lvl.statementName
|
||||||
|
: DidOpenLevelParams
|
||||||
|
}
|
||||||
}
|
}
|
||||||
capabilities := p.capabilities
|
|
||||||
trace := p.trace
|
partial def handleServerEvent (ev : ServerEvent) : GameServerM Bool := do
|
||||||
workspaceFolders? := p.workspaceFolders?
|
match ev with
|
||||||
}
|
| ServerEvent.clientMsg msg =>
|
||||||
|
match msg with
|
||||||
|
| Message.notification "$/game/setInventory" params =>
|
||||||
|
let p := (← parseParams SetInventoryParams (toJson params))
|
||||||
|
let s ← get
|
||||||
|
set {s with inventory := p.inventory, difficulty := p.difficulty}
|
||||||
|
let st ← read
|
||||||
|
let workers ← st.fileWorkersRef.get
|
||||||
|
for (_, fw) in workers do
|
||||||
|
fw.stdin.writeLspMessage msg
|
||||||
|
|
||||||
|
return true
|
||||||
|
| Message.request id "loadInventoryOverview" _ =>
|
||||||
|
let s ← get
|
||||||
|
let some game ← getGame? s.game
|
||||||
|
| return false
|
||||||
|
|
||||||
|
let mut overview : Array WorldOverview := #[]
|
||||||
|
for ⟨worldId, world⟩ in game.worlds.nodes.toList do
|
||||||
|
let mut tactics : Array InventoryTile := #[]
|
||||||
|
let mut theorems : Array InventoryTile := #[]
|
||||||
|
let mut definitions : Array InventoryTile := #[]
|
||||||
|
for ⟨_levelId, lvl⟩ in world.levels.toList do
|
||||||
|
tactics := tactics ++ lvl.tactics.tiles.filterMap (fun tile => match tile.new with
|
||||||
|
| true => some {tile with new := false, locked := true, disabled := false}
|
||||||
|
| false => none)
|
||||||
|
theorems := theorems ++ lvl.lemmas.tiles.filterMap (fun tile => match tile.new with
|
||||||
|
| true => some {tile with new := false, locked := true, disabled := false}
|
||||||
|
| false => none)
|
||||||
|
definitions := definitions ++ lvl.definitions.tiles.filterMap (fun tile => match tile.new with
|
||||||
|
| true => some {tile with new := false, locked := true, disabled := false}
|
||||||
|
| false => none)
|
||||||
|
overview := overview.push {
|
||||||
|
world := worldId,
|
||||||
|
tactics := tactics,
|
||||||
|
lemmas := theorems,
|
||||||
|
definitions := definitions }
|
||||||
|
let c ← read
|
||||||
|
c.hOut.writeLspResponse ⟨id, ToJson.toJson overview⟩
|
||||||
|
return true
|
||||||
|
-- -- All Levels have the same tiles, so we just load them from level 1 of an arbitrary world
|
||||||
|
-- -- and reset `new`, `disabled` and `unlocked`.
|
||||||
|
-- -- Note: as we allow worlds without any levels (for developing), we might need
|
||||||
|
-- -- to try until we find the first world with levels.
|
||||||
|
-- for ⟨worldId, _⟩ in game.worlds.nodes.toList do
|
||||||
|
-- let some lvl ← getLevel? {game := s.game, world := worldId, level := 1}
|
||||||
|
-- | do continue
|
||||||
|
-- let inventory : InventoryOverview := {
|
||||||
|
-- tactics := lvl.tactics.tiles.map
|
||||||
|
-- ({ · with locked := true, disabled := false, new := false }),
|
||||||
|
-- lemmas := lvl.lemmas.tiles.map
|
||||||
|
-- ({ · with locked := true, disabled := false, new := false }),
|
||||||
|
-- definitions := lvl.definitions.tiles.map
|
||||||
|
-- ({ · with locked := true, disabled := false, new := false }),
|
||||||
|
-- lemmaTab := none
|
||||||
|
-- }
|
||||||
|
-- let c ← read
|
||||||
|
-- c.hOut.writeLspResponse ⟨id, ToJson.toJson inventory⟩
|
||||||
|
-- return true
|
||||||
|
-- return false
|
||||||
|
| _ => return false
|
||||||
|
| _ => return false
|
||||||
|
|
||||||
end Game
|
end Game
|
||||||
|
|||||||
@@ -18,14 +18,6 @@ instance [ToJson β] : ToJson (Graph Name β) := {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Just a dummy implementation for now:
|
|
||||||
instance : FromJson (Graph Name β) := {
|
|
||||||
fromJson? := fun _ => .ok {
|
|
||||||
nodes := {}
|
|
||||||
edges := {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
instance : EmptyCollection (Graph α β) := ⟨default⟩
|
instance : EmptyCollection (Graph α β) := ⟨default⟩
|
||||||
|
|
||||||
def Graph.insertNode (g : Graph α β) (a : α) (b : β) :=
|
def Graph.insertNode (g : Graph α β) (a : α) (b : β) :=
|
||||||
|
|||||||
@@ -1,190 +0,0 @@
|
|||||||
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 []
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
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}}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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."
|
|
||||||
}
|
|
||||||
@@ -46,8 +46,8 @@ partial def matchExpr (pattern : Expr) (e : Expr) (bij : FVarBijection := {}) :
|
|||||||
| .bvar i1, .bvar i2 => if i1 == i2 then bij else none
|
| .bvar i1, .bvar i2 => if i1 == i2 then bij else none
|
||||||
| .fvar i1, .fvar i2 => bij.insert? i1 i2
|
| .fvar i1, .fvar i2 => bij.insert? i1 i2
|
||||||
| .mvar _, .mvar _ => bij
|
| .mvar _, .mvar _ => bij
|
||||||
| .sort _u1, .sort _u2 => bij -- TODO?
|
| .sort u1, .sort u2 => bij -- TODO?
|
||||||
| .const n1 _ls1, .const n2 _ls2 =>
|
| .const n1 ls1, .const n2 ls2 =>
|
||||||
if n1 == n2 then bij else none -- && (← (ls1.zip ls2).allM fun (l1, l2) => Meta.isLevelDefEq l1 l2)
|
if n1 == n2 then bij else none -- && (← (ls1.zip ls2).allM fun (l1, l2) => Meta.isLevelDefEq l1 l2)
|
||||||
| .app f1 a1, .app f2 a2 =>
|
| .app f1 a1, .app f2 a2 =>
|
||||||
some bij
|
some bij
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
namespace GameData
|
|
||||||
def gameDataPath : System.FilePath := ".lake" / "gamedata"
|
|
||||||
def gameFileName := s!"game.json"
|
|
||||||
def docFileName := fun (inventoryType : InventoryType) (name : Name) => s!"doc__{inventoryType}__{name}.json"
|
|
||||||
def levelFileName := fun (worldId : Name) (levelId : Nat) => s!"level__{worldId}__{levelId}.json"
|
|
||||||
def inventoryFileName := s!"inventory.json"
|
|
||||||
end GameData
|
|
||||||
|
|
||||||
open GameData in
|
|
||||||
-- TODO: register all of this as ToJson instance?
|
|
||||||
def saveGameData (allItemsByType : HashMap InventoryType (HashSet Name))
|
|
||||||
(inventory : InventoryOverview): CommandElabM Unit := do
|
|
||||||
let game ← getCurGame
|
|
||||||
let env ← getEnv
|
|
||||||
let path := (← IO.currentDir) / gameDataPath
|
|
||||||
|
|
||||||
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 / levelFileName worldId levelId) (toString (toJson (level.toInfo env)))
|
|
||||||
|
|
||||||
IO.FS.writeFile (path / gameFileName) (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 / docFileName inventoryType name) (toString (toJson item))
|
|
||||||
|
|
||||||
IO.FS.writeFile (path / inventoryFileName) (toString (toJson inventory))
|
|
||||||
|
|
||||||
open GameData
|
|
||||||
|
|
||||||
def loadData (f : System.FilePath) (α : Type) [FromJson α] : IO α := do
|
|
||||||
let str ← IO.FS.readFile f
|
|
||||||
let json ← match Json.parse str with
|
|
||||||
| .ok v => pure v
|
|
||||||
| .error e => throw (IO.userError e)
|
|
||||||
let data ← match fromJson? json with
|
|
||||||
| .ok v => pure v
|
|
||||||
| .error e => throw (IO.userError e)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def loadGameData (gameDir : System.FilePath) : IO Game :=
|
|
||||||
loadData (gameDir / gameDataPath / gameFileName) Game
|
|
||||||
|
|
||||||
def loadLevelData (gameDir : System.FilePath) (worldId : Name) (levelId : Nat) : IO LevelInfo :=
|
|
||||||
loadData (gameDir / gameDataPath / levelFileName worldId levelId) LevelInfo
|
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
/- This file is mostly copied from `Lean/Server/Watchdog.lean`. -/
|
||||||
|
import Lean.Server.Watchdog
|
||||||
|
import GameServer.Game
|
||||||
|
|
||||||
|
namespace MyServer.Watchdog
|
||||||
|
open Lean
|
||||||
|
open Server
|
||||||
|
open Watchdog
|
||||||
|
open IO
|
||||||
|
open Lsp
|
||||||
|
open JsonRpc
|
||||||
|
open System.Uri
|
||||||
|
|
||||||
|
partial def mainLoop (clientTask : Task ServerEvent) : GameServerM Unit := do
|
||||||
|
let st ← read
|
||||||
|
let workers ← st.fileWorkersRef.get
|
||||||
|
let mut workerTasks := #[]
|
||||||
|
for (_, fw) in workers do
|
||||||
|
if let WorkerState.running := fw.state then
|
||||||
|
workerTasks := workerTasks.push <| fw.commTask.map (ServerEvent.workerEvent fw)
|
||||||
|
|
||||||
|
let ev ← IO.waitAny (clientTask :: workerTasks.toList)
|
||||||
|
|
||||||
|
if ← Game.handleServerEvent ev then -- handle Game requests
|
||||||
|
mainLoop (←runClientTask)
|
||||||
|
else
|
||||||
|
match ev with
|
||||||
|
| ServerEvent.clientMsg msg =>
|
||||||
|
match msg with
|
||||||
|
| Message.request id "shutdown" _ =>
|
||||||
|
shutdown
|
||||||
|
st.hOut.writeLspResponse ⟨id, Json.null⟩
|
||||||
|
| Message.request id method (some params) =>
|
||||||
|
handleRequest id method (toJson params)
|
||||||
|
mainLoop (←runClientTask)
|
||||||
|
| Message.response .. =>
|
||||||
|
-- TODO: handle client responses
|
||||||
|
mainLoop (←runClientTask)
|
||||||
|
| Message.responseError _ _ e .. =>
|
||||||
|
throwServerError s!"Unhandled response error: {e}"
|
||||||
|
| Message.notification method (some params) =>
|
||||||
|
if method == "textDocument/didOpen" then
|
||||||
|
-- for lean4game, we need to pass in extra information when a level is opened:
|
||||||
|
Game.handleDidOpenLevel (← parseParams _ (toJson params))
|
||||||
|
else
|
||||||
|
handleNotification method (toJson params)
|
||||||
|
mainLoop (←runClientTask)
|
||||||
|
| _ => throwServerError "Got invalid JSON-RPC message"
|
||||||
|
| ServerEvent.clientError e => throw e
|
||||||
|
| ServerEvent.workerEvent fw ev =>
|
||||||
|
match ev with
|
||||||
|
| WorkerEvent.ioError e =>
|
||||||
|
throwServerError s!"IO error while processing events for {fw.doc.uri}: {e}"
|
||||||
|
| WorkerEvent.crashed _ =>
|
||||||
|
handleCrash fw.doc.uri #[]
|
||||||
|
mainLoop clientTask
|
||||||
|
| WorkerEvent.terminated =>
|
||||||
|
throwServerError "Internal server error: got termination event for worker that should have been removed"
|
||||||
|
| .importsChanged =>
|
||||||
|
startFileWorker fw.doc
|
||||||
|
mainLoop clientTask
|
||||||
|
|
||||||
|
def initAndRunWatchdogAux : GameServerM Unit := do
|
||||||
|
let st ← read
|
||||||
|
try
|
||||||
|
discard $ st.hIn.readLspNotificationAs "initialized" InitializedParams
|
||||||
|
let clientTask ← runClientTask
|
||||||
|
mainLoop clientTask
|
||||||
|
catch err =>
|
||||||
|
shutdown
|
||||||
|
throw err
|
||||||
|
/- NOTE(WN): It looks like instead of sending the `exit` notification,
|
||||||
|
VSCode just closes the stream. In that case, pretend we got an `exit`. -/
|
||||||
|
let Message.notification "exit" none ←
|
||||||
|
try st.hIn.readLspMessage
|
||||||
|
catch _ => pure (Message.notification "exit" none)
|
||||||
|
| throwServerError "Got `shutdown` request, expected an `exit` notification"
|
||||||
|
|
||||||
|
def createEnv (gameDir : String) (module : String) : IO Environment := do
|
||||||
|
-- Determine search paths of the game project by running `lake env printenv LEAN_PATH`.
|
||||||
|
let out ← IO.Process.output
|
||||||
|
{ cwd := gameDir, cmd := "lake", args := #["env","printenv","LEAN_PATH"] }
|
||||||
|
if out.exitCode != 0 then
|
||||||
|
throwServerError s!"Error while running Lake: {out.stderr}"
|
||||||
|
|
||||||
|
-- Make the paths relative to the current directory
|
||||||
|
let paths : List System.FilePath := System.SearchPath.parse out.stdout.trim
|
||||||
|
let currentDir ← IO.currentDir
|
||||||
|
let paths := paths.map fun p => currentDir / (gameDir : System.FilePath) / p
|
||||||
|
|
||||||
|
-- Set the search path
|
||||||
|
Lean.searchPathRef.set paths
|
||||||
|
|
||||||
|
let env ← importModules #[{ module := `Init : Import }, { module := module : Import }] {} 0
|
||||||
|
return env
|
||||||
|
|
||||||
|
def initAndRunWatchdog (args : List String) (i o e : FS.Stream) : IO Unit := do
|
||||||
|
if args.length < 2 then
|
||||||
|
throwServerError s!"Expected 1-3 command line arguments in addition to `--server`:
|
||||||
|
game directory, the name of the main module (optional), and the name of the game (optional)."
|
||||||
|
let gameDir := args[1]!
|
||||||
|
let module := if args.length < 3 then defaultGameModule else args[2]!
|
||||||
|
let gameName := if args.length < 4 then defaultGameName else args[3]!
|
||||||
|
let workerPath := "./gameserver"
|
||||||
|
-- TODO: Do the following commands slow us down?
|
||||||
|
let srcSearchPath ← initSrcSearchPath (← getBuildDir)
|
||||||
|
let references ← IO.mkRef (← loadReferences)
|
||||||
|
let fileWorkersRef ← IO.mkRef (RBMap.empty : FileWorkerMap)
|
||||||
|
let i ← maybeTee "wdIn.txt" false i
|
||||||
|
let o ← maybeTee "wdOut.txt" true o
|
||||||
|
let e ← maybeTee "wdErr.txt" true e
|
||||||
|
let state := {
|
||||||
|
env := ← createEnv gameDir module,
|
||||||
|
game := gameName,
|
||||||
|
gameDir := gameDir,
|
||||||
|
inventory := #[]
|
||||||
|
difficulty := 0
|
||||||
|
}
|
||||||
|
let initRequest ← i.readLspRequestAs "initialize" InitializeParams
|
||||||
|
-- We misuse the `rootUri` field to the gameName
|
||||||
|
let rootUri? := gameName
|
||||||
|
let initRequest := {initRequest with param := {initRequest.param with rootUri?}}
|
||||||
|
o.writeLspResponse {
|
||||||
|
id := initRequest.id
|
||||||
|
result := {
|
||||||
|
capabilities := mkLeanServerCapabilities
|
||||||
|
serverInfo? := some {
|
||||||
|
name := "Lean 4 Game Server"
|
||||||
|
version? := "0.1.1"
|
||||||
|
}
|
||||||
|
: InitializeResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let context : ServerContext := {
|
||||||
|
hIn := i
|
||||||
|
hOut := o
|
||||||
|
hLog := e
|
||||||
|
args := args
|
||||||
|
fileWorkersRef := fileWorkersRef
|
||||||
|
initParams := initRequest.param
|
||||||
|
workerPath
|
||||||
|
srcSearchPath
|
||||||
|
references
|
||||||
|
}
|
||||||
|
discard $ ReaderT.run (StateT.run initAndRunWatchdogAux state) context
|
||||||
|
|
||||||
|
def watchdogMain (args : List String) : IO UInt32 := do
|
||||||
|
let i ← IO.getStdin
|
||||||
|
let o ← IO.getStdout
|
||||||
|
let e ← IO.getStderr
|
||||||
|
try
|
||||||
|
initAndRunWatchdog args i o e
|
||||||
|
return 0
|
||||||
|
catch err =>
|
||||||
|
e.putStrLn s!"Watchdog error: {err}"
|
||||||
|
return 1
|
||||||
|
|
||||||
|
end MyServer.Watchdog
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import GameServer.FileWorker
|
import GameServer.FileWorker
|
||||||
|
import GameServer.Watchdog
|
||||||
import GameServer.Commands
|
import GameServer.Commands
|
||||||
|
|
||||||
-- TODO: The only reason we import `Commands` is so that it gets built to on `lake build`
|
-- TODO: The only reason we import `Commands` is so that it gets built to on `lake build`
|
||||||
@@ -9,9 +10,13 @@ unsafe def main : List String → IO UInt32 := fun args => do
|
|||||||
|
|
||||||
Lean.enableInitializersExecution
|
Lean.enableInitializersExecution
|
||||||
|
|
||||||
-- TODO: remove this argument
|
|
||||||
if args[0]? == some "--server" then
|
if args[0]? == some "--server" then
|
||||||
MyServer.FileWorker.workerMain {} args
|
MyServer.Watchdog.watchdogMain args
|
||||||
|
else if args[0]? == some "--worker" then
|
||||||
|
MyServer.FileWorker.workerMain {}
|
||||||
else
|
else
|
||||||
e.putStrLn s!"Expected `--server`"
|
e.putStrLn s!"Expected `--server` or `--worker`"
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
-- TODO: Potentially it could be useful to pass in the `gameName` via the websocket connection
|
||||||
@@ -4,10 +4,10 @@
|
|||||||
[{"url": "https://github.com/leanprover/std4.git",
|
[{"url": "https://github.com/leanprover/std4.git",
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"subDir": null,
|
"subDir": null,
|
||||||
"rev": "ee49cf8fada1bf5a15592c399a925c401848227f",
|
"rev": "2e4a3586a8f16713f16b2d2b3af3d8e65f3af087",
|
||||||
"name": "std",
|
"name": "std",
|
||||||
"manifestFile": "lake-manifest.json",
|
"manifestFile": "lake-manifest.json",
|
||||||
"inputRev": "v4.5.0-rc1",
|
"inputRev": "v4.3.0",
|
||||||
"inherited": false,
|
"inherited": false,
|
||||||
"configFile": "lakefile.lean"}],
|
"configFile": "lakefile.lean"}],
|
||||||
"name": "GameServer",
|
"name": "GameServer",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ lean_lib GameServer
|
|||||||
|
|
||||||
@[default_target]
|
@[default_target]
|
||||||
lean_exe gameserver {
|
lean_exe gameserver {
|
||||||
root := `GameServer
|
root := `Main
|
||||||
supportInterpreter := true
|
supportInterpreter := true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
leanprover/lean4:v4.5.0-rc1
|
leanprover/lean4:v4.3.0
|
||||||
|
|||||||
Reference in New Issue
Block a user