Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb8be70bcb | ||
|
|
85fef9373d | ||
|
|
be34fe9cda | ||
|
|
6d61bc1942 | ||
|
|
2613c16bbb | ||
|
|
926a013b10 | ||
|
|
4e45111dd8 | ||
|
|
32a2ff3e18 | ||
|
|
8d29761579 | ||
|
|
3ff46aeb54 | ||
|
|
3d79c4ea60 | ||
|
|
11dde6aad9 | ||
|
|
8a6486bdd5 | ||
|
|
f6063023b4 |
@@ -5,17 +5,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: install elan
|
- uses: actions/checkout@v3
|
||||||
run: |
|
|
||||||
set -o pipefail
|
|
||||||
curl -sSfL https://github.com/leanprover/elan/releases/download/v3.0.0/elan-x86_64-unknown-linux-gnu.tar.gz | tar xz
|
|
||||||
./elan-init -y
|
|
||||||
echo "$HOME/.elan/bin" >> $GITHUB_PATH
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
- name: print lean and lake versions
|
|
||||||
run: |
|
|
||||||
lean --version
|
|
||||||
lake --version
|
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
|
|||||||
+5
-2
@@ -1,5 +1,8 @@
|
|||||||
node_modules
|
node_modules
|
||||||
client/dist
|
client/dist
|
||||||
games/
|
server/build
|
||||||
server/.lake
|
server/lakefile.olean
|
||||||
|
server32bit
|
||||||
|
**/lake-packages/
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
|
client/public/server.*
|
||||||
|
|||||||
@@ -8,27 +8,21 @@ Please follow the tutorial [Creating a Game](doc/create_game.md). In particular,
|
|||||||
|
|
||||||
* Step 5: [How to Run Games Locally](doc/running_locally.md)
|
* Step 5: [How to Run Games Locally](doc/running_locally.md)
|
||||||
* Step 7: [How to Update an existing Game](doc/update_game.md)
|
* Step 7: [How to Update an existing Game](doc/update_game.md)
|
||||||
* Step 8: [How to Publishing a Game](doc/publish_game.md)
|
|
||||||
|
### Publishing a Game
|
||||||
|
|
||||||
|
We encourage anybody to have games hosted on our [Lean Game Server](https://adam.math.hhu.de) for anybody to play. For that you simply need to contact us with the link to your game repo. We are also happy to add work-in-progress games and games in any language.
|
||||||
|
|
||||||
|
For example, you can [contact Jon on Zulip](https://leanprover.zulipchat.com/#narrow/dm/385895-Jon-Eugster). Or [via Email](https://www.math.hhu.de/en/lehrstuehle-/-personen-/-ansprechpartner/innen/lehrstuehle-des-mathematischen-instituts/lehrstuhl-fuer-algebraische-geometrie/team/jon-eugster).
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
The documentation is very much work in progress but the linked documentation here
|
The documentation for the game engine itself is still missing, but there is [Creating a Game](doc/create_game.md) explaining the API to create a game.
|
||||||
should be up-to-date:
|
|
||||||
|
|
||||||
### Game creation API
|
Some documentation:
|
||||||
|
|
||||||
- [Creating a Game](doc/create_game.md): **the main document to consult**.
|
- [NPM Scripts](doc/npm_scripts.md)
|
||||||
- [More about Hints](doc/hints.md): describes the `Hint` and `Branch` tactic.
|
- [Old documentation](doc/DOCUMENTATION.md)
|
||||||
|
|
||||||
### Frontend API
|
|
||||||
|
|
||||||
* [How to Run Games Locally](doc/running_locally.md): play a game on your computer
|
|
||||||
* [How to Update an existing Game](doc/update_game.md): update to a new lean version
|
|
||||||
* [How to Publishing a Game](doc/publish_game.md): load your game to adam.math.hhu.de for others to play
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
|
|
||||||
not written yet
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
@@ -42,5 +36,5 @@ Providing the use access to a Lean instance running on the server is a severe se
|
|||||||
|
|
||||||
The project is based on ideas from the [Lean Game Maker](https://github.com/mpedramfar/Lean-game-maker) and the [Natural Number Game
|
The project is based on ideas from the [Lean Game Maker](https://github.com/mpedramfar/Lean-game-maker) and the [Natural Number Game
|
||||||
(NNG)](https://www.ma.imperial.ac.uk/~buzzard/xena/natural_number_game/)
|
(NNG)](https://www.ma.imperial.ac.uk/~buzzard/xena/natural_number_game/)
|
||||||
by Kevin Buzzard and Mohammad Pedramfar.
|
of Kevin Buzzard and Mohammad Pedramfar.
|
||||||
The project is based on Patrick Massot's prototype: [NNG4](https://github.com/PatrickMassot/NNG4).
|
The project is based on Patrick Massot's prototype: [NNG4](https://github.com/PatrickMassot/NNG4).
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
|
||||||
|
var stderrBuffer = ""
|
||||||
|
var messageBuffer = []
|
||||||
|
var initialized = false;
|
||||||
|
var flushing = false;
|
||||||
|
|
||||||
|
var headerMode = true;
|
||||||
|
var header="";
|
||||||
|
var re = /Content-Length: (\d+)\r\n/i;
|
||||||
|
var contentLength = 0;
|
||||||
|
var content = []
|
||||||
|
var utf8decoder = new TextDecoder();
|
||||||
|
|
||||||
|
|
||||||
|
function flushMessageBuffer(){
|
||||||
|
if (initialized && !flushing) {
|
||||||
|
while(messageBuffer.length > 0) {
|
||||||
|
flushing = true;
|
||||||
|
var msg = messageBuffer.shift();
|
||||||
|
console.log(`Send message: ${msg}`);
|
||||||
|
Module.ccall('send_message', 'void', ['string'], [msg]);
|
||||||
|
console.log(`Message done: ${msg}`);
|
||||||
|
}
|
||||||
|
flushing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var Module = {
|
||||||
|
"arguments": ["--worker"],
|
||||||
|
"preRun": [function() {
|
||||||
|
function stdin() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stdout(asciiCode) {
|
||||||
|
if (headerMode) {
|
||||||
|
header += String.fromCharCode(asciiCode)
|
||||||
|
if (header.endsWith('\r\n\r\n')) {
|
||||||
|
const found = header.match(re)
|
||||||
|
if (found == null) { console.error(`Invalid header: ${header}`) }
|
||||||
|
contentLength = parseInt(found[1])
|
||||||
|
content = []
|
||||||
|
headerMode = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content.push(asciiCode)
|
||||||
|
if (content.length == contentLength) {
|
||||||
|
const message = utf8decoder.decode(new Uint8Array(content))
|
||||||
|
console.log(`Server: ${message}`)
|
||||||
|
postMessage(message);
|
||||||
|
headerMode = true
|
||||||
|
header = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stderr(asciiCode) {
|
||||||
|
stderrBuffer += String.fromCharCode(asciiCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
FS.init(stdin, stdout, stderr);
|
||||||
|
}],
|
||||||
|
"noInitialRun": true,
|
||||||
|
"onRuntimeInitialized": () => {
|
||||||
|
Module.ccall('main', 'void', [], []);
|
||||||
|
initialized = true;
|
||||||
|
if (stderrBuffer !== "") {
|
||||||
|
console.log(stderrBuffer);
|
||||||
|
stderrBuffer = ""
|
||||||
|
}
|
||||||
|
flushMessageBuffer();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
importScripts("server.js")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
onmessage = (ev) => {
|
||||||
|
console.log(`Client: ${ev.data}`)
|
||||||
|
messageBuffer.push(ev.data);
|
||||||
|
flushMessageBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
if (stderrBuffer !== "") {
|
||||||
|
console.log(stderrBuffer);
|
||||||
|
stderrBuffer = ""
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
setTimeout(() =>{
|
||||||
|
|
||||||
|
},2000)
|
||||||
@@ -10,7 +10,6 @@ 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 { useWindowDimensions } from './window_width';
|
import { useWindowDimensions } from './window_width';
|
||||||
import { connection } from './connection';
|
|
||||||
|
|
||||||
export const GameIdContext = React.createContext<string>(undefined);
|
export const GameIdContext = React.createContext<string>(undefined);
|
||||||
|
|
||||||
@@ -20,10 +19,6 @@ function App() {
|
|||||||
const {width, height} = useWindowDimensions()
|
const {width, height} = useWindowDimensions()
|
||||||
const [mobile, setMobile] = React.useState(width < 800)
|
const [mobile, setMobile] = React.useState(width < 800)
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
connection.startLeanClient(gameId);
|
|
||||||
}, [gameId])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<GameIdContext.Provider value={gameId}>
|
<GameIdContext.Provider value={gameId}>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 398 KiB |
@@ -1,7 +1,6 @@
|
|||||||
import { GameHint } from "./infoview/rpc_api";
|
import { GameHint } from "./infoview/rpc_api";
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Markdown from './markdown';
|
import Markdown from './markdown';
|
||||||
import { ProofStep } from "./infoview/context";
|
|
||||||
|
|
||||||
export function Hint({hint, step, selected, toggleSelection, lastLevel} : {hint: GameHint, step: number, selected: number, toggleSelection: any, lastLevel?: boolean}) {
|
export function Hint({hint, step, selected, toggleSelection, lastLevel} : {hint: GameHint, step: number, selected: number, toggleSelection: any, lastLevel?: boolean}) {
|
||||||
return <div className={`message information step-${step}` + (step == selected ? ' selected' : '') + (lastLevel ? ' recent' : '')} onClick={toggleSelection}>
|
return <div className={`message information step-${step}` + (step == selected ? ' selected' : '') + (lastLevel ? ' recent' : '')} onClick={toggleSelection}>
|
||||||
@@ -44,24 +43,3 @@ export function DeletedHints({hints} : {hints: GameHint[]}) {
|
|||||||
{hiddenHints.map((hint, i) => <DeletedHint key={`deleted-hidden-hint-${i}`} hint={hint}/>)}
|
{hiddenHints.map((hint, i) => <DeletedHint key={`deleted-hidden-hint-${i}`} hint={hint}/>)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Filter hints to not show consequtive identical hints twice.
|
|
||||||
*
|
|
||||||
* This function takes a `ProofStep[]` and extracts the hints in form of an
|
|
||||||
* element of type `GameHint[][]` where it removes hints that are identical to hints
|
|
||||||
* appearing in the previous step. Hidden hints are not filtered.
|
|
||||||
*
|
|
||||||
* This effectively means we prevent consequtive identical hints from being shown.
|
|
||||||
*/
|
|
||||||
export function filterHints(proof: ProofStep[]): GameHint[][] {
|
|
||||||
return proof.map((step, i) => {
|
|
||||||
if (i == 0){
|
|
||||||
return step.hints
|
|
||||||
} else {
|
|
||||||
// TODO: Writing all fields explicitely is somewhat fragile to changes, is there a
|
|
||||||
// good way to shallow-compare objects?
|
|
||||||
return step.hints.filter((hint) => hint.hidden ||
|
|
||||||
(proof[i-1].hints.find((x) => (x.text == hint.text && x.hidden == hint.hidden)) === undefined))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import { Button } from '../button';
|
|||||||
import { CircularProgress } from '@mui/material';
|
import { CircularProgress } from '@mui/material';
|
||||||
import { GameHint } from './rpc_api';
|
import { GameHint } from './rpc_api';
|
||||||
import { store } from '../../state/store';
|
import { store } from '../../state/store';
|
||||||
import { Hints, filterHints } from '../hints';
|
import { Hints } from '../hints';
|
||||||
|
|
||||||
/** Wrapper for the two editors. It is important that the `div` with `codeViewRef` is
|
/** Wrapper for the two editors. It is important that the `div` with `codeViewRef` is
|
||||||
* always present, or the monaco editor cannot start.
|
* always present, or the monaco editor cannot start.
|
||||||
@@ -156,7 +156,7 @@ export function Main(props: { world: string, level: number, data: LevelInfo}) {
|
|||||||
|
|
||||||
const completed = useAppSelector(selectCompleted(gameId, props.world, props.level))
|
const completed = useAppSelector(selectCompleted(gameId, props.world, props.level))
|
||||||
|
|
||||||
console.debug(`template: ${props.data?.template}`)
|
console.debug(`template: ${props.data.template}`)
|
||||||
|
|
||||||
// React.useEffect (() => {
|
// React.useEffect (() => {
|
||||||
// if (props.data.template) {
|
// if (props.data.template) {
|
||||||
@@ -347,7 +347,6 @@ export function TypewriterInterface({props}) {
|
|||||||
const uri = model.uri.toString()
|
const uri = model.uri.toString()
|
||||||
|
|
||||||
const [disableInput, setDisableInput] = React.useState<boolean>(false)
|
const [disableInput, setDisableInput] = React.useState<boolean>(false)
|
||||||
const [loadingProgress, setLoadingProgress] = React.useState<number>(0)
|
|
||||||
const { setDeletedChat, showHelp, setShowHelp } = React.useContext(DeletedChatContext)
|
const { setDeletedChat, showHelp, setShowHelp } = React.useContext(DeletedChatContext)
|
||||||
const {mobile} = React.useContext(MobileContext)
|
const {mobile} = React.useContext(MobileContext)
|
||||||
const { proof } = React.useContext(ProofContext)
|
const { proof } = React.useContext(ProofContext)
|
||||||
@@ -367,9 +366,9 @@ export function TypewriterInterface({props}) {
|
|||||||
function deleteProof(line: number) {
|
function deleteProof(line: number) {
|
||||||
return (ev) => {
|
return (ev) => {
|
||||||
let deletedChat: Array<GameHint> = []
|
let deletedChat: Array<GameHint> = []
|
||||||
filterHints(proof).slice(line).map((hintsAtStep, i) => {
|
proof.slice(line).map((step, i) => {
|
||||||
// Only add these hidden hints to the deletion stack which were visible
|
// Only add these hidden hints to the deletion stack which were visible
|
||||||
deletedChat = [...deletedChat, ...hintsAtStep.filter(hint => (!hint.hidden || showHelp.has(line + i)))]
|
deletedChat = [...deletedChat, ...step.hints.filter(hint => (!hint.hidden || showHelp.has(line + i)))]
|
||||||
})
|
})
|
||||||
setDeletedChat(deletedChat)
|
setDeletedChat(deletedChat)
|
||||||
|
|
||||||
@@ -456,17 +455,6 @@ export function TypewriterInterface({props}) {
|
|||||||
|
|
||||||
let lastStepErrors = proof.length ? hasInteractiveErrors(proof[proof.length - 1].errors) : false
|
let lastStepErrors = proof.length ? hasInteractiveErrors(proof[proof.length - 1].errors) : false
|
||||||
|
|
||||||
|
|
||||||
useServerNotificationEffect("$/game/loading", (params : any) => {
|
|
||||||
if (params.kind == "loadConstants") {
|
|
||||||
setLoadingProgress(params.counter/100*50)
|
|
||||||
} else if (params.kind == "finalizeExtensions") {
|
|
||||||
setLoadingProgress(50 + params.counter/150*50)
|
|
||||||
} else {
|
|
||||||
console.error(`Unknown loading kind: ${params.kind}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return <div className="typewriter-interface">
|
return <div className="typewriter-interface">
|
||||||
<RpcContext.Provider value={rpcSess}>
|
<RpcContext.Provider value={rpcSess}>
|
||||||
<div className="content">
|
<div className="content">
|
||||||
@@ -533,7 +521,7 @@ export function TypewriterInterface({props}) {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</> : <CircularProgress variant="determinate" value={loadingProgress} />
|
</> : <CircularProgress />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|||||||
import { faLock, faBan } from '@fortawesome/free-solid-svg-icons'
|
import { faLock, faBan } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { GameIdContext } from '../app';
|
import { GameIdContext } from '../app';
|
||||||
import Markdown from './markdown';
|
import Markdown from './markdown';
|
||||||
import { useLoadDocQuery, InventoryTile, LevelInfo, InventoryOverview, useLoadInventoryOverviewQuery, WorldOverview } from '../state/api';
|
import { useLoadDocQuery, InventoryTile, LevelInfo, InventoryOverview, useLoadInventoryOverviewQuery } 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';
|
||||||
@@ -37,79 +37,6 @@ export function Inventory({levelInfo, openDoc, enableAll=false} :
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OverviewInventory({data, openDoc, enableAll=false, showOverview=true} :
|
|
||||||
{
|
|
||||||
data: WorldOverview[],
|
|
||||||
openDoc: (props: {name: string, type: string}) => void,
|
|
||||||
enableAll?: boolean,
|
|
||||||
showOverview?: boolean
|
|
||||||
}) {
|
|
||||||
|
|
||||||
const gameId = React.useContext(GameIdContext)
|
|
||||||
const difficulty = useSelector(selectDifficulty(gameId))
|
|
||||||
const [tab, setTab] = useState<string>()
|
|
||||||
let inv: string[] = selectInventory(gameId)(store.getState())
|
|
||||||
|
|
||||||
let levelInfo : InventoryOverview = {
|
|
||||||
tactics : data ? data.map(world => (world.tactics.map(tile => inv.includes(tile.name) ? {...tile, locked: false} : tile))).flat() : [],
|
|
||||||
lemmas : data ? data.map(world => (world.lemmas.map(tile => inv.includes(tile.name) ? {...tile, locked: false} : tile))).flat() : [],
|
|
||||||
definitions : data ? data.map(world => (world.definitions.map(tile => inv.includes(tile.name) ? {...tile, locked: false} : tile))).flat() : [],
|
|
||||||
lemmaTab: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Inventory levelInfo={levelInfo} openDoc={openDoc} enableAll={enableAll}/>
|
|
||||||
{showOverview &&
|
|
||||||
<div className="inventory">
|
|
||||||
{data && <>
|
|
||||||
<h2>Overviews</h2>
|
|
||||||
<div className="tab-bar">
|
|
||||||
{data.map(world => {
|
|
||||||
return <div key={`category-${world.world}`} className={`tab ${world.world == tab ? "active": ""}`} onClick={() => { setTab((tab == world.world) ? null : world.world) }}>{world.world}</div>
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className="inventory-list">
|
|
||||||
{/* TODO: a bit hacky, do we need to redesign inventory completely and provide it in a better order? */}
|
|
||||||
{data.map(world => {
|
|
||||||
if (world.world == tab) {
|
|
||||||
return [
|
|
||||||
...world.tactics.map(item => {
|
|
||||||
return <InventoryItem
|
|
||||||
key={`${world.world}-${item.name}`}
|
|
||||||
showDoc={() => {openDoc({name: item.name, type: 'Tactic'})}}
|
|
||||||
name={item.name} displayName={item.displayName}
|
|
||||||
locked={difficulty > 0 ? !inv.includes(item.name) : false}
|
|
||||||
disabled={item.disabled} newly={false} enableAll={enableAll} />
|
|
||||||
}),
|
|
||||||
...world.lemmas.map(item => {
|
|
||||||
return <InventoryItem
|
|
||||||
key={`${world.world}-${item.name}`}
|
|
||||||
showDoc={() => {openDoc({name: item.name, type: 'Lemma'})}}
|
|
||||||
name={item.name} displayName={item.displayName}
|
|
||||||
locked={difficulty > 0 ? item.locked : false}
|
|
||||||
disabled={item.disabled} newly={false} enableAll={enableAll} />
|
|
||||||
}),
|
|
||||||
...world.definitions.map(item => {
|
|
||||||
return <InventoryItem
|
|
||||||
key={`${world.world}-${item.name}`}
|
|
||||||
showDoc={() => {openDoc({name: item.name, type: 'Definition'})}}
|
|
||||||
name={item.name} displayName={item.displayName}
|
|
||||||
locked={difficulty > 0 ? item.locked : false}
|
|
||||||
disabled={item.disabled} newly={false} enableAll={enableAll} />
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</>}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function InventoryList({items, docType, openDoc, defaultTab=null, level=undefined, enableAll=false} :
|
function InventoryList({items, docType, openDoc, defaultTab=null, level=undefined, enableAll=false} :
|
||||||
{
|
{
|
||||||
items: InventoryTile[],
|
items: InventoryTile[],
|
||||||
@@ -159,7 +86,7 @@ function InventoryList({items, docType, openDoc, defaultTab=null, level=undefine
|
|||||||
{[...modifiedItems].sort(
|
{[...modifiedItems].sort(
|
||||||
// For lemas, sort entries `available > disabled > locked`
|
// For lemas, sort entries `available > disabled > locked`
|
||||||
// otherwise alphabetically
|
// otherwise alphabetically
|
||||||
(x, y) => +(docType == "Lemma") * (+x.locked - +y.locked || +x.disabled - +y.disabled) || x.displayName.localeCompare(y.displayName)
|
(x, y) => +(docType == "Lemma") * (+x.locked - +y.locked || +x.disabled - +y.disabled)
|
||||||
).filter(item => !item.hidden && ((tab ?? categories[0]) == item.category)).map((item, i) => {
|
).filter(item => !item.hidden && ((tab ?? categories[0]) == item.category)).map((item, i) => {
|
||||||
return <InventoryItem key={`${item.category}-${item.name}`}
|
return <InventoryItem key={`${item.category}-${item.name}`}
|
||||||
showDoc={() => {openDoc({name: item.name, type: docType})}}
|
showDoc={() => {openDoc({name: item.name, type: docType})}}
|
||||||
@@ -217,21 +144,3 @@ export function InventoryPanel({levelInfo, visible = true}) {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The panel (on the welcome page) showing the user's inventory with tactics, definitions, and lemmas */
|
|
||||||
export function InventoryOverviewPanel({data, visible = true, showOverview=true} : {data : WorldOverview[], visible?: boolean, showOverview?: boolean}) {
|
|
||||||
const gameId = React.useContext(GameIdContext)
|
|
||||||
|
|
||||||
// The inventory is overlayed by the doc entry of a clicked item
|
|
||||||
const [inventoryDoc, setInventoryDoc] = useState<{name: string, type: string}>(null)
|
|
||||||
// Set `inventoryDoc` to `null` to close the doc
|
|
||||||
function closeInventoryDoc() {setInventoryDoc(null)}
|
|
||||||
|
|
||||||
return <div className={`column inventory-panel ${visible ? '' : 'hidden'}`}>
|
|
||||||
{inventoryDoc ?
|
|
||||||
<Documentation name={inventoryDoc.name} type={inventoryDoc.type} handleClose={closeInventoryDoc}/>
|
|
||||||
:
|
|
||||||
<OverviewInventory data={data} openDoc={setInventoryDoc} enableAll={true} showOverview={showOverview}/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,12 +7,11 @@ import '@fontsource/roboto/500.css';
|
|||||||
import '@fontsource/roboto/700.css';
|
import '@fontsource/roboto/700.css';
|
||||||
|
|
||||||
import '../css/landing_page.css'
|
import '../css/landing_page.css'
|
||||||
|
import coverRobo from '../assets/covers/formaloversum.png'
|
||||||
import bgImage from '../assets/bg.jpg'
|
import bgImage from '../assets/bg.jpg'
|
||||||
|
|
||||||
import Markdown from './markdown';
|
import Markdown from './markdown';
|
||||||
import {PrivacyPolicyPopup} from './popup/privacy_policy'
|
import {PrivacyPolicyPopup} from './popup/privacy_policy'
|
||||||
import { GameTile, useGetGameInfoQuery } from '../state/api'
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
const flag = {
|
const flag = {
|
||||||
'Dutch': '🇳🇱',
|
'Dutch': '🇳🇱',
|
||||||
@@ -20,7 +19,6 @@ const flag = {
|
|||||||
'French': '🇫🇷',
|
'French': '🇫🇷',
|
||||||
'German': '🇩🇪',
|
'German': '🇩🇪',
|
||||||
'Italian': '🇮🇹',
|
'Italian': '🇮🇹',
|
||||||
'Spanish': '🇪🇸',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function GithubIcon({url='https://github.com'}) {
|
function GithubIcon({url='https://github.com'}) {
|
||||||
@@ -34,42 +32,47 @@ function GithubIcon({url='https://github.com'}) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
function Tile({gameId, data}: {gameId: string, data: GameTile|undefined}) {
|
function GameTile({
|
||||||
|
title,
|
||||||
|
gameId,
|
||||||
|
intro, // Catchy intro phrase.
|
||||||
|
image=null,
|
||||||
|
worlds='?',
|
||||||
|
levels='?',
|
||||||
|
prereq='–', // Optional list of games that this game builds on. Use markdown.
|
||||||
|
description, // Longer description. Supports Markdown.
|
||||||
|
language}) {
|
||||||
|
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
const routeChange = () =>{
|
const routeChange = () =>{
|
||||||
navigate(gameId);
|
navigate(gameId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof data === 'undefined') {
|
|
||||||
return <></>
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className="game" onClick={routeChange}>
|
return <div className="game" onClick={routeChange}>
|
||||||
<div className="wrapper">
|
<div className="wrapper">
|
||||||
<div className="title">{data.title}</div>
|
<div className="title">{title}</div>
|
||||||
<div className="short-description">{data.short}
|
<div className="short-description">{intro}
|
||||||
</div>
|
</div>
|
||||||
{ data.image ? <img className="image" src={path.join("data", gameId, data.image)} alt="" /> : <div className="image"/> }
|
{ image ? <img className="image" src={image} alt="" /> : <div className="image"/> }
|
||||||
<div className="long description"><Markdown>{data.long}</Markdown></div>
|
<div className="long description"><Markdown>{description}</Markdown></div>
|
||||||
</div>
|
</div>
|
||||||
<table className="info">
|
<table className="info">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td title="consider playing these games first.">Prerequisites</td>
|
<td title="consider playing these games first.">Prerequisites</td>
|
||||||
<td><Markdown>{data.prerequisites.join(', ')}</Markdown></td>
|
<td><Markdown>{prereq}</Markdown></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Worlds</td>
|
<td>Worlds</td>
|
||||||
<td>{data.worlds}</td>
|
<td>{worlds}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Levels</td>
|
<td>Levels</td>
|
||||||
<td>{data.levels}</td>
|
<td>{levels}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Language</td>
|
<td>Language</td>
|
||||||
<td title={`in ${data.languages.join(', ')}`}>{data.languages.map((lan) => flag[lan]).join(', ')}</td>
|
<td title={`in ${language}`}>{flag[language]}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -85,58 +88,6 @@ function LandingPage() {
|
|||||||
const openImpressum = () => setImpressum(true);
|
const openImpressum = () => setImpressum(true);
|
||||||
const closeImpressum = () => setImpressum(false);
|
const closeImpressum = () => setImpressum(false);
|
||||||
|
|
||||||
// const [allGames, setAllGames] = React.useState([])
|
|
||||||
// const [allTiles, setAllTiles] = React.useState([])
|
|
||||||
|
|
||||||
// const getTiles=()=>{
|
|
||||||
// fetch('featured_games.json', {
|
|
||||||
// headers : {
|
|
||||||
// 'Content-Type': 'application/json',
|
|
||||||
// 'Accept': 'application/json'
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// ).then(function(response){
|
|
||||||
// return response.json()
|
|
||||||
// }).then(function(data) {
|
|
||||||
// setAllGames(data.featured_games)
|
|
||||||
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// React.useEffect(()=>{
|
|
||||||
// getTiles()
|
|
||||||
// },[])
|
|
||||||
|
|
||||||
// React.useEffect(()=>{
|
|
||||||
|
|
||||||
// Promise.allSettled(
|
|
||||||
// allGames.map((gameId) => (
|
|
||||||
// fetch(`data/g/${gameId}/game.json`).catch(err => {return undefined})))
|
|
||||||
// ).then(responses =>
|
|
||||||
// responses.forEach((result) => console.log(result)))
|
|
||||||
// // Promise.all(responses.map(res => {
|
|
||||||
// // if (res.status == "fulfilled") {
|
|
||||||
// // console.log(res.value.json())
|
|
||||||
// // return res.value.json()
|
|
||||||
// // } else {
|
|
||||||
// // return undefined
|
|
||||||
// // }
|
|
||||||
// // }))
|
|
||||||
// // ).then(allData => {
|
|
||||||
// // setAllTiles(allData.map(data => data?.tile))
|
|
||||||
// // })
|
|
||||||
// },[allGames])
|
|
||||||
|
|
||||||
// TODO: I would like to read the supported games list form a JSON,
|
|
||||||
// Then load all these games in
|
|
||||||
//
|
|
||||||
let allGames = [
|
|
||||||
"leanprover-community/nng4",
|
|
||||||
"hhu-adam/robo",
|
|
||||||
"djvelleman/stg4",
|
|
||||||
"miguelmarco/STG4",
|
|
||||||
]
|
|
||||||
let allTiles = allGames.map((gameId) => (useGetGameInfoQuery({game: `g/${gameId}`}).data?.tile))
|
|
||||||
|
|
||||||
return <div className="landing-page">
|
return <div className="landing-page">
|
||||||
<header style={{backgroundImage: `url(${bgImage})`}}>
|
<header style={{backgroundImage: `url(${bgImage})`}}>
|
||||||
@@ -153,26 +104,49 @@ function LandingPage() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="game-list">
|
<div className="game-list">
|
||||||
{allTiles.length == 0 ?
|
|
||||||
<p>No Games loaded. Use <a>http://localhost:3000/#/g/local/FOLDER</a> to open a
|
<GameTile
|
||||||
game directly from a local folder.
|
title="Natural Number Game"
|
||||||
</p>
|
gameId="g/hhu-adam/NNG4"
|
||||||
: allGames.map((id, i) => (
|
intro="The classical introduction game for Lean."
|
||||||
<Tile
|
description="In this game you recreate the natural numbers $\mathbb{N}$ from the Peano axioms,
|
||||||
key={id}
|
learning the basics about theorem proving in Lean.
|
||||||
gameId={`g/${id}`}
|
|
||||||
data={allTiles[i]}
|
This is a good first introduction to Lean!"
|
||||||
/>
|
worlds="4"
|
||||||
))
|
levels="30"
|
||||||
}
|
language="English"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<GameTile
|
||||||
|
title="Formaloversum"
|
||||||
|
gameId="g/hhu-adam/Robo"
|
||||||
|
intro="Erkunde das Leansche Universum mit deinem Robo, welcher dir bei der Verständigung mit den Formalosophen zur Seite steht."
|
||||||
|
description="
|
||||||
|
Dieses Spiel führt die Grundlagen zur Beweisführung in Lean ein und schneidet danach verschiedene Bereiche des Bachelorstudiums an.
|
||||||
|
|
||||||
|
(Das Spiel befindet sich noch in der Entstehungsphase.)"
|
||||||
|
image={coverRobo}
|
||||||
|
language="German"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<GameTile
|
||||||
|
title="NNG (OLD)"
|
||||||
|
gameId="g/hhu-adam/nng4-old"
|
||||||
|
intro="The old version of the NNG copied from lean3."
|
||||||
|
description="This version is not maintained and might break at any point. You should play the new version instead"
|
||||||
|
worlds="9"
|
||||||
|
levels="72"
|
||||||
|
language="English"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<section>
|
<section>
|
||||||
<div className="wrapper">
|
<div className="wrapper">
|
||||||
<h2>Development notes</h2>
|
<h2>Development notes</h2>
|
||||||
<p>
|
<p>
|
||||||
As this server runs lean on our university machines, it has a limited capacity.
|
As this server runs lean on our university machines, it has a limited capacity.
|
||||||
Our current estimate is about 70 simultaneous games.
|
Our current estimate is about 55 copies of the NNG or 25 copies of games importing
|
||||||
We hope to address and test this limitation better in the future.
|
mathlib. We hope to address this limitation in the future.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Most aspects of the games and the infrastructure are still in development. Feel free to
|
Most aspects of the games and the infrastructure are still in development. Feel free to
|
||||||
@@ -186,19 +160,18 @@ function LandingPage() {
|
|||||||
<h2>Adding new games</h2>
|
<h2>Adding new games</h2>
|
||||||
<p>
|
<p>
|
||||||
If you are considering writing your own game, you should use
|
If you are considering writing your own game, you should use
|
||||||
the <a target="_blank" href="https://github.com/hhu-adam/GameSkeleton">GameSkeleton Github Repo</a> as
|
the <a target="_blank" href="https://github.com/hhu-adam/NNG4">NNG Github Repo</a> as
|
||||||
a template and read <a target="_blank" href="https://github.com/leanprover-community/lean4game/">How to Create a Game</a>.
|
a template.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
You can directly load your games into the server and play it using
|
There is an option to load and run your own games directly on the server,
|
||||||
the correct URL. The <a target="_blank" href="https://github.com/leanprover-community/lean4game/">instructions above</a> also
|
instructions are in the NNG repo. Since this is still in development we'd like to
|
||||||
explain the details for how to load your game to the server.
|
encourage you to contact us for support creating your own game. The documentation is
|
||||||
|
not polished yet.
|
||||||
We'd like to encourage you to contact us if you have any questions.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Featured games on this page are added manually.
|
To add games to this main page, you should get in contact as
|
||||||
Please get in contact and we-ll happily add yours.
|
games will need to be added manually.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -216,6 +189,9 @@ function LandingPage() {
|
|||||||
<a className="link" onClick={openImpressum}>Impressum</a>
|
<a className="link" onClick={openImpressum}>Impressum</a>
|
||||||
{impressum? <PrivacyPolicyPopup handleClose={closeImpressum} />: null}
|
{impressum? <PrivacyPolicyPopup handleClose={closeImpressum} />: null}
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
{/* <PrivacyPolicy/> */}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,19 +22,18 @@ import { ConnectionContext, connection, useLeanClient } from '../connection'
|
|||||||
import { useAppDispatch, useAppSelector } from '../hooks'
|
import { useAppDispatch, useAppSelector } from '../hooks'
|
||||||
import { useGetGameInfoQuery, useLoadInventoryOverviewQuery, useLoadLevelQuery } from '../state/api'
|
import { useGetGameInfoQuery, useLoadInventoryOverviewQuery, useLoadLevelQuery } from '../state/api'
|
||||||
import { changedSelection, codeEdited, selectCode, selectSelections, selectCompleted, helpEdited,
|
import { changedSelection, codeEdited, selectCode, selectSelections, selectCompleted, helpEdited,
|
||||||
selectHelp, selectDifficulty, selectInventory, selectTypewriterMode, changeTypewriterMode } from '../state/progress'
|
selectHelp, selectDifficulty, selectInventory } from '../state/progress'
|
||||||
import { store } from '../state/store'
|
import { store } from '../state/store'
|
||||||
import { Button } from './button'
|
import { Button } from './button'
|
||||||
import Markdown from './markdown'
|
import Markdown from './markdown'
|
||||||
import {InventoryOverviewPanel, InventoryPanel} from './inventory'
|
import {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'
|
||||||
import { DualEditor } from './infoview/main'
|
import { DualEditor } from './infoview/main'
|
||||||
import { GameHint } from './infoview/rpc_api'
|
import { GameHint } from './infoview/rpc_api'
|
||||||
import { DeletedHints, Hint, Hints, filterHints } from './hints'
|
import { DeletedHints, Hint, Hints } from './hints'
|
||||||
import { PrivacyPolicyPopup } from './popup/privacy_policy'
|
import { PrivacyPolicyPopup } from './popup/privacy_policy'
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
import '@fontsource/roboto/300.css'
|
import '@fontsource/roboto/300.css'
|
||||||
import '@fontsource/roboto/400.css'
|
import '@fontsource/roboto/400.css'
|
||||||
@@ -49,6 +48,7 @@ function Level() {
|
|||||||
const params = useParams()
|
const params = useParams()
|
||||||
const levelId = parseInt(params.levelId)
|
const levelId = parseInt(params.levelId)
|
||||||
const worldId = params.worldId
|
const worldId = params.worldId
|
||||||
|
// useLoadWorldFiles(worldId)
|
||||||
|
|
||||||
const [impressum, setImpressum] = React.useState(false)
|
const [impressum, setImpressum] = React.useState(false)
|
||||||
|
|
||||||
@@ -138,24 +138,19 @@ function ChatPanel({lastLevel}) {
|
|||||||
|
|
||||||
let introText: Array<string> = level?.data?.introduction.split(/\n(\s*\n)+/)
|
let introText: Array<string> = level?.data?.introduction.split(/\n(\s*\n)+/)
|
||||||
|
|
||||||
// experimental: Remove all hints that appeared identically in the previous step
|
|
||||||
// This effectively prevent consequtive hints being shown.
|
|
||||||
let modifiedHints : GameHint[][] = filterHints(proof)
|
|
||||||
|
|
||||||
return <div className="chat-panel">
|
return <div className="chat-panel">
|
||||||
<div ref={chatRef} className="chat">
|
<div ref={chatRef} className="chat">
|
||||||
{introText?.filter(t => t.trim()).map(((t, i) =>
|
{introText?.filter(t => t.trim()).map(((t, i) =>
|
||||||
// Show the level's intro text as hints, too
|
|
||||||
<Hint key={`intro-p-${i}`}
|
<Hint key={`intro-p-${i}`}
|
||||||
hint={{text: t, hidden: false}} step={0} selected={selectedStep} toggleSelection={toggleSelection(0)} />
|
hint={{text: t, hidden: false}} step={0} selected={selectedStep} toggleSelection={toggleSelection(0)} />
|
||||||
))}
|
))}
|
||||||
{modifiedHints.map((step, i) => {
|
{proof.map((step, i) => {
|
||||||
// It the last step has errors, it will have the same hints
|
// It the last step has errors, it will have the same hints
|
||||||
// as the second-to-last step. Therefore we should not display them.
|
// as the second-to-last step. Therefore we should not display them.
|
||||||
if (!(i == proof.length - 1 && withErr)) {
|
if (!(i == proof.length - 1 && withErr)) {
|
||||||
// TODO: Should not use index as key.
|
// TODO: Should not use index as key.
|
||||||
return <Hints key={`hints-${i}`}
|
return <Hints key={`hints-${i}`}
|
||||||
hints={step} showHidden={showHelp.has(i)} step={i}
|
hints={step.hints} showHidden={showHelp.has(i)} step={i}
|
||||||
selected={selectedStep} toggleSelection={toggleSelection(i)} lastLevel={i == proof.length - 1}/>
|
selected={selectedStep} toggleSelection={toggleSelection(i)} lastLevel={i == proof.length - 1}/>
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
@@ -209,16 +204,11 @@ function PlayableLevel({impressum, setImpressum}) {
|
|||||||
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
||||||
const {mobile} = React.useContext(MobileContext)
|
const {mobile} = React.useContext(MobileContext)
|
||||||
|
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
|
|
||||||
const difficulty = useSelector(selectDifficulty(gameId))
|
const difficulty = useSelector(selectDifficulty(gameId))
|
||||||
const initialCode = useAppSelector(selectCode(gameId, worldId, levelId))
|
const initialCode = useAppSelector(selectCode(gameId, worldId, levelId))
|
||||||
const initialSelections = useAppSelector(selectSelections(gameId, worldId, levelId))
|
const initialSelections = useAppSelector(selectSelections(gameId, worldId, levelId))
|
||||||
const inventory: Array<String> = useSelector(selectInventory(gameId))
|
const inventory: Array<String> = useSelector(selectInventory(gameId))
|
||||||
|
|
||||||
const typewriterMode = useSelector(selectTypewriterMode(gameId))
|
|
||||||
const setTypewriterMode = (newTypewriterMode: boolean) => dispatch(changeTypewriterMode({game: gameId, typewriterMode: newTypewriterMode}))
|
|
||||||
|
|
||||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||||
const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
||||||
|
|
||||||
@@ -231,11 +221,12 @@ function PlayableLevel({impressum, setImpressum}) {
|
|||||||
const [showHelp, setShowHelp] = useState<Set<number>>(new Set())
|
const [showHelp, setShowHelp] = useState<Set<number>>(new Set())
|
||||||
// Only for mobile layout
|
// Only for mobile layout
|
||||||
const [pageNumber, setPageNumber] = useState(0)
|
const [pageNumber, setPageNumber] = useState(0)
|
||||||
|
const [typewriterMode, setTypewriterMode] = useState(true)
|
||||||
// set to true to prevent switching between typewriter and editor
|
// set to true to prevent switching between typewriter and editor
|
||||||
const [lockInputMode, setLockInputMode] = useState(false)
|
const [lockInputMode, setLockInputMode] = useState(false)
|
||||||
const [typewriterInput, setTypewriterInput] = useState("")
|
const [typewriterInput, setTypewriterInput] = useState("")
|
||||||
const lastLevel = levelId >= gameInfo.data?.worldSize[worldId]
|
const lastLevel = levelId >= gameInfo.data?.worldSize[worldId]
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
// impressum pop-up
|
// impressum pop-up
|
||||||
function toggleImpressum() {setImpressum(!impressum)}
|
function toggleImpressum() {setImpressum(!impressum)}
|
||||||
@@ -329,6 +320,8 @@ function PlayableLevel({impressum, setImpressum}) {
|
|||||||
console.debug(`not inserting template.`)
|
console.debug(`not inserting template.`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
setTypewriterMode(true)
|
||||||
}
|
}
|
||||||
}, [level, levelId, worldId, gameId, editor])
|
}, [level, levelId, worldId, gameId, editor])
|
||||||
|
|
||||||
@@ -343,7 +336,7 @@ function PlayableLevel({impressum, setImpressum}) {
|
|||||||
}, [gameId, worldId, levelId])
|
}, [gameId, worldId, levelId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!typewriterMode && editor) {
|
if (!typewriterMode) {
|
||||||
// Delete last input attempt from command line
|
// Delete last input attempt from command line
|
||||||
editor.executeEdits("typewriter", [{
|
editor.executeEdits("typewriter", [{
|
||||||
range: editor.getSelection(),
|
range: editor.getSelection(),
|
||||||
@@ -473,11 +466,6 @@ function Introduction({impressum, setImpressum}) {
|
|||||||
|
|
||||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||||
|
|
||||||
const {worldId} = useContext(WorldLevelIdContext)
|
|
||||||
|
|
||||||
let image: string = gameInfo.data?.worlds.nodes[worldId].image
|
|
||||||
|
|
||||||
|
|
||||||
const toggleImpressum = () => {
|
const toggleImpressum = () => {
|
||||||
setImpressum(!impressum)
|
setImpressum(!impressum)
|
||||||
}
|
}
|
||||||
@@ -491,14 +479,8 @@ function Introduction({impressum, setImpressum}) {
|
|||||||
:
|
:
|
||||||
<Split minSize={0} snapOffset={200} sizes={[25, 50, 25]} className={`app-content level`}>
|
<Split minSize={0} snapOffset={200} sizes={[25, 50, 25]} className={`app-content level`}>
|
||||||
<IntroductionPanel gameInfo={gameInfo} />
|
<IntroductionPanel gameInfo={gameInfo} />
|
||||||
<div className="world-image-container empty">
|
<div className="world-image-container empty"></div>
|
||||||
{image &&
|
<InventoryPanel levelInfo={inventory?.data} />
|
||||||
// TODO: Temporary for testing
|
|
||||||
<img className={worldId=="Proposition" ? "cover" : "contain"} src={path.join("data", gameId, image)} alt="" />
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<InventoryOverviewPanel data={inventory?.data} />
|
|
||||||
</Split>
|
</Split>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -633,7 +615,6 @@ function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChange
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
editorConnection.api.sendClientNotification(uriStr, "textDocument/didClose", {textDocument: {uri: uriStr}})
|
editorConnection.api.sendClientNotification(uriStr, "textDocument/didClose", {textDocument: {uri: uriStr}})
|
||||||
model.dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [editor, levelId, connection, leanClientStarted])
|
}, [editor, levelId, connection, leanClientStarted])
|
||||||
@@ -656,3 +637,27 @@ function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChange
|
|||||||
|
|
||||||
return {editor, infoProvider, editorConnection}
|
return {editor, infoProvider, editorConnection}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Open all files in this world on the server so that they will load faster when accessed */
|
||||||
|
function useLoadWorldFiles(worldId) {
|
||||||
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (gameInfo.data) {
|
||||||
|
const models = []
|
||||||
|
for (let levelId = 1; levelId <= gameInfo.data.worldSize[worldId]; levelId++) {
|
||||||
|
const uri = monaco.Uri.parse(`file:///${worldId}/${levelId}`)
|
||||||
|
let model = monaco.editor.getModel(uri)
|
||||||
|
if (model) {
|
||||||
|
models.push(model)
|
||||||
|
} else {
|
||||||
|
const code = selectCode(gameId, worldId, levelId)(store.getState())
|
||||||
|
models.push(monaco.editor.createModel(code, 'lean4', uri))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return () => { for (let model of models) { model.dispose() } }
|
||||||
|
}
|
||||||
|
}, [gameInfo.data, worldId])
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ 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 { InventoryOverviewPanel, InventoryPanel } from './inventory'
|
import { 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'
|
||||||
@@ -111,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} />
|
||||||
:
|
:
|
||||||
<InventoryOverviewPanel data={inventory?.data} />
|
<InventoryPanel levelInfo={inventory?.data} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
@@ -119,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} />
|
||||||
<InventoryOverviewPanel data={inventory?.data} />
|
<InventoryPanel levelInfo={inventory?.data} />
|
||||||
</Split>
|
</Split>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
|
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
|
||||||
import { LeanClient } from 'lean4web/client/src/editor/leanclient';
|
import { LeanClient } from './leanclient';
|
||||||
|
|
||||||
export class Connection {
|
export class Connection {
|
||||||
private game: string = undefined // We only keep a connection to a single game at a time
|
private game: string = undefined // We only keep a connection to a single game at a time
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ a {
|
|||||||
border: 1px solid rgb(140, 140, 140);
|
border: 1px solid rgb(140, 140, 140);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
box-shadow: 5px 5px 8px rgb(140, 140, 140);
|
box-shadow: 5px 5px 8px rgb(140, 140, 140);
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -336,21 +336,6 @@ td code {
|
|||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.world-image-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.world-image-container img.contain {
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.world-image-container img.cover {
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.typewriter-interface .proof {
|
.typewriter-interface .proof {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,15 @@ import { monacoSetup } from 'lean4web/client/src/monacoSetup'
|
|||||||
|
|
||||||
monacoSetup()
|
monacoSetup()
|
||||||
|
|
||||||
|
// // Do not show the landing page in the dev-container context
|
||||||
|
// let root_path: RouteObject = (process.env.LEAN4GAME_SINGLE_GAME == "true") ? {
|
||||||
|
// path: "/",
|
||||||
|
// loader: () => redirect("/g/local/game")
|
||||||
|
// } : {
|
||||||
|
// path: "/",
|
||||||
|
// element: <LandingPage />,
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
// 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
|
||||||
@@ -34,11 +43,6 @@ const router = createHashRouter([
|
|||||||
path: "/game/nng",
|
path: "/game/nng",
|
||||||
loader: () => redirect("/g/hhu-adam/NNG4")
|
loader: () => redirect("/g/hhu-adam/NNG4")
|
||||||
},
|
},
|
||||||
{
|
|
||||||
// For backwards compatibility
|
|
||||||
path: "/g/hhu-adam/NNG4",
|
|
||||||
loader: () => redirect("/g/leanprover-community/NNG4")
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/g/:owner/:repo",
|
path: "/g/:owner/:repo",
|
||||||
element: <App />,
|
element: <App />,
|
||||||
|
|||||||
@@ -0,0 +1,612 @@
|
|||||||
|
/* This file is based on `vscode-lean4/src/leanclient.ts` */
|
||||||
|
|
||||||
|
import {
|
||||||
|
TextDocument, EventEmitter, Diagnostic,
|
||||||
|
DocumentHighlight, Range, DocumentHighlightKind, workspace,
|
||||||
|
Disposable, Uri, ConfigurationChangeEvent, OutputChannel, DiagnosticCollection,
|
||||||
|
WorkspaceFolder, window
|
||||||
|
} from 'vscode'
|
||||||
|
import {
|
||||||
|
DidChangeTextDocumentParams,
|
||||||
|
DidCloseTextDocumentParams,
|
||||||
|
DidOpenTextDocumentNotification,
|
||||||
|
DocumentFilter,
|
||||||
|
InitializeResult,
|
||||||
|
MonacoLanguageClient as LanguageClient,
|
||||||
|
LanguageClientOptions,
|
||||||
|
PublishDiagnosticsParams,
|
||||||
|
CloseAction, ErrorAction,
|
||||||
|
RevealOutputChannelOn,
|
||||||
|
} from 'monaco-languageclient'
|
||||||
|
import { State } from 'vscode-languageclient'
|
||||||
|
import * as ls from 'vscode-languageserver-protocol'
|
||||||
|
import { toSocket } from 'vscode-ws-jsonrpc'
|
||||||
|
|
||||||
|
import {
|
||||||
|
// toolchainPath, lakePath, addServerEnvPaths, serverArgs, serverLoggingEnabled, serverLoggingPath, shouldAutofocusOutput,
|
||||||
|
getElaborationDelay
|
||||||
|
// lakeEnabled
|
||||||
|
} from 'lean4web/client/src/editor/config'
|
||||||
|
// import { assert } from './utils/assert'
|
||||||
|
import { LeanFileProgressParams, LeanFileProgressProcessingInfo } from '@leanprover/infoview-api'
|
||||||
|
// import { LocalStorageService } from './utils/localStorage'
|
||||||
|
// import { batchExecute } from './utils/batch'
|
||||||
|
// import { readLeanVersion } from './utils/projectInfo'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import { URL } from 'url'
|
||||||
|
import { join } from 'path'
|
||||||
|
// import { logger } from './utils/logger'
|
||||||
|
import { SemVer } from 'semver'
|
||||||
|
// import { fileExists, isFileInFolder } from './utils/fsHelper'
|
||||||
|
import { c2pConverter, p2cConverter, patchConverters } from 'lean4web/client/src/editor/utils/converters'
|
||||||
|
import { WasmReader, WasmWriter, WebSocketMessageWriter, WebSocketMessageReader } from './wasm'
|
||||||
|
|
||||||
|
const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
|
|
||||||
|
export type ServerProgress = Map<Uri, LeanFileProgressProcessingInfo[]>
|
||||||
|
|
||||||
|
export function getFullRange (diag: Diagnostic): Range {
|
||||||
|
return (diag as any)?.fullRange || diag.range
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LeanClient implements Disposable {
|
||||||
|
running: boolean = false
|
||||||
|
private client: LanguageClient | undefined
|
||||||
|
// private toolchainPath: string
|
||||||
|
// private readonly outputChannel: OutputChannel
|
||||||
|
// private readonly storageManager: LocalStorageService
|
||||||
|
private readonly workspaceFolder: WorkspaceFolder | undefined
|
||||||
|
private readonly folderUri: Uri
|
||||||
|
private readonly subscriptions: Disposable[] = []
|
||||||
|
private noPrompt: boolean = false
|
||||||
|
private showingRestartMessage: boolean = false
|
||||||
|
// private readonly elanDefaultToolchain: string
|
||||||
|
|
||||||
|
private readonly didChangeEmitter = new EventEmitter<DidChangeTextDocumentParams>()
|
||||||
|
didChange = this.didChangeEmitter.event
|
||||||
|
|
||||||
|
private readonly diagnosticsEmitter = new EventEmitter<PublishDiagnosticsParams>()
|
||||||
|
diagnostics = this.diagnosticsEmitter.event
|
||||||
|
|
||||||
|
private readonly didSetLanguageEmitter = new EventEmitter<string>()
|
||||||
|
didSetLanguage = this.didSetLanguageEmitter.event
|
||||||
|
|
||||||
|
private readonly didCloseEmitter = new EventEmitter<DidCloseTextDocumentParams>()
|
||||||
|
didClose = this.didCloseEmitter.event
|
||||||
|
|
||||||
|
private readonly customNotificationEmitter = new EventEmitter<{ method: string, params: any }>()
|
||||||
|
/** Fires whenever a custom notification (i.e. one not defined in LSP) is received. */
|
||||||
|
customNotification = this.customNotificationEmitter.event
|
||||||
|
|
||||||
|
/** saved progress info in case infoview is opened, it needs to get all of it. */
|
||||||
|
progress: ServerProgress = new Map()
|
||||||
|
|
||||||
|
private readonly progressChangedEmitter = new EventEmitter<[string, LeanFileProgressProcessingInfo[]]>()
|
||||||
|
progressChanged = this.progressChangedEmitter.event
|
||||||
|
|
||||||
|
private readonly stoppedEmitter = new EventEmitter()
|
||||||
|
stopped = this.stoppedEmitter.event
|
||||||
|
|
||||||
|
private readonly restartedEmitter = new EventEmitter()
|
||||||
|
restarted = this.restartedEmitter.event
|
||||||
|
|
||||||
|
private readonly restartingEmitter = new EventEmitter()
|
||||||
|
restarting = this.restartingEmitter.event
|
||||||
|
|
||||||
|
private readonly restartedWorkerEmitter = new EventEmitter<string>()
|
||||||
|
restartedWorker = this.restartedWorkerEmitter.event
|
||||||
|
|
||||||
|
private readonly serverFailedEmitter = new EventEmitter<string>()
|
||||||
|
serverFailed = this.serverFailedEmitter.event
|
||||||
|
|
||||||
|
/** Files which are open. */
|
||||||
|
private readonly isOpen: Map<string, TextDocument> = new Map()
|
||||||
|
|
||||||
|
constructor (private readonly socketUrl: string, workspaceFolder: WorkspaceFolder | undefined, folderUri: Uri,
|
||||||
|
public readonly showRestartMessage: () => void) {
|
||||||
|
// this.storageManager = storageManager
|
||||||
|
// this.outputChannel WebSocketMessageWriter= outputChannel
|
||||||
|
this.workspaceFolder = workspaceFolder // can be null when opening adhoc files.
|
||||||
|
this.folderUri = folderUri
|
||||||
|
// this.elanDefaultToolchain = elanDefaultToolchain
|
||||||
|
// this.subscriptions.push(workspace.onDidChangeConfiguration((e) => this.configChanged(e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose (): void {
|
||||||
|
this.subscriptions.forEach((s) => s.dispose())
|
||||||
|
if (this.isStarted()) void this.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// async showRestartMessage (restartFile: boolean = false): Promise<void> {
|
||||||
|
// // if (!this.showingRestartMessage) {
|
||||||
|
// // this.showingRestartMessage = true
|
||||||
|
// // let restartItem: string
|
||||||
|
// // let messageTitle: string
|
||||||
|
// // if (!restartFile) {
|
||||||
|
// // restartItem = 'Restart Lean Server'
|
||||||
|
// // messageTitle = 'Lean Server has stopped unexpectedly.'
|
||||||
|
// // } else {
|
||||||
|
// // restartItem = 'Restart Lean Server on this file'
|
||||||
|
// // messageTitle = 'The Lean Server has stopped processing this file.'
|
||||||
|
// // }
|
||||||
|
// // const item = await this.showErrorMessage(messageTitle, restartItem)
|
||||||
|
// // this.showingRestartMessage = false
|
||||||
|
// // if (item === restartItem) {
|
||||||
|
// // void this.start()
|
||||||
|
// // // if (restartFile && (window.activeTextEditor != null)) {
|
||||||
|
// // // await this.restartFile(window.activeTextEditor.document)
|
||||||
|
// // // } else {
|
||||||
|
// // // void this.start()
|
||||||
|
// // // }
|
||||||
|
// // }
|
||||||
|
// // }
|
||||||
|
// }
|
||||||
|
|
||||||
|
async restart (): Promise<void> {
|
||||||
|
const startTime = Date.now()
|
||||||
|
|
||||||
|
console.log('[LeanClient] Restarting Lean Server')
|
||||||
|
if (this.isStarted()) {
|
||||||
|
await this.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.restartingEmitter.fire(undefined)
|
||||||
|
// this.toolchainPath = this.storageManager.getLeanPath()
|
||||||
|
// if (!this.toolchainPath) this.toolchainPath = toolchainPath()
|
||||||
|
// let version = this.storageManager.getLeanVersion()
|
||||||
|
// const env = addServerEnvPaths(process.env)
|
||||||
|
// if (serverLoggingEnabled()) {
|
||||||
|
// env.LEAN_SERVER_LOG_DIR = serverLoggingPath()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let executable = lakePath() ||
|
||||||
|
// (this.toolchainPath ? join(this.toolchainPath, 'bin', 'lake') : 'lake')
|
||||||
|
|
||||||
|
// check if the lake process will start (skip it on scheme: 'untitled' files)
|
||||||
|
// let useLake = lakeEnabled() && this.folderUri && this.folderUri.scheme === 'file'
|
||||||
|
// if (useLake) {
|
||||||
|
// let knownDate = false
|
||||||
|
// const lakefile = Uri.joinPath(this.folderUri, 'lakefile.lean')
|
||||||
|
// if (!await fileExists(new URL(lakefile.toString()))) {
|
||||||
|
// useLake = false
|
||||||
|
// } else {
|
||||||
|
// // see if we can avoid the more expensive checkLakeVersion call.
|
||||||
|
// const date = await this.checkToolchainVersion(this.folderUri)
|
||||||
|
// if (date != null) {
|
||||||
|
// // Feb 16 2022 is when the 3.1.0.pre was released.
|
||||||
|
// useLake = date >= new Date(2022, 1, 16)
|
||||||
|
// knownDate = true
|
||||||
|
// }
|
||||||
|
// if (useLake && !knownDate) {
|
||||||
|
// useLake = await this.checkLakeVersion(executable, version)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!useLake) {
|
||||||
|
// executable = (this.toolchainPath) ? join(this.toolchainPath, 'bin', 'lean') : 'lean'
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const cwd = this.folderUri?.fsPath
|
||||||
|
// if (!cwd && !version) {
|
||||||
|
// // Fixes issue #227, for adhoc files it would pick up the cwd from the open folder
|
||||||
|
// // which is not what we want. For adhoc files we want the (default) toolchain instead.
|
||||||
|
// version = this.elanDefaultToolchain
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let options = version ? ['+' + version] : []
|
||||||
|
// if (useLake) {
|
||||||
|
// options = options.concat(['serve', '--'])
|
||||||
|
// } else {
|
||||||
|
// options = options.concat(['--server'])
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Add folder name to command-line so that it shows up in `ps aux`.
|
||||||
|
// if (cwd) {
|
||||||
|
// options.push('' + cwd)
|
||||||
|
// } else {
|
||||||
|
// options.push('untitled')
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const serverOptions: ServerOptions = {
|
||||||
|
// command: executable,
|
||||||
|
// args: options.concat(serverArgs()),
|
||||||
|
// options: {
|
||||||
|
// cwd,
|
||||||
|
// env
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
const clientOptions: LanguageClientOptions = {
|
||||||
|
// use a language id as a document selector
|
||||||
|
documentSelector: ['lean4'],
|
||||||
|
initializationOptions: {
|
||||||
|
editDelay: getElaborationDelay(), hasWidgets: true
|
||||||
|
},
|
||||||
|
connectionOptions: {
|
||||||
|
maxRestartCount: 0,
|
||||||
|
cancellationStrategy: undefined as any
|
||||||
|
},
|
||||||
|
// disable the default error handler
|
||||||
|
errorHandler: {
|
||||||
|
error: () => ({ action: ErrorAction.Continue }),
|
||||||
|
closed: () => ({ action: CloseAction.DoNotRestart })
|
||||||
|
},
|
||||||
|
middleware: {
|
||||||
|
handleDiagnostics: (uri, diagnostics, next) => {
|
||||||
|
next(uri, diagnostics)
|
||||||
|
if (this.client == null) return
|
||||||
|
const uri_ = c2pConverter.asUri(uri)
|
||||||
|
const diagnostics_ = []
|
||||||
|
for (const d of diagnostics) {
|
||||||
|
const d_: ls.Diagnostic = {
|
||||||
|
...c2pConverter.asDiagnostic(d)
|
||||||
|
}
|
||||||
|
diagnostics_.push(d_)
|
||||||
|
}
|
||||||
|
this.diagnosticsEmitter.fire({ uri: uri_, diagnostics: diagnostics_ })
|
||||||
|
},
|
||||||
|
|
||||||
|
// didOpen: async () => {
|
||||||
|
// // Note: as per the LSP spec: An open notification must not be sent more than once
|
||||||
|
// // without a corresponding close notification send before. This means open and close
|
||||||
|
// // notification must be balanced and the max open count for a particular textDocument
|
||||||
|
// // is one. So this even does nothing the notification is handled by the
|
||||||
|
// // openLean4Document method below after the 'lean4' languageId is established and
|
||||||
|
// // it has weeded out documents opened to invisible editors (like 'git:' schemes and
|
||||||
|
// // invisible editors created for Ctrl+Hover events. A side effect of unbalanced
|
||||||
|
// // open/close notification is leaking 'lean --worker' processes.
|
||||||
|
// // See https://github.com/microsoft/vscode/issues/78453).
|
||||||
|
|
||||||
|
// },
|
||||||
|
|
||||||
|
didChange: async (data, next) => {
|
||||||
|
await next(data)
|
||||||
|
if (!this.running || (this.client == null)) return // there was a problem starting lean server.
|
||||||
|
const params = c2pConverter.asChangeTextDocumentParams(data)
|
||||||
|
this.didChangeEmitter.fire(params)
|
||||||
|
},
|
||||||
|
|
||||||
|
didClose: async (doc, next) => {
|
||||||
|
if (!this.isOpen.delete(doc.uri.toString())) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await next(doc)
|
||||||
|
if (!this.running || (this.client == null)) return // there was a problem starting lean server.
|
||||||
|
const params = c2pConverter.asCloseTextDocumentParams(doc)
|
||||||
|
this.didCloseEmitter.fire(params)
|
||||||
|
},
|
||||||
|
|
||||||
|
provideDocumentHighlights: async (doc, pos, ctok, next) => {
|
||||||
|
const leanHighlights = await next(doc, pos, ctok)
|
||||||
|
if (leanHighlights?.length) return leanHighlights
|
||||||
|
|
||||||
|
// vscode doesn't fall back to textual highlights,
|
||||||
|
// so we need to do that manually
|
||||||
|
await new Promise((res) => setTimeout(res, 250))
|
||||||
|
if (ctok.isCancellationRequested) return
|
||||||
|
|
||||||
|
const wordRange = doc.getWordRangeAtPosition(pos)
|
||||||
|
if (wordRange == null) return
|
||||||
|
const word = doc.getText(wordRange)
|
||||||
|
|
||||||
|
const highlights: DocumentHighlight[] = []
|
||||||
|
const text = doc.getText()
|
||||||
|
const nonWordPattern = '[`~@$%^&*()-=+\\[{\\]}⟨⟩⦃⦄⟦⟧⟮⟯‹›\\\\|;:\",./\\s]|^|$'
|
||||||
|
const regexp = new RegExp(`(?<=${nonWordPattern})${escapeRegExp(word)}(?=${nonWordPattern})`, 'g')
|
||||||
|
for (const match of text.matchAll(regexp)) {
|
||||||
|
const start = doc.positionAt(match.index ?? 0)
|
||||||
|
highlights.push({
|
||||||
|
range: new Range(start, start.translate(0, match[0].length)),
|
||||||
|
kind: DocumentHighlightKind.Text
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return highlights
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!this.client) {
|
||||||
|
this.client = new LanguageClient({
|
||||||
|
id: 'lean4',
|
||||||
|
name: 'Lean 4',
|
||||||
|
clientOptions,
|
||||||
|
connectionProvider: {
|
||||||
|
get: async () => {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const worker = new Worker("worker.js")
|
||||||
|
const reader = new WasmReader(worker)
|
||||||
|
const writer = new WasmWriter(worker)
|
||||||
|
resolve({
|
||||||
|
reader,
|
||||||
|
writer
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await this.client.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// HACK: Prevent monaco from panicking when the Lean server crashes
|
||||||
|
this.client.handleFailedRequest = (type, token: any, error: any, defaultValue, showNotification?: boolean) => {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
let insideRestart = true
|
||||||
|
patchConverters(this.client.protocol2CodeConverter, this.client.code2ProtocolConverter)
|
||||||
|
try {
|
||||||
|
this.client.onDidChangeState(async (s) => {
|
||||||
|
// see https://github.com/microsoft/vscode-languageserver-node/issues/825
|
||||||
|
if (s.newState === State.Starting) {
|
||||||
|
console.log('[LeanClient] starting')
|
||||||
|
} else if (s.newState === State.Running) {
|
||||||
|
const end = Date.now()
|
||||||
|
console.log(`[LeanClient] running, started in ${end - startTime} ms`)
|
||||||
|
this.running = true // may have been auto restarted after it failed.
|
||||||
|
if (!insideRestart) {
|
||||||
|
this.restartedEmitter.fire(undefined)
|
||||||
|
}
|
||||||
|
} else if (s.newState === State.Stopped) {
|
||||||
|
this.running = false
|
||||||
|
console.log('[LeanClient] has stopped or it failed to start')
|
||||||
|
if (!this.noPrompt) {
|
||||||
|
// only raise this event and show the message if we are not the ones
|
||||||
|
// who called the stop() method.
|
||||||
|
this.stoppedEmitter.fire({ message: 'Lean server has stopped.', reason: '' })
|
||||||
|
await this.showRestartMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await this.client.start()
|
||||||
|
// tell the new client about the documents that are already open!
|
||||||
|
// for (const key of this.isOpen.keys()) {
|
||||||
|
// const doc = this.isOpen.get(key)
|
||||||
|
// if (doc != null) this.notifyDidOpen(doc)
|
||||||
|
// }
|
||||||
|
// if we got this far then the client is happy so we are running!
|
||||||
|
this.running = true
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
this.serverFailedEmitter.fire('' + error)
|
||||||
|
insideRestart = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// HACK(WN): Register a default notification handler to fire on custom notifications.
|
||||||
|
// A mechanism to do this is provided in vscode-jsonrpc. One can register a `StarNotificationHandler`
|
||||||
|
// here: https://github.com/microsoft/vscode-languageserver-node/blob/b2fc85d28a1a44c22896559ee5f4d3ba37a02ef5/jsonrpc/src/common/connection.ts#L497
|
||||||
|
// which fires on any LSP notifications not in the standard, for example the `$/lean/..` ones.
|
||||||
|
// However this mechanism is not exposed in vscode-languageclient, so we hack around its implementation.
|
||||||
|
const starHandler = (method: string, params_: any) => {
|
||||||
|
if (method === '$/lean/fileProgress' && (this.client != null)) {
|
||||||
|
const params = params_ as LeanFileProgressParams
|
||||||
|
const uri = p2cConverter.asUri(params.textDocument.uri)
|
||||||
|
this.progressChangedEmitter.fire([uri.toString(), params.processing])
|
||||||
|
// save the latest progress on this Uri in case infoview needs it later.
|
||||||
|
this.progress.set(uri, params.processing)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.customNotificationEmitter.fire({ method, params: params_ })
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
|
this.client.onNotification(starHandler as any, () => {})
|
||||||
|
|
||||||
|
// Reveal the standard error output channel when the server prints something to stderr.
|
||||||
|
// The vscode-languageclient library already takes care of writing it to the output channel.
|
||||||
|
// let stderrMsgBoxVisible = false;
|
||||||
|
// (this.client)._serverProcess.stderr.on('data', async (chunk: Buffer) => {
|
||||||
|
// if (shouldAutofocusOutput()) {
|
||||||
|
// this.client?.outputChannel.show(true)
|
||||||
|
// } else if (!stderrMsgBoxVisible) {
|
||||||
|
// stderrMsgBoxVisible = true
|
||||||
|
// const outputItem = 'Show stderr output'
|
||||||
|
// const outPrompt = `Lean server printed an error:\n${chunk.toString()}`
|
||||||
|
// if (await window.showErrorMessage(outPrompt, outputItem) === outputItem) {
|
||||||
|
// this.outputChannel.show(false)
|
||||||
|
// }
|
||||||
|
// stderrMsgBoxVisible = false
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
this.restartedEmitter.fire(undefined)
|
||||||
|
insideRestart = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async openLean4Document (doc: TextDocument) {
|
||||||
|
if (this.isOpen.has(doc.uri.toString())) return
|
||||||
|
if (!await this.isSameWorkspace(doc.uri)) {
|
||||||
|
// skip it, this file belongs to a different workspace...
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isOpen.set(doc.uri.toString(), doc)
|
||||||
|
|
||||||
|
if (!this.running) return // there was a problem starting lean server.
|
||||||
|
|
||||||
|
// didOpenEditor may have also changed the language, so we fire the
|
||||||
|
// event here because the InfoView should be wired up to receive it now.
|
||||||
|
this.didSetLanguageEmitter.fire(doc.languageId)
|
||||||
|
|
||||||
|
this.notifyDidOpen(doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyDidOpen (doc: TextDocument) {
|
||||||
|
// BUG: was `DidOpenTextDocumentNotification.type` instead of the string, but that failed
|
||||||
|
void this.client?.sendNotification('textDocument/didOpen', {
|
||||||
|
textDocument: {
|
||||||
|
uri: doc.uri.toString(),
|
||||||
|
languageId: doc.languageId,
|
||||||
|
version: 1,
|
||||||
|
text: doc.getText()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async isSameWorkspace (uri: Uri): Promise<boolean> {
|
||||||
|
// if (this.folderUri) {
|
||||||
|
// if (this.folderUri.scheme !== uri.scheme) return false
|
||||||
|
// if (this.folderUri.scheme === 'file') {
|
||||||
|
// const realPath1 = await fs.promises.realpath(this.folderUri.fsPath)
|
||||||
|
// const realPath2 = await fs.promises.realpath(uri.fsPath)
|
||||||
|
// return isFileInFolder(realPath2, realPath1)
|
||||||
|
// } else {
|
||||||
|
// return uri.toString().startsWith(this.folderUri.toString())
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// return uri.scheme === 'untitled'
|
||||||
|
// }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
getWorkspaceFolder (): string {
|
||||||
|
return this.folderUri?.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
async start (): Promise<void> {
|
||||||
|
return await this.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
isStarted (): boolean {
|
||||||
|
return this.client !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
isRunning (): boolean {
|
||||||
|
if (this.client != null) {
|
||||||
|
return this.running
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop (): Promise<void> {
|
||||||
|
// assert(() => this.isStarted())
|
||||||
|
if ((this.client != null) && this.running) {
|
||||||
|
this.noPrompt = true
|
||||||
|
try {
|
||||||
|
// some timing conditions can happen while running unit tests that cause
|
||||||
|
// this to throw an exception which then causes those tests to fail.
|
||||||
|
await this.client.stop()
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[LeanClient] Error stopping language client: ${e}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.noPrompt = false
|
||||||
|
this.progress = new Map()
|
||||||
|
this.client = undefined
|
||||||
|
this.running = false
|
||||||
|
}
|
||||||
|
|
||||||
|
configChanged (e: ConfigurationChangeEvent): void {
|
||||||
|
// let newToolchainPath = this.storageManager.getLeanPath()
|
||||||
|
// if (!newToolchainPath) newToolchainPath = toolchainPath()
|
||||||
|
// if (this.toolchainPath !== newToolchainPath) {
|
||||||
|
// void this.restart()
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
async restartFile (doc: TextDocument): Promise<void> {
|
||||||
|
if (!this.running) return // there was a problem starting lean server.
|
||||||
|
|
||||||
|
// assert(() => this.isStarted())
|
||||||
|
|
||||||
|
if (!await this.isSameWorkspace(doc.uri)) {
|
||||||
|
// skip it, this file belongs to a different workspace...
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const uri = doc.uri.toString()
|
||||||
|
console.log(`[LeanClient] Restarting File: ${uri}`)
|
||||||
|
// This causes a text document version number discontinuity. In
|
||||||
|
// (didChange (oldVersion) => restartFile => didChange (newVersion))
|
||||||
|
// the client emits newVersion = oldVersion + 1, despite the fact that the
|
||||||
|
// didOpen packet emitted below initializes the version number to be 1.
|
||||||
|
// This is not a problem though, since both client and server are fine
|
||||||
|
// as long as the version numbers are monotonous.
|
||||||
|
void this.client?.sendNotification('textDocument/didClose', {
|
||||||
|
textDocument: {
|
||||||
|
uri
|
||||||
|
}
|
||||||
|
})
|
||||||
|
void this.client?.sendNotification('textDocument/didOpen', {
|
||||||
|
textDocument: {
|
||||||
|
uri,
|
||||||
|
languageId: 'lean4',
|
||||||
|
version: 1,
|
||||||
|
text: doc.getText()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.restartedWorkerEmitter.fire(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
|
async sendRequest (method: string, params: any): Promise<any> {
|
||||||
|
return this.running && (this.client != null)
|
||||||
|
? await this.client.sendRequest(method, params)
|
||||||
|
: await new Promise<any>((_, reject) => { reject('Client is not running') })
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
|
sendNotification (method: string, params: any): Promise<void> | undefined {
|
||||||
|
return this.running && (this.client != null) ? this.client.sendNotification(method, params) : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDiagnosticParams (uri: Uri, diagnostics: readonly Diagnostic[]): Promise<PublishDiagnosticsParams> {
|
||||||
|
const params: PublishDiagnosticsParams = {
|
||||||
|
uri: c2pConverter.asUri(uri),
|
||||||
|
diagnostics: await c2pConverter.asDiagnostics(diagnostics as Diagnostic[])
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
getDiagnostics (): DiagnosticCollection | undefined {
|
||||||
|
return this.running ? this.client?.diagnostics : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
get initializeResult (): InitializeResult | undefined {
|
||||||
|
return this.running ? this.client?.initializeResult : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkToolchainVersion (folderUri: Uri): Promise<Date | undefined> {
|
||||||
|
// see if we have a well known toolchain label that corresponds
|
||||||
|
// to a known date like 'leanprover/lean4:nightly-2022-02-01'
|
||||||
|
// const toolchainVersion = await readLeanVersion(folderUri)
|
||||||
|
// if (toolchainVersion) {
|
||||||
|
// const match = /^leanprover\/lean4:nightly-(\d+)-(\d+)-(\d+)$/.exec(toolchainVersion)
|
||||||
|
// if (match != null) {
|
||||||
|
// return new Date(parseInt(match[1]), parseInt(match[2]) - 1, parseInt(match[3]))
|
||||||
|
// }
|
||||||
|
// if (toolchainVersion === 'leanprover/lean4:stable') {
|
||||||
|
// return new Date(2022, 2, 1)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// async checkLakeVersion (executable: string, version: string): Promise<boolean> {
|
||||||
|
// // Check that the Lake version is high enough to support "lake serve" option.
|
||||||
|
// const versionOptions = version ? ['+' + version, '--version'] : ['--version']
|
||||||
|
// const start = Date.now()
|
||||||
|
// const lakeVersion = await batchExecute(executable, versionOptions, this.folderUri?.fsPath, undefined)
|
||||||
|
// logger.log(`[LeanClient] Ran '${executable} ${versionOptions.join(' ')}' in ${Date.now() - start} ms`)
|
||||||
|
// const actual = this.extractVersion(lakeVersion)
|
||||||
|
// if (actual.compare('3.0.0') > 0) {
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
|
||||||
|
// private extractVersion (v: string | undefined): SemVer {
|
||||||
|
// if (!v) return new SemVer('0.0.0')
|
||||||
|
// const prefix = 'Lake version'
|
||||||
|
// if (v.startsWith(prefix)) v = v.slice(prefix.length).trim()
|
||||||
|
// const pos = v.indexOf('(')
|
||||||
|
// if (pos > 0) v = v.slice(0, pos).trim()
|
||||||
|
// try {
|
||||||
|
// return new SemVer(v)
|
||||||
|
// } catch {
|
||||||
|
// return new SemVer('0.0.0')
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
+25
-28
@@ -2,29 +2,16 @@
|
|||||||
* @fileOverview Define API of the server-client communication
|
* @fileOverview Define API of the server-client communication
|
||||||
*/
|
*/
|
||||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
||||||
|
import { Connection } from '../connection'
|
||||||
|
|
||||||
export interface GameTile {
|
|
||||||
title: string
|
|
||||||
short: string
|
|
||||||
long: string
|
|
||||||
languages: Array<string>
|
|
||||||
prerequisites: Array<string>
|
|
||||||
worlds: number
|
|
||||||
levels: number
|
|
||||||
image: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GameInfo {
|
export interface GameInfo {
|
||||||
title: null|string,
|
title: null|string,
|
||||||
introduction: null|string,
|
introduction: null|string,
|
||||||
info: null|string,
|
info: null|string,
|
||||||
worlds: null|{nodes: {[id:string]: {id: string, title: string, introduction: string, image: string}}, edges: string[][]},
|
worlds: null|{nodes: {[id:string]: {id: string, title: string, introduction: string}}, edges: string[][]},
|
||||||
worldSize: null|{[key: string]: number},
|
worldSize: null|{[key: string]: number},
|
||||||
authors: null|string[],
|
authors: null|string[],
|
||||||
conclusion: null|string,
|
conclusion: null|string,
|
||||||
tile: null|GameTile,
|
|
||||||
image: null|string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InventoryTile {
|
export interface InventoryTile {
|
||||||
@@ -50,8 +37,7 @@ export interface LevelInfo {
|
|||||||
lemmaTab: null|string,
|
lemmaTab: null|string,
|
||||||
statementName: null|string,
|
statementName: null|string,
|
||||||
displayName: null|string,
|
displayName: null|string,
|
||||||
template: null|string,
|
template: null|string
|
||||||
image: null|string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Used to display the inventory on the welcome page */
|
/** Used to display the inventory on the welcome page */
|
||||||
@@ -71,29 +57,40 @@ interface Doc {
|
|||||||
category: string,
|
category: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorldOverview {
|
|
||||||
world: string
|
const customBaseQuery = async (
|
||||||
tactics: InventoryTile[]
|
args : {game: string, method: string, params?: any},
|
||||||
lemmas: InventoryTile[]
|
{ signal, dispatch, getState, extra },
|
||||||
definitions: InventoryTile[]
|
extraOptions
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const connection : Connection = extra.connection
|
||||||
|
let leanClient = await connection.startLeanClient(args.game)
|
||||||
|
console.log(`Sending request ${args.method}`)
|
||||||
|
let res = await leanClient.sendRequest(args.method, args.params)
|
||||||
|
console.log('Received response') //, res)
|
||||||
|
return {'data': res}
|
||||||
|
} catch (e) {
|
||||||
|
return {'error': e}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define a service using a base URL and expected endpoints
|
// Define a service using a base URL and expected endpoints
|
||||||
export const apiSlice = createApi({
|
export const apiSlice = createApi({
|
||||||
reducerPath: 'gameApi',
|
reducerPath: 'gameApi',
|
||||||
baseQuery: fetchBaseQuery({ baseUrl: window.location.origin + "/data" }),
|
baseQuery: customBaseQuery,
|
||||||
endpoints: (builder) => ({
|
endpoints: (builder) => ({
|
||||||
getGameInfo: builder.query<GameInfo, {game: string}>({
|
getGameInfo: builder.query<GameInfo, {game: string}>({
|
||||||
query: ({game}) => `${game}/game.json`,
|
query: ({game}) => {return {game, method: 'info', params: {}}},
|
||||||
}),
|
}),
|
||||||
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}) => {return {game, method: "loadLevel", params: {world, level}}},
|
||||||
}),
|
}),
|
||||||
loadInventoryOverview: builder.query<WorldOverview[], {game: string}>({
|
loadInventoryOverview: builder.query<InventoryOverview, {game: string}>({
|
||||||
query: ({game}) => `${game}/inventory.json`,
|
query: ({game}) => {return {game, method: "loadInventoryOverview", params: {}}},
|
||||||
}),
|
}),
|
||||||
loadDoc: builder.query<Doc, {game: string, name: string, type: "lemma"|"tactic"}>({
|
loadDoc: builder.query<Doc, {game: string, name: string, type: "lemma"|"tactic"}>({
|
||||||
query: ({game, type, name}) => `${game}/doc__${type}__${name}.json`,
|
query: ({game, name, type}) => {return {game, method: "loadDoc", params: {name, type}}},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ export interface GameProgressState {
|
|||||||
inventory: string[],
|
inventory: string[],
|
||||||
difficulty: number,
|
difficulty: number,
|
||||||
openedIntro: boolean,
|
openedIntro: boolean,
|
||||||
data: WorldProgressState,
|
data: WorldProgressState
|
||||||
typewriterMode?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -127,11 +126,6 @@ export const progressSlice = createSlice({
|
|||||||
addGameProgress(state, action)
|
addGameProgress(state, action)
|
||||||
state.games[action.payload.game].openedIntro = action.payload.openedIntro
|
state.games[action.payload.game].openedIntro = action.payload.openedIntro
|
||||||
},
|
},
|
||||||
/** set the typewriter mode */
|
|
||||||
changeTypewriterMode(state: ProgressState, action: PayloadAction<{game: string, typewriterMode: boolean}>) {
|
|
||||||
addGameProgress(state, action)
|
|
||||||
state.games[action.payload.game].typewriterMode = action.payload.typewriterMode
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -202,14 +196,7 @@ export function selectOpenedIntro(game: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** return typewriter mode for the current game if it exists */
|
|
||||||
export function selectTypewriterMode(game: string) {
|
|
||||||
return (state) => {
|
|
||||||
return state.progress.games[game]?.typewriterMode ?? true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Export actions to modify the progress */
|
/** Export actions to modify the progress */
|
||||||
export const { changedSelection, codeEdited, levelCompleted, deleteProgress,
|
export const { changedSelection, codeEdited, levelCompleted, deleteProgress,
|
||||||
deleteLevelProgress, loadProgress, helpEdited, changedInventory, changedOpenedIntro,
|
deleteLevelProgress, loadProgress, helpEdited, changedInventory, changedOpenedIntro,
|
||||||
changedDifficulty, changeTypewriterMode} = progressSlice.actions
|
changedDifficulty } = progressSlice.actions
|
||||||
|
|||||||
@@ -0,0 +1,341 @@
|
|||||||
|
|
||||||
|
|
||||||
|
import { DataCallback, AbstractMessageReader, MessageReader } from 'vscode-jsonrpc/lib/common/messageReader.js';
|
||||||
|
|
||||||
|
import { Message } from 'vscode-jsonrpc/lib/common/messages.js';
|
||||||
|
import { AbstractMessageWriter, MessageWriter } from 'vscode-jsonrpc/lib/common/messageWriter.js';
|
||||||
|
import { Emitter } from 'vscode-jsonrpc/lib/common/events.js';
|
||||||
|
import { Disposable, IWebSocket } from 'vscode-ws-jsonrpc/.';
|
||||||
|
|
||||||
|
declare var IO: any;
|
||||||
|
|
||||||
|
export class WasmWriter implements MessageWriter {
|
||||||
|
protected errorCount = 0;
|
||||||
|
errorEmitter
|
||||||
|
closeEmitter
|
||||||
|
constructor(private worker: Worker) {
|
||||||
|
this.errorEmitter = new Emitter()
|
||||||
|
this.closeEmitter = new Emitter()
|
||||||
|
}
|
||||||
|
dispose() {
|
||||||
|
this.errorEmitter.dispose();
|
||||||
|
this.closeEmitter.dispose();
|
||||||
|
}
|
||||||
|
get onError() {
|
||||||
|
return this.errorEmitter.event;
|
||||||
|
}
|
||||||
|
fireError(error, message, count) {
|
||||||
|
this.errorEmitter.fire([this.asError(error), message, count]);
|
||||||
|
}
|
||||||
|
get onClose() {
|
||||||
|
return this.closeEmitter.event;
|
||||||
|
}
|
||||||
|
fireClose() {
|
||||||
|
this.closeEmitter.fire(undefined);
|
||||||
|
}
|
||||||
|
asError(error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return new Error(`Writer received error. Reason: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
end(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
async write(msg: Message): Promise<void> {
|
||||||
|
try {
|
||||||
|
const content = JSON.stringify(msg);
|
||||||
|
this.worker.postMessage(content)
|
||||||
|
} catch (e) {
|
||||||
|
this.errorCount++;
|
||||||
|
this.fireError(e, msg, this.errorCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class WasmReader implements MessageReader {
|
||||||
|
protected state: 'initial' | 'listening' | 'closed' = 'initial';
|
||||||
|
protected callback: DataCallback | undefined;
|
||||||
|
protected readonly events: { message?: any, error?: any }[] = [];
|
||||||
|
|
||||||
|
constructor(private worker: Worker) {
|
||||||
|
this.worker.onmessage = (ev) => {
|
||||||
|
this.readMessage(ev.data)
|
||||||
|
}
|
||||||
|
// this.socket.onMessage(message =>
|
||||||
|
// this.readMessage(message)
|
||||||
|
// );
|
||||||
|
// this.socket.onError(error =>
|
||||||
|
// this.fireError(error)
|
||||||
|
// );
|
||||||
|
// this.socket.onClose((code, reason) => {
|
||||||
|
// if (code !== 1000) {
|
||||||
|
// const error: Error = {
|
||||||
|
// name: '' + code,
|
||||||
|
// message: `Error during socket reconnect: code = ${code}, reason = ${reason}`
|
||||||
|
// };
|
||||||
|
// this.fireError(error);
|
||||||
|
// }
|
||||||
|
// this.fireClose();
|
||||||
|
// });
|
||||||
|
this.errorEmitter = new Emitter()
|
||||||
|
this.closeEmitter = new Emitter()
|
||||||
|
this.partialMessageEmitter = new Emitter()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected errorCount = 0;
|
||||||
|
errorEmitter
|
||||||
|
closeEmitter
|
||||||
|
partialMessageEmitter
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.errorEmitter.dispose();
|
||||||
|
this.closeEmitter.dispose();
|
||||||
|
}
|
||||||
|
get onError() {
|
||||||
|
return this.errorEmitter.event;
|
||||||
|
}
|
||||||
|
get onClose() {
|
||||||
|
return this.closeEmitter.event;
|
||||||
|
}
|
||||||
|
get onPartialMessage() {
|
||||||
|
return this.partialMessageEmitter.event;
|
||||||
|
}
|
||||||
|
firePartialMessage(info) {
|
||||||
|
this.partialMessageEmitter.fire(info);
|
||||||
|
}
|
||||||
|
asError(error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return new Error(`Reader received error. Reason: ${error.message ? error.message : 'unknown'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listen(callback: DataCallback): Disposable {
|
||||||
|
if (this.state === 'initial') {
|
||||||
|
this.state = 'listening';
|
||||||
|
this.callback = callback;
|
||||||
|
while (this.events.length !== 0) {
|
||||||
|
const event = this.events.pop()!;
|
||||||
|
if (event.message) {
|
||||||
|
this.readMessage(event.message);
|
||||||
|
} else if (event.error) {
|
||||||
|
this.fireError(event.error);
|
||||||
|
} else {
|
||||||
|
this.fireClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
dispose: () => {
|
||||||
|
if (this.callback === callback) {
|
||||||
|
this.callback = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected readMessage(message: any): void {
|
||||||
|
if (this.state === 'initial') {
|
||||||
|
this.events.splice(0, 0, { message });
|
||||||
|
} else if (this.state === 'listening') {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(message);
|
||||||
|
this.callback!(data);
|
||||||
|
} catch (err) {
|
||||||
|
const error: Error = {
|
||||||
|
name: '' + 400,
|
||||||
|
message: `Error during message parsing, reason = ${typeof err === 'object' ? (err as any).message : 'unknown'}`
|
||||||
|
};
|
||||||
|
this.fireError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fireError(error: any): void {
|
||||||
|
if (this.state === 'initial') {
|
||||||
|
this.events.splice(0, 0, { error });
|
||||||
|
} else if (this.state === 'listening') {
|
||||||
|
|
||||||
|
this.errorEmitter.fire(this.asError(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fireClose(): void {
|
||||||
|
if (this.state === 'initial') {
|
||||||
|
this.events.splice(0, 0, {});
|
||||||
|
} else if (this.state === 'listening') {
|
||||||
|
this.closeEmitter.fire(undefined);
|
||||||
|
}
|
||||||
|
this.state = 'closed';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class WebSocketMessageWriter implements MessageWriter {
|
||||||
|
protected errorCount = 0;
|
||||||
|
errorEmitter
|
||||||
|
closeEmitter
|
||||||
|
|
||||||
|
constructor(protected readonly socket: IWebSocket) {
|
||||||
|
this.errorEmitter = new Emitter();
|
||||||
|
this.closeEmitter = new Emitter();
|
||||||
|
}
|
||||||
|
dispose() {
|
||||||
|
this.errorEmitter.dispose();
|
||||||
|
this.closeEmitter.dispose();
|
||||||
|
}
|
||||||
|
get onError() {
|
||||||
|
return this.errorEmitter.event;
|
||||||
|
}
|
||||||
|
fireError(error, message, count) {
|
||||||
|
this.errorEmitter.fire([this.asError(error), message, count]);
|
||||||
|
}
|
||||||
|
get onClose() {
|
||||||
|
return this.closeEmitter.event;
|
||||||
|
}
|
||||||
|
fireClose() {
|
||||||
|
this.closeEmitter.fire(undefined);
|
||||||
|
}
|
||||||
|
asError(error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return new Error(`Writer received error. Reason: ${(error.message) ? error.message : 'unknown'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
async write(msg: Message): Promise<void> {
|
||||||
|
console.log("WRITE",msg)
|
||||||
|
try {
|
||||||
|
const content = JSON.stringify(msg);
|
||||||
|
this.socket.send(content);
|
||||||
|
} catch (e) {
|
||||||
|
this.errorCount++;
|
||||||
|
this.fireError(e, msg, this.errorCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export class WebSocketMessageReader implements MessageReader {
|
||||||
|
protected state: 'initial' | 'listening' | 'closed' = 'initial';
|
||||||
|
protected callback: DataCallback | undefined;
|
||||||
|
protected readonly events: { message?: any, error?: any }[] = [];
|
||||||
|
errorEmitter
|
||||||
|
closeEmitter
|
||||||
|
partialMessageEmitter
|
||||||
|
|
||||||
|
constructor(protected readonly socket: IWebSocket) {
|
||||||
|
this.errorEmitter = new Emitter();
|
||||||
|
this.closeEmitter = new Emitter();
|
||||||
|
this.partialMessageEmitter = new Emitter();
|
||||||
|
this.socket.onMessage(message =>{
|
||||||
|
console.log("READ", message)
|
||||||
|
this.readMessage(message)
|
||||||
|
});
|
||||||
|
this.socket.onError(error =>
|
||||||
|
this.fireError(error)
|
||||||
|
);
|
||||||
|
this.socket.onClose((code, reason) => {
|
||||||
|
if (code !== 1000) {
|
||||||
|
const error: Error = {
|
||||||
|
name: '' + code,
|
||||||
|
message: `Error during socket reconnect: code = ${code}, reason = ${reason}`
|
||||||
|
};
|
||||||
|
this.fireError(error);
|
||||||
|
}
|
||||||
|
this.fireClose();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
dispose() {
|
||||||
|
this.errorEmitter.dispose();
|
||||||
|
this.closeEmitter.dispose();
|
||||||
|
}
|
||||||
|
get onError() {
|
||||||
|
return this.errorEmitter.event;
|
||||||
|
}
|
||||||
|
get onClose() {
|
||||||
|
return this.closeEmitter.event;
|
||||||
|
}
|
||||||
|
get onPartialMessage() {
|
||||||
|
return this.partialMessageEmitter.event;
|
||||||
|
}
|
||||||
|
firePartialMessage(info) {
|
||||||
|
this.partialMessageEmitter.fire(info);
|
||||||
|
}
|
||||||
|
asError(error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return new Error(`Reader received error. Reason: ${(error.message) ? error.message : 'unknown'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listen(callback: DataCallback): Disposable {
|
||||||
|
if (this.state === 'initial') {
|
||||||
|
this.state = 'listening';
|
||||||
|
this.callback = callback;
|
||||||
|
while (this.events.length !== 0) {
|
||||||
|
const event = this.events.pop()!;
|
||||||
|
if (event.message) {
|
||||||
|
this.readMessage(event.message);
|
||||||
|
} else if (event.error) {
|
||||||
|
this.fireError(event.error);
|
||||||
|
} else {
|
||||||
|
this.fireClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
dispose: () => {
|
||||||
|
if (this.callback === callback) {
|
||||||
|
this.callback = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected readMessage(message: any): void {
|
||||||
|
if (this.state === 'initial') {
|
||||||
|
this.events.splice(0, 0, { message });
|
||||||
|
} else if (this.state === 'listening') {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(message);
|
||||||
|
this.callback!(data);
|
||||||
|
} catch (err) {
|
||||||
|
const error: Error = {
|
||||||
|
name: '' + 400,
|
||||||
|
message: `Error during message parsing, reason = ${typeof err === 'object' ? (err as any).message : 'unknown'}`
|
||||||
|
};
|
||||||
|
this.fireError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fireError(error: any): void {
|
||||||
|
if (this.state === 'initial') {
|
||||||
|
this.events.splice(0, 0, { error });
|
||||||
|
} else if (this.state === 'listening') {
|
||||||
|
this.errorEmitter.fire(this.asError(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fireClose(): void {
|
||||||
|
if (this.state === 'initial') {
|
||||||
|
this.events.splice(0, 0, {});
|
||||||
|
} else if (this.state === 'listening') {
|
||||||
|
this.closeEmitter.fire(undefined);
|
||||||
|
}
|
||||||
|
this.state = 'closed';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
**NOTE! This document is deprecated! The current documentation is [How To Create A Game](create_game.md)**
|
|
||||||
|
|
||||||
# Creating a game.
|
# Creating a game.
|
||||||
|
|
||||||
Ideally one takes the [GameSkeleton template](https://github.com/hhu-adam/GameSkeleton) to create a new game.
|
Ideally one takes the [NNG template](https://github.com/hhu-adam/NNG4) to create a new game.
|
||||||
|
|
||||||
## Game Structure
|
## Game Structure
|
||||||
|
|
||||||
@@ -297,9 +295,8 @@ cd lean4game
|
|||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
TODO: This is outdated!
|
|
||||||
If you are developing a game other than `Robo` or `NNG4`, adapt the
|
If you are developing a game other than `Robo` or `NNG4`, adapt the
|
||||||
code at the beginning of `lean4game/relay/index.mjs`:
|
code at the beginning of `lean4game/server/index.mjs`:
|
||||||
```typescript
|
```typescript
|
||||||
const games = {
|
const games = {
|
||||||
"g/hhu-adam/robo": {
|
"g/hhu-adam/robo": {
|
||||||
|
|||||||
+16
-27
@@ -223,40 +223,29 @@ One thing to keep in mind is that the game will look at the main proof to figure
|
|||||||
Most important for game development are probably the `Hints`.
|
Most important for game development are probably the `Hints`.
|
||||||
|
|
||||||
The hints will be displayed whenever the player's current goal matches the goal the hint is
|
The hints will be displayed whenever the player's current goal matches the goal the hint is
|
||||||
placed at inside the sample proof. You can use `Branch` to place hints in dead ends or alternative proof strands.
|
placed at inside the sample proof. You can use `Branch` to place hints in dead ends or alternative proof strands. If you specify
|
||||||
|
|
||||||
Read [More about Hints](doc/hints.md) for how they work and what the options are.
|
```
|
||||||
|
Hint (strict := true) "some hidden hint"
|
||||||
|
```
|
||||||
|
|
||||||
### 6. e) Extra: Images
|
a hint only matches iff the assumptions match exactly one-to-one. (Otherwise, it does not care if there are additional assumptions in context)
|
||||||
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"`
|
Further, you can choose to hide hints and only have them displayed when the player presses "More Help":
|
||||||
in one of the files you created in 2), 3), or 4) (i.e. game/world/level).
|
```
|
||||||
|
Hint (hidden := true) "some hidden hint"
|
||||||
|
```
|
||||||
|
|
||||||
NOTE: At present, only the images for a world are displayed. They appear in the introduction of the world.
|
Lastly, you should put variable names in hints inside brackets:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hint "now use `rw [{h}]` to use your assumption {h}."
|
||||||
|
```
|
||||||
|
That way, the game will replace it with the actual name the assumption has in the player's proof state.
|
||||||
|
|
||||||
## 7. Update your game
|
## 7. Update your game
|
||||||
|
|
||||||
In principle, it is as simple as modifying `lean-toolchain` to update your game to a new Lean version. However, you should read about the details in [Update An Existing Game](doc/update_game.md).
|
In principle, it is as simple as modifying `lean-toolchain` to update your game to a new Lean version. However, you should read about the details in [Update An Existing Game](https://github.com/leanprover-community/lean4game/blob/main/doc/update_game.md).
|
||||||
|
|
||||||
## 8. Publish your game
|
|
||||||
|
|
||||||
To publish your game on the official server, see [Publishing a game](doc/publish_game.md)
|
|
||||||
|
|
||||||
There are a few more options you can add in `Game.lean` before the `MakeGame` command, which describe the tile that is visible on the server's landing page:
|
|
||||||
|
|
||||||
```lean
|
|
||||||
Languages "English"
|
|
||||||
CaptionShort "Game Template"
|
|
||||||
CaptionLong "You should use this game as a template for your own game and add your own levels."
|
|
||||||
Prerequisites "NNG"
|
|
||||||
CoverImage "images/cover.png"
|
|
||||||
```
|
|
||||||
* `Languages`: Currently only a single language (capital English name). The tile will show a corresponding flag.
|
|
||||||
* `CaptionShort`: One catch phrase. Appears above the image.
|
|
||||||
* `CaptionLong`: 2-4 sentences to describe the game.
|
|
||||||
* `Prerequisites` a list of other games you should play before this one, e.g. `Prerequisites "NNG" "STG"`. The game names are free-text.
|
|
||||||
* `CoverImage`: You can create a folder `images/` and put images there for the game to use. The maximal ratio is ca. 500x200 (W x H) but it might be cropped horizontally on narrow screens.
|
|
||||||
|
|
||||||
## Further Notes
|
## Further Notes
|
||||||
|
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
# Hints
|
|
||||||
|
|
||||||
Most important for game development are probably the "Hints". You can add Hints at any place in your proof using the `Hint` tactic
|
|
||||||
|
|
||||||
```
|
|
||||||
Statement .... := by
|
|
||||||
Hint "Hint to show at the start"
|
|
||||||
rw [h]
|
|
||||||
Hint "some tip after using rw"
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
## 1. When do hints show?
|
|
||||||
|
|
||||||
A hint will be displayed if the player's goal matches the one where the hint was placed in the
|
|
||||||
sample solutions and the entire context from the sample solutions is present in the
|
|
||||||
player's context. The player's context may contain additional items.
|
|
||||||
|
|
||||||
This means if you have multiple identical
|
|
||||||
subgoals, you should only place a single hint in one of them and it will be displayed in
|
|
||||||
all of them.
|
|
||||||
|
|
||||||
However, identical (non-hidden) hints which where already present in the step
|
|
||||||
before are omitted. This is to allow a player to add new assumptions to context, for example
|
|
||||||
with `have`, without seeing the same hint over and over again.
|
|
||||||
Hidden hints are not filtered.
|
|
||||||
|
|
||||||
## 2. Alternative Proofs / `Branch`
|
|
||||||
|
|
||||||
You can use `Branch` to place hints
|
|
||||||
in dead ends or alternative proof strands.
|
|
||||||
|
|
||||||
A proof inside a `Branch`-block is normally evaluated by lean but it's discarded at the end
|
|
||||||
so that no progress has been made on proofing the goal.
|
|
||||||
|
|
||||||
```
|
|
||||||
Statement .... := by
|
|
||||||
Hint "Huse `rw` or `rewrite`."
|
|
||||||
Branch
|
|
||||||
rewrite [h]
|
|
||||||
Hint "now you still need `rfl`"
|
|
||||||
rw [h]
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. Variables names
|
|
||||||
|
|
||||||
Put variables in the hint text inside brackets like this: `{h}`! This way the server can replace
|
|
||||||
the variable's name with the one the user actually used.
|
|
||||||
|
|
||||||
For example, if the sample proof contains
|
|
||||||
|
|
||||||
```
|
|
||||||
have h : True := trivial
|
|
||||||
Hint "Now use `rw [{h}]` to use your assumption `{h}`."
|
|
||||||
```
|
|
||||||
|
|
||||||
but the player writes `have g : True := trivial`, they will see a hint saying
|
|
||||||
"Now use `rw [g]` to use your assumption `g`."
|
|
||||||
|
|
||||||
## 4. Hidden hints
|
|
||||||
|
|
||||||
Some hints can be hidden, and only show after the user clicks on a button to get additional
|
|
||||||
help. You mark a hint as hidden with `(hidden := true)`:
|
|
||||||
|
|
||||||
```
|
|
||||||
Hint (hidden := true) "some hidden hint"
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. Strict context matching
|
|
||||||
|
|
||||||
If you use the attribute `(strict := true)` a hint is only shown if the entire context
|
|
||||||
matches exactly the one where the hint is placed. With `(hint := false)`, which is the default,
|
|
||||||
it does not matter if additional assumptions are present in the player's context.
|
|
||||||
|
|
||||||
```
|
|
||||||
Hint (strict := true) "now use `have` to create a new assumption."
|
|
||||||
```
|
|
||||||
|
|
||||||
You should probably use `(strict := true)` if you want to give fine-grained details about
|
|
||||||
tactics like `have` which do not modify the goal or any existing assumptions, but only
|
|
||||||
create new assumptions.
|
|
||||||
|
|
||||||
## 6. Formatting
|
|
||||||
|
|
||||||
You can add use markdown to format your hints, for example you can use KaTex: `$\\iff$`
|
|
||||||
|
|
||||||
TODO: Write a doc about latex/markdown options available.
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# Publishing games
|
|
||||||
|
|
||||||
You can publish your game on the official (Lean Game Server)[https://adam.math.hhu.de] in a few simple
|
|
||||||
steps.
|
|
||||||
|
|
||||||
## 1. Upload Game to github
|
|
||||||
|
|
||||||
First, you need your game in a public Github repository and make sure the github action has run.
|
|
||||||
You can check this by spotting the green checkmark on the start page, or by looking at the "Actions"
|
|
||||||
tab.
|
|
||||||
|
|
||||||
## 2. Import the game
|
|
||||||
|
|
||||||
You call the URL that's listed under "What's Next?" in the latest action run. Explicitely you call
|
|
||||||
the URL of the form
|
|
||||||
|
|
||||||
> adam.math.hhu.de/import/trigger/{USER}/{REPOSITORY}
|
|
||||||
|
|
||||||
where `{USER}` and `{REPOSITORY}` are replaced with the github user and repository name.
|
|
||||||
|
|
||||||
You should see a white screen which shows import updates and eventually reports "Done."
|
|
||||||
|
|
||||||
## 3. Play the game
|
|
||||||
|
|
||||||
Now you can immediately play the game at `adam.math.hhu.de/#/g/{USER}/{REPOSITORY}`!
|
|
||||||
|
|
||||||
## 4. Main page
|
|
||||||
|
|
||||||
Adding games to the main page happens manually by the server maintainers. Tell us if you want us
|
|
||||||
to add a tile for your game!
|
|
||||||
|
|
||||||
For example, you can [contact Jon on Zulip](https://leanprover.zulipchat.com/#narrow/dm/385895-Jon-Eugster). Or [via Email](https://www.math.hhu.de/en/lehrstuehle-/-personen-/-ansprechpartner/innen/lehrstuehle-des-mathematischen-instituts/lehrstuhl-fuer-algebraische-geometrie/team/jon-eugster).
|
|
||||||
@@ -85,11 +85,12 @@ git clone https://github.com/hhu-adam/GameSkeleton.git
|
|||||||
Download dependencies and build the game:
|
Download dependencies and build the game:
|
||||||
```bash
|
```bash
|
||||||
cd GameSkeleton
|
cd GameSkeleton
|
||||||
lake update -R
|
lake update
|
||||||
|
lake exe cache get # if your game depends on mathlib
|
||||||
lake build
|
lake build
|
||||||
```
|
```
|
||||||
|
|
||||||
Clone the server repository into a directory next to the game:
|
Clone the game repository into a directory next to the game:
|
||||||
```bash
|
```bash
|
||||||
cd ..
|
cd ..
|
||||||
git clone https://github.com/leanprover-community/lean4game.git
|
git clone https://github.com/leanprover-community/lean4game.git
|
||||||
@@ -112,12 +113,12 @@ This takes a little time. Eventually, the game is available on http://localhost:
|
|||||||
|
|
||||||
## Modifying the GameServer
|
## Modifying the GameServer
|
||||||
|
|
||||||
When modifying the game engine itself (in particular the content in `lean4game/server`) you can test it live with the same setup as above (manual installation) by using `lake update -R -Klean4game.local`:
|
When modifying the game engine itself (in particular the content in `lean4game/server`) you can test it live with the same setup as above (manual installation) by setting `export LEAN4GAME=local` inside your local game before building it:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd NNG4
|
cd NNG4
|
||||||
lake update -R -Klean4game.local
|
export LEAN4GAME=local
|
||||||
|
lake update
|
||||||
lake build
|
lake build
|
||||||
```
|
```
|
||||||
This causes lake to search locally for the `GameServer` lake package instead of using the version from github. Therefore, you can the local copy of the edit `GameServer` in `../lean4game` and
|
This causes lake to search locally for the `GameServer` lake package instead of using the version from github. Therefore, when you `lake build` your game, it will rebuild with the modified `GameServer`.
|
||||||
`lake build` will then directly use this modified copy to build your game.
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
|
|
||||||
# Notes for Server maintainer
|
|
||||||
|
|
||||||
In order to set up the server to allow imports, one needs to create a
|
|
||||||
[Github Access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens). A fine-grained access token with only reading rights for public
|
|
||||||
repos will suffice.
|
|
||||||
|
|
||||||
You need to set the environment variables `LEAN4GAME_GITHUB_USER` and `LEAN4GAME_GITHUB_TOKEN`
|
|
||||||
with your user name and access token. For example, you can seet these in `ecosystem.config.cjs` if
|
|
||||||
you're using `pm2`
|
|
||||||
|
|
||||||
Then people can call:
|
|
||||||
|
|
||||||
> https://{website}/import/trigger/{owner}/{repo}
|
|
||||||
|
|
||||||
where you replace:
|
|
||||||
- website: The website your server runs on, e.g. `localhost:3000`
|
|
||||||
- owner, repo: The owner and repository name of the game you want to load from github.
|
|
||||||
|
|
||||||
will trigger to download the latest version of your game from github onto your server.
|
|
||||||
Once this import reports "Done", you should be able to play your game under:
|
|
||||||
|
|
||||||
> https://{website}/#/g/{owner}/{repo}
|
|
||||||
|
|
||||||
## data management
|
|
||||||
Everything downloaded remains in the folder `lean4game/games`.
|
|
||||||
the subfolder `tmp` contains downloaded artifacts and can be deleted without loss.
|
|
||||||
The other folders should only contain the built lean-games, sorted by owner and repo.
|
|
||||||
+9
-24
@@ -10,43 +10,28 @@ Before you continue, make sure there [exists a `v4.X.0`-tag in this repo](https:
|
|||||||
Then, depending on the setup you use, do one of the following:
|
Then, depending on the setup you use, do one of the following:
|
||||||
|
|
||||||
* Dev Container: Rebuild the VSCode Devcontainer.
|
* Dev Container: Rebuild the VSCode Devcontainer.
|
||||||
* Local Setup: in your game's folder run the following:
|
* Local Setup: run `lake update` (followed by `lake exe cache get` if you depend on mathlib.)
|
||||||
```
|
|
||||||
lake update -R
|
|
||||||
lake build
|
|
||||||
```
|
|
||||||
|
|
||||||
* Additionally, if you have a local copy of the server `lean4game`,
|
|
||||||
you should update this one to the matching version. Run the following in the folder `lean4game/`:
|
|
||||||
```
|
|
||||||
git fetch
|
|
||||||
git checkout {VERSION_TAG}
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
where `{VERSION_TAG}` is the tag from above of the form `v4.X.0`
|
|
||||||
* Gitpod/Codespaces: Create a fresh one
|
* 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 `lean4game` and `mathlib` in your project to the new lean version.
|
||||||
|
|
||||||
## Newest developing setup
|
## Newest developing setup
|
||||||
|
|
||||||
There are a few files in your game repository which are used for the developing setup
|
There are a few files in your game repository which are used for the developing setup
|
||||||
(dev container/codespaces/gitpod). If you need to update your developing setup, for example because it doesn't work
|
(dev container/codespaces/gitpod). If you need to update your're developing setup, for example because it doesn't work
|
||||||
anymore, you will need to copy the relevant files from the [GameSkeleton](https://github.com/hhu-adam/GameSkeleton) template into your game repo.
|
anymore, you will need to copy the relevant files from the [GameSkeleton](https://github.com/hhu-adam/GameSkeleton) template into your game repo.
|
||||||
|
|
||||||
The relevant files are:
|
The relevant files are:
|
||||||
|
|
||||||
```
|
```
|
||||||
.devcontainer/
|
|
||||||
.docker/
|
|
||||||
.github/
|
|
||||||
.gitpod/
|
|
||||||
.vscode/
|
|
||||||
lakefile.lean
|
lakefile.lean
|
||||||
|
.devcontainer/**
|
||||||
|
.docker/**
|
||||||
|
.gitpod
|
||||||
|
.vscode/**
|
||||||
```
|
```
|
||||||
|
|
||||||
simply copy them from the `GameSkeleton` into your game and proceed as above,
|
simply copy them from the `GameSkeleton` into your game.
|
||||||
i.e. `lake update -R && lake build`.
|
|
||||||
|
|
||||||
(Note: You should not need to modify any of these files, with the exception of the `lakefile.lean`,
|
(Note: You should not need to modify any of these files, with the exception of the `lakefile.lean`,
|
||||||
where you need to add any dependencies of your game, or remove mathlib if you don't need it.)
|
where you need to add any dependencies of your game.)
|
||||||
|
|||||||
@@ -2,12 +2,10 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
apps : [{
|
apps : [{
|
||||||
name : "lean4game",
|
name : "lean4game",
|
||||||
script : "relay/index.mjs",
|
script : "server/index.mjs",
|
||||||
env: {
|
env: {
|
||||||
LEAN4GAME_GITHUB_USER: "",
|
NODE_ENV: "production",
|
||||||
LEAN4GAME_GITHUB_TOKEN: "",
|
PORT: 8002
|
||||||
NODE_ENV: "production",
|
|
||||||
PORT: 8002
|
|
||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</noscript>
|
</noscript>
|
||||||
|
<script src="coi-serviceworker.js"></script>
|
||||||
<script type="module" src="/client/src/index.tsx"></script>
|
<script type="module" src="/client/src/index.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
Generated
+70
-457
@@ -20,6 +20,7 @@
|
|||||||
"@types/cytoscape": "^3.19.9",
|
"@types/cytoscape": "^3.19.9",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@vitejs/plugin-react-swc": "^3.4.0",
|
"@vitejs/plugin-react-swc": "^3.4.0",
|
||||||
|
"coi-serviceworker": "^0.1.7",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cytoscape": "^3.23.0",
|
"cytoscape": "^3.23.0",
|
||||||
"cytoscape-elk": "^2.1.0",
|
"cytoscape-elk": "^2.1.0",
|
||||||
@@ -40,7 +41,6 @@
|
|||||||
"rehype-katex": "^6.0.2",
|
"rehype-katex": "^6.0.2",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remark-math": "^5.1.1",
|
"remark-math": "^5.1.1",
|
||||||
"request": "^2.88.2",
|
|
||||||
"request-progress": "^3.0.0",
|
"request-progress": "^3.0.0",
|
||||||
"vite": "^4.5.0",
|
"vite": "^4.5.0",
|
||||||
"vite-plugin-static-copy": "^0.17.0",
|
"vite-plugin-static-copy": "^0.17.0",
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
"concurrently": "^7.6.0",
|
"concurrently": "^7.6.0",
|
||||||
"css-loader": "^6.7.3",
|
"css-loader": "^6.7.3",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"nodemon": "^3.0.1",
|
"nodemon": "^2.0.20",
|
||||||
"react-refresh": "^0.14.0",
|
"react-refresh": "^0.14.0",
|
||||||
"style-loader": "^3.3.1",
|
"style-loader": "^3.3.1",
|
||||||
"ts-loader": "^9.4.2",
|
"ts-loader": "^9.4.2",
|
||||||
@@ -6015,6 +6015,7 @@
|
|||||||
"version": "6.12.6",
|
"version": "6.12.6",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
@@ -6183,22 +6184,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
||||||
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
|
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
|
||||||
},
|
},
|
||||||
"node_modules/asn1": {
|
|
||||||
"version": "0.2.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
|
||||||
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"safer-buffer": "~2.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/assert-plus": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ast-types": {
|
"node_modules/ast-types": {
|
||||||
"version": "0.15.2",
|
"version": "0.15.2",
|
||||||
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.15.2.tgz",
|
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.15.2.tgz",
|
||||||
@@ -6228,11 +6213,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
|
||||||
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
|
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
|
||||||
},
|
},
|
||||||
"node_modules/asynckit": {
|
|
||||||
"version": "0.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
|
||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
|
||||||
},
|
|
||||||
"node_modules/available-typed-arrays": {
|
"node_modules/available-typed-arrays": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
|
||||||
@@ -6245,40 +6225,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/aws-sign2": {
|
|
||||||
"version": "0.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
|
|
||||||
"integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==",
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/aws4": {
|
|
||||||
"version": "1.12.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz",
|
|
||||||
"integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg=="
|
|
||||||
},
|
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.6.2",
|
"version": "0.24.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
|
||||||
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
|
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.0",
|
"follow-redirects": "^1.14.4"
|
||||||
"form-data": "^4.0.0",
|
|
||||||
"proxy-from-env": "^1.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/axios/node_modules/form-data": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
|
||||||
"dependencies": {
|
|
||||||
"asynckit": "^0.4.0",
|
|
||||||
"combined-stream": "^1.0.8",
|
|
||||||
"mime-types": "^2.1.12"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/babel-core": {
|
"node_modules/babel-core": {
|
||||||
@@ -6430,14 +6382,6 @@
|
|||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/bcrypt-pbkdf": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
|
|
||||||
"dependencies": {
|
|
||||||
"tweetnacl": "^0.14.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/before-after-hook": {
|
"node_modules/before-after-hook": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
|
||||||
@@ -6728,11 +6672,6 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/caseless": {
|
|
||||||
"version": "0.12.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
|
|
||||||
"integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="
|
|
||||||
},
|
|
||||||
"node_modules/ccount": {
|
"node_modules/ccount": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
|
||||||
@@ -6952,6 +6891,11 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/coi-serviceworker": {
|
||||||
|
"version": "0.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/coi-serviceworker/-/coi-serviceworker-0.1.7.tgz",
|
||||||
|
"integrity": "sha512-bjSUqEngCPOkErY2vbyWsaIGCNRODYzlNycaREVw5s12/C8SM+RnRUUeX6pZbTtov6C52ZLY/+tvHK+BDxuUuA=="
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "1.9.3",
|
"version": "1.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||||
@@ -6970,17 +6914,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
|
||||||
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="
|
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="
|
||||||
},
|
},
|
||||||
"node_modules/combined-stream": {
|
|
||||||
"version": "1.0.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
|
||||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
|
||||||
"dependencies": {
|
|
||||||
"delayed-stream": "~1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/comma-separated-tokens": {
|
"node_modules/comma-separated-tokens": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
||||||
@@ -7497,17 +7430,6 @@
|
|||||||
"cytoscape": "^3.2.0"
|
"cytoscape": "^3.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dashdash": {
|
|
||||||
"version": "1.14.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
|
||||||
"integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
|
|
||||||
"dependencies": {
|
|
||||||
"assert-plus": "^1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/date-fns": {
|
"node_modules/date-fns": {
|
||||||
"version": "2.30.0",
|
"version": "2.30.0",
|
||||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
||||||
@@ -7674,14 +7596,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/delayed-stream": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/denodeify": {
|
"node_modules/denodeify": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz",
|
||||||
@@ -7840,15 +7754,6 @@
|
|||||||
"tslib": "^2.0.3"
|
"tslib": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ecc-jsbn": {
|
|
||||||
"version": "0.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
|
||||||
"integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
|
|
||||||
"dependencies": {
|
|
||||||
"jsbn": "~0.1.0",
|
|
||||||
"safer-buffer": "^2.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ecdsa-sig-formatter": {
|
"node_modules/ecdsa-sig-formatter": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||||
@@ -8237,18 +8142,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
|
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
|
||||||
},
|
},
|
||||||
"node_modules/extsprintf": {
|
|
||||||
"version": "1.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
|
|
||||||
"integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==",
|
|
||||||
"engines": [
|
|
||||||
"node >=0.6.0"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
@@ -8268,7 +8166,8 @@
|
|||||||
"node_modules/fast-json-stable-stringify": {
|
"node_modules/fast-json-stable-stringify": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
|
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/fast-plist": {
|
"node_modules/fast-plist": {
|
||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
@@ -8453,27 +8352,6 @@
|
|||||||
"is-callable": "^1.1.3"
|
"is-callable": "^1.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/forever-agent": {
|
|
||||||
"version": "0.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
|
|
||||||
"integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==",
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/form-data": {
|
|
||||||
"version": "2.3.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
|
|
||||||
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"asynckit": "^0.4.0",
|
|
||||||
"combined-stream": "^1.0.6",
|
|
||||||
"mime-types": "^2.1.12"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@@ -8603,14 +8481,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/getpass": {
|
|
||||||
"version": "0.1.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
|
|
||||||
"integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
|
|
||||||
"dependencies": {
|
|
||||||
"assert-plus": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "7.2.3",
|
"version": "7.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
@@ -8681,27 +8551,6 @@
|
|||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/har-schema": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/har-validator": {
|
|
||||||
"version": "5.1.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz",
|
|
||||||
"integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
|
|
||||||
"deprecated": "this library is no longer supported",
|
|
||||||
"dependencies": {
|
|
||||||
"ajv": "^6.12.3",
|
|
||||||
"har-schema": "^2.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/has": {
|
"node_modules/has": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz",
|
||||||
@@ -9132,20 +8981,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/http-signature": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"assert-plus": "^1.0.0",
|
|
||||||
"jsprim": "^1.2.2",
|
|
||||||
"sshpk": "^1.7.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.8",
|
|
||||||
"npm": ">=1.3.7"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/human-signals": {
|
"node_modules/human-signals": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||||
@@ -9653,11 +9488,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-typedarray": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="
|
|
||||||
},
|
|
||||||
"node_modules/is-unicode-supported": {
|
"node_modules/is-unicode-supported": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
|
||||||
@@ -9718,11 +9548,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/isstream": {
|
|
||||||
"version": "0.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
|
||||||
"integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g=="
|
|
||||||
},
|
|
||||||
"node_modules/jest-environment-node": {
|
"node_modules/jest-environment-node": {
|
||||||
"version": "29.7.0",
|
"version": "29.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz",
|
||||||
@@ -10131,11 +9956,6 @@
|
|||||||
"js-yaml": "bin/js-yaml.js"
|
"js-yaml": "bin/js-yaml.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jsbn": {
|
|
||||||
"version": "0.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
|
||||||
"integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg=="
|
|
||||||
},
|
|
||||||
"node_modules/jsc-android": {
|
"node_modules/jsc-android": {
|
||||||
"version": "250231.0.0",
|
"version": "250231.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsc-android/-/jsc-android-250231.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsc-android/-/jsc-android-250231.0.0.tgz",
|
||||||
@@ -10263,20 +10083,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
||||||
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
|
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
|
||||||
},
|
},
|
||||||
"node_modules/json-schema": {
|
|
||||||
"version": "0.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
|
|
||||||
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="
|
|
||||||
},
|
|
||||||
"node_modules/json-schema-traverse": {
|
"node_modules/json-schema-traverse": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
|
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||||
},
|
"dev": true
|
||||||
"node_modules/json-stringify-safe": {
|
|
||||||
"version": "5.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
|
||||||
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="
|
|
||||||
},
|
},
|
||||||
"node_modules/json5": {
|
"node_modules/json5": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
@@ -10348,20 +10159,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||||
},
|
},
|
||||||
"node_modules/jsprim": {
|
|
||||||
"version": "1.4.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
|
|
||||||
"integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==",
|
|
||||||
"dependencies": {
|
|
||||||
"assert-plus": "1.0.0",
|
|
||||||
"extsprintf": "1.3.0",
|
|
||||||
"json-schema": "0.4.0",
|
|
||||||
"verror": "1.10.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.6.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jwa": {
|
"node_modules/jwa": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
|
||||||
@@ -10438,22 +10235,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lean4": {
|
"node_modules/lean4": {
|
||||||
"version": "0.0.119",
|
"version": "0.0.99",
|
||||||
"resolved": "https://gitpkg.now.sh/leanprover/vscode-lean4/vscode-lean4?8d0cc34dcfa00da8b4a48394ba1fb3a600e3f985",
|
"resolved": "https://gitpkg.now.sh/leanprover/vscode-lean4/vscode-lean4?79d345c739bf707024eef5057ccef81b404a2ddf",
|
||||||
"integrity": "sha512-hA7bb0EFlNwtk7+/7gO5OZS7HtrhmoRacE/b2etFBYG4FbkZd/NKTcs5PHGcKsYXeVghkOOZjw0uhNWaeW3gvw==",
|
"integrity": "sha512-jgl4gGQXvP+RhdAr9r0+K6I5SpPkad++zAql9BawHI7OE7vMwK+7GbYXoa5aAP8T1BSIZQ+y3w0cl8FCk5C++A==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@leanprover/infoview": "~0.4.3",
|
"@leanprover/infoview": "~0.3.0",
|
||||||
"@leanprover/infoview-api": "~0.2.1",
|
"@leanprover/infoview-api": "~0.2.0",
|
||||||
"axios": "^1.6.2",
|
"axios": "~0.24.0",
|
||||||
"cheerio": "^1.0.0-rc.10",
|
"cheerio": "^1.0.0-rc.10",
|
||||||
|
"es-module-shims": "^1.6.2",
|
||||||
"mobx": "5.15.7",
|
"mobx": "5.15.7",
|
||||||
"semver": "^7.5.4",
|
"semver": "=7.3.5",
|
||||||
"vscode-languageclient": "=8.0.2",
|
"vscode-languageclient": "=8.0.2"
|
||||||
"zod": "^3.22.4"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.75.0"
|
"vscode": "^1.70.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lean4-infoview": {
|
"node_modules/lean4-infoview": {
|
||||||
@@ -10472,6 +10269,19 @@
|
|||||||
"vscode-languageserver-protocol": "^3.17.2"
|
"vscode-languageserver-protocol": "^3.17.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lean4/node_modules/@leanprover/infoview": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@leanprover/infoview/-/infoview-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-wVMd9Qc6AC8Ub4cfkENcyCqs342lqwS+4C1SpX+3z0RETxL+TxTFPdM8bYoLgtAg/eApdHhV1TDLSrBNkTWi5g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@leanprover/infoview-api": "~0.2.0",
|
||||||
|
"@vscode/codicons": "^0.0.32",
|
||||||
|
"marked": "^4.2.2",
|
||||||
|
"react-fast-compare": "^3.2.0",
|
||||||
|
"tachyons": "^4.12.0",
|
||||||
|
"vscode-languageserver-protocol": "^3.17.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lean4/node_modules/lru-cache": {
|
"node_modules/lean4/node_modules/lru-cache": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
@@ -10493,9 +10303,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lean4/node_modules/semver": {
|
"node_modules/lean4/node_modules/semver": {
|
||||||
"version": "7.5.4",
|
"version": "7.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
|
||||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lru-cache": "^6.0.0"
|
"lru-cache": "^6.0.0"
|
||||||
},
|
},
|
||||||
@@ -10513,7 +10323,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#6fc9c11179934cce7ca1f78140c57b6931186b42",
|
"resolved": "git+ssh://git@github.com/hhu-adam/lean4web.git#b02044f3a3b9130b663c3619ce56c603fa3a4cf4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.11.1",
|
"@emotion/react": "^11.11.1",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
@@ -10522,13 +10332,12 @@
|
|||||||
"@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.3",
|
"@leanprover/infoview": "^0.4.1",
|
||||||
"@mui/material": "^5.13.7",
|
"@mui/material": "^5.13.7",
|
||||||
"@vitejs/plugin-react-swc": "^3.4.0",
|
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"ip-anonymize": "^0.1.0",
|
"ip-anonymize": "^0.1.0",
|
||||||
"lean4": "https://gitpkg.now.sh/leanprover/vscode-lean4/vscode-lean4?8d0cc34dcfa00da8b4a48394ba1fb3a600e3f985",
|
"lean4": "https://gitpkg.now.sh/leanprover/vscode-lean4/vscode-lean4?79d345c739bf707024eef5057ccef81b404a2ddf",
|
||||||
"mobx": "^6.6.2",
|
"mobx": "^6.6.2",
|
||||||
"moment-timezone": "^0.5.39",
|
"moment-timezone": "^0.5.39",
|
||||||
"monaco-editor": "^0.34.1",
|
"monaco-editor": "^0.34.1",
|
||||||
@@ -10543,7 +10352,6 @@
|
|||||||
"react-popper": "^2.3.0",
|
"react-popper": "^2.3.0",
|
||||||
"react-split": "^2.0.14",
|
"react-split": "^2.0.14",
|
||||||
"stream-http": "^3.2.0",
|
"stream-http": "^3.2.0",
|
||||||
"vite": "^4.5.0",
|
|
||||||
"vscode-ws-jsonrpc": "^2.0.0",
|
"vscode-ws-jsonrpc": "^2.0.0",
|
||||||
"ws": "^8.9.0"
|
"ws": "^8.9.0"
|
||||||
}
|
}
|
||||||
@@ -12809,9 +12617,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nodemon": {
|
"node_modules/nodemon": {
|
||||||
"version": "3.0.1",
|
"version": "2.0.22",
|
||||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz",
|
||||||
"integrity": "sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==",
|
"integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^3.5.2",
|
"chokidar": "^3.5.2",
|
||||||
@@ -12819,8 +12627,8 @@
|
|||||||
"ignore-by-default": "^1.0.1",
|
"ignore-by-default": "^1.0.1",
|
||||||
"minimatch": "^3.1.2",
|
"minimatch": "^3.1.2",
|
||||||
"pstree.remy": "^1.1.8",
|
"pstree.remy": "^1.1.8",
|
||||||
"semver": "^7.5.3",
|
"semver": "^5.7.1",
|
||||||
"simple-update-notifier": "^2.0.0",
|
"simple-update-notifier": "^1.0.7",
|
||||||
"supports-color": "^5.5.0",
|
"supports-color": "^5.5.0",
|
||||||
"touch": "^3.1.0",
|
"touch": "^3.1.0",
|
||||||
"undefsafe": "^2.0.5"
|
"undefsafe": "^2.0.5"
|
||||||
@@ -12829,7 +12637,7 @@
|
|||||||
"nodemon": "bin/nodemon.js"
|
"nodemon": "bin/nodemon.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=8.10.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -12845,39 +12653,15 @@
|
|||||||
"ms": "^2.1.1"
|
"ms": "^2.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nodemon/node_modules/lru-cache": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"yallist": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/nodemon/node_modules/semver": {
|
"node_modules/nodemon/node_modules/semver": {
|
||||||
"version": "7.5.4",
|
"version": "5.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
|
||||||
"lru-cache": "^6.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nodemon/node_modules/yallist": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/nopt": {
|
"node_modules/nopt": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
|
||||||
@@ -12928,14 +12712,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
|
||||||
"integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="
|
"integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="
|
||||||
},
|
},
|
||||||
"node_modules/oauth-sign": {
|
|
||||||
"version": "0.9.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
|
|
||||||
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ob1": {
|
"node_modules/ob1": {
|
||||||
"version": "0.76.8",
|
"version": "0.76.8",
|
||||||
"resolved": "https://registry.npmjs.org/ob1/-/ob1-0.76.8.tgz",
|
"resolved": "https://registry.npmjs.org/ob1/-/ob1-0.76.8.tgz",
|
||||||
@@ -13344,11 +13120,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/performance-now": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="
|
|
||||||
},
|
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||||
@@ -13579,16 +13350,6 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/proxy-from-env": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
|
||||||
},
|
|
||||||
"node_modules/psl": {
|
|
||||||
"version": "1.9.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
|
||||||
"integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="
|
|
||||||
},
|
|
||||||
"node_modules/pstree.remy": {
|
"node_modules/pstree.remy": {
|
||||||
"version": "1.1.8",
|
"version": "1.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||||
@@ -13599,6 +13360,7 @@
|
|||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
|
||||||
"integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
|
"integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
@@ -14310,37 +14072,6 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/request": {
|
|
||||||
"version": "2.88.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
|
|
||||||
"integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
|
|
||||||
"deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142",
|
|
||||||
"dependencies": {
|
|
||||||
"aws-sign2": "~0.7.0",
|
|
||||||
"aws4": "^1.8.0",
|
|
||||||
"caseless": "~0.12.0",
|
|
||||||
"combined-stream": "~1.0.6",
|
|
||||||
"extend": "~3.0.2",
|
|
||||||
"forever-agent": "~0.6.1",
|
|
||||||
"form-data": "~2.3.2",
|
|
||||||
"har-validator": "~5.1.3",
|
|
||||||
"http-signature": "~1.2.0",
|
|
||||||
"is-typedarray": "~1.0.0",
|
|
||||||
"isstream": "~0.1.2",
|
|
||||||
"json-stringify-safe": "~5.0.1",
|
|
||||||
"mime-types": "~2.1.19",
|
|
||||||
"oauth-sign": "~0.9.0",
|
|
||||||
"performance-now": "^2.1.0",
|
|
||||||
"qs": "~6.5.2",
|
|
||||||
"safe-buffer": "^5.1.2",
|
|
||||||
"tough-cookie": "~2.5.0",
|
|
||||||
"tunnel-agent": "^0.6.0",
|
|
||||||
"uuid": "^3.3.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/request-progress": {
|
"node_modules/request-progress": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz",
|
||||||
@@ -14349,23 +14080,6 @@
|
|||||||
"throttleit": "^1.0.0"
|
"throttleit": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/request/node_modules/qs": {
|
|
||||||
"version": "6.5.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
|
|
||||||
"integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/request/node_modules/uuid": {
|
|
||||||
"version": "3.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
|
||||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
|
|
||||||
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
|
|
||||||
"bin": {
|
|
||||||
"uuid": "bin/uuid"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/require-directory": {
|
"node_modules/require-directory": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
@@ -14869,50 +14583,26 @@
|
|||||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
|
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
|
||||||
},
|
},
|
||||||
"node_modules/simple-update-notifier": {
|
"node_modules/simple-update-notifier": {
|
||||||
"version": "2.0.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz",
|
||||||
"integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
|
"integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"semver": "^7.5.3"
|
"semver": "~7.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=8.10.0"
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/simple-update-notifier/node_modules/lru-cache": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"yallist": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/simple-update-notifier/node_modules/semver": {
|
"node_modules/simple-update-notifier/node_modules/semver": {
|
||||||
"version": "7.5.4",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz",
|
||||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
"integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
|
||||||
"lru-cache": "^6.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/simple-update-notifier/node_modules/yallist": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/sisteransi": {
|
"node_modules/sisteransi": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||||||
@@ -15045,30 +14735,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
|
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
|
||||||
},
|
},
|
||||||
"node_modules/sshpk": {
|
|
||||||
"version": "1.18.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
|
|
||||||
"integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"asn1": "~0.2.3",
|
|
||||||
"assert-plus": "^1.0.0",
|
|
||||||
"bcrypt-pbkdf": "^1.0.0",
|
|
||||||
"dashdash": "^1.12.0",
|
|
||||||
"ecc-jsbn": "~0.1.1",
|
|
||||||
"getpass": "^0.1.1",
|
|
||||||
"jsbn": "~0.1.0",
|
|
||||||
"safer-buffer": "^2.0.2",
|
|
||||||
"tweetnacl": "~0.14.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"sshpk-conv": "bin/sshpk-conv",
|
|
||||||
"sshpk-sign": "bin/sshpk-sign",
|
|
||||||
"sshpk-verify": "bin/sshpk-verify"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/stack-utils": {
|
"node_modules/stack-utils": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||||
@@ -15472,18 +15138,6 @@
|
|||||||
"nodetouch": "bin/nodetouch.js"
|
"nodetouch": "bin/nodetouch.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tough-cookie": {
|
|
||||||
"version": "2.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
|
|
||||||
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
|
|
||||||
"dependencies": {
|
|
||||||
"psl": "^1.1.28",
|
|
||||||
"punycode": "^2.1.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tr46": {
|
"node_modules/tr46": {
|
||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
@@ -15653,22 +15307,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||||
},
|
},
|
||||||
"node_modules/tunnel-agent": {
|
|
||||||
"version": "0.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
|
||||||
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
|
||||||
"dependencies": {
|
|
||||||
"safe-buffer": "^5.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tweetnacl": {
|
|
||||||
"version": "0.14.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
|
||||||
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="
|
|
||||||
},
|
|
||||||
"node_modules/type-detect": {
|
"node_modules/type-detect": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
|
||||||
@@ -15966,6 +15604,7 @@
|
|||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
@@ -16062,24 +15701,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/verror": {
|
|
||||||
"version": "1.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
|
||||||
"integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
|
|
||||||
"engines": [
|
|
||||||
"node >=0.6.0"
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"assert-plus": "^1.0.0",
|
|
||||||
"core-util-is": "1.0.2",
|
|
||||||
"extsprintf": "^1.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/verror/node_modules/core-util-is": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="
|
|
||||||
},
|
|
||||||
"node_modules/vfile": {
|
"node_modules/vfile": {
|
||||||
"version": "5.3.7",
|
"version": "5.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz",
|
||||||
@@ -16122,9 +15743,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "4.5.1",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
|
||||||
"integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==",
|
"integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.18.10",
|
"esbuild": "^0.18.10",
|
||||||
"postcss": "^8.4.27",
|
"postcss": "^8.4.27",
|
||||||
@@ -17025,14 +16646,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod": {
|
|
||||||
"version": "3.22.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
|
|
||||||
"integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/zwitch": {
|
"node_modules/zwitch": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
||||||
|
|||||||
+4
-6
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lean4-game",
|
"name": "lean4-game",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": ".",
|
"homepage": ".",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -17,6 +16,7 @@
|
|||||||
"@types/cytoscape": "^3.19.9",
|
"@types/cytoscape": "^3.19.9",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@vitejs/plugin-react-swc": "^3.4.0",
|
"@vitejs/plugin-react-swc": "^3.4.0",
|
||||||
|
"coi-serviceworker": "^0.1.7",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cytoscape": "^3.23.0",
|
"cytoscape": "^3.23.0",
|
||||||
"cytoscape-elk": "^2.1.0",
|
"cytoscape-elk": "^2.1.0",
|
||||||
@@ -37,7 +37,6 @@
|
|||||||
"rehype-katex": "^6.0.2",
|
"rehype-katex": "^6.0.2",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remark-math": "^5.1.1",
|
"remark-math": "^5.1.1",
|
||||||
"request": "^2.88.2",
|
|
||||||
"request-progress": "^3.0.0",
|
"request-progress": "^3.0.0",
|
||||||
"vite": "^4.5.0",
|
"vite": "^4.5.0",
|
||||||
"vite-plugin-static-copy": "^0.17.0",
|
"vite-plugin-static-copy": "^0.17.0",
|
||||||
@@ -54,7 +53,7 @@
|
|||||||
"concurrently": "^7.6.0",
|
"concurrently": "^7.6.0",
|
||||||
"css-loader": "^6.7.3",
|
"css-loader": "^6.7.3",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"nodemon": "^3.0.1",
|
"nodemon": "^2.0.20",
|
||||||
"react-refresh": "^0.14.0",
|
"react-refresh": "^0.14.0",
|
||||||
"style-loader": "^3.3.1",
|
"style-loader": "^3.3.1",
|
||||||
"ts-loader": "^9.4.2",
|
"ts-loader": "^9.4.2",
|
||||||
@@ -63,13 +62,12 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "concurrently -n server,client -c blue,green \"npm run start_server\" \"npm run start_client\"",
|
"start": "concurrently -n server,client -c blue,green \"npm run start_server\" \"npm run start_client\"",
|
||||||
"start_server": "(cd server && lake build) && (cd relay && cross-env NODE_ENV=development nodemon -e mjs --exec \"node ./index.mjs\")",
|
"start_server": "cd server && lake build && cross-env NODE_ENV=development nodemon -e mjs --exec \"node ./index.mjs\"",
|
||||||
"start_client": "cross-env NODE_ENV=development vite --host",
|
"start_client": "cross-env NODE_ENV=development vite --host",
|
||||||
"build": "npm run build_server && npm run build_client",
|
"build": "npm run build_server && npm run build_client",
|
||||||
"preview": "vite preview",
|
|
||||||
"build_server": "cd server && lake build",
|
"build_server": "cd server && lake build",
|
||||||
"build_client": "cross-env NODE_ENV=production vite build",
|
"build_client": "cross-env NODE_ENV=production vite build",
|
||||||
"production": "cross-env NODE_ENV=production node relay/index.mjs"
|
"production": "cross-env NODE_ENV=production node server/index.mjs"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
#/bin/bash
|
|
||||||
|
|
||||||
ARTIFACT_ID=$1
|
|
||||||
OWNER=$2
|
|
||||||
REPO=$3
|
|
||||||
|
|
||||||
# mkdir -p games
|
|
||||||
cd games
|
|
||||||
|
|
||||||
# mkdir -p tmp
|
|
||||||
mkdir -p ${OWNER}
|
|
||||||
|
|
||||||
echo "Unpacking ZIP."
|
|
||||||
unzip -o tmp/${OWNER}_${REPO}_${ARTIFACT_ID}.zip -d tmp/${OWNER}_${REPO}_${ARTIFACT_ID}
|
|
||||||
echo "Unpacking game."
|
|
||||||
|
|
||||||
# exit the npm project to avoid reloading. TODO: Where should we actually save these?
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
echo "Delete old version of the game"
|
|
||||||
rm -rf ${OWNER}/${REPO}
|
|
||||||
mkdir -p ${OWNER}/${REPO}
|
|
||||||
|
|
||||||
for f in tmp/${OWNER}_${REPO}_${ARTIFACT_ID}/* #Should only be one file
|
|
||||||
do
|
|
||||||
echo "Unpacking $f"
|
|
||||||
#tar -xvzf $f -C games/${OWNER}/${REPO}
|
|
||||||
unzip -q -o $f -d ${OWNER}/${REPO}
|
|
||||||
done
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.lake
|
||||||
|
adam
|
||||||
|
lakefile32.olean
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import Lean.Server.Watchdog
|
||||||
|
import GameServer.Commands
|
||||||
|
import GameServer.Game
|
||||||
|
|
||||||
|
Game "TestGame"
|
||||||
|
Title "Hello Test"
|
||||||
|
|
||||||
|
World "Test"
|
||||||
|
Level 1
|
||||||
|
|
||||||
|
Statement : 1 = 1 := sorry
|
||||||
|
|
||||||
|
MakeGame
|
||||||
@@ -46,9 +46,7 @@ elab "Title" t:str : command => do
|
|||||||
match ← getCurLayer with
|
match ← getCurLayer with
|
||||||
| .Level => modifyCurLevel fun level => pure {level with title := t.getString}
|
| .Level => modifyCurLevel fun level => pure {level with title := t.getString}
|
||||||
| .World => modifyCurWorld fun world => pure {world with title := t.getString}
|
| .World => modifyCurWorld fun world => pure {world with title := t.getString}
|
||||||
| .Game => modifyCurGame fun game => pure {game with
|
| .Game => modifyCurGame fun game => pure {game with title := t.getString}
|
||||||
title := t.getString
|
|
||||||
tile := {game.tile with title := t.getString}}
|
|
||||||
|
|
||||||
/-- Define the introduction of the current game/world/level. -/
|
/-- Define the introduction of the current game/world/level. -/
|
||||||
elab "Introduction" t:str : command => do
|
elab "Introduction" t:str : command => do
|
||||||
@@ -60,32 +58,10 @@ elab "Introduction" t:str : command => do
|
|||||||
/-- Define the info of the current game. Used for e.g. credits -/
|
/-- Define the info of the current game. Used for e.g. credits -/
|
||||||
elab "Info" t:str : command => do
|
elab "Info" t:str : command => do
|
||||||
match ← getCurLayer with
|
match ← getCurLayer with
|
||||||
| .Level =>
|
| .Level => pure ()
|
||||||
logError "Can't use `Info` in a level!"
|
| .World => pure ()
|
||||||
pure ()
|
|
||||||
| .World =>
|
|
||||||
logError "Can't use `Info` in a world"
|
|
||||||
pure ()
|
|
||||||
| .Game => modifyCurGame fun game => pure {game with info := t.getString}
|
| .Game => modifyCurGame fun game => pure {game with info := t.getString}
|
||||||
|
|
||||||
/-- Provide the location of the image for the current game/world/level.
|
|
||||||
Paths are relative to the lean project's root. -/
|
|
||||||
elab "Image" t:str : command => do
|
|
||||||
let file := t.getString
|
|
||||||
if not <| ← System.FilePath.pathExists file then
|
|
||||||
logWarningAt t s!"Make sure the cover image '{file}' exists."
|
|
||||||
if not <| file.startsWith "images/" then
|
|
||||||
logWarningAt t s!"The file name should start with `images/`. Make sure all images are in that folder."
|
|
||||||
|
|
||||||
match ← getCurLayer with
|
|
||||||
| .Level =>
|
|
||||||
logWarning "Level-images not implemented yet" -- TODO
|
|
||||||
modifyCurLevel fun level => pure {level with image := file}
|
|
||||||
| .World =>
|
|
||||||
modifyCurWorld fun world => pure {world with image := file}
|
|
||||||
| .Game =>
|
|
||||||
logWarning "Main image of the game not implemented yet" -- TODO
|
|
||||||
modifyCurGame fun game => pure {game with image := file}
|
|
||||||
|
|
||||||
/-- Define the conclusion of the current game or current level if some
|
/-- Define the conclusion of the current game or current level if some
|
||||||
building a level. -/
|
building a level. -/
|
||||||
@@ -95,38 +71,6 @@ elab "Conclusion" t:str : command => do
|
|||||||
| .World => modifyCurWorld fun world => pure {world with conclusion := t.getString}
|
| .World => modifyCurWorld fun world => pure {world with conclusion := t.getString}
|
||||||
| .Game => modifyCurGame fun game => pure {game with conclusion := t.getString}
|
| .Game => modifyCurGame fun game => pure {game with conclusion := t.getString}
|
||||||
|
|
||||||
/-- A list of games that should be played before this one. Example `Prerequisites "NNG" "STG"`. -/
|
|
||||||
elab "Prerequisites" t:str* : command => do
|
|
||||||
modifyCurGame fun game => pure {game with
|
|
||||||
tile := {game.tile with prerequisites := t.map (·.getString) |>.toList}}
|
|
||||||
|
|
||||||
/-- Short caption for the game (1 sentence) -/
|
|
||||||
elab "CaptionShort" t:str : command => do
|
|
||||||
modifyCurGame fun game => pure {game with
|
|
||||||
tile := {game.tile with short := t.getString}}
|
|
||||||
|
|
||||||
/-- More detailed description what the game is about (2-4 sentences). -/
|
|
||||||
elab "CaptionLong" t:str : command => do
|
|
||||||
modifyCurGame fun game => pure {game with
|
|
||||||
tile := {game.tile with long := t.getString}}
|
|
||||||
|
|
||||||
/-- A list of Languages the game is translated to. For example `Languages "German" "English"`.
|
|
||||||
NOTE: For the time being, only a single language is supported.
|
|
||||||
-/
|
|
||||||
elab "Languages" t:str* : command => do
|
|
||||||
modifyCurGame fun game => pure {game with
|
|
||||||
tile := {game.tile with languages := t.map (·.getString) |>.toList}}
|
|
||||||
|
|
||||||
/-- The Image of the game (optional). TODO: Not impementeds -/
|
|
||||||
elab "CoverImage" t:str : command => do
|
|
||||||
let file := t.getString
|
|
||||||
if not <| ← System.FilePath.pathExists file then
|
|
||||||
logWarningAt t s!"Make sure the cover image '{file}' exists."
|
|
||||||
if not <| file.startsWith "images/" then
|
|
||||||
logWarningAt t s!"The file name should start with `images/`. Make sure all images are in that folder."
|
|
||||||
|
|
||||||
modifyCurGame fun game => pure {game with
|
|
||||||
tile := {game.tile with image := file}}
|
|
||||||
|
|
||||||
/-! # Inventory
|
/-! # Inventory
|
||||||
|
|
||||||
@@ -682,27 +626,6 @@ elab "Template" tacs:tacticSeq : tactic => do
|
|||||||
modifyLevel (←getCurLevelId) fun level => do
|
modifyLevel (←getCurLevelId) fun level => do
|
||||||
return {level with template := s!"{template}"}
|
return {level with template := s!"{template}"}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
@@ -721,47 +644,6 @@ def copyImages : IO Unit := do
|
|||||||
|
|
||||||
/-! # Make Game -/
|
/-! # Make Game -/
|
||||||
|
|
||||||
#eval IO.FS.createDirAll ".lake/gamedata/"
|
|
||||||
|
|
||||||
-- TODO: register all of this as ToJson instance?
|
|
||||||
def saveGameData (allItemsByType : HashMap InventoryType (HashSet Name)) : CommandElabM Unit := do
|
|
||||||
let game ← getCurGame
|
|
||||||
let env ← getEnv
|
|
||||||
let path : System.FilePath := s!"{← IO.currentDir}" / ".lake" / "gamedata"
|
|
||||||
|
|
||||||
if ← path.isDir then
|
|
||||||
IO.FS.removeDirAll path
|
|
||||||
IO.FS.createDirAll path
|
|
||||||
|
|
||||||
-- copy the images folder
|
|
||||||
copyImages
|
|
||||||
|
|
||||||
for (worldId, world) in game.worlds.nodes.toArray do
|
|
||||||
for (levelId, level) in world.levels.toArray do
|
|
||||||
IO.FS.writeFile (path / s!"level__{worldId}__{levelId}.json") (toString (toJson (level.toInfo env)))
|
|
||||||
|
|
||||||
IO.FS.writeFile (path / s!"game.json") (toString (getGameJson game))
|
|
||||||
|
|
||||||
for inventoryType in [InventoryType.Lemma, .Tactic, .Definition] do
|
|
||||||
for name in allItemsByType.findD inventoryType {} do
|
|
||||||
let some item ← getInventoryItem? name inventoryType
|
|
||||||
| throwError "Expected item to exist: {name}"
|
|
||||||
IO.FS.writeFile (path / s!"doc__{inventoryType}__{name}.json") (toString (toJson item))
|
|
||||||
|
|
||||||
let getTiles (type : InventoryType) : CommandElabM (Array InventoryTile) := do
|
|
||||||
(allItemsByType.findD type {}).toArray.mapM (fun name => do
|
|
||||||
let some item ← getInventoryItem? name type
|
|
||||||
| throwError "Expected item to exist: {name}"
|
|
||||||
return item.toTile)
|
|
||||||
let inventory : InventoryOverview := {
|
|
||||||
lemmas := ← getTiles .Lemma
|
|
||||||
tactics := ← getTiles .Tactic
|
|
||||||
definitions := ← getTiles .Definition
|
|
||||||
lemmaTab := none
|
|
||||||
}
|
|
||||||
IO.FS.writeFile (path / s!"inventory.json") (toString (toJson inventory))
|
|
||||||
|
|
||||||
|
|
||||||
def GameLevel.getInventory (level : GameLevel) : InventoryType → InventoryInfo
|
def GameLevel.getInventory (level : GameLevel) : InventoryType → InventoryInfo
|
||||||
| .Tactic => level.tactics
|
| .Tactic => level.tactics
|
||||||
| .Definition => level.definitions
|
| .Definition => level.definitions
|
||||||
@@ -928,12 +810,8 @@ elab "MakeGame" : command => do
|
|||||||
-- Items that should not be displayed in inventory
|
-- Items that should not be displayed in inventory
|
||||||
let mut hiddenItems : HashSet Name := {}
|
let mut hiddenItems : HashSet Name := {}
|
||||||
|
|
||||||
let allWorlds := game.worlds.nodes.toArray
|
|
||||||
let nrWorlds := allWorlds.size
|
|
||||||
let mut nrLevels := 0
|
|
||||||
|
|
||||||
-- Calculate which "items" are used/new in which world
|
-- Calculate which "items" are used/new in which world
|
||||||
for (worldId, world) in allWorlds do
|
for (worldId, world) in game.worlds.nodes.toArray do
|
||||||
let mut usedItems : HashSet Name := {}
|
let mut usedItems : HashSet Name := {}
|
||||||
let mut newItems : HashSet Name := {}
|
let mut newItems : HashSet Name := {}
|
||||||
for inventoryType in #[.Tactic, .Definition, .Lemma] do
|
for inventoryType in #[.Tactic, .Definition, .Lemma] do
|
||||||
@@ -972,12 +850,9 @@ elab "MakeGame" : command => do
|
|||||||
-- logInfo m!"{worldId} uses: {usedItems.toList}"
|
-- logInfo m!"{worldId} uses: {usedItems.toList}"
|
||||||
-- logInfo m!"{worldId} introduces: {newItems.toList}"
|
-- logInfo m!"{worldId} introduces: {newItems.toList}"
|
||||||
|
|
||||||
-- Moreover, count the number of levels in the game
|
|
||||||
nrLevels := nrLevels + world.levels.toArray.size
|
|
||||||
|
|
||||||
/- for each "item" this is a HashSet of `worldId`s that introduce this item -/
|
/- for each "item" this is a HashSet of `worldId`s that introduce this item -/
|
||||||
let mut worldsWithNewItem : HashMap Name (HashSet Name) := {}
|
let mut worldsWithNewItem : HashMap Name (HashSet Name) := {}
|
||||||
for (worldId, _world) in allWorlds do
|
for (worldId, _world) in game.worlds.nodes.toArray do
|
||||||
for newItem in newItemsInWorld.findD worldId {} do
|
for newItem in newItemsInWorld.findD worldId {} do
|
||||||
worldsWithNewItem := worldsWithNewItem.insert newItem $
|
worldsWithNewItem := worldsWithNewItem.insert newItem $
|
||||||
(worldsWithNewItem.findD newItem {}).insert worldId
|
(worldsWithNewItem.findD newItem {}).insert worldId
|
||||||
@@ -989,7 +864,7 @@ elab "MakeGame" : command => do
|
|||||||
let mut dependencyReasons : HashMap (Name × Name) (HashSet Name) := {}
|
let mut dependencyReasons : HashMap (Name × Name) (HashSet Name) := {}
|
||||||
|
|
||||||
-- Calculate world dependency graph `game.worlds`
|
-- Calculate world dependency graph `game.worlds`
|
||||||
for (dependentWorldId, _dependentWorld) in allWorlds do
|
for (dependentWorldId, _dependentWorld) in game.worlds.nodes.toArray do
|
||||||
let mut dependsOnWorlds : HashSet Name := {}
|
let mut dependsOnWorlds : HashSet Name := {}
|
||||||
-- Adding manual dependencies that were specified via the `Dependency` command.
|
-- Adding manual dependencies that were specified via the `Dependency` command.
|
||||||
for (sourceId, targetId) in game.worlds.edges do
|
for (sourceId, targetId) in game.worlds.edges do
|
||||||
@@ -1040,25 +915,14 @@ elab "MakeGame" : command => do
|
|||||||
logError m!"{w1} depends on {w2} because of {items.toList}."
|
logError m!"{w1} depends on {w2} because of {items.toList}."
|
||||||
else
|
else
|
||||||
worldDependsOnWorlds ← removeTransitive worldDependsOnWorlds
|
worldDependsOnWorlds ← removeTransitive worldDependsOnWorlds
|
||||||
|
|
||||||
-- need to delete all existing edges as they are already present in `worldDependsOnWorlds`.
|
|
||||||
modifyCurGame fun game =>
|
|
||||||
pure {game with worlds := {game.worlds with edges := Array.empty}}
|
|
||||||
|
|
||||||
for (dependentWorldId, worldIds) in worldDependsOnWorlds.toArray do
|
for (dependentWorldId, worldIds) in worldDependsOnWorlds.toArray do
|
||||||
modifyCurGame fun game =>
|
modifyCurGame fun game =>
|
||||||
pure {game with worlds := {game.worlds with
|
pure {game with worlds := {game.worlds with
|
||||||
edges := game.worlds.edges.append (worldIds.toArray.map fun wid => (wid, dependentWorldId))}}
|
edges := game.worlds.edges.append (worldIds.toArray.map fun wid => (wid, dependentWorldId))}}
|
||||||
|
|
||||||
-- Add the number of levels and worlds to the tile for the landing page
|
|
||||||
modifyCurGame fun game => pure {game with tile := {game.tile with
|
|
||||||
levels := nrLevels
|
|
||||||
worlds := nrWorlds }}
|
|
||||||
|
|
||||||
-- Apparently we need to reload `game` to get the changes to `game.worlds` we just made
|
-- Apparently we need to reload `game` to get the changes to `game.worlds` we just made
|
||||||
let game ← getCurGame
|
let game ← getCurGame
|
||||||
|
|
||||||
let mut allItemsByType : HashMap InventoryType (HashSet Name) := {}
|
|
||||||
-- Compute which inventory items are available in which level:
|
-- Compute which inventory items are available in which level:
|
||||||
for inventoryType in #[.Tactic, .Definition, .Lemma] do
|
for inventoryType in #[.Tactic, .Definition, .Lemma] do
|
||||||
|
|
||||||
@@ -1188,9 +1052,6 @@ elab "MakeGame" : command => do
|
|||||||
|
|
||||||
modifyLevel ⟨← getCurGameId, worldId, levelId⟩ fun level => do
|
modifyLevel ⟨← getCurGameId, worldId, levelId⟩ fun level => do
|
||||||
return level.setComputedInventory inventoryType itemsArray
|
return level.setComputedInventory inventoryType itemsArray
|
||||||
allItemsByType := allItemsByType.insert inventoryType allItems
|
|
||||||
|
|
||||||
saveGameData allItemsByType
|
|
||||||
|
|
||||||
/-! # Debugging tools -/
|
/-! # Debugging tools -/
|
||||||
|
|
||||||
|
|||||||
@@ -108,12 +108,6 @@ structure InventoryTile where
|
|||||||
hidden := false
|
hidden := false
|
||||||
deriving ToJson, FromJson, Repr, Inhabited
|
deriving ToJson, FromJson, Repr, Inhabited
|
||||||
|
|
||||||
def InventoryItem.toTile (item : InventoryItem) : InventoryTile := {
|
|
||||||
name := item.name,
|
|
||||||
displayName := item.displayName
|
|
||||||
category := item.category
|
|
||||||
}
|
|
||||||
|
|
||||||
/-- The extension that stores the doc templates. Note that you can only add, but never modify
|
/-- The extension that stores the doc templates. Note that you can only add, but never modify
|
||||||
entries! -/
|
entries! -/
|
||||||
initialize inventoryTemplateExt :
|
initialize inventoryTemplateExt :
|
||||||
@@ -141,12 +135,7 @@ def getInventoryItem? [Monad m] [MonadEnv m] (n : Name) (type : InventoryType) :
|
|||||||
m (Option InventoryItem) := do
|
m (Option InventoryItem) := do
|
||||||
return (inventoryExt.getState (← getEnv)).find? (fun x => x.name == n && x.type == type)
|
return (inventoryExt.getState (← getEnv)).find? (fun x => x.name == n && x.type == type)
|
||||||
|
|
||||||
structure InventoryOverview where
|
|
||||||
tactics : Array InventoryTile
|
|
||||||
lemmas : Array InventoryTile
|
|
||||||
definitions : Array InventoryTile
|
|
||||||
lemmaTab : Option String
|
|
||||||
deriving ToJson, FromJson
|
|
||||||
|
|
||||||
/-! ## Environment extensions for game specification -/
|
/-! ## Environment extensions for game specification -/
|
||||||
|
|
||||||
@@ -265,68 +254,8 @@ structure GameLevel where
|
|||||||
lemmas: InventoryInfo := default
|
lemmas: InventoryInfo := default
|
||||||
/-- A proof template that is printed in an empty editor. -/
|
/-- A proof template that is printed in an empty editor. -/
|
||||||
template: Option String := none
|
template: Option String := none
|
||||||
/-- The image for this level. -/
|
deriving Inhabited, Repr
|
||||||
image : String := default
|
|
||||||
deriving Inhabited, Repr
|
|
||||||
|
|
||||||
structure WorldOverview where
|
|
||||||
world: Name
|
|
||||||
tactics: Array InventoryTile := default
|
|
||||||
definitions: Array InventoryTile := default
|
|
||||||
lemmas: Array InventoryTile := default
|
|
||||||
deriving FromJson, ToJson, Inhabited, Repr
|
|
||||||
|
|
||||||
/-- Json-encodable version of `GameLevel`
|
|
||||||
Fields:
|
|
||||||
- description: Lemma in mathematical language.
|
|
||||||
- descriptionGoal: Lemma printed as Lean-Code.
|
|
||||||
-/
|
|
||||||
structure LevelInfo where
|
|
||||||
index : Nat
|
|
||||||
title : String
|
|
||||||
tactics : Array InventoryTile
|
|
||||||
lemmas : Array InventoryTile
|
|
||||||
definitions : Array InventoryTile
|
|
||||||
introduction : String
|
|
||||||
conclusion : String
|
|
||||||
descrText : Option String := none
|
|
||||||
descrFormat : String := ""
|
|
||||||
lemmaTab : Option String
|
|
||||||
displayName : Option String
|
|
||||||
statementName : Option String
|
|
||||||
template : Option String
|
|
||||||
image: Option String
|
|
||||||
deriving ToJson, FromJson
|
|
||||||
|
|
||||||
def GameLevel.toInfo (lvl : GameLevel) (env : Environment) : LevelInfo :=
|
|
||||||
{ index := lvl.index,
|
|
||||||
title := lvl.title,
|
|
||||||
tactics := lvl.tactics.tiles,
|
|
||||||
lemmas := lvl.lemmas.tiles,
|
|
||||||
definitions := lvl.definitions.tiles,
|
|
||||||
descrText := lvl.descrText,
|
|
||||||
descrFormat := lvl.descrFormat --toExpr <| format (lvl.goal.raw) --toString <| Syntax.formatStx (lvl.goal.raw) --Syntax.formatStx (lvl.goal.raw) , -- TODO
|
|
||||||
introduction := lvl.introduction
|
|
||||||
conclusion := lvl.conclusion
|
|
||||||
lemmaTab := match lvl.lemmaTab with
|
|
||||||
| some tab => tab
|
|
||||||
| none =>
|
|
||||||
-- Try to set the lemma tab to the category of the first added lemma
|
|
||||||
match lvl.lemmas.tiles.find? (·.new) with
|
|
||||||
| some tile => tile.category
|
|
||||||
| none => none
|
|
||||||
statementName := lvl.statementName.toString
|
|
||||||
displayName := match lvl.statementName with
|
|
||||||
| .anonymous => none
|
|
||||||
| name => match (inventoryExt.getState env).find?
|
|
||||||
(fun x => x.name == name && x.type == .Lemma) with
|
|
||||||
| some n => n.displayName
|
|
||||||
| none => name.toString
|
|
||||||
-- Note: we could call `.find!` because we check in `Statement` that the
|
|
||||||
-- lemma doc must exist.
|
|
||||||
template := lvl.template
|
|
||||||
image := lvl.image
|
|
||||||
}
|
|
||||||
|
|
||||||
/-! ## World -/
|
/-! ## World -/
|
||||||
|
|
||||||
@@ -342,46 +271,17 @@ structure World where
|
|||||||
conclusion : String := default
|
conclusion : String := default
|
||||||
/-- The levels of the world. -/
|
/-- The levels of the world. -/
|
||||||
levels: HashMap Nat GameLevel := default
|
levels: HashMap Nat GameLevel := default
|
||||||
/-- The introduction image of the world. -/
|
|
||||||
image: String := default
|
|
||||||
deriving Inhabited
|
deriving Inhabited
|
||||||
|
|
||||||
instance : ToJson World := ⟨
|
instance : ToJson World := ⟨
|
||||||
fun world => Json.mkObj [
|
fun world => Json.mkObj [
|
||||||
("name", toJson world.name),
|
("name", toJson world.name),
|
||||||
("title", world.title),
|
("title", world.title),
|
||||||
("introduction", world.introduction),
|
("introduction", world.introduction)]
|
||||||
("image", world.image)]
|
|
||||||
⟩
|
⟩
|
||||||
|
|
||||||
/-! ## Game -/
|
/-! ## Game -/
|
||||||
|
|
||||||
/-- A tile as they are displayed on the servers landing page. -/
|
|
||||||
structure GameTile where
|
|
||||||
/-- The title of the game -/
|
|
||||||
title: String
|
|
||||||
/-- One catch phrase about the game -/
|
|
||||||
short: String := default
|
|
||||||
/-- One paragraph description what the game is about -/
|
|
||||||
long: String := default
|
|
||||||
/-- List of languages the game supports
|
|
||||||
|
|
||||||
TODO: What's the expectected format
|
|
||||||
TODO: Must be a list with a single language currently
|
|
||||||
-/
|
|
||||||
languages: List String := default
|
|
||||||
/-- A list of games which this one builds upon -/
|
|
||||||
prerequisites: List String := default
|
|
||||||
/-- Number of worlds in the game -/
|
|
||||||
worlds: Nat := default
|
|
||||||
/-- Number of levels in the game -/
|
|
||||||
levels: Nat := default
|
|
||||||
/-- A cover image of the game
|
|
||||||
|
|
||||||
TODO: What's the format? -/
|
|
||||||
image: String := default
|
|
||||||
deriving Inhabited, ToJson
|
|
||||||
|
|
||||||
structure Game where
|
structure Game where
|
||||||
/-- Internal name of the game. -/
|
/-- Internal name of the game. -/
|
||||||
name : Name
|
name : Name
|
||||||
@@ -396,19 +296,8 @@ structure Game where
|
|||||||
/-- TODO: currently unused. -/
|
/-- TODO: currently unused. -/
|
||||||
authors : List String := default
|
authors : List String := default
|
||||||
worlds : Graph Name World := default
|
worlds : Graph Name World := default
|
||||||
/-- The tile displayed on the server's landing page. -/
|
|
||||||
tile : GameTile := default
|
|
||||||
/-- The path to the background image of the world. -/
|
|
||||||
image : String := default
|
|
||||||
deriving Inhabited, ToJson
|
deriving Inhabited, ToJson
|
||||||
|
|
||||||
def getGameJson (game : «Game») : Json := Id.run do
|
|
||||||
let gameJson : Json := toJson game
|
|
||||||
-- Add world sizes to Json object
|
|
||||||
let worldSize := game.worlds.nodes.toList.map (fun (n, w) => (n.toString, w.levels.size))
|
|
||||||
let gameJson := gameJson.mergeObj (Json.mkObj [("worldSize", Json.mkObj worldSize)])
|
|
||||||
return gameJson
|
|
||||||
|
|
||||||
/-! ## Game environment extension -/
|
/-! ## Game environment extension -/
|
||||||
|
|
||||||
def HashMap.merge [BEq α] [Hashable α] (old : HashMap α β) (new : HashMap α β) (merge : β → β → β) :
|
def HashMap.merge [BEq α] [Hashable α] (old : HashMap α β) (new : HashMap α β) (merge : β → β → β) :
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/- This file is mostly copied from `Lean/Server/FileWorker.lean`. -/
|
/- This file is mostly copied from `Lean/Server/FileWorker.lean`. -/
|
||||||
import Lean.Server.FileWorker
|
import Lean.Server.FileWorker
|
||||||
import GameServer.Game
|
import GameServer.Game
|
||||||
import GameServer.ImportModules
|
|
||||||
|
|
||||||
namespace MyModule
|
namespace MyModule
|
||||||
open Lean
|
open Lean
|
||||||
@@ -396,26 +395,9 @@ section Initialization
|
|||||||
fileName := (System.Uri.fileUriToPath? doc.uri).getD doc.uri |>.toString
|
fileName := (System.Uri.fileUriToPath? doc.uri).getD doc.uri |>.toString
|
||||||
fileMap := default
|
fileMap := default
|
||||||
|
|
||||||
def compileHeader (m : DocumentMeta) (hOut : FS.Stream) (opts : Options) (hasWidgets : Bool)
|
def mkHeaderTask (m : DocumentMeta) (hOut : FS.Stream) (paths : List System.FilePath)
|
||||||
(levelParams : Game.DidOpenLevelParams) (initParams : InitializeParams) :
|
(env : Environment) (opts : Options) (hasWidgets : Bool) :
|
||||||
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`.
|
|
||||||
let out ← IO.Process.output
|
|
||||||
{ cwd := levelParams.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 / (levelParams.gameDir : System.FilePath) / p
|
|
||||||
|
|
||||||
-- Set the search path
|
|
||||||
Lean.searchPathRef.set paths
|
|
||||||
|
|
||||||
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
|
||||||
{m.mkInputContext with
|
{m.mkInputContext with
|
||||||
@@ -457,11 +439,32 @@ section Initialization
|
|||||||
publishDiagnostics m headerSnap.diagnostics.toArray hOut
|
publishDiagnostics m headerSnap.diagnostics.toArray hOut
|
||||||
return (headerSnap, srcSearchPath)
|
return (headerSnap, srcSearchPath)
|
||||||
|
|
||||||
|
def compileHeader (m : DocumentMeta) (hOut : FS.Stream) (opts : Options) (hasWidgets : Bool)
|
||||||
|
(levelParams : Game.DidOpenLevelParams) :
|
||||||
|
IO (Syntax × Task (Except Error (Snapshot × SearchPath))) := do
|
||||||
|
-- Determine search paths of the game project by running `lake env printenv LEAN_PATH`.
|
||||||
|
let out ← IO.Process.output
|
||||||
|
{ cwd := levelParams.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 / (levelParams.gameDir : System.FilePath) / p
|
||||||
|
|
||||||
|
-- Set the search path
|
||||||
|
Lean.searchPathRef.set paths
|
||||||
|
|
||||||
|
let env ← importModules #[{ module := `Init : Import }, { module := levelParams.levelModule : Import }] {} 0
|
||||||
|
-- return (env, paths)
|
||||||
|
mkHeaderTask m hOut paths env opts hasWidgets
|
||||||
|
|
||||||
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)
|
||||||
(levelParams : Game.DidOpenLevelParams) : 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)
|
||||||
levelParams initParams
|
levelParams
|
||||||
let cancelTk ← CancelToken.new
|
let cancelTk ← CancelToken.new
|
||||||
let ctx :=
|
let ctx :=
|
||||||
{ hIn := i
|
{ hIn := i
|
||||||
@@ -516,10 +519,8 @@ section MessageHandling
|
|||||||
end MessageHandling
|
end MessageHandling
|
||||||
|
|
||||||
section MainLoop
|
section MainLoop
|
||||||
partial def mainLoop : GameWorkerM Unit := do
|
partial def mainLoop1 (msg : JsonRpc.Message): GameWorkerM Bool := do
|
||||||
let ctx ← read
|
|
||||||
let mut st ← StateT.lift get
|
let mut st ← StateT.lift get
|
||||||
let msg ← ctx.hIn.readLspMessage
|
|
||||||
let filterFinishedTasks (acc : PendingRequestMap) (id : RequestID) (task : Task (Except IO.Error Unit))
|
let filterFinishedTasks (acc : PendingRequestMap) (id : RequestID) (task : Task (Except IO.Error Unit))
|
||||||
: IO PendingRequestMap := do
|
: IO PendingRequestMap := do
|
||||||
if (← hasFinished task) then
|
if (← hasFinished task) then
|
||||||
@@ -542,11 +543,11 @@ section MainLoop
|
|||||||
match msg with
|
match msg with
|
||||||
| Message.request id method (some params) =>
|
| Message.request id method (some params) =>
|
||||||
handleRequest id method (toJson params)
|
handleRequest id method (toJson params)
|
||||||
mainLoop
|
return false
|
||||||
| Message.notification "exit" none =>
|
| Message.notification "exit" none =>
|
||||||
let doc := st.doc
|
let doc := st.doc
|
||||||
doc.cancelTk.set
|
doc.cancelTk.set
|
||||||
return ()
|
return true
|
||||||
| Message.notification "$/game/setInventory" params =>
|
| Message.notification "$/game/setInventory" params =>
|
||||||
let p := (← parseParams Game.SetInventoryParams (toJson params))
|
let p := (← parseParams Game.SetInventoryParams (toJson params))
|
||||||
let s ← get
|
let s ← get
|
||||||
@@ -554,11 +555,19 @@ section MainLoop
|
|||||||
set {s with levelParams := {s.levelParams with
|
set {s with levelParams := {s.levelParams with
|
||||||
inventory := p.inventory,
|
inventory := p.inventory,
|
||||||
difficulty := p.difficulty}}
|
difficulty := p.difficulty}}
|
||||||
mainLoop
|
return false
|
||||||
| Message.notification method (some params) =>
|
| Message.notification method (some params) =>
|
||||||
handleNotification method (toJson params)
|
handleNotification method (toJson params)
|
||||||
mainLoop
|
return false
|
||||||
| _ => throwServerError "Got invalid JSON-RPC message"
|
| _ => throwServerError "Got invalid JSON-RPC message"
|
||||||
|
|
||||||
|
|
||||||
|
partial def mainLoop : GameWorkerM Unit := do
|
||||||
|
let ctx ← read
|
||||||
|
let msg ← ctx.hIn.readLspMessage
|
||||||
|
if not (← mainLoop1 msg) then
|
||||||
|
mainLoop
|
||||||
|
|
||||||
end MainLoop
|
end MainLoop
|
||||||
|
|
||||||
def initAndRunWorker (i o e : FS.Stream) (opts : Options) : IO UInt32 := do
|
def initAndRunWorker (i o e : FS.Stream) (opts : Options) : IO UInt32 := do
|
||||||
|
|||||||
+148
-44
@@ -25,6 +25,53 @@ open Lsp
|
|||||||
open JsonRpc
|
open JsonRpc
|
||||||
open IO
|
open IO
|
||||||
|
|
||||||
|
def getGame (game : Name): GameServerM Json := do
|
||||||
|
let some game ← getGame? game
|
||||||
|
| throwServerError "Game not found"
|
||||||
|
let gameJson : Json := toJson game
|
||||||
|
-- Add world sizes to Json object
|
||||||
|
let worldSize := game.worlds.nodes.toList.map (fun (n, w) => (n.toString, w.levels.size))
|
||||||
|
let gameJson := gameJson.mergeObj (Json.mkObj [("worldSize", Json.mkObj worldSize)])
|
||||||
|
return gameJson
|
||||||
|
|
||||||
|
/--
|
||||||
|
Fields:
|
||||||
|
- description: Lemma in mathematical language.
|
||||||
|
- descriptionGoal: Lemma printed as Lean-Code.
|
||||||
|
-/
|
||||||
|
structure LevelInfo where
|
||||||
|
index : Nat
|
||||||
|
title : String
|
||||||
|
tactics : Array InventoryTile
|
||||||
|
lemmas : Array InventoryTile
|
||||||
|
definitions : Array InventoryTile
|
||||||
|
introduction : String
|
||||||
|
conclusion : String
|
||||||
|
descrText : Option String := none
|
||||||
|
descrFormat : String := ""
|
||||||
|
lemmaTab : Option String
|
||||||
|
displayName : Option String
|
||||||
|
statementName : Option String
|
||||||
|
template : Option String
|
||||||
|
deriving ToJson, FromJson
|
||||||
|
|
||||||
|
structure InventoryOverview where
|
||||||
|
tactics : Array InventoryTile
|
||||||
|
lemmas : Array InventoryTile
|
||||||
|
definitions : Array InventoryTile
|
||||||
|
lemmaTab : Option String
|
||||||
|
deriving ToJson, FromJson
|
||||||
|
|
||||||
|
structure LoadLevelParams where
|
||||||
|
world : Name
|
||||||
|
level : Nat
|
||||||
|
deriving ToJson, FromJson
|
||||||
|
|
||||||
|
-- structure LoadTemplateParams where
|
||||||
|
-- world : Name
|
||||||
|
-- level : Nat
|
||||||
|
-- deriving ToJson, FromJson
|
||||||
|
|
||||||
structure DidOpenLevelParams where
|
structure DidOpenLevelParams where
|
||||||
uri : String
|
uri : String
|
||||||
gameDir : String
|
gameDir : String
|
||||||
@@ -44,6 +91,11 @@ structure DidOpenLevelParams where
|
|||||||
statementName : Name
|
statementName : Name
|
||||||
deriving ToJson, FromJson
|
deriving ToJson, FromJson
|
||||||
|
|
||||||
|
structure LoadDocParams where
|
||||||
|
name : Name
|
||||||
|
type : InventoryType
|
||||||
|
deriving ToJson, FromJson
|
||||||
|
|
||||||
structure SetInventoryParams where
|
structure SetInventoryParams where
|
||||||
inventory : Array String
|
inventory : Array String
|
||||||
difficulty : Nat
|
difficulty : Nat
|
||||||
@@ -79,10 +131,86 @@ def handleDidOpenLevel (params : Json) : GameServerM Unit := do
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
partial def handleServerEvent (ev : ServerEvent) : GameServerM Bool := do
|
partial def handleServerEvent (ev : ServerEvent) : GameServerM Bool := do
|
||||||
match ev with
|
match ev with
|
||||||
| ServerEvent.clientMsg msg =>
|
| ServerEvent.clientMsg msg =>
|
||||||
match msg with
|
match msg with
|
||||||
|
| Message.request id "info" _ =>
|
||||||
|
let s ← get
|
||||||
|
let c ← read
|
||||||
|
c.hOut.writeLspResponse ⟨id, (← getGame s.game)⟩
|
||||||
|
return true
|
||||||
|
| Message.request id "loadLevel" params =>
|
||||||
|
let p ← parseParams LoadLevelParams (toJson params)
|
||||||
|
let s ← get
|
||||||
|
let c ← read
|
||||||
|
let some lvl ← getLevel? {game := s.game, world := p.world, level := p.level}
|
||||||
|
| do
|
||||||
|
c.hOut.writeLspResponseError ⟨id, .invalidParams, s!"Level not found: world {p.world}, level {p.level}", none⟩
|
||||||
|
return true
|
||||||
|
|
||||||
|
let env ← getEnv
|
||||||
|
|
||||||
|
let levelInfo : LevelInfo :=
|
||||||
|
{ index := lvl.index,
|
||||||
|
title := lvl.title,
|
||||||
|
tactics := lvl.tactics.tiles,
|
||||||
|
lemmas := lvl.lemmas.tiles,
|
||||||
|
definitions := lvl.definitions.tiles,
|
||||||
|
descrText := lvl.descrText,
|
||||||
|
descrFormat := lvl.descrFormat --toExpr <| format (lvl.goal.raw) --toString <| Syntax.formatStx (lvl.goal.raw) --Syntax.formatStx (lvl.goal.raw) , -- TODO
|
||||||
|
introduction := lvl.introduction
|
||||||
|
conclusion := lvl.conclusion
|
||||||
|
lemmaTab := match lvl.lemmaTab with
|
||||||
|
| some tab => tab
|
||||||
|
| none =>
|
||||||
|
-- Try to set the lemma tab to the category of the first added lemma
|
||||||
|
match lvl.lemmas.tiles.find? (·.new) with
|
||||||
|
| some tile => tile.category
|
||||||
|
| none => none
|
||||||
|
statementName := lvl.statementName.toString
|
||||||
|
displayName := match lvl.statementName with
|
||||||
|
| .anonymous => none
|
||||||
|
| name => match (inventoryExt.getState env).find?
|
||||||
|
(fun x => x.name == name && x.type == .Lemma) with
|
||||||
|
| some n => n.displayName
|
||||||
|
| none => name.toString
|
||||||
|
-- Note: we could call `.find!` because we check in `Statement` that the
|
||||||
|
-- lemma doc must exist.
|
||||||
|
template := lvl.template
|
||||||
|
}
|
||||||
|
c.hOut.writeLspResponse ⟨id, ToJson.toJson levelInfo⟩
|
||||||
|
return true
|
||||||
|
-- | Message.request id "loadTemplate" params =>
|
||||||
|
-- let p ← parseParams LoadTemplateParams (toJson params)
|
||||||
|
-- let s ← get
|
||||||
|
-- let c ← read
|
||||||
|
|
||||||
|
-- let some game ← getGame? s.game
|
||||||
|
-- | throwServerError "Game not found"
|
||||||
|
-- let some world := game.worlds.nodes.find? p.world
|
||||||
|
-- | throwServerError "World not found"
|
||||||
|
|
||||||
|
-- let mut templates : Array <| Option String := #[]
|
||||||
|
-- for (_, level) in world.levels.toArray do
|
||||||
|
-- templates := templates.push level.template
|
||||||
|
-- c.hOut.writeLspResponse ⟨id, ToJson.toJson templates⟩
|
||||||
|
-- return true
|
||||||
|
| Message.request id "loadDoc" params =>
|
||||||
|
let p ← parseParams LoadDocParams (toJson params)
|
||||||
|
let c ← read
|
||||||
|
let some doc ← getInventoryItem? p.name p.type
|
||||||
|
| do
|
||||||
|
c.hOut.writeLspResponseError ⟨id, .invalidParams,
|
||||||
|
s!"Documentation not found: {p.name}", none⟩
|
||||||
|
return true
|
||||||
|
-- TODO: not necessary at all?
|
||||||
|
-- Here we only need to convert the fields that were not `String` in the `InventoryDocEntry`
|
||||||
|
-- let doc : InventoryItem := { doc with
|
||||||
|
-- name := doc.name.toString }
|
||||||
|
c.hOut.writeLspResponse ⟨id, ToJson.toJson doc⟩
|
||||||
|
return true
|
||||||
| Message.notification "$/game/setInventory" params =>
|
| Message.notification "$/game/setInventory" params =>
|
||||||
let p := (← parseParams SetInventoryParams (toJson params))
|
let p := (← parseParams SetInventoryParams (toJson params))
|
||||||
let s ← get
|
let s ← get
|
||||||
@@ -97,50 +225,26 @@ partial def handleServerEvent (ev : ServerEvent) : GameServerM Bool := do
|
|||||||
let s ← get
|
let s ← get
|
||||||
let some game ← getGame? s.game
|
let some game ← getGame? s.game
|
||||||
| return false
|
| return false
|
||||||
|
-- All Levels have the same tiles, so we just load them from level 1 of an arbitrary world
|
||||||
let mut overview : Array WorldOverview := #[]
|
-- and reset `new`, `disabled` and `unlocked`.
|
||||||
for ⟨worldId, world⟩ in game.worlds.nodes.toList do
|
-- Note: as we allow worlds without any levels (for developing), we might need
|
||||||
let mut tactics : Array InventoryTile := #[]
|
-- to try until we find the first world with levels.
|
||||||
let mut theorems : Array InventoryTile := #[]
|
for ⟨worldId, _⟩ in game.worlds.nodes.toList do
|
||||||
let mut definitions : Array InventoryTile := #[]
|
let some lvl ← getLevel? {game := s.game, world := worldId, level := 1}
|
||||||
for ⟨_levelId, lvl⟩ in world.levels.toList do
|
| do continue
|
||||||
tactics := tactics ++ lvl.tactics.tiles.filterMap (fun tile => match tile.new with
|
let inventory : InventoryOverview := {
|
||||||
| true => some {tile with new := false, locked := true, disabled := false}
|
tactics := lvl.tactics.tiles.map
|
||||||
| false => none)
|
({ · with locked := true, disabled := false, new := false }),
|
||||||
theorems := theorems ++ lvl.lemmas.tiles.filterMap (fun tile => match tile.new with
|
lemmas := lvl.lemmas.tiles.map
|
||||||
| true => some {tile with new := false, locked := true, disabled := false}
|
({ · with locked := true, disabled := false, new := false }),
|
||||||
| false => none)
|
definitions := lvl.definitions.tiles.map
|
||||||
definitions := definitions ++ lvl.definitions.tiles.filterMap (fun tile => match tile.new with
|
({ · with locked := true, disabled := false, new := false }),
|
||||||
| true => some {tile with new := false, locked := true, disabled := false}
|
lemmaTab := none
|
||||||
| false => none)
|
}
|
||||||
overview := overview.push {
|
let c ← read
|
||||||
world := worldId,
|
c.hOut.writeLspResponse ⟨id, ToJson.toJson inventory⟩
|
||||||
tactics := tactics,
|
return true
|
||||||
lemmas := theorems,
|
return false
|
||||||
definitions := definitions }
|
|
||||||
let c ← read
|
|
||||||
c.hOut.writeLspResponse ⟨id, ToJson.toJson overview⟩
|
|
||||||
return true
|
|
||||||
-- -- All Levels have the same tiles, so we just load them from level 1 of an arbitrary world
|
|
||||||
-- -- and reset `new`, `disabled` and `unlocked`.
|
|
||||||
-- -- Note: as we allow worlds without any levels (for developing), we might need
|
|
||||||
-- -- to try until we find the first world with levels.
|
|
||||||
-- for ⟨worldId, _⟩ in game.worlds.nodes.toList do
|
|
||||||
-- let some lvl ← getLevel? {game := s.game, world := worldId, level := 1}
|
|
||||||
-- | do continue
|
|
||||||
-- let inventory : InventoryOverview := {
|
|
||||||
-- tactics := lvl.tactics.tiles.map
|
|
||||||
-- ({ · with locked := true, disabled := false, new := false }),
|
|
||||||
-- lemmas := lvl.lemmas.tiles.map
|
|
||||||
-- ({ · with locked := true, disabled := false, new := false }),
|
|
||||||
-- definitions := lvl.definitions.tiles.map
|
|
||||||
-- ({ · with locked := true, disabled := false, new := false }),
|
|
||||||
-- lemmaTab := none
|
|
||||||
-- }
|
|
||||||
-- let c ← read
|
|
||||||
-- c.hOut.writeLspResponse ⟨id, ToJson.toJson inventory⟩
|
|
||||||
-- return true
|
|
||||||
-- return false
|
|
||||||
| _ => return false
|
| _ => return false
|
||||||
| _ => return false
|
| _ => return false
|
||||||
|
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
import Lean.Environment
|
|
||||||
import Std.Tactic.OpenPrivate
|
|
||||||
import Lean.Data.Lsp.Communication
|
|
||||||
|
|
||||||
open Lean
|
|
||||||
|
|
||||||
inductive LoadingKind := | finalizeExtensions | loadConstants
|
|
||||||
deriving ToJson
|
|
||||||
|
|
||||||
structure LoadingParams : Type where
|
|
||||||
counter : Nat
|
|
||||||
kind : LoadingKind
|
|
||||||
deriving ToJson
|
|
||||||
|
|
||||||
-- Code adapted from `Lean/Environment.lean`
|
|
||||||
|
|
||||||
partial def importModulesCore' (imports : Array Import) : ImportStateM Unit := do
|
|
||||||
for i in imports do
|
|
||||||
if i.runtimeOnly || (← get).moduleNameSet.contains i.module then
|
|
||||||
continue
|
|
||||||
modify fun s => { s with moduleNameSet := s.moduleNameSet.insert i.module }
|
|
||||||
let mFile ← findOLean i.module
|
|
||||||
unless (← mFile.pathExists) do
|
|
||||||
throw <| IO.userError s!"object file '{mFile}' of module {i.module} does not exist"
|
|
||||||
let (mod, region) ← readModuleData mFile
|
|
||||||
importModulesCore' mod.imports
|
|
||||||
modify fun s => { s with
|
|
||||||
moduleData := s.moduleData.push mod
|
|
||||||
regions := s.regions.push region
|
|
||||||
moduleNames := s.moduleNames.push i.module
|
|
||||||
}
|
|
||||||
|
|
||||||
open private mkInitialExtensionStates Environment.mk setImportedEntries finalizePersistentExtensions
|
|
||||||
ensureExtensionsArraySize from Lean.Environment
|
|
||||||
|
|
||||||
private partial def finalizePersistentExtensions' (env : Environment) (mods : Array ModuleData) (opts : Options) : IO Environment := do
|
|
||||||
loop 0 env
|
|
||||||
where
|
|
||||||
loop (i : Nat) (env : Environment) : IO Environment := do
|
|
||||||
(← IO.getStdout).writeLspNotification {
|
|
||||||
method := "$/game/loading",
|
|
||||||
param := {counter := i, kind := .finalizeExtensions : LoadingParams} }
|
|
||||||
-- Recall that the size of the array stored `persistentEnvExtensionRef` may increase when we import user-defined environment extensions.
|
|
||||||
let pExtDescrs ← persistentEnvExtensionsRef.get
|
|
||||||
if i < pExtDescrs.size then
|
|
||||||
let extDescr := pExtDescrs[i]!
|
|
||||||
let s := extDescr.toEnvExtension.getState env
|
|
||||||
let prevSize := (← persistentEnvExtensionsRef.get).size
|
|
||||||
let prevAttrSize ← getNumBuiltinAttributes
|
|
||||||
let newState ← extDescr.addImportedFn s.importedEntries { env := env, opts := opts }
|
|
||||||
let mut env := extDescr.toEnvExtension.setState env { s with state := newState }
|
|
||||||
env ← ensureExtensionsArraySize env
|
|
||||||
if (← persistentEnvExtensionsRef.get).size > prevSize || (← getNumBuiltinAttributes) > prevAttrSize then
|
|
||||||
-- This branch is executed when `pExtDescrs[i]` is the extension associated with the `init` attribute, and
|
|
||||||
-- a user-defined persistent extension is imported.
|
|
||||||
-- Thus, we invoke `setImportedEntries` to update the array `importedEntries` with the entries for the new extensions.
|
|
||||||
env ← setImportedEntries env mods prevSize
|
|
||||||
-- See comment at `updateEnvAttributesRef`
|
|
||||||
env ← updateEnvAttributes env
|
|
||||||
loop (i + 1) env
|
|
||||||
else
|
|
||||||
return env
|
|
||||||
|
|
||||||
def finalizeImport' (s : ImportState) (imports : Array Import) (opts : Options) (trustLevel : UInt32 := 0) : IO Environment := do
|
|
||||||
let numConsts := s.moduleData.foldl (init := 0) fun numConsts mod =>
|
|
||||||
numConsts + mod.constants.size + mod.extraConstNames.size
|
|
||||||
let mut const2ModIdx : HashMap Name ModuleIdx := mkHashMap (capacity := numConsts)
|
|
||||||
let mut constantMap : HashMap Name ConstantInfo := mkHashMap (capacity := numConsts)
|
|
||||||
for h:modIdx in [0:s.moduleData.size] do
|
|
||||||
if modIdx % 100 = 0 then
|
|
||||||
let percentage := modIdx * 100 / s.moduleData.size
|
|
||||||
(← IO.getStdout).writeLspNotification {
|
|
||||||
method := "$/game/loading",
|
|
||||||
param := {counter := percentage, kind := .loadConstants : LoadingParams} }
|
|
||||||
let mod := s.moduleData[modIdx]'h.upper
|
|
||||||
for cname in mod.constNames, cinfo in mod.constants do
|
|
||||||
match constantMap.insert' cname cinfo with
|
|
||||||
| (constantMap', replaced) =>
|
|
||||||
constantMap := constantMap'
|
|
||||||
if replaced then
|
|
||||||
throwAlreadyImported s const2ModIdx modIdx cname
|
|
||||||
const2ModIdx := const2ModIdx.insert cname modIdx
|
|
||||||
for cname in mod.extraConstNames do
|
|
||||||
const2ModIdx := const2ModIdx.insert cname modIdx
|
|
||||||
let constants : ConstMap := SMap.fromHashMap constantMap false
|
|
||||||
let exts ← mkInitialExtensionStates
|
|
||||||
let env : Environment := Environment.mk
|
|
||||||
(const2ModIdx := const2ModIdx)
|
|
||||||
(constants := constants)
|
|
||||||
(extraConstNames := {})
|
|
||||||
(extensions := exts)
|
|
||||||
(header := {
|
|
||||||
quotInit := !imports.isEmpty -- We assume `core.lean` initializes quotient module
|
|
||||||
trustLevel := trustLevel
|
|
||||||
imports := imports
|
|
||||||
regions := s.regions
|
|
||||||
moduleNames := s.moduleNames
|
|
||||||
moduleData := s.moduleData
|
|
||||||
})
|
|
||||||
|
|
||||||
let env ← setImportedEntries env s.moduleData
|
|
||||||
finalizePersistentExtensions' env s.moduleData opts
|
|
||||||
|
|
||||||
def importModules' (imports : Array Import) : IO Environment := do
|
|
||||||
withImporting do
|
|
||||||
let (_, s) ← importModulesCore' imports |>.run
|
|
||||||
let env ← finalizeImport' s imports {} 0
|
|
||||||
return env
|
|
||||||
@@ -78,7 +78,7 @@ partial def matchExpr (pattern : Expr) (e : Expr) (bij : FVarBijection := {}) :
|
|||||||
| _, _ => none
|
| _, _ => none
|
||||||
|
|
||||||
/-- Check if each fvar in `patterns` has a matching fvar in `fvars` -/
|
/-- Check if each fvar in `patterns` has a matching fvar in `fvars` -/
|
||||||
def matchDecls (patterns : Array Expr) (fvars : Array Expr) (strict := true) (initBij : FVarBijection := {}) : MetaM (Option FVarBijection) := do
|
def matchDecls (patterns : Array Expr) (fvars : Array Expr) (strict := true) (initBij : FVarBijection := {}) : MetaM Bool := do
|
||||||
-- We iterate through the array backwards hoping that this will find us faster results
|
-- We iterate through the array backwards hoping that this will find us faster results
|
||||||
-- TODO: implement backtracking
|
-- TODO: implement backtracking
|
||||||
let mut bij := initBij
|
let mut bij := initBij
|
||||||
@@ -97,11 +97,11 @@ def matchDecls (patterns : Array Expr) (fvars : Array Expr) (strict := true) (in
|
|||||||
-- usedFvars := usedFvars.set! (fvars.size - j - 1) true
|
-- usedFvars := usedFvars.set! (fvars.size - j - 1) true
|
||||||
bij := bij'.insert pattern.fvarId! fvar.fvarId!
|
bij := bij'.insert pattern.fvarId! fvar.fvarId!
|
||||||
break
|
break
|
||||||
if ! bij.forward.contains pattern.fvarId! then return none
|
if ! bij.forward.contains pattern.fvarId! then return false
|
||||||
|
|
||||||
if !strict || fvars.all (fun fvar => bij.backward.contains fvar.fvarId!)
|
if strict then
|
||||||
then return some bij
|
return fvars.all (fun fvar => bij.backward.contains fvar.fvarId!)
|
||||||
else return none
|
return true
|
||||||
|
|
||||||
unsafe def evalHintMessageUnsafe : Expr → MetaM (Array Expr → MessageData) :=
|
unsafe def evalHintMessageUnsafe : Expr → MetaM (Array Expr → MessageData) :=
|
||||||
evalExpr (Array Expr → MessageData)
|
evalExpr (Array Expr → MessageData)
|
||||||
@@ -122,11 +122,10 @@ def findHints (goal : MVarId) (doc : FileWorker.EditableDocument) (initParams :
|
|||||||
if let some fvarBij := matchExpr (← instantiateMVars $ hintGoal) (← instantiateMVars $ ← inferType $ mkMVar goal)
|
if let some fvarBij := matchExpr (← instantiateMVars $ hintGoal) (← instantiateMVars $ ← inferType $ mkMVar goal)
|
||||||
then
|
then
|
||||||
let lctx := (← goal.getDecl).lctx
|
let lctx := (← goal.getDecl).lctx
|
||||||
if let some bij ← matchDecls hintFVars lctx.getFVars (strict := hint.strict) (initBij := fvarBij)
|
if ← matchDecls hintFVars lctx.getFVars (strict := hint.strict) (initBij := fvarBij)
|
||||||
then
|
then
|
||||||
let userFVars := hintFVars.map fun v => bij.forward.findD v.fvarId! v.fvarId!
|
let text := (← evalHintMessage hint.text) hintFVars
|
||||||
let text := (← evalHintMessage hint.text) (userFVars.map Expr.fvar)
|
let ctx := {env := ← getEnv, mctx := ← getMCtx, lctx := ← getLCtx, opts := {}}
|
||||||
let ctx := {env := ← getEnv, mctx := ← getMCtx, lctx := lctx, opts := {}}
|
|
||||||
let text ← (MessageData.withContext ctx text).toString
|
let text ← (MessageData.withContext ctx text).toString
|
||||||
return some { text := text, hidden := hint.hidden }
|
return some { text := text, hidden := hint.hidden }
|
||||||
else return none
|
else return none
|
||||||
|
|||||||
@@ -0,0 +1,257 @@
|
|||||||
|
import Lean.Server.Watchdog
|
||||||
|
import GameServer.FileWorker
|
||||||
|
import GameServer.EnvExtensions
|
||||||
|
import GameServer.Game
|
||||||
|
|
||||||
|
namespace WasmServer.Watchdog
|
||||||
|
open Lean
|
||||||
|
open Server
|
||||||
|
open Watchdog
|
||||||
|
open IO
|
||||||
|
open Lsp
|
||||||
|
open JsonRpc
|
||||||
|
open System.Uri
|
||||||
|
|
||||||
|
open MyServer.FileWorker
|
||||||
|
|
||||||
|
structure WasmFileState :=
|
||||||
|
fileWorkerState : FileWorker.WorkerState
|
||||||
|
gameWorkerState : GameWorkerState
|
||||||
|
headerTask : Task (Except Error (Snapshots.Snapshot × SearchPath))
|
||||||
|
|
||||||
|
structure WasmServerState :=
|
||||||
|
initParams? : Option InitializeParams
|
||||||
|
gameServerState : GameServerState
|
||||||
|
fileState : HashMap String WasmFileState := {}
|
||||||
|
|
||||||
|
def wasmSearchPath : SearchPath := ["/lib", "/gamelib"]
|
||||||
|
|
||||||
|
@[export game_make_state]
|
||||||
|
unsafe def makeState : IO WasmServerState := do
|
||||||
|
let e ← IO.getStderr
|
||||||
|
try
|
||||||
|
Lean.enableInitializersExecution
|
||||||
|
searchPathRef.set wasmSearchPath
|
||||||
|
let env ← importModules #[
|
||||||
|
{ module := `GameServer : Import }
|
||||||
|
] {} 0
|
||||||
|
let state : GameServerState := {
|
||||||
|
env,
|
||||||
|
game := `TestGame,
|
||||||
|
gameDir := "test",
|
||||||
|
inventory := #[]
|
||||||
|
difficulty := 0
|
||||||
|
}
|
||||||
|
return ⟨none, state, {}⟩
|
||||||
|
catch err =>
|
||||||
|
e.putStrLn s!"Import error: {err}"
|
||||||
|
throw err
|
||||||
|
|
||||||
|
def readMessage (s : String) : IO JsonRpc.Message := do
|
||||||
|
let j ← ofExcept (Json.parse s)
|
||||||
|
let m ← match fromJson? j with
|
||||||
|
| Except.ok (m : JsonRpc.Message) => pure m
|
||||||
|
| Except.error inner => throw $ userError s!"JSON '{j.compress}' did not have the format of a JSON-RPC message.\n{inner}"
|
||||||
|
return m
|
||||||
|
|
||||||
|
def readLspRequestAs (s : String) (expectedMethod : String) (α : Type) [FromJson α] : IO (Request α) := do
|
||||||
|
let m ← readMessage s
|
||||||
|
match m with
|
||||||
|
| Message.request id method params? =>
|
||||||
|
if method = expectedMethod then
|
||||||
|
let j := toJson params?
|
||||||
|
match fromJson? j with
|
||||||
|
| Except.ok v => pure $ JsonRpc.Request.mk id expectedMethod (v : α)
|
||||||
|
| Except.error inner => throw $ userError s!"Unexpected param '{j.compress}' for method '{expectedMethod}'\n{inner}"
|
||||||
|
else
|
||||||
|
throw $ userError s!"Expected method '{expectedMethod}', got method '{method}'"
|
||||||
|
| _ => throw $ userError s!"Expected JSON-RPC request, got: '{(toJson m).compress}'"
|
||||||
|
|
||||||
|
def initializeServer (id : RequestID) : IO Unit := do
|
||||||
|
let o ← IO.getStdout
|
||||||
|
o.writeLspResponse {
|
||||||
|
id := id
|
||||||
|
result := {
|
||||||
|
capabilities := mkLeanServerCapabilities
|
||||||
|
serverInfo? := some {
|
||||||
|
name := "Lean 4 Game Server"
|
||||||
|
version? := "0.1.1"
|
||||||
|
}
|
||||||
|
: InitializeResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ()
|
||||||
|
|
||||||
|
def mkServerContext (state : WasmServerState) : IO ServerContext := do
|
||||||
|
let i ← IO.getStdin
|
||||||
|
let o ← IO.getStdout
|
||||||
|
let e ← IO.getStderr
|
||||||
|
let srcSearchPath ← searchPathRef.get
|
||||||
|
let references ← IO.mkRef (← loadReferences)
|
||||||
|
let fileWorkersRef ← IO.mkRef (RBMap.empty : FileWorkerMap)
|
||||||
|
let workerPath := "no-worker-path"
|
||||||
|
let some initParams := state.initParams?
|
||||||
|
| throwServerError "no yet initialized"
|
||||||
|
return {
|
||||||
|
hIn := i
|
||||||
|
hOut := o
|
||||||
|
hLog := e
|
||||||
|
args := []
|
||||||
|
fileWorkersRef := fileWorkersRef
|
||||||
|
initParams
|
||||||
|
workerPath
|
||||||
|
srcSearchPath
|
||||||
|
references
|
||||||
|
}
|
||||||
|
|
||||||
|
def runGameServerM (state : WasmServerState) (x : GameServerM α) : IO (α × WasmServerState) := do
|
||||||
|
let (res, gameServerState) ← ReaderT.run
|
||||||
|
(StateT.run x state.gameServerState)
|
||||||
|
(← mkServerContext state)
|
||||||
|
return (res, {state with gameServerState})
|
||||||
|
|
||||||
|
def mkWorkerContext (state : WasmServerState) (headerTask : Task (Except Error (Snapshots.Snapshot × SearchPath))) :
|
||||||
|
IO FileWorker.WorkerContext := do
|
||||||
|
let i ← IO.getStdin
|
||||||
|
let o ← IO.getStdout
|
||||||
|
let e ← IO.getStderr
|
||||||
|
let some initParams := state.initParams?
|
||||||
|
| throwServerError "no yet initialized"
|
||||||
|
let clientHasWidgets := initParams.initializationOptions?.bind (·.hasWidgets?) |>.getD false
|
||||||
|
return {
|
||||||
|
hIn := i
|
||||||
|
hOut := o
|
||||||
|
hLog := e
|
||||||
|
headerTask := headerTask
|
||||||
|
initParams := initParams
|
||||||
|
clientHasWidgets
|
||||||
|
}
|
||||||
|
|
||||||
|
def runGameWorkerM (state : WasmServerState) (fileState : WasmFileState) (x : GameWorkerM α) :
|
||||||
|
IO (α × WasmFileState) := do
|
||||||
|
let s := fileState.fileWorkerState
|
||||||
|
let ctx ← mkWorkerContext state fileState.headerTask
|
||||||
|
let ((res, gameWorkerState), s) ← StateRefT'.run (s := s) <| ReaderT.run (r := ctx) <|
|
||||||
|
StateT.run (s := fileState.gameWorkerState) <| x
|
||||||
|
let fileState := {fileState with gameWorkerState := gameWorkerState, fileWorkerState := s}
|
||||||
|
return (res, fileState)
|
||||||
|
|
||||||
|
def parseParams {paramType : Type} [FromJson paramType] (params : Json) : IO paramType :=
|
||||||
|
match fromJson? params with
|
||||||
|
| Except.ok parsed => pure parsed
|
||||||
|
| Except.error inner => throwServerError s!"Got param with wrong structure: {params.compress}\n{inner}"
|
||||||
|
|
||||||
|
def requestWorkerUri (method : String) (params : Json) : IO (Option DocumentUri) := do
|
||||||
|
if method == "$/lean/rpc/connect" then
|
||||||
|
let ps : Lsp.RpcConnectParams ← parseParams params
|
||||||
|
pure <| fileSource ps
|
||||||
|
else match (← routeLspRequest method params) with
|
||||||
|
| Except.error e =>
|
||||||
|
throwServerError e.message
|
||||||
|
| Except.ok uri => pure uri
|
||||||
|
|
||||||
|
open FileWorker in
|
||||||
|
def handleDidOpen (params : DidOpenTextDocumentParams) (state : WasmServerState) : IO WasmServerState := do
|
||||||
|
let some initParams := state.initParams?
|
||||||
|
| throwServerError "no yet initialized"
|
||||||
|
let (_, state) ← runGameServerM state do
|
||||||
|
let some lvl ← GameServer.getLevelByFileName? initParams
|
||||||
|
((System.Uri.fileUriToPath? params.textDocument.uri).getD params.textDocument.uri |>.toString)
|
||||||
|
| throwServerError s!"Level not found: {params.textDocument.uri} | {initParams.rootUri?}"
|
||||||
|
|
||||||
|
let env ← importModules #[
|
||||||
|
{ module := lvl.module : Import }
|
||||||
|
] {} 0
|
||||||
|
|
||||||
|
(← getStderr).putStr "Import for level completed"
|
||||||
|
|
||||||
|
let doc := params.textDocument
|
||||||
|
let meta : DocumentMeta := ⟨doc.uri, doc.version, doc.text.toFileMap, .always⟩
|
||||||
|
let clientHasWidgets := initParams.initializationOptions?.bind (·.hasWidgets?) |>.getD false
|
||||||
|
|
||||||
|
let (headerStx, headerTask) ← mkHeaderTask meta (← getStdout) wasmSearchPath env {} clientHasWidgets
|
||||||
|
let cancelTk ← CancelToken.new
|
||||||
|
|
||||||
|
let levelParams := {
|
||||||
|
uri := meta.uri
|
||||||
|
gameDir := state.gameServerState.gameDir
|
||||||
|
levelModule := lvl.module
|
||||||
|
tactics := lvl.tactics.tiles
|
||||||
|
lemmas := lvl.lemmas.tiles
|
||||||
|
definitions := lvl.definitions.tiles
|
||||||
|
inventory := state.gameServerState.inventory
|
||||||
|
difficulty := state.gameServerState.difficulty
|
||||||
|
statementName := lvl.statementName
|
||||||
|
: Game.DidOpenLevelParams
|
||||||
|
}
|
||||||
|
|
||||||
|
let ctx ← mkWorkerContext state headerTask
|
||||||
|
let cmdSnaps ← EIO.mapTask (t := headerTask) (match · with
|
||||||
|
| Except.ok (s, _) => unfoldSnaps meta #[s] cancelTk levelParams ctx (startAfterMs := 0)
|
||||||
|
| Except.error e => throw (e : ElabTaskError))
|
||||||
|
let doc : EditableDocument := { meta, cmdSnaps := AsyncList.delayed cmdSnaps, cancelTk }
|
||||||
|
|
||||||
|
|
||||||
|
let s : WasmFileState := {
|
||||||
|
fileWorkerState := {
|
||||||
|
doc := doc
|
||||||
|
initHeaderStx := headerStx
|
||||||
|
pendingRequests := RBMap.empty
|
||||||
|
rpcSessions := RBMap.empty
|
||||||
|
}
|
||||||
|
gameWorkerState := { levelParams }
|
||||||
|
headerTask
|
||||||
|
}
|
||||||
|
let fileState := state.fileState.insert params.textDocument.uri s
|
||||||
|
return {state with fileState}
|
||||||
|
return state
|
||||||
|
|
||||||
|
@[export game_send_message]
|
||||||
|
unsafe def sendMessage (s : String) (state : WasmServerState) : IO WasmServerState := do
|
||||||
|
let e ← IO.getStderr
|
||||||
|
try
|
||||||
|
let m ← readMessage s
|
||||||
|
match m with
|
||||||
|
| Message.request id "initialize" (some params) =>
|
||||||
|
let p : InitializeParams ← parseParams (toJson params)
|
||||||
|
initializeServer id
|
||||||
|
let p := {p with rootUri? := some (toString state.gameServerState.game)}
|
||||||
|
return {state with initParams? := some p}
|
||||||
|
| _ =>
|
||||||
|
let (isGameEv, state) ← runGameServerM state (Game.handleServerEvent (.clientMsg m))
|
||||||
|
if isGameEv then
|
||||||
|
return state
|
||||||
|
else
|
||||||
|
match m with
|
||||||
|
| Message.notification method (some params) =>
|
||||||
|
let handle := (fun α [FromJson α] (handler : α → WasmServerState → IO WasmServerState)
|
||||||
|
=> parseParams (toJson params) >>= (handler · state))
|
||||||
|
match method with --TODO
|
||||||
|
| "textDocument/didOpen" => handle DidOpenTextDocumentParams handleDidOpen
|
||||||
|
-- | "textDocument/didChange" => handle DidChangeTextDocumentParams handleDidChange
|
||||||
|
-- | "textDocument/didClose" => handle DidCloseTextDocumentParams handleDidClose
|
||||||
|
-- | "workspace/didChangeWatchedFiles" => handle DidChangeWatchedFilesParams handleDidChangeWatchedFiles
|
||||||
|
-- | "$/cancelRequest" => handle CancelParams handleCancelRequest
|
||||||
|
-- | "$/lean/rpc/connect" => handle RpcConnectParams (forwardNotification method)
|
||||||
|
-- | "$/lean/rpc/release" => handle RpcReleaseParams (forwardNotification method)
|
||||||
|
-- | "$/lean/rpc/keepAlive" => handle RpcKeepAliveParams (forwardNotification method)
|
||||||
|
| _ => return state
|
||||||
|
| Message.request id method (some params) =>
|
||||||
|
let some uri ← requestWorkerUri method (toJson params)
|
||||||
|
| throwServerError s!"Could not find Uri for request: {method}"
|
||||||
|
let some fileState := state.fileState.find? uri
|
||||||
|
| throwServerError s!"File not open: {uri}"
|
||||||
|
let (_, fileState) ← runGameWorkerM state fileState do
|
||||||
|
MyServer.FileWorker.mainLoop1 m
|
||||||
|
let fileState := state.fileState.insert uri fileState
|
||||||
|
return {state with fileState}
|
||||||
|
| Message.responseError _ _ e .. =>
|
||||||
|
throwServerError s!"Unhandled response error: {e}"
|
||||||
|
| _ => throwServerError "Got invalid JSON-RPC message"
|
||||||
|
-- match m with
|
||||||
|
-- | _ =>
|
||||||
|
-- e.putStrLn s!"Expected JSON-RPC request, got: '{(toJson m).compress}'"
|
||||||
|
-- return state
|
||||||
|
catch err =>
|
||||||
|
e.putStrLn s!"Server error: {err}"
|
||||||
|
return state
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import GameServer.FileWorker
|
import GameServer.FileWorker
|
||||||
import GameServer.Watchdog
|
import GameServer.Watchdog
|
||||||
|
import GameServer.WasmServer
|
||||||
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`
|
||||||
|
|||||||
@@ -2,15 +2,11 @@
|
|||||||
|
|
||||||
ELAN_HOME=$(lake env printenv ELAN_HOME)
|
ELAN_HOME=$(lake env printenv ELAN_HOME)
|
||||||
|
|
||||||
# $1 : the game directory
|
|
||||||
# $2 : the lean4game folder
|
|
||||||
# $3 : the gameserver executable
|
|
||||||
|
|
||||||
(exec bwrap\
|
(exec bwrap\
|
||||||
--bind $2 /lean4game \
|
--ro-bind ../../lean4game /lean4game \
|
||||||
--bind $1 /game \
|
--ro-bind ../../$1 /game \
|
||||||
--bind $ELAN_HOME /elan \
|
--ro-bind $ELAN_HOME /elan \
|
||||||
--bind /usr /usr \
|
--ro-bind /usr /usr \
|
||||||
--dev /dev \
|
--dev /dev \
|
||||||
--proc /proc \
|
--proc /proc \
|
||||||
--symlink usr/lib /lib\
|
--symlink usr/lib /lib\
|
||||||
@@ -26,6 +22,6 @@ ELAN_HOME=$(lake env printenv ELAN_HOME)
|
|||||||
--unshare-uts \
|
--unshare-uts \
|
||||||
--unshare-cgroup \
|
--unshare-cgroup \
|
||||||
--die-with-parent \
|
--die-with-parent \
|
||||||
--chdir "/game/.lake/packages/GameServer/server/.lake/build/bin/" \
|
--chdir "/lean4game/server/build/bin/" \
|
||||||
./gameserver --server /game
|
./gameserver --server /game
|
||||||
)
|
)
|
||||||
@@ -11,7 +11,6 @@ const __filename = fileURLToPath(import.meta.url);
|
|||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
const TOKEN = process.env.LEAN4GAME_GITHUB_TOKEN
|
const TOKEN = process.env.LEAN4GAME_GITHUB_TOKEN
|
||||||
const USERNAME = process.env.LEAN4GAME_GITHUB_USER
|
|
||||||
const octokit = new Octokit({
|
const octokit = new Octokit({
|
||||||
auth: TOKEN
|
auth: TOKEN
|
||||||
})
|
})
|
||||||
@@ -42,10 +41,9 @@ async function download(id, url, dest) {
|
|||||||
requestProgress(request({
|
requestProgress(request({
|
||||||
url,
|
url,
|
||||||
headers: {
|
headers: {
|
||||||
'accept': 'application/vnd.github+json',
|
'User-Agent': 'abentkamp',
|
||||||
'User-Agent': USERNAME,
|
|
||||||
'X-GitHub-Api-Version': '2022-11-28',
|
'X-GitHub-Api-Version': '2022-11-28',
|
||||||
'Authorization': 'Bearer ' + TOKEN
|
'Authorization': 'Bearer '+TOKEN
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.on('progress', function (state) {
|
.on('progress', function (state) {
|
||||||
@@ -78,44 +76,35 @@ async function doImport (owner, repo, id) {
|
|||||||
.reduce((acc, cur) => acc.created_at < cur.created_at ? cur : acc)
|
.reduce((acc, cur) => acc.created_at < cur.created_at ? cur : acc)
|
||||||
artifactId = artifact.id
|
artifactId = artifact.id
|
||||||
const url = artifact.archive_download_url
|
const url = artifact.archive_download_url
|
||||||
// Make sure the download folder exists
|
if (!fs.existsSync("tmp")){
|
||||||
if (!fs.existsSync(path.join(__dirname, "..", "games"))){
|
fs.mkdirSync("tmp");
|
||||||
fs.mkdirSync(path.join(__dirname, "..", "games"));
|
|
||||||
}
|
|
||||||
if (!fs.existsSync(path.join(__dirname, "..", "games", "tmp"))){
|
|
||||||
fs.mkdirSync(path.join(__dirname, "..", "games", "tmp"));
|
|
||||||
}
|
}
|
||||||
progress[id].output += `Download from ${url}\n`
|
progress[id].output += `Download from ${url}\n`
|
||||||
await download(id, url, path.join(__dirname, "..", "games", "tmp", `${owner.toLowerCase()}_${repo.toLowerCase()}_${artifactId}.zip`))
|
await download(id, url, `tmp/artifact_${artifactId}.zip`)
|
||||||
progress[id].output += `Download finished.\n`
|
progress[id].output += `Download finished.\n`
|
||||||
|
await runProcess(id, "/bin/bash", [`${__dirname}/unpack.sh`, artifactId],".")
|
||||||
await runProcess(id, "/bin/bash", [path.join(__dirname, "unpack.sh"), artifactId, owner.toLowerCase(), repo.toLowerCase()], path.join(__dirname, ".."))
|
let manifest = fs.readFileSync(`tmp/artifact_${artifactId}_inner/manifest.json`);
|
||||||
|
manifest = JSON.parse(manifest);
|
||||||
|
if (manifest.length !== 1) {
|
||||||
// let manifest = fs.readFileSync(`tmp/artifact_${artifactId}_inner/manifest.json`);
|
throw `Unexpected manifest: ${JSON.stringify(manifest)}`
|
||||||
// manifest = JSON.parse(manifest);
|
}
|
||||||
// if (manifest.length !== 1) {
|
manifest[0].RepoTags = [`g/${owner.toLowerCase()}/${repo.toLowerCase()}:latest`]
|
||||||
// throw `Unexpected manifest: ${JSON.stringify(manifest)}`
|
fs.writeFileSync(`tmp/artifact_${artifactId}_inner/manifest.json`, JSON.stringify(manifest));
|
||||||
// }
|
await runProcess(id, "tar", ["-cvf", `../archive_${artifactId}.tar`, "."], `tmp/artifact_${artifactId}_inner/`)
|
||||||
// manifest[0].RepoTags = [`g/${owner.toLowerCase()}/${repo.toLowerCase()}:latest`]
|
await runProcess(id, "docker", ["load", "-i", `tmp/archive_${artifactId}.tar`])
|
||||||
// fs.writeFileSync(`tmp/artifact_${artifactId}_inner/manifest.json`, JSON.stringify(manifest));
|
|
||||||
// await runProcess(id, "tar", ["-cvf", `../archive_${artifactId}.tar`, "."], `tmp/artifact_${artifactId}_inner/`)
|
|
||||||
// // await runProcess(id, "docker", ["load", "-i", `tmp/archive_${artifactId}.tar`])
|
|
||||||
|
|
||||||
progress[id].done = true
|
progress[id].done = true
|
||||||
progress[id].output += `Done!\n`
|
progress[id].output += `Done.\n`
|
||||||
progress[id].output += `Play the game at: {your website}/#/g/${owner}/${repo}\n`
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
progress[id].output += `Error: ${e.toString()}\n${e.stack}`
|
progress[id].output += `Error: ${e.toString()}\n${e.stack}`
|
||||||
} finally {
|
} finally {
|
||||||
// clean-up temp. files
|
|
||||||
if (artifactId) {
|
if (artifactId) {
|
||||||
fs.rmSync(path.join(__dirname, "..", "games", "tmp", `${owner}_${repo}_${artifactId}.zip`), {force: true, recursive: false});
|
fs.rmSync(`tmp/artifact_${artifactId}.zip`, {force: true, recursive: true});
|
||||||
fs.rmSync(path.join(__dirname, "..", "games", "tmp", `${owner}_${repo}_${artifactId}`), {force: true, recursive: true});
|
fs.rmSync(`tmp/artifact_${artifactId}`, {force: true, recursive: true});
|
||||||
|
fs.rmSync(`tmp/artifact_${artifactId}_inner`, {force: true, recursive: true});
|
||||||
|
fs.rmSync(`tmp/archive_${artifactId}.tar`, {force: true, recursive: true});
|
||||||
}
|
}
|
||||||
progress[id].done = true
|
progress[id].done = true
|
||||||
}
|
}
|
||||||
await new Promise(resolve => setTimeout(resolve, 10000))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const importTrigger = (req, res) => {
|
export const importTrigger = (req, res) => {
|
||||||
@@ -6,20 +6,25 @@ import * as url from 'url';
|
|||||||
import * as rpc from 'vscode-ws-jsonrpc';
|
import * as rpc from 'vscode-ws-jsonrpc';
|
||||||
import * as jsonrpcserver from 'vscode-ws-jsonrpc/server';
|
import * as jsonrpcserver from 'vscode-ws-jsonrpc/server';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import fs from 'fs';
|
|
||||||
import anonymize from 'ip-anonymize';
|
import anonymize from 'ip-anonymize';
|
||||||
import { importTrigger, importStatus } from './import.mjs'
|
// import { importTrigger, importStatus } from './import.mjs'
|
||||||
// import fs from 'fs'
|
// import fs from 'fs'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a game here if the server should keep a queue of pre-loaded games ready at all times.
|
|
||||||
*
|
|
||||||
* IMPORTANT! Tags here need to be lower case!
|
|
||||||
*/
|
*/
|
||||||
const queueLength = {
|
const games = {
|
||||||
"g/hhu-adam/robo": 2,
|
"g/hhu-adam/robo": {
|
||||||
"g/hhu-adam/nng4": 5,
|
dir: "Robo",
|
||||||
"g/djvelleman/stg4": 2,
|
queueLength: 5
|
||||||
|
},
|
||||||
|
"g/hhu-adam/nng4": {
|
||||||
|
dir: "NNG4",
|
||||||
|
queueLength: 5
|
||||||
|
},
|
||||||
|
"g/hhu-adam/nng4-old": {
|
||||||
|
dir: "NNG4-OLD",
|
||||||
|
queueLength: 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const __filename = url.fileURLToPath(import.meta.url);
|
const __filename = url.fileURLToPath(import.meta.url);
|
||||||
@@ -31,18 +36,11 @@ const PORT = process.env.PORT || 8080;
|
|||||||
|
|
||||||
var router = express.Router();
|
var router = express.Router();
|
||||||
|
|
||||||
router.get('/import/status/:owner/:repo', importStatus)
|
// router.get('/import/status/:owner/:repo', importStatus)
|
||||||
router.get('/import/trigger/:owner/:repo', importTrigger)
|
// router.get('/import/trigger/:owner/:repo', importTrigger)
|
||||||
|
|
||||||
const server = app
|
const server = app
|
||||||
.use(express.static(path.join(__dirname, '..', 'client', 'dist'))) // TODO: add a dist folder from inside the game
|
.use(express.static(path.join(__dirname, '../client/dist/')))
|
||||||
.use('/data/g/:owner/:repo/*', (req, res, next) => {
|
|
||||||
const owner = req.params.owner;
|
|
||||||
const repo = req.params.repo
|
|
||||||
const filename = req.params[0];
|
|
||||||
req.url = filename;
|
|
||||||
express.static(path.join(getGameDir(owner,repo),".lake","gamedata"))(req, res, next);
|
|
||||||
})
|
|
||||||
.use('/', router)
|
.use('/', router)
|
||||||
.listen(PORT, () => console.log(`Listening on ${PORT}`));
|
.listen(PORT, () => console.log(`Listening on ${PORT}`));
|
||||||
|
|
||||||
@@ -56,61 +54,41 @@ const isDevelopment = environment === 'development'
|
|||||||
/** We keep queues of started Lean Server processes to be ready when a user arrives */
|
/** We keep queues of started Lean Server processes to be ready when a user arrives */
|
||||||
const queue = {}
|
const queue = {}
|
||||||
|
|
||||||
function getTag(owner, repo) {
|
function tag(owner, repo) {
|
||||||
return `g/${owner.toLowerCase()}/${repo.toLowerCase()}`
|
return `g/${owner.toLowerCase()}/${repo.toLowerCase()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGameDir(owner, repo) {
|
function startServerProcess(owner, repo) {
|
||||||
owner = owner.toLowerCase()
|
let game_dir = (owner == 'local') ?
|
||||||
|
repo : games[tag(owner, repo)]?.dir
|
||||||
|
|
||||||
if (owner == 'local') {
|
if (owner == 'local') {
|
||||||
if(!isDevelopment) {
|
if(!isDevelopment) {
|
||||||
console.error(`No local games in production mode.`)
|
console.error(`No local games in production mode.`)
|
||||||
return ""
|
return
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if(!fs.existsSync(path.join(__dirname, '..', 'games'))) {
|
|
||||||
console.error(`Did not find the following folder: ${path.join(__dirname, '..', 'games')}`)
|
|
||||||
console.error('Did you already import any games?')
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
// TODO: This test does not work
|
||||||
|
// if (!fs.existsSync(path.join("../", game_dir))) {
|
||||||
|
// console.error(`Game folder does not exists: ${game_dir}`)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
let game_dir = (owner == 'local') ?
|
if (!game_dir) {
|
||||||
path.join(__dirname, '..', '..', repo) : // note: here we need `repo` to be case sensitive
|
console.error(`Unknown game: ${tag(owner, repo)}`)
|
||||||
path.join(__dirname, '..', 'games', `${owner}`, `${repo.toLowerCase()}`)
|
return
|
||||||
|
|
||||||
if(!fs.existsSync(game_dir)) {
|
|
||||||
console.error(`Game '${game_dir}' does not exist!`)
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return game_dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
function startServerProcess(owner, repo) {
|
|
||||||
|
|
||||||
let game_dir = getGameDir(owner, repo)
|
|
||||||
if (!game_dir) return;
|
|
||||||
|
|
||||||
let serverProcess
|
let serverProcess
|
||||||
if (isDevelopment) {
|
if (isDevelopment) {
|
||||||
let args = ["--server", game_dir]
|
let args = ["--server", path.join("../../../../", game_dir)]
|
||||||
let binDir = path.join(game_dir, ".lake", "packages", "GameServer", "server", ".lake", "build", "bin")
|
serverProcess = cp.spawn("./gameserver", args,
|
||||||
// Note: `cwd` is important to be the `bin` directory as `Watchdog` calls `./gameserver` again
|
{ cwd: path.join(__dirname, "./build/bin/") })
|
||||||
if (fs.existsSync(binDir)) {
|
|
||||||
// Try to use the game's own copy of `gameserver`.
|
|
||||||
serverProcess = cp.spawn("./gameserver", args, { cwd: binDir })
|
|
||||||
} else {
|
|
||||||
// If the game is built with `-Klean4game.local` there is no copy in the lake packages.
|
|
||||||
serverProcess = cp.spawn("./gameserver", args,
|
|
||||||
{ cwd: path.join(__dirname, "..", "server", ".lake", "build", "bin") })
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
serverProcess = cp.spawn("./bubblewrap.sh",
|
serverProcess = cp.spawn("./bubblewrap.sh",
|
||||||
[ game_dir, path.join(__dirname, '..')],
|
[game_dir],
|
||||||
{ cwd: __dirname })
|
{ cwd: __dirname })
|
||||||
}
|
}
|
||||||
|
|
||||||
serverProcess.on('error', error =>
|
serverProcess.on('error', error =>
|
||||||
console.error(`Launching Lean Server failed: ${error}`)
|
console.error(`Launching Lean Server failed: ${error}`)
|
||||||
)
|
)
|
||||||
@@ -123,21 +101,15 @@ function startServerProcess(owner, repo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** start Lean Server processes to refill the queue */
|
/** start Lean Server processes to refill the queue */
|
||||||
function fillQueue(tag) {
|
function fillQueue(owner, repo) {
|
||||||
while (queue[tag].length < queueLength[tag]) {
|
while (queue[tag(owner, repo)].length < games[tag(owner, repo)].queueLength) {
|
||||||
let serverProcess
|
const serverProcess = startServerProcess(tag(owner, repo))
|
||||||
serverProcess = startServerProcess(tag)
|
queue[tag(owner, repo)].push(serverProcess)
|
||||||
if (serverProcess == null) {
|
|
||||||
console.error('serverProcess was undefined/null')
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
queue[tag].push(serverProcess)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// // TODO: We disabled queue for now
|
|
||||||
// if (!isDevelopment) { // Don't use queue in development
|
// if (!isDevelopment) { // Don't use queue in development
|
||||||
// for (let tag in queueLength) {
|
// for (let tag in games) {
|
||||||
// queue[tag] = []
|
// queue[tag] = []
|
||||||
// fillQueue(tag)
|
// fillQueue(tag)
|
||||||
// }
|
// }
|
||||||
@@ -150,21 +122,19 @@ wss.addListener("connection", function(ws, req) {
|
|||||||
if (!reRes) { console.error(`Connection refused because of invalid URL: ${req.url}`); return; }
|
if (!reRes) { console.error(`Connection refused because of invalid URL: ${req.url}`); return; }
|
||||||
const owner = reRes[1]
|
const owner = reRes[1]
|
||||||
const repo = reRes[2]
|
const repo = reRes[2]
|
||||||
|
// const tag = `g/${owner.toLowerCase()}/${repo.toLowerCase()}`
|
||||||
|
|
||||||
const tag = getTag(owner, repo)
|
// // TODO
|
||||||
|
// if (isDevelopment && process.env.DEV_CONTAINER) {
|
||||||
|
// tag = `g/local/game`
|
||||||
|
// }
|
||||||
|
|
||||||
let ps
|
let ps;
|
||||||
if (!queue[tag] || queue[tag].length == 0) {
|
if (!queue[tag(owner, repo)] || queue[tag(owner, repo)].length == 0) {
|
||||||
ps = startServerProcess(owner, repo)
|
ps = startServerProcess(owner, repo)
|
||||||
} else {
|
} else {
|
||||||
console.info('Got process from the queue')
|
ps = queue[tag(owner, repo)].shift() // Pick the first Lean process; it's likely to be ready immediately
|
||||||
ps = queue[tag].shift() // Pick the first Lean process; it's likely to be ready immediately
|
fillQueue(owner, repo)
|
||||||
fillQueue(tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps == null) {
|
|
||||||
console.error('server process is undefined/null')
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
socketCounter += 1;
|
socketCounter += 1;
|
||||||
@@ -177,14 +147,14 @@ wss.addListener("connection", function(ws, req) {
|
|||||||
onClose: (cb) => { ws.on("close", cb) },
|
onClose: (cb) => { ws.on("close", cb) },
|
||||||
send: (data, cb) => { ws.send(data,cb) }
|
send: (data, cb) => { ws.send(data,cb) }
|
||||||
}
|
}
|
||||||
const reader = new rpc.WebSocketMessageReader(socket)
|
const reader = new rpc.WebSocketMessageReader(socket);
|
||||||
const writer = new rpc.WebSocketMessageWriter(socket)
|
const writer = new rpc.WebSocketMessageWriter(socket);
|
||||||
const socketConnection = jsonrpcserver.createConnection(reader, writer, () => ws.close())
|
const socketConnection = jsonrpcserver.createConnection(reader, writer, () => ws.close())
|
||||||
const serverConnection = jsonrpcserver.createProcessStreamConnection(ps)
|
const serverConnection = jsonrpcserver.createProcessStreamConnection(ps);
|
||||||
socketConnection.forward(serverConnection, message => {
|
socketConnection.forward(serverConnection, message => {
|
||||||
if (isDevelopment) {console.log(`CLIENT: ${JSON.stringify(message)}`)}
|
if (isDevelopment) {console.log(`CLIENT: ${JSON.stringify(message)}`)}
|
||||||
return message;
|
return message;
|
||||||
})
|
});
|
||||||
serverConnection.forward(socketConnection, message => {
|
serverConnection.forward(socketConnection, message => {
|
||||||
if (isDevelopment) {console.log(`SERVER: ${JSON.stringify(message)}`)}
|
if (isDevelopment) {console.log(`SERVER: ${JSON.stringify(message)}`)}
|
||||||
return message;
|
return message;
|
||||||
@@ -195,9 +165,9 @@ wss.addListener("connection", function(ws, req) {
|
|||||||
|
|
||||||
ws.on('close', () => {
|
ws.on('close', () => {
|
||||||
console.log(`[${new Date()}] Socket closed - ${ip}`)
|
console.log(`[${new Date()}] Socket closed - ${ip}`)
|
||||||
socketCounter -= 1
|
socketCounter -= 1;
|
||||||
})
|
})
|
||||||
|
|
||||||
socketConnection.onClose(() => serverConnection.dispose())
|
socketConnection.onClose(() => serverConnection.dispose());
|
||||||
serverConnection.onClose(() => socketConnection.dispose())
|
serverConnection.onClose(() => socketConnection.dispose());
|
||||||
})
|
})
|
||||||
@@ -1,14 +1,4 @@
|
|||||||
{"version": 7,
|
{"version": 6,
|
||||||
"packagesDir": ".lake/packages",
|
"packagesDir": "lake-packages",
|
||||||
"packages":
|
"packages": [],
|
||||||
[{"url": "https://github.com/leanprover/std4.git",
|
"name": "GameServer"}
|
||||||
"type": "git",
|
|
||||||
"subDir": null,
|
|
||||||
"rev": "2e4a3586a8f16713f16b2d2b3af3d8e65f3af087",
|
|
||||||
"name": "std",
|
|
||||||
"manifestFile": "lake-manifest.json",
|
|
||||||
"inputRev": "v4.3.0",
|
|
||||||
"inherited": false,
|
|
||||||
"configFile": "lakefile.lean"}],
|
|
||||||
"name": "GameServer",
|
|
||||||
"lakeDir": ".lake"}
|
|
||||||
|
|||||||
+1
-15
@@ -3,11 +3,7 @@ open Lake DSL
|
|||||||
|
|
||||||
package GameServer
|
package GameServer
|
||||||
|
|
||||||
-- Using this assumes that each dependency has a tag of the form `v4.X.0`.
|
@[default_target]
|
||||||
def leanVersion : String := s!"v{Lean.versionString}"
|
|
||||||
|
|
||||||
require std from git "https://github.com/leanprover/std4.git" @ leanVersion
|
|
||||||
|
|
||||||
lean_lib GameServer
|
lean_lib GameServer
|
||||||
|
|
||||||
@[default_target]
|
@[default_target]
|
||||||
@@ -15,13 +11,3 @@ lean_exe gameserver {
|
|||||||
root := `Main
|
root := `Main
|
||||||
supportInterpreter := true
|
supportInterpreter := true
|
||||||
}
|
}
|
||||||
|
|
||||||
/--
|
|
||||||
When a package depending on GameServer updates its dependencies,
|
|
||||||
build the `gameserver` executable.
|
|
||||||
-/
|
|
||||||
post_update pkg do
|
|
||||||
let rootPkg ← getRootPackage
|
|
||||||
if rootPkg.name = pkg.name then
|
|
||||||
return -- do not run in GameServer itself
|
|
||||||
discard <| runBuild gameserver.build >>= (·.await)
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import Lake
|
||||||
|
open Lake DSL
|
||||||
|
|
||||||
|
package GameServer {
|
||||||
|
buildDir := ".lake/build32"
|
||||||
|
}
|
||||||
|
|
||||||
|
@[default_target]
|
||||||
|
lean_lib GameServer
|
||||||
|
|
||||||
|
@[default_target]
|
||||||
|
lean_exe gameserver {
|
||||||
|
root := `Main
|
||||||
|
supportInterpreter := true
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
leanprover/lean4:v4.3.0
|
leanprover/lean4:v4.3.0-rc2
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <lean/lean.h>
|
||||||
|
|
||||||
|
extern lean_object* game_send_message(lean_object*, lean_object*, lean_object*);
|
||||||
|
extern lean_object* game_make_state(lean_object*);
|
||||||
|
|
||||||
|
// see https://leanprover.github.io/lean4/doc/dev/ffi.html#initialization
|
||||||
|
extern void lean_initialize_runtime_module();
|
||||||
|
extern void lean_initialize();
|
||||||
|
extern void lean_io_mark_end_initialization();
|
||||||
|
extern lean_object * initialize_GameServer_WasmServer(uint8_t builtin, lean_object *);
|
||||||
|
|
||||||
|
lean_object * state;
|
||||||
|
lean_object * io_world;
|
||||||
|
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
lean_initialize_runtime_module();
|
||||||
|
lean_initialize();
|
||||||
|
lean_object * res;
|
||||||
|
// use same default as for Lean executables
|
||||||
|
uint8_t builtin = 1;
|
||||||
|
io_world = lean_io_mk_world();
|
||||||
|
res = initialize_GameServer_WasmServer(builtin, io_world);
|
||||||
|
if (lean_io_result_is_ok(res)) {
|
||||||
|
lean_dec(res);
|
||||||
|
} else {
|
||||||
|
lean_io_result_show_error(res);
|
||||||
|
lean_dec(res);
|
||||||
|
return; // do not access Lean declarations if initialization failed
|
||||||
|
}
|
||||||
|
lean_init_task_manager();
|
||||||
|
lean_io_mark_end_initialization();
|
||||||
|
|
||||||
|
res = game_make_state(io_world);
|
||||||
|
if (lean_io_result_is_ok(res)) {
|
||||||
|
state = lean_io_result_get_value(res);
|
||||||
|
lean_inc(state);
|
||||||
|
lean_dec(res);
|
||||||
|
} else {
|
||||||
|
lean_io_result_show_error(res);
|
||||||
|
lean_dec(res);
|
||||||
|
return; // do not access Lean declarations if initialization failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void send_message(char* msg){
|
||||||
|
lean_object * s = lean_mk_string(msg);
|
||||||
|
lean_object * res = game_send_message(s, state, io_world);
|
||||||
|
if (lean_io_result_is_ok(res)) {
|
||||||
|
state = lean_io_result_get_value(res);
|
||||||
|
lean_inc(state);
|
||||||
|
lean_dec(res);
|
||||||
|
} else {
|
||||||
|
lean_io_result_show_error(res);
|
||||||
|
lean_dec(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#/bin/bash
|
||||||
|
|
||||||
|
ARTIFACT_ID=$1
|
||||||
|
|
||||||
|
echo "Unpacking ZIP."
|
||||||
|
unzip -o tmp/artifact_${ARTIFACT_ID}.zip -d tmp/artifact_${ARTIFACT_ID}
|
||||||
|
echo "Unpacking TAR."
|
||||||
|
for f in tmp/artifact_${ARTIFACT_ID}/* #Should only be one file
|
||||||
|
do
|
||||||
|
echo "Unpacking $f"
|
||||||
|
mkdir tmp/artifact_${ARTIFACT_ID}_inner
|
||||||
|
tar -xvf $f -C tmp/artifact_${ARTIFACT_ID}_inner
|
||||||
|
done
|
||||||
+1
-1
@@ -12,5 +12,5 @@
|
|||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
},
|
},
|
||||||
"exclude": ["server", "relay"]
|
"exclude": ["server"]
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-15
@@ -5,12 +5,6 @@ import svgr from "vite-plugin-svgr"
|
|||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
//root: 'client/src',
|
|
||||||
build: {
|
|
||||||
// Relative to the root
|
|
||||||
// Note: This has to match the path in `relay/index.mjs`
|
|
||||||
outDir: 'client/dist',
|
|
||||||
},
|
|
||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
svgr({
|
svgr({
|
||||||
@@ -23,14 +17,15 @@ export default defineConfig({
|
|||||||
{
|
{
|
||||||
src: 'node_modules/@leanprover/infoview/dist/*.production.min.js',
|
src: 'node_modules/@leanprover/infoview/dist/*.production.min.js',
|
||||||
dest: '.'
|
dest: '.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'node_modules/coi-serviceworker/coi-serviceworker.js',
|
||||||
|
dest: '.'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
publicDir: "client/public",
|
publicDir: "client/public",
|
||||||
optimizeDeps: {
|
|
||||||
exclude: ['games']
|
|
||||||
},
|
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
proxy: {
|
proxy: {
|
||||||
@@ -38,12 +33,6 @@ export default defineConfig({
|
|||||||
target: 'ws://localhost:8080',
|
target: 'ws://localhost:8080',
|
||||||
ws: true
|
ws: true
|
||||||
},
|
},
|
||||||
'/import': {
|
|
||||||
target: 'http://localhost:8080',
|
|
||||||
},
|
|
||||||
'/data': {
|
|
||||||
target: 'http://localhost:8080',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
cd server
|
||||||
|
|
||||||
|
mkdir -p .lake/toolchains
|
||||||
|
if [ ! -f .lake/toolchains/lean-4.3.0-rc2-linux_wasm32.tar.zst ]
|
||||||
|
then
|
||||||
|
wget -P .lake/toolchains https://github.com/leanprover/lean4/releases/download/v4.3.0-rc2/lean-4.3.0-rc2-linux_wasm32.tar.zst
|
||||||
|
tar --use-compress-program=unzstd -xvf .lake/toolchains/lean-4.3.0-rc2-linux_wasm32.tar.zst -C .lake/toolchains
|
||||||
|
fi
|
||||||
|
if [ ! -f .lake/toolchains/lean-4.3.0-rc2-linux_x86.tar.zst ]
|
||||||
|
then
|
||||||
|
wget -P .lake/toolchains https://github.com/leanprover/lean4/releases/download/v4.3.0-rc2/lean-4.3.0-rc2-linux_x86.tar.zst
|
||||||
|
tar --use-compress-program=unzstd -xvf .lake/toolchains/lean-4.3.0-rc2-linux_x86.tar.zst -C .lake/toolchains
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Linking will fail, but that's ok. We only need the c files.
|
||||||
|
.lake/toolchains/lean-4.3.0-rc2-linux_x86/bin/lake build -f=lakefile32.lean
|
||||||
|
|
||||||
|
|
||||||
|
lake build
|
||||||
|
|
||||||
|
|
||||||
|
OUT_DIR=../client/public
|
||||||
|
LEAN_SYSROOT=.lake/toolchains/lean-4.3.0-rc2-linux_wasm32
|
||||||
|
LEAN_LIBDIR=$LEAN_SYSROOT/lib/lean
|
||||||
|
|
||||||
|
emcc -o $OUT_DIR/server.js main.c -I $LEAN_SYSROOT/include -L $LEAN_LIBDIR .lake/build/ir/GameServer/*.c -lInit -lLean -lleancpp -lleanrt \
|
||||||
|
-sFORCE_FILESYSTEM -lnodefs.js -s EXIT_RUNTIME=0 -s MAIN_MODULE=1 -s LINKABLE=1 -s EXPORT_ALL=1 -s ALLOW_MEMORY_GROWTH=1 -fwasm-exceptions -pthread -flto \
|
||||||
|
-sPTHREAD_POOL_SIZE_STRICT=2 \
|
||||||
|
--preload-file "${LEAN_SYSROOT}/lib/lean/Init"@/lib/Init \
|
||||||
|
--preload-file "${LEAN_SYSROOT}/lib/lean/Init.olean"@/lib/Init.olean \
|
||||||
|
--preload-file "${LEAN_SYSROOT}/lib/lean/Init.ilean"@/lib/Init.ilean \
|
||||||
|
--preload-file "${LEAN_SYSROOT}/lib/lean/Lean"@/lib/Lean \
|
||||||
|
--preload-file "${LEAN_SYSROOT}/lib/lean/Lean.olean"@/lib/Lean.olean \
|
||||||
|
--preload-file "${LEAN_SYSROOT}/lib/lean/Lean.ilean"@/lib/Lean.ilean \
|
||||||
|
--preload-file "./.lake/build32/lib"@/gamelib
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
|
||||||
|
const WebpackShellPluginNext = require('webpack-shell-plugin-next');
|
||||||
|
|
||||||
|
module.exports = env => {
|
||||||
|
|
||||||
|
const single_game = process.env.LEAN4GAME_SINGLE_GAME
|
||||||
|
|
||||||
|
|
||||||
|
const environment = process.env.NODE_ENV
|
||||||
|
const isDevelopment = environment === 'development'
|
||||||
|
|
||||||
|
const babelOptions = {
|
||||||
|
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'],
|
||||||
|
plugins: [
|
||||||
|
isDevelopment && require.resolve('react-refresh/babel'),
|
||||||
|
].filter(Boolean),
|
||||||
|
};
|
||||||
|
|
||||||
|
global.$RefreshReg$ = () => {};
|
||||||
|
global.$RefreshSig$ = () => () => {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
entry: [single_game ? "./client/src/index_local.tsx" : "./client/src/index.tsx"],
|
||||||
|
mode: isDevelopment ? 'development' : 'production',
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(js|jsx)$/,
|
||||||
|
exclude: [/server/, /node_modules/],
|
||||||
|
use: [{
|
||||||
|
loader: require.resolve('babel-loader'),
|
||||||
|
options: babelOptions,
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
use: [{
|
||||||
|
loader: 'ts-loader',
|
||||||
|
options: { allowTsInNodeModules: true }
|
||||||
|
}],
|
||||||
|
// exclude: /node_modules(?!\/(lean4web|lean4|lean4-infoview))/,
|
||||||
|
// Allow .ts imports from node_modules/lean4web and node_modules/lean4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: ["style-loader", "css-loader"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(jpg|png)$/,
|
||||||
|
use: {
|
||||||
|
loader: 'file-loader',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ["*", ".js", ".jsx", ".tsx", ".ts"],
|
||||||
|
fallback: {
|
||||||
|
"http": require.resolve("stream-http") ,
|
||||||
|
"path": require.resolve("path-browserify")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, "client/dist/"),
|
||||||
|
filename: "bundle.js",
|
||||||
|
},
|
||||||
|
devServer: {
|
||||||
|
proxy: {
|
||||||
|
'/websocket': {
|
||||||
|
target: 'ws://localhost:8080',
|
||||||
|
ws: true
|
||||||
|
},
|
||||||
|
'/import': {
|
||||||
|
target: 'http://localhost:3000',
|
||||||
|
router: () => 'http://localhost:8080',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
static: path.join(__dirname, 'client/public/'),
|
||||||
|
port: 3000,
|
||||||
|
hot: true,
|
||||||
|
},
|
||||||
|
devtool: "source-map",
|
||||||
|
plugins: [
|
||||||
|
!isDevelopment && new WebpackShellPluginNext({
|
||||||
|
onBuildEnd:{
|
||||||
|
scripts: [
|
||||||
|
// It's hard to set up webpack to copy the index.html correctly,
|
||||||
|
// so we copy it explicitly after every build:
|
||||||
|
'cp client/public/index.html client/dist/',
|
||||||
|
// Similarly, I haven't been able to load `onigasm.wasm` properly:
|
||||||
|
'cp client/public/onigasm.wasm client/dist/',],
|
||||||
|
blocking: false,
|
||||||
|
parallel: true
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
isDevelopment && new ReactRefreshWebpackPlugin(),
|
||||||
|
].filter(Boolean),
|
||||||
|
|
||||||
|
// Webpack is not happy about the dynamically loaded widget code in the function
|
||||||
|
// `dynamicallyLoadComponent` in `infoview/userWidget.tsx`. If we want to support
|
||||||
|
// dynamically loaded widget code, we need to make sure that the files are available.
|
||||||
|
ignoreWarnings: [/Critical dependency: the request of a dependency is an expression/]
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user