Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66aa8e688e | ||
|
|
a523bab1d6 | ||
|
|
499bb00d4f | ||
|
|
67b03d9ccf | ||
|
|
a15dd0a1bd | ||
|
|
d456621875 | ||
|
|
74059bd5af | ||
|
|
ce71bc81c6 | ||
|
|
7cedfc5038 | ||
|
|
a8d3169ebb | ||
|
|
9b05a27888 | ||
|
|
3124d90cd6 | ||
|
|
5a768c25b7 | ||
|
|
39be34e83a | ||
|
|
dd7a2d1bc1 | ||
|
|
684a1bc72b | ||
|
|
10391b616c | ||
|
|
aaa6c3b576 | ||
|
|
d60dc3fcb2 | ||
|
|
1a54edffd4 | ||
|
|
ad1add5264 | ||
|
|
a26022e3fc | ||
|
|
7c44d49b2a | ||
|
|
a111814973 | ||
|
|
a7784ef66b | ||
|
|
389e120117 | ||
|
|
ee9fd18a56 | ||
|
|
8b29f88407 | ||
|
|
ebda6cc162 | ||
|
|
7e9514fe96 | ||
|
|
038dbe71b8 | ||
|
|
19d2ea363a | ||
|
|
3ac8fdace7 | ||
|
|
8d0493acb5 | ||
|
|
27c661f08f | ||
|
|
b37f050da5 | ||
|
|
09c81ea43f | ||
|
|
ff12b34295 | ||
|
|
45bc0468df | ||
|
|
c24efb1377 | ||
|
|
830bffaf11 | ||
|
|
a9447a70d4 | ||
|
|
eaa4eecad2 | ||
|
|
1828e73b30 | ||
|
|
ce9f5c7840 | ||
|
|
64d7879c32 | ||
|
|
cb711205a2 | ||
|
|
f42829422d | ||
|
|
ee6741232f | ||
|
|
fa4ae5672d | ||
|
|
9bc0a3de46 | ||
|
|
6cdbfbd9cb | ||
|
|
f55581a5f2 | ||
|
|
381930e547 | ||
|
|
e07570181c | ||
|
|
87689e1c3a | ||
|
|
47297e4194 | ||
|
|
f3f077741d | ||
|
|
edf1085310 | ||
|
|
68f84a3426 | ||
|
|
217f86ce5e | ||
|
|
dd60093dfc | ||
|
|
85347a54d9 | ||
|
|
3b4afd6e0e | ||
|
|
2c12872a6e | ||
|
|
ad819bf7ff | ||
|
|
c0f366abba | ||
|
|
f72ebdf050 | ||
|
|
af8463ca5d | ||
|
|
d0a444205a | ||
|
|
1796c76a84 | ||
|
|
a75a4a81ac | ||
|
|
45d84103c1 | ||
|
|
92e9ed38b2 | ||
|
|
d689c7ec86 | ||
|
|
16c979a6c2 | ||
|
|
2b85386373 | ||
|
|
8008b68fd6 | ||
|
|
2649f985fa | ||
|
|
698a88c545 | ||
|
|
3775ad98c8 | ||
|
|
780514e45a | ||
|
|
19f2ceface | ||
|
|
800d1f3308 | ||
|
|
c0acde14e2 | ||
|
|
976d1c6901 | ||
|
|
5bb6c559bc | ||
|
|
11ee6c1535 | ||
|
|
3998fb2fc9 | ||
|
|
538f74004c | ||
|
|
6aebb8993f | ||
|
|
6472ef5b31 | ||
|
|
72ffab5b46 | ||
|
|
3b660c5185 | ||
|
|
ebb8c98145 | ||
|
|
4abf05b77e | ||
|
|
df423aaace | ||
|
|
a67dcb306f | ||
|
|
37582e04d4 | ||
|
|
9d4a6df139 | ||
|
|
26202e5f36 | ||
|
|
ed017fa605 | ||
|
|
36bc52c960 | ||
|
|
e085f2f106 | ||
|
|
e975c455fe | ||
|
|
c006be5a9f | ||
|
|
49379a0930 | ||
|
|
03076b0161 | ||
|
|
bfb60ffedd | ||
|
|
ca576542ba | ||
|
|
fd5e507541 | ||
|
|
c103eeacfa | ||
|
|
e277a48749 | ||
|
|
16ff701518 | ||
|
|
20ca385e14 | ||
|
|
5a38543e3b | ||
|
|
a4a6f2e725 | ||
|
|
cc89e78e4c | ||
|
|
c533a635c2 | ||
|
|
ab98eaa3ba | ||
|
|
7114a8c4cb | ||
|
|
b7eb15184d | ||
|
|
892c70165a | ||
|
|
8929813e48 | ||
|
|
5d88cd6739 | ||
|
|
d16956da9b | ||
|
|
e15e5af126 | ||
|
|
8c93b3c5b3 | ||
|
|
cd1d212a3c | ||
|
|
406c2799b1 | ||
|
|
e579071a3b | ||
|
|
8cdac88b5a | ||
|
|
7882958f54 | ||
|
|
c9f97b3285 | ||
|
|
614b762b6c | ||
|
|
c4f4dbdc6b | ||
|
|
5a78118bb6 | ||
|
|
a6775d5495 | ||
|
|
03a370464b | ||
|
|
eaa214ec37 | ||
|
|
d7f1f70d41 | ||
|
|
2d0f69d337 | ||
|
|
1ab50710f5 | ||
|
|
e76e287763 | ||
|
|
5fd49abb90 | ||
|
|
4b7d540a80 | ||
|
|
aec196deff | ||
|
|
ae636a03ed | ||
|
|
deec431620 | ||
|
|
9f692ccf61 | ||
|
|
47d7f606c6 | ||
|
|
5aa0764844 | ||
|
|
93c55dc9f7 | ||
|
|
aab7441323 | ||
|
|
2ba36b91d5 | ||
|
|
a2726ae287 | ||
|
|
7515561883 | ||
|
|
8c39fb6664 | ||
|
|
5f52e23f29 | ||
|
|
b067dea6e7 | ||
|
|
4edf67f0d6 | ||
|
|
004f81835f | ||
|
|
527f58e3a4 | ||
|
|
b239a5d3dc | ||
|
|
63cf5e8b72 | ||
|
|
25f166f57f | ||
|
|
c89e2e4020 | ||
|
|
f6738faf46 | ||
|
|
cb7224934c | ||
|
|
13d54ff0ff | ||
|
|
72e4011c62 | ||
|
|
4f5256fa88 | ||
|
|
c2b9175fe5 | ||
|
|
a1a6862b5a | ||
|
|
0a057913be | ||
|
|
bedb2ad5ec | ||
|
|
e02e73c1c0 | ||
|
|
c82a88867f | ||
|
|
8b43aed596 | ||
|
|
8c84d3fae7 | ||
|
|
d5697d052e | ||
|
|
0e652256f8 | ||
|
|
9492c1011a | ||
|
|
54bab2a016 | ||
|
|
3f8b180b04 | ||
|
|
23b1074aa2 | ||
|
|
0964a06f2f | ||
|
|
b17c8fc4cb | ||
|
|
0c4ae92856 | ||
|
|
088711b5d1 | ||
|
|
4c93b3a091 | ||
|
|
aa00e359c4 | ||
|
|
f6a2632d80 | ||
|
|
f2190d648f | ||
|
|
d7fd8709cb | ||
|
|
7a03c4fe0d | ||
|
|
333c9498f1 | ||
|
|
121b36b542 | ||
|
|
6bced7575b | ||
|
|
d18b48db2f | ||
|
|
97dc648452 | ||
|
|
472e2c66df | ||
|
|
622e9d3897 | ||
|
|
7f91ae7da8 | ||
|
|
28a7c65db2 | ||
|
|
56525c6234 | ||
|
|
44f7b6703e | ||
|
|
8851cd8b1f | ||
|
|
bae360874c | ||
|
|
07b6525c58 | ||
|
|
8e8026aa38 | ||
|
|
a3a421f504 | ||
|
|
084e25c0dc | ||
|
|
6580afb622 | ||
|
|
0c0a7ab400 | ||
|
|
dfb2f10219 | ||
|
|
a57a5af111 | ||
|
|
946a7fa673 | ||
|
|
244c373192 | ||
|
|
374afa318a | ||
|
|
d0636b1d85 | ||
|
|
2e121363b3 | ||
|
|
590d68ccfb | ||
|
|
f6727e5c9f | ||
|
|
241ef4b67a | ||
|
|
ea4250f38d | ||
|
|
1d2420331d | ||
|
|
2213862998 | ||
|
|
81073b24b8 | ||
|
|
33068f1183 | ||
|
|
8ab37d4344 | ||
|
|
9d9902cce1 | ||
|
|
f53316c591 | ||
|
|
427ce43e95 | ||
|
|
15d79244d4 | ||
|
|
f13027f75c | ||
|
|
5fcdee4f71 | ||
|
|
7ad23dab24 | ||
|
|
89e19cc019 | ||
|
|
4db5260ed8 | ||
|
|
d19046aebd | ||
|
|
6e0fcb1d50 | ||
|
|
04c70ba522 | ||
|
|
1b3993ad3e | ||
|
|
45a95bbdc4 | ||
|
|
7c9f3d7a0a | ||
|
|
823000d5d4 | ||
|
|
c706b66af1 | ||
|
|
933394bb6f | ||
|
|
e09c016c4c |
@@ -1,11 +1,23 @@
|
|||||||
name: Build
|
name: Build
|
||||||
run-name: Build the project
|
run-name: Build the project
|
||||||
on: [push]
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- name: install elan
|
||||||
|
run: |
|
||||||
|
set -o pipefail
|
||||||
|
curl -sSfL https://github.com/leanprover/elan/releases/download/v3.0.0/elan-x86_64-unknown-linux-gnu.tar.gz | tar xz
|
||||||
|
./elan-init -y
|
||||||
|
echo "$HOME/.elan/bin" >> $GITHUB_PATH
|
||||||
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
|
- name: print lean and lake versions
|
||||||
|
run: |
|
||||||
|
lean --version
|
||||||
|
lake --version
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
|
|||||||
+2
-3
@@ -1,6 +1,5 @@
|
|||||||
node_modules
|
node_modules
|
||||||
client/dist
|
client/dist
|
||||||
server/build
|
games/
|
||||||
server/lakefile.olean
|
server/.lake
|
||||||
**/lake-packages/
|
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
|
|||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
GameSkeleton
|
||||||
|
HiddenTactic
|
||||||
|
subgoals
|
||||||
|
KaTex
|
||||||
|
gameserver
|
||||||
|
lakefile
|
||||||
|
Zulip
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Lean 4 Game
|
# Lean 4 Game
|
||||||
|
|
||||||
This is the source code for a Lean 4 game platform hosted at [adam.math.hhu.de](https://adam.math.hhu.de).
|
This is the source code for a Lean game platform hosted at [adam.math.hhu.de](https://adam.math.hhu.de).
|
||||||
|
|
||||||
## Creating a Game
|
## Creating a Game
|
||||||
|
|
||||||
@@ -8,21 +8,30 @@ 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
|
* [Troubleshooting](doc/troubleshoot.md)
|
||||||
|
|
||||||
We encourage anybody to have games hosted on our [Lean Game Server](https://adam.math.hhu.de) for anybody to play. For that you simply need to contact us with the link to your game repo. We are also happy to add work-in-progress games and games in any language.
|
|
||||||
|
|
||||||
For example, you can [contact Jon on Zulip](https://leanprover.zulipchat.com/#narrow/dm/385895-Jon-Eugster). Or [via Email](https://www.math.hhu.de/en/lehrstuehle-/-personen-/-ansprechpartner/innen/lehrstuehle-des-mathematischen-instituts/lehrstuhl-fuer-algebraische-geometrie/team/jon-eugster).
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
The documentation for the game engine itself is still missing, but there is [Creating a Game](doc/create_game.md) explaining the API to create a game.
|
The documentation is very much work in progress but the linked documentation here
|
||||||
|
should be up-to-date:
|
||||||
|
|
||||||
Some documentation:
|
### Game creation API
|
||||||
|
|
||||||
- [NPM Scripts](doc/npm_scripts.md)
|
- [Creating a Game](doc/create_game.md): **the main document to consult**.
|
||||||
- [Old documentation](doc/DOCUMENTATION.md)
|
- [More about Hints](doc/hints.md): describes the `Hint` and `Branch` tactic.
|
||||||
|
|
||||||
|
### Frontend API
|
||||||
|
|
||||||
|
* [How to Run Games Locally](doc/running_locally.md): play a game on your computer
|
||||||
|
* [How to Update an existing Game](doc/update_game.md): update to a new lean version
|
||||||
|
* [How to Publishing a Game](doc/publish_game.md): load your game to adam.math.hhu.de for others to play
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
not fully written yet.
|
||||||
|
|
||||||
|
* [Server](doc/DOCUMENTATION.md): describes the server part (i.e. the content of `server/` und `relay/`).
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
@@ -34,7 +43,8 @@ Providing the use access to a Lean instance running on the server is a severe se
|
|||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
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 has pimarily been developed by Alexander Bentkamp and Jon Eugster.
|
||||||
|
|
||||||
|
It is based on ideas from the [Lean Game Maker](https://github.com/mpedramfar/Lean-game-maker) and the [Natural Number Game
|
||||||
(NNG)](https://www.ma.imperial.ac.uk/~buzzard/xena/natural_number_game/)
|
(NNG)](https://www.ma.imperial.ac.uk/~buzzard/xena/natural_number_game/)
|
||||||
of Kevin Buzzard and Mohammad Pedramfar.
|
by Kevin Buzzard and Mohammad Pedramfar, and 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,152 @@
|
|||||||
|
const lean4gameConfig = require("./src/config.json")
|
||||||
|
|
||||||
|
const typescriptTransform = require('i18next-scanner-typescript');
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const chalk = require('chalk');
|
||||||
|
const eol = require('eol');
|
||||||
|
const path = require('path');
|
||||||
|
const VirtualFile = require('vinyl');
|
||||||
|
|
||||||
|
function flush(done) {
|
||||||
|
const { parser } = this;
|
||||||
|
const { options } = parser;
|
||||||
|
|
||||||
|
// Flush to resource store
|
||||||
|
const resStore = parser.get({ sort: options.sort });
|
||||||
|
const { jsonIndent } = options.resource;
|
||||||
|
const lineEnding = String(options.resource.lineEnding).toLowerCase();
|
||||||
|
|
||||||
|
Object.keys(resStore).forEach((lng) => {
|
||||||
|
const namespaces = resStore[lng];
|
||||||
|
|
||||||
|
Object.keys(namespaces).forEach((ns) => {
|
||||||
|
const resPath = parser.formatResourceSavePath(lng, ns);
|
||||||
|
let resContent;
|
||||||
|
try {
|
||||||
|
resContent = JSON.parse(
|
||||||
|
fs.readFileSync(
|
||||||
|
fs.realpathSync(path.join('public', 'locales', resPath))
|
||||||
|
).toString('utf-8')
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("no previous translation found!")
|
||||||
|
resContent = {};
|
||||||
|
}
|
||||||
|
const obj = { ...namespaces[ns], ...resContent };
|
||||||
|
let text = JSON.stringify(obj, null, jsonIndent) + '\n';
|
||||||
|
|
||||||
|
if (lineEnding === 'auto') {
|
||||||
|
text = eol.auto(text);
|
||||||
|
} else if (lineEnding === '\r\n' || lineEnding === 'crlf') {
|
||||||
|
text = eol.crlf(text);
|
||||||
|
} else if (lineEnding === '\n' || lineEnding === 'lf') {
|
||||||
|
text = eol.lf(text);
|
||||||
|
} else if (lineEnding === '\r' || lineEnding === 'cr') {
|
||||||
|
text = eol.cr(text);
|
||||||
|
} else { // Defaults to LF
|
||||||
|
text = eol.lf(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
let contents = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// "Buffer.from(string[, encoding])" is added in Node.js v5.10.0
|
||||||
|
contents = Buffer.from(text);
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback to "new Buffer(string[, encoding])" which is deprecated since Node.js v6.0.0
|
||||||
|
contents = new Buffer(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.push(new VirtualFile({
|
||||||
|
path: resPath,
|
||||||
|
contents: contents
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
input: [
|
||||||
|
'client/src/**/*.{tsx,ts}',
|
||||||
|
// Use ! to filter out files or directories
|
||||||
|
'!client/i18n/**',
|
||||||
|
'!**/node_modules/**',
|
||||||
|
],
|
||||||
|
options: {
|
||||||
|
debug: true,
|
||||||
|
removeUnusedKeys: true,
|
||||||
|
func: {
|
||||||
|
list: ['i18next.t', 'i18n.t', 't'],
|
||||||
|
extensions: ['.js', '.jsx'] // not .ts or .tsx since we use i18next-scanner-typescript!
|
||||||
|
},
|
||||||
|
trans: {
|
||||||
|
component: 'Trans',
|
||||||
|
i18nKey: 'i18nKey',
|
||||||
|
defaultsKey: 'defaults',
|
||||||
|
extensions: ['.js', '.jsx'], // not .ts or .tsx since we use i18next-scanner-typescript!
|
||||||
|
fallbackKey: (ns, value) => {return value},
|
||||||
|
|
||||||
|
// https://react.i18next.com/latest/trans-component#usage-with-simple-html-elements-like-less-than-br-greater-than-and-others-v10.4.0
|
||||||
|
supportBasicHtmlNodes: true, // Enables keeping the name of simple nodes (e.g. <br/>) in translations instead of indexed keys.
|
||||||
|
keepBasicHtmlNodesFor: ['br', 'strong', 'i', 'p'], // Which nodes are allowed to be kept in translations during defaultValue generation of <Trans>.
|
||||||
|
|
||||||
|
// // https://github.com/acornjs/acorn/tree/master/acorn#interface
|
||||||
|
// acorn: {
|
||||||
|
// ecmaVersion: 2020,
|
||||||
|
// sourceType: 'module', // defaults to 'module'
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
lngs: lean4gameConfig.languages.map(e => e.iso),
|
||||||
|
ns: [],
|
||||||
|
defaultLng: 'en',
|
||||||
|
defaultNs: 'translation',
|
||||||
|
defaultValue: (lng, ns, key) => {
|
||||||
|
if (lng === 'en') {
|
||||||
|
return key; // Use key as value for base language
|
||||||
|
}
|
||||||
|
return ''; // Return empty string for other languages
|
||||||
|
},
|
||||||
|
resource: {
|
||||||
|
loadPath: './client/public/locales/{{lng}}/{{ns}}.json',
|
||||||
|
savePath: './client/public/locales/{{lng}}/{{ns}}.json',
|
||||||
|
jsonIndent: 2,
|
||||||
|
lineEnding: '\n'
|
||||||
|
},
|
||||||
|
nsSeparator: false, // namespace separator
|
||||||
|
keySeparator: false, // key separator
|
||||||
|
plurals: false,
|
||||||
|
interpolation: {
|
||||||
|
prefix: '{{',
|
||||||
|
suffix: '}}'
|
||||||
|
},
|
||||||
|
metadata: {},
|
||||||
|
allowDynamicKeys: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
transform: typescriptTransform(
|
||||||
|
// options
|
||||||
|
{
|
||||||
|
// default value for extensions
|
||||||
|
extensions: [".ts", ".tsx"],
|
||||||
|
// optional ts configuration
|
||||||
|
tsOptions: {
|
||||||
|
target: "es2017",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
function(outputText, file, enc, done) {
|
||||||
|
'use strict';
|
||||||
|
const parser = this.parser;
|
||||||
|
|
||||||
|
parser.parseTransFromString(outputText);
|
||||||
|
parser.parseFuncFromString(outputText);
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
};
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
{
|
||||||
|
"Tactics": "Taktiken",
|
||||||
|
"Lean Game Server": "Lean-Spielserver",
|
||||||
|
"<p>Game rules determine if it is allowed to skip levels and if the games runs checks to only allow unlocked tactics and theorems in proofs.</p><1>Note: \"Unlocked\" tactics (or theorems) are determined by two things: The set of minimal tactics needed to solve a level, plus any tactics you unlocked in another level. That means if you unlock <1>simp</1> in a level, you can use it henceforth in any level.</1><p>The options are:</p>": "<p>Die Spielregeln bestimmen ob es erlaubt ist, Levels zu überspringen und ob das Spiel überprüft welche Taktiken und Theoreme freigeschaltet sind und nur diese im Beweis akzeptiert.</p><1>Bemerkung: \"Freigeschaltete\" Taktiken (und Theoreme) werden durch zwei Faktoren bestimmt: The Menge der Taktiken die minimal notwending sind um den Level zu lösen und dazu die Menge aller Taktiken, die in einem anderen Level freigeschaltet wurden. Das bedeutet wenn <1>simp</1> in einem Level freigeschaltet wird, kann diese Taktik danach in jeglichen Levels verwendet werden.",
|
||||||
|
"Game Rules": "Spielregeln",
|
||||||
|
"levels": "Levels",
|
||||||
|
"tactics": "Taktiken",
|
||||||
|
"regular": "regulär",
|
||||||
|
"relaxed": "relaxed",
|
||||||
|
"none": "keine",
|
||||||
|
"Rules": "Regeln",
|
||||||
|
"Intro": "Inführung",
|
||||||
|
"Game Introduction": "Spieleinführung",
|
||||||
|
"World selection": "Weltenauswahl",
|
||||||
|
"Start": "Start",
|
||||||
|
"Inventory": "Inventar",
|
||||||
|
"next level": "nächstes Level",
|
||||||
|
"Next": "Weiter",
|
||||||
|
"back to world selection": "Zurück zur Weltenauswahl",
|
||||||
|
"Leave World": "Welt verlassen",
|
||||||
|
"previous level": "voheriges Level",
|
||||||
|
"Previous": "Zurück",
|
||||||
|
"Editor mode is enforced!": "Editor kann nicht verlassen werden!",
|
||||||
|
"Editor mode": "Editor",
|
||||||
|
"Typewriter mode": "Schreibmaschine",
|
||||||
|
"information, Impressum, privacy policy": "Informationen, Impressum, Privacy Policy",
|
||||||
|
"Preferences": "Einstellungen",
|
||||||
|
"Game Info & Credits": "Spielinfo & Credits",
|
||||||
|
"Game Info": "Spielinfo",
|
||||||
|
"Clear Progress": "Spielstand löschen",
|
||||||
|
"Erase": "Löschen",
|
||||||
|
"Download Progress": "Spielstand herunterladen",
|
||||||
|
"Download": "Herunterladen",
|
||||||
|
"Load Progress from JSON": "Spielstand von JSON laden",
|
||||||
|
"Upload": "Laden",
|
||||||
|
"Home": "Home",
|
||||||
|
"back to games selection": "Zurück zur Spielauswahl",
|
||||||
|
"close inventory": "Inventar schliessen",
|
||||||
|
"show inventory": "Inventar öffnen",
|
||||||
|
"World": "Welt",
|
||||||
|
"Show more help!": "Mehr Hilfe",
|
||||||
|
"Goal": "Goal",
|
||||||
|
"Current Goal": "Aktuelles Goal",
|
||||||
|
"Objects": "Objekte",
|
||||||
|
"Assumptions": "Annahmen",
|
||||||
|
"Further Goals": "Weitere Goals",
|
||||||
|
"No Goals": "Keine Goals",
|
||||||
|
"Loading goal…": "Goal wird geladen…",
|
||||||
|
"Click somewhere in the Lean file to enable the infoview.": "Ein Klick in den Lean-Code aktiviert den Infoview.",
|
||||||
|
"Waiting for Lean server to start…": "Warte auf das Starten des Lean-Servers…",
|
||||||
|
"Level completed! 🎉": "Level gelöst! 🎉",
|
||||||
|
"Level completed with warnings 🎭": "Level mit Warnungen abgeschlossen 🎭",
|
||||||
|
"Active Goal": "Aktuelles Goal",
|
||||||
|
"Crashed! Go to editor mode and fix your proof! Last server response:": "",
|
||||||
|
"Line": "Zeile",
|
||||||
|
"Character": "Charakter",
|
||||||
|
"Loading messages…": "",
|
||||||
|
"Execute": "Ausführen",
|
||||||
|
"Definitions": "Definitionen",
|
||||||
|
"Theorems": "Theoreme",
|
||||||
|
"Not unlocked yet": "Noch nicht verfügbar",
|
||||||
|
"Not available in this level": "In diesem Level nicht verfügbar",
|
||||||
|
"A repository of learning games for the proof assistant <1>Lean</1> <i>(Lean 4)</i> and its mathematical library <5>mathlib</5>": "Eine Sammlung von Lernspielen für den Beweisassistenten <1>Lean</1> <i>(Lean 4)</i> und dessen mathematische Bibliothek <5>mathlib</5>",
|
||||||
|
"No Games loaded. Use <1>http://localhost:3000/#/g/local/FOLDER</1> to open a game directly from a local folder.": "Kein Spiel geladen. öffne <1>http://localhost:3000/#/g/local/FOLDER</1> um ein Spiel direkt von einem lokalen Ordner zu laden.",
|
||||||
|
"<p>As this server runs lean on our university machines, it has a limited capacity. Our current estimate is about 70 simultaneous games. We hope to address and test this limitation better in the future.</p><1>Most aspects of the games and the infrastructure are still in development. Feel free to file a <1>GitHub Issue</1> about any problems you experience!</1>": "",
|
||||||
|
"This server has been developed as part of the project <1>ADAM : Anticipating the Digital Age of Mathematics</1> at Heinrich-Heine-Universität in Düsseldorf.": "",
|
||||||
|
"Prerequisites": "Voraussetzungen",
|
||||||
|
"Worlds": "Welten",
|
||||||
|
"Levels": "Levels",
|
||||||
|
"Language": "Sprachen",
|
||||||
|
"Development notes": "Entwicklungshinweis",
|
||||||
|
"Adding new games": "Neue Spiele hinzufügen",
|
||||||
|
"Funding": "Finanzierung",
|
||||||
|
"<p>Do you want to delete your saved progress irreversibly?</p><p>(This deletes your proofs and your collected inventory. Saves from other games are not deleted.)</p>": "<p>Soll der Spielstand unwiderruflich gelöscht werden?</p><p>(Dies löscht sämtliche Beweise und das gesammelte Inventar. Spielstände anderer Spiele werden nicht gelöscht.)</p>",
|
||||||
|
"Delete Progress?": "Spielstand löschen?",
|
||||||
|
"Delete": "Löschen",
|
||||||
|
"Download & Delete": "Herunterladen & Löschen",
|
||||||
|
"Cancel": "Abbrechen",
|
||||||
|
"Layout": "Seitenlayout",
|
||||||
|
"Always visible": "Immer sichtbar",
|
||||||
|
"Save my settings (in the browser store)": "Einstellungen im Browser speichern.",
|
||||||
|
"<p>Select a JSON file with the saved game progress to load your progress.</p><1><0>Warning:</0> This will delete your current game progress! Consider <2>downloading your current progress</2> first!</1>": "<p>Wähle eine JSON-Datei mit dem Spielstand um diesen zu laden.</p><1><0>Achtung:</0> Diese Aktion überschreibt den aktuellen Spielstand! Es wird empfohlen zuerst den <2>aktuellen Spielstand herunterzuladen</2>!</1>",
|
||||||
|
"Upload Saved Progress": "Spielstand hochladen",
|
||||||
|
"Load selected file": "Ausgewählte Datei hochladen",
|
||||||
|
"Mobile": "Mobil",
|
||||||
|
"Auto": "Auto",
|
||||||
|
"Desktop": "Desktop",
|
||||||
|
"<0>If you are considering writing your own game, you should use the <1>GameSkeleton Github Repo</1> as a template and read <3>How to Create a Game</3>.</0><1>You can directly load your games into the server and play it using the correct URL. The <1>instructions above</1> also explain the details for how to load your game to the server. We'd like to encourage you to contact us if you have any questions.</1><p>Featured games on this page are added manually. Please get in contact and we'll happily add yours.</p>": "",
|
||||||
|
"Level": "",
|
||||||
|
"Introduction": "",
|
||||||
|
"Retry proof from here": "",
|
||||||
|
"Retry": "",
|
||||||
|
"Failed command": ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
{
|
||||||
|
"Tactics": "Tactics",
|
||||||
|
"Lean Game Server": "Lean Game Server",
|
||||||
|
"<p>Game rules determine if it is allowed to skip levels and if the games runs checks to only allow unlocked tactics and theorems in proofs.</p><1>Note: \"Unlocked\" tactics (or theorems) are determined by two things: The set of minimal tactics needed to solve a level, plus any tactics you unlocked in another level. That means if you unlock <1>simp</1> in a level, you can use it henceforth in any level.</1><p>The options are:</p>": "<p>Game rules determine if it is allowed to skip levels and if the games runs checks to only allow unlocked tactics and theorems in proofs.</p><1>Note: \"Unlocked\" tactics (or theorems) are determined by two things: The set of minimal tactics needed to solve a level, plus any tactics you unlocked in another level. That means if you unlock <1>simp</1> in a level, you can use it henceforth in any level.</1><p>The options are:</p>",
|
||||||
|
"Game Rules": "Game Rules",
|
||||||
|
"levels": "levels",
|
||||||
|
"tactics": "tactics",
|
||||||
|
"regular": "regular",
|
||||||
|
"relaxed": "relaxed",
|
||||||
|
"none": "none",
|
||||||
|
"Rules": "Rules",
|
||||||
|
"Intro": "Intro",
|
||||||
|
"Game Introduction": "Game Introduction",
|
||||||
|
"World selection": "World selection",
|
||||||
|
"Start": "Start",
|
||||||
|
"Inventory": "Inventory",
|
||||||
|
"next level": "next level",
|
||||||
|
"Next": "Next",
|
||||||
|
"back to world selection": "back to world selection",
|
||||||
|
"Leave World": "Leave World",
|
||||||
|
"previous level": "previous level",
|
||||||
|
"Previous": "Previous",
|
||||||
|
"Editor mode is enforced!": "Editor mode is enforced!",
|
||||||
|
"Editor mode": "Editor mode",
|
||||||
|
"Typewriter mode": "Typewriter mode",
|
||||||
|
"information, Impressum, privacy policy": "information, Impressum, privacy policy",
|
||||||
|
"Preferences": "Preferences",
|
||||||
|
"Game Info & Credits": "Game Info & Credits",
|
||||||
|
"Game Info": "Game Info",
|
||||||
|
"Clear Progress": "Clear Progress",
|
||||||
|
"Erase": "Erase",
|
||||||
|
"Download Progress": "Download Progress",
|
||||||
|
"Download": "Download",
|
||||||
|
"Load Progress from JSON": "Load Progress from JSON",
|
||||||
|
"Upload": "Upload",
|
||||||
|
"Home": "Home",
|
||||||
|
"back to games selection": "back to games selection",
|
||||||
|
"close inventory": "close inventory",
|
||||||
|
"show inventory": "show inventory",
|
||||||
|
"World": "World",
|
||||||
|
"Show more help!": "Show more help!",
|
||||||
|
"Goal": "Goal",
|
||||||
|
"Current Goal": "Current Goal",
|
||||||
|
"Objects": "Objects",
|
||||||
|
"Assumptions": "Assumptions",
|
||||||
|
"Further Goals": "Further Goals",
|
||||||
|
"No Goals": "No Goals",
|
||||||
|
"Loading goal…": "Loading goal…",
|
||||||
|
"Click somewhere in the Lean file to enable the infoview.": "Click somewhere in the Lean file to enable the infoview.",
|
||||||
|
"Waiting for Lean server to start…": "Waiting for Lean server to start…",
|
||||||
|
"Level completed! 🎉": "Level completed! 🎉",
|
||||||
|
"Level completed with warnings 🎭": "Level completed with warnings 🎭",
|
||||||
|
"Active Goal": "Active Goal",
|
||||||
|
"Crashed! Go to editor mode and fix your proof! Last server response:": "Crashed! Go to editor mode and fix your proof! Last server response:",
|
||||||
|
"Line": "Line",
|
||||||
|
"Character": "Character",
|
||||||
|
"Loading messages…": "Loading messages…",
|
||||||
|
"Execute": "Execute",
|
||||||
|
"Definitions": "Definitions",
|
||||||
|
"Theorems": "Theorems",
|
||||||
|
"Not unlocked yet": "Not unlocked yet",
|
||||||
|
"Not available in this level": "Not available in this level",
|
||||||
|
"A repository of learning games for the proof assistant <1>Lean</1> <i>(Lean 4)</i> and its mathematical library <5>mathlib</5>": "A repository of learning games for the proof assistant <1>Lean</1> <i>(Lean 4)</i> and its mathematical library <5>mathlib</5>",
|
||||||
|
"No Games loaded. Use <1>http://localhost:3000/#/g/local/FOLDER</1> to open a game directly from a local folder.": "No Games loaded. Use <1>http://localhost:3000/#/g/local/FOLDER</1> to open a game directly from a local folder.",
|
||||||
|
"<p>As this server runs lean on our university machines, it has a limited capacity. Our current estimate is about 70 simultaneous games. We hope to address and test this limitation better in the future.</p><1>Most aspects of the games and the infrastructure are still in development. Feel free to file a <1>GitHub Issue</1> about any problems you experience!</1>": "<p>As this server runs lean on our university machines, it has a limited capacity. Our current estimate is about 70 simultaneous games. We hope to address and test this limitation better in the future.</p><1>Most aspects of the games and the infrastructure are still in development. Feel free to file a <1>GitHub Issue</1> about any problems you experience!</1>",
|
||||||
|
"This server has been developed as part of the project <1>ADAM : Anticipating the Digital Age of Mathematics</1> at Heinrich-Heine-Universität in Düsseldorf.": "This server has been developed as part of the project <1>ADAM : Anticipating the Digital Age of Mathematics</1> at Heinrich-Heine-Universität in Düsseldorf.",
|
||||||
|
"Prerequisites": "Prerequisites",
|
||||||
|
"Worlds": "Worlds",
|
||||||
|
"Levels": "Levels",
|
||||||
|
"Language": "Language",
|
||||||
|
"Development notes": "Development notes",
|
||||||
|
"Adding new games": "Adding new games",
|
||||||
|
"Funding": "Funding",
|
||||||
|
"<p>Do you want to delete your saved progress irreversibly?</p><p>(This deletes your proofs and your collected inventory. Saves from other games are not deleted.)</p>": "<p>Do you want to delete your saved progress irreversibly?</p><p>(This deletes your proofs and your collected inventory. Saves from other games are not deleted.)</p>",
|
||||||
|
"Delete Progress?": "Delete Progress?",
|
||||||
|
"Delete": "Delete",
|
||||||
|
"Download & Delete": "Download & Delete",
|
||||||
|
"Cancel": "Cancel",
|
||||||
|
"Layout": "Layout",
|
||||||
|
"Always visible": "Always visible",
|
||||||
|
"Save my settings (in the browser store)": "Save my settings (in the browser store)",
|
||||||
|
"<p>Select a JSON file with the saved game progress to load your progress.</p><1><0>Warning:</0> This will delete your current game progress! Consider <2>downloading your current progress</2> first!</1>": "<p>Select a JSON file with the saved game progress to load your progress.</p><1><0>Warning:</0> This will delete your current game progress! Consider <2>downloading your current progress</2> first!</1>",
|
||||||
|
"Upload Saved Progress": "Upload Saved Progress",
|
||||||
|
"Load selected file": "Load selected file",
|
||||||
|
"Mobile": "Mobile",
|
||||||
|
"Auto": "Auto",
|
||||||
|
"Desktop": "Desktop",
|
||||||
|
"<0>If you are considering writing your own game, you should use the <1>GameSkeleton Github Repo</1> as a template and read <3>How to Create a Game</3>.</0><1>You can directly load your games into the server and play it using the correct URL. The <1>instructions above</1> also explain the details for how to load your game to the server. We'd like to encourage you to contact us if you have any questions.</1><p>Featured games on this page are added manually. Please get in contact and we'll happily add yours.</p>": "<0>If you are considering writing your own game, you should use the <1>GameSkeleton Github Repo</1> as a template and read <3>How to Create a Game</3>.</0><1>You can directly load your games into the server and play it using the correct URL. The <1>instructions above</1> also explain the details for how to load your game to the server. We'd like to encourage you to contact us if you have any questions.</1><p>Featured games on this page are added manually. Please get in contact and we'll happily add yours.</p>",
|
||||||
|
"Level": "Level",
|
||||||
|
"Introduction": "Introduction",
|
||||||
|
"Retry proof from here": "Retry proof from here",
|
||||||
|
"Retry": "Retry",
|
||||||
|
"Failed command": "Failed command"
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
{
|
||||||
|
"Tactics": "策略",
|
||||||
|
"Lean Game Server": "",
|
||||||
|
"<p>Game rules determine if it is allowed to skip levels and if the games runs checks to only allow unlocked tactics and theorems in proofs.</p><1>Note: \"Unlocked\" tactics (or theorems) are determined by two things: The set of minimal tactics needed to solve a level, plus any tactics you unlocked in another level. That means if you unlock <1>simp</1> in a level, you can use it henceforth in any level.</1><p>The options are:</p>": "<p>游戏规则决定是否允许跳过关卡,以及游戏是否允许在证明中使用未解锁的策略和定理。<p><1>注意:“解锁”的策略(或定理)由两个因素决定:解决关卡所需的最小策略集合,加上你在另一个关卡中解锁的任何策略。这意味着,如果你在某个关卡中解锁了<1>simp</1>,那么你可以在任何关卡中使用它。</1><p>选项是:</p>",
|
||||||
|
"Game Rules": "游戏规则",
|
||||||
|
"levels": "关卡",
|
||||||
|
"tactics": "策略",
|
||||||
|
"regular": "标准",
|
||||||
|
"relaxed": "休闲",
|
||||||
|
"none": "自由",
|
||||||
|
"Rules": "规则",
|
||||||
|
"Intro": "介绍",
|
||||||
|
"Game Introduction": "游戏介绍",
|
||||||
|
"World selection": "世界选择",
|
||||||
|
"Start": "开始",
|
||||||
|
"Inventory": "定理清单",
|
||||||
|
"next level": "下一关",
|
||||||
|
"Next": "下一关",
|
||||||
|
"back to world selection": "返回世界选择",
|
||||||
|
"Leave World": "离开世界",
|
||||||
|
"previous level": "上一关",
|
||||||
|
"Previous": "上一关",
|
||||||
|
"Editor mode is enforced!": "编辑器模式开启!",
|
||||||
|
"Editor mode": "编辑器模式",
|
||||||
|
"Typewriter mode": "打字机模式",
|
||||||
|
"information, Impressum, privacy policy": "",
|
||||||
|
"Preferences": "",
|
||||||
|
"Game Info & Credits": "游戏信息和荣誉",
|
||||||
|
"Game Info": "游戏信息",
|
||||||
|
"Clear Progress": "清除进度",
|
||||||
|
"Erase": "删除",
|
||||||
|
"Download Progress": "下载进度",
|
||||||
|
"Download": "下载",
|
||||||
|
"Load Progress from JSON": "从 JSON 加载进度",
|
||||||
|
"Upload": "上传",
|
||||||
|
"Home": "首页",
|
||||||
|
"back to games selection": "返回游戏选择",
|
||||||
|
"close inventory": "关闭定理清单面板",
|
||||||
|
"show inventory": "打开定理清单面板",
|
||||||
|
"World": "世界",
|
||||||
|
"Show more help!": "显示更多帮助!",
|
||||||
|
"Goal": "目标",
|
||||||
|
"Current Goal": "当前目标",
|
||||||
|
"Objects": "对象",
|
||||||
|
"Assumptions": "假设",
|
||||||
|
"Further Goals": "Further Goals",
|
||||||
|
"No Goals": "无目标",
|
||||||
|
"Loading goal…": "加载目标。。。",
|
||||||
|
"Click somewhere in the Lean file to enable the infoview.": "单击Lean文件中的某处以启用信息视图。",
|
||||||
|
"Waiting for Lean server to start…": "等待 Lean 服务器启动。。。",
|
||||||
|
"Level completed! 🎉": "完成关卡!🎉",
|
||||||
|
"Level completed with warnings 🎭": "关卡完成(带有警告) 🎭",
|
||||||
|
"Retry proof from here": "从这里重新试着证明",
|
||||||
|
"Active Goal": "当前目标",
|
||||||
|
"Crashed! Go to editor mode and fix your proof! Last server response:": "程序崩溃!请转到编辑器模式,修复您的证明!最后一次服务器响应:",
|
||||||
|
"Line": "行",
|
||||||
|
"Character": "字符",
|
||||||
|
"Loading messages…": "正在加载信息。。。",
|
||||||
|
"Execute": "执行",
|
||||||
|
"Definitions": "定义",
|
||||||
|
"Theorems": "定理",
|
||||||
|
"Not unlocked yet": "尚未解锁",
|
||||||
|
"Not available in this level": "本关卡不提供",
|
||||||
|
"A repository of learning games for the proof assistant <1>Lean</1> <i>(Lean 4)</i> and its mathematical library <5>mathlib</5>": "",
|
||||||
|
"No Games loaded. Use <1>http://localhost:3000/#/g/local/FOLDER</1> to open a game directly from a local folder.": "没有加载游戏。使用<1>http://localhost:3000/#/g/local/FOLDER</1>直接从本地文件夹打开游戏。",
|
||||||
|
"<p>As this server runs lean on our university machines, it has a limited capacity. Our current estimate is about 70 simultaneous games. We hope to address and test this limitation better in the future.</p><1>Most aspects of the games and the infrastructure are still in development. Feel free to file a <1>GitHub Issue</1> about any problems you experience!</1>": "",
|
||||||
|
"<0>If you are considering writing your own game, you should use the <1>GameSkeleton Github Repo</1> as a template and read <3>How to Create a Game</3>.</0><1>You can directly load your games into the server and play it using the correct URL. The <1>instructions above</1> also explain the details for how to load your game to the server. We'd like to encourage you to contact us if you have any questions.</1><p>Featured games on this page are added manually. Please get in contact and we'll happily add yours.</p>": "",
|
||||||
|
"This server has been developed as part of the project <1>ADAM : Anticipating the Digital Age of Mathematics</1> at Heinrich-Heine-Universität in Düsseldorf.": "",
|
||||||
|
"Prerequisites": "前置条件",
|
||||||
|
"Worlds": "世界(Worlds)",
|
||||||
|
"Levels": "关卡",
|
||||||
|
"Language": "语言",
|
||||||
|
"Development notes": "",
|
||||||
|
"Adding new games": "",
|
||||||
|
"Funding": "",
|
||||||
|
"<p>Do you want to delete your saved progress irreversibly?</p><p>(This deletes your proofs and your collected inventory. Saves from other games are not deleted.)</p>": "<p>您是否想要不可逆转地删除您的游戏进度?</p><p>(这将删除您的证明和您收集的定理和策略。其他游戏的进度不会被删除。)</p>",
|
||||||
|
"Delete Progress?": "删除进度?",
|
||||||
|
"Delete": "删除",
|
||||||
|
"Download & Delete": "下载和删除",
|
||||||
|
"Cancel": "取消",
|
||||||
|
"Layout": "布局",
|
||||||
|
"Always visible": "始终可见",
|
||||||
|
"Save my settings (in the browser store)": "",
|
||||||
|
"<p>Select a JSON file with the saved game progress to load your progress.</p><1><0>Warning:</0> This will delete your current game progress! Consider <2>downloading your current progress</2> first!</1>": "<p>选择一个包含已保存游戏进度的JSON文件来加载您的进度。</p><1><0>警告:</0>这将删除您当前的游戏进度!首先考虑<2>下载您当前的进度</2>!</1>",
|
||||||
|
"Upload Saved Progress": "上传保存的进度",
|
||||||
|
"Load selected file": "加载所选文件",
|
||||||
|
"Mobile": "移动端",
|
||||||
|
"Auto": "自动",
|
||||||
|
"Desktop": "桌面端",
|
||||||
|
"Level": "",
|
||||||
|
"Introduction": "",
|
||||||
|
"Retry": "",
|
||||||
|
"Failed command": ""
|
||||||
|
}
|
||||||
+14
-6
@@ -8,23 +8,31 @@ import '@fontsource/roboto/700.css';
|
|||||||
|
|
||||||
import './css/reset.css';
|
import './css/reset.css';
|
||||||
import './css/app.css';
|
import './css/app.css';
|
||||||
import { MobileContext } from './components/infoview/context';
|
import { PreferencesContext} from './components/infoview/context';
|
||||||
import { useWindowDimensions } from './window_width';
|
import UsePreferences from "./state/hooks/use_preferences"
|
||||||
|
import i18n from './i18n';
|
||||||
|
|
||||||
export const GameIdContext = React.createContext<string>(undefined);
|
export const GameIdContext = React.createContext<string>(undefined);
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const gameId = "g/" + params.owner + "/" + params.repo
|
const gameId = "g/" + params.owner + "/" + params.repo
|
||||||
const {width, height} = useWindowDimensions()
|
|
||||||
const [mobile, setMobile] = React.useState(width < 800)
|
const {mobile, layout, isSavePreferences, language, setLayout, setIsSavePreferences, setLanguage} = UsePreferences()
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
i18n.changeLanguage(language)
|
||||||
|
}, [language])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<GameIdContext.Provider value={gameId}>
|
<GameIdContext.Provider value={gameId}>
|
||||||
<MobileContext.Provider value={{mobile, setMobile}}>
|
<PreferencesContext.Provider value={{mobile, layout, isSavePreferences, language, setLayout, setIsSavePreferences, setLanguage}}>
|
||||||
|
<React.Suspense>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</MobileContext.Provider>
|
</React.Suspense>
|
||||||
|
</PreferencesContext.Provider>
|
||||||
</GameIdContext.Provider>
|
</GameIdContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 398 KiB |
@@ -5,30 +5,32 @@ import * as React from 'react'
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faDownload, faUpload, faEraser, faBook, faBookOpen, faGlobe, faHome,
|
import { faDownload, faUpload, faEraser, faBook, faBookOpen, faGlobe, faHome,
|
||||||
faArrowRight, faArrowLeft, faXmark, faBars, faCode,
|
faArrowRight, faArrowLeft, faXmark, faBars, faCode,
|
||||||
faCircleInfo, faTerminal } from '@fortawesome/free-solid-svg-icons'
|
faCircleInfo, faTerminal, faGear } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { GameIdContext } from "../app"
|
import { GameIdContext } from "../app"
|
||||||
import { InputModeContext, MobileContext, WorldLevelIdContext } from "./infoview/context"
|
import { InputModeContext, PreferencesContext, WorldLevelIdContext } from "./infoview/context"
|
||||||
import { GameInfo, useGetGameInfoQuery } from '../state/api'
|
import { GameInfo, useGetGameInfoQuery } from '../state/api'
|
||||||
import { changedOpenedIntro, selectCompleted, selectDifficulty, selectProgress } from '../state/progress'
|
import { changedOpenedIntro, selectCompleted, selectDifficulty, selectProgress } from '../state/progress'
|
||||||
import { useAppDispatch, useAppSelector } from '../hooks'
|
import { useAppDispatch, useAppSelector } from '../hooks'
|
||||||
import { Button } from './button'
|
import { Button } from './button'
|
||||||
import { downloadProgress } from './popup/erase'
|
import { downloadProgress } from './popup/erase'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
/** navigation buttons for mobile welcome page to switch between intro/tree/inventory. */
|
/** navigation buttons for mobile welcome page to switch between intro/tree/inventory. */
|
||||||
function MobileNavButtons({pageNumber, setPageNumber}:
|
function MobileNavButtons({pageNumber, setPageNumber}:
|
||||||
{ pageNumber: number,
|
{ pageNumber: number,
|
||||||
setPageNumber: any}) {
|
setPageNumber: any}) {
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
const { t } = useTranslation()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
// if `prevText` or `prevIcon` is set, show a button to go back
|
// if `prevText` or `prevIcon` is set, show a button to go back
|
||||||
let prevText = {0: null, 1: "Intro", 2: null}[pageNumber]
|
let prevText = {0: null, 1: t("Intro"), 2: null}[pageNumber]
|
||||||
let prevIcon = {0: null, 1: null, 2: faBookOpen}[pageNumber]
|
let prevIcon = {0: null, 1: null, 2: faBookOpen}[pageNumber]
|
||||||
let prevTitle = {0: null, 1: "Game Introduction", 2: "World selection"}[pageNumber]
|
let prevTitle = {0: null, 1: t("Game Introduction"), 2: t("World selection")}[pageNumber]
|
||||||
// if `nextText` or `nextIcon` is set, show a button to go forward
|
// if `nextText` or `nextIcon` is set, show a button to go forward
|
||||||
let nextText = {0: "Start", 1: null, 2: null}[pageNumber]
|
let nextText = {0: t("Start"), 1: null, 2: null}[pageNumber]
|
||||||
let nextIcon = {0: null, 1: faBook, 2: null}[pageNumber]
|
let nextIcon = {0: null, 1: faBook, 2: null}[pageNumber]
|
||||||
let nextTitle = {0: "World selection", 1: "Inventory", 2: null}[pageNumber]
|
let nextTitle = {0: t("World selection"), 1: t("Inventory"), 2: null}[pageNumber]
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
{(prevText || prevIcon) &&
|
{(prevText || prevIcon) &&
|
||||||
@@ -53,7 +55,7 @@ function MobileNavButtons({pageNumber, setPageNumber}:
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** button to toggle dropdown menu. */
|
/** button to toggle dropdown menu. */
|
||||||
function MenuButton({navOpen, setNavOpen}) {
|
export function MenuButton({navOpen, setNavOpen}) {
|
||||||
return <Button to="" className="btn toggle-width" id="menu-btn" onClick={(ev) => {setNavOpen(!navOpen)}}>
|
return <Button to="" className="btn toggle-width" id="menu-btn" onClick={(ev) => {setNavOpen(!navOpen)}}>
|
||||||
{navOpen ? <FontAwesomeIcon icon={faXmark} /> : <FontAwesomeIcon icon={faBars} />}
|
{navOpen ? <FontAwesomeIcon icon={faXmark} /> : <FontAwesomeIcon icon={faBars} />}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -63,18 +65,19 @@ function MenuButton({navOpen, setNavOpen}) {
|
|||||||
* for the last level, this button turns into a button going back to the welcome page.
|
* for the last level, this button turns into a button going back to the welcome page.
|
||||||
*/
|
*/
|
||||||
function NextButton({worldSize, difficulty, completed, setNavOpen}) {
|
function NextButton({worldSize, difficulty, completed, setNavOpen}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
|
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
|
||||||
return (levelId < worldSize ?
|
return (levelId < worldSize ?
|
||||||
<Button inverted="true"
|
<Button inverted="true"
|
||||||
to={`/${gameId}/world/${worldId}/level/${levelId + 1}`} title="next level"
|
to={`/${gameId}/world/${worldId}/level/${levelId + 1}`} title={t("next level")}
|
||||||
disabled={difficulty >= 2 && !(completed || levelId == 0)}
|
disabled={difficulty >= 2 && !(completed || levelId == 0)}
|
||||||
onClick={() => setNavOpen(false)}>
|
onClick={() => setNavOpen(false)}>
|
||||||
<FontAwesomeIcon icon={faArrowRight} /> {levelId ? "Next" : "Start"}
|
<FontAwesomeIcon icon={faArrowRight} /> {levelId ? t("Next") : t("Start")}
|
||||||
</Button>
|
</Button>
|
||||||
:
|
:
|
||||||
<Button to={`/${gameId}`} inverted="true" title="back to world selection" id="home-btn">
|
<Button to={`/${gameId}`} inverted="true" title={t("back to world selection")} id="home-btn">
|
||||||
<FontAwesomeIcon icon={faHome} /> Leave World
|
<FontAwesomeIcon icon={faHome} /> {t("Leave World")}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -83,56 +86,106 @@ function NextButton({worldSize, difficulty, completed, setNavOpen}) {
|
|||||||
* only renders if the current level id is > 0.
|
* only renders if the current level id is > 0.
|
||||||
*/
|
*/
|
||||||
function PreviousButton({setNavOpen}) {
|
function PreviousButton({setNavOpen}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
|
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
|
||||||
return (levelId > 0 && <>
|
return (levelId > 0 && <>
|
||||||
<Button disabled={levelId <= 0} inverted="true"
|
<Button disabled={levelId <= 0} inverted="true"
|
||||||
to={`/${gameId}/world/${worldId}/level/${levelId - 1}`}
|
to={`/${gameId}/world/${worldId}/level/${levelId - 1}`}
|
||||||
title="previous level"
|
title={t("previous level")}
|
||||||
onClick={() => setNavOpen(false)}>
|
onClick={() => setNavOpen(false)}>
|
||||||
<FontAwesomeIcon icon={faArrowLeft} /> Previous
|
<FontAwesomeIcon icon={faArrowLeft} /> {t("Previous")}
|
||||||
</Button>
|
</Button>
|
||||||
</>)
|
</>)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** button to toggle between editor and typewriter */
|
/** button to toggle between editor and typewriter */
|
||||||
function InputModeButton({setNavOpen, isDropdown}) {
|
function InputModeButton({setNavOpen, isDropdown}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
const {levelId} = React.useContext(WorldLevelIdContext)
|
const {levelId} = React.useContext(WorldLevelIdContext)
|
||||||
const {typewriterMode, setTypewriterMode, lockInputMode} = React.useContext(InputModeContext)
|
const {typewriterMode, setTypewriterMode, lockEditorMode} = React.useContext(InputModeContext)
|
||||||
|
|
||||||
/** toggle input mode if allowed */
|
/** toggle input mode if allowed */
|
||||||
function toggleInputMode(ev: React.MouseEvent) {
|
function toggleInputMode(ev: React.MouseEvent) {
|
||||||
if (!lockInputMode){
|
if (!lockEditorMode){
|
||||||
setTypewriterMode(!typewriterMode)
|
setTypewriterMode(!typewriterMode)
|
||||||
setNavOpen(false)
|
setNavOpen(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Button
|
return <Button
|
||||||
className={`btn btn-inverted ${isDropdown? '' : 'toggle-width'}`} disabled={levelId <= 0 || lockInputMode}
|
className={`btn btn-inverted ${isDropdown? '' : 'toggle-width'}`} disabled={levelId <= 0 || lockEditorMode}
|
||||||
inverted="true" to=""
|
inverted="true" to=""
|
||||||
onClick={(ev) => toggleInputMode(ev)}
|
onClick={(ev) => toggleInputMode(ev)}
|
||||||
title={lockInputMode ? "Editor mode is enforced!" : typewriterMode ? "Editor mode" : "Typewriter mode"}>
|
title={lockEditorMode ? t("Editor mode is enforced!") : typewriterMode ? t("Editor mode") : t("Typewriter mode")}>
|
||||||
<FontAwesomeIcon icon={typewriterMode ? faCode : faTerminal} />
|
<FontAwesomeIcon icon={(typewriterMode && !lockEditorMode) ? faCode : faTerminal} />
|
||||||
{isDropdown && (typewriterMode ? <> Editor mode</> : <> Typewriter mode</>)}
|
{isDropdown && ((typewriterMode && !lockEditorMode) ? <> {t("Editor mode")}</> : <> {t("Typewriter mode")}</>)}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** button to toggle iimpressum popup */
|
/** button to toggle iimpressum popup
|
||||||
function ImpressumButton({setNavOpen, toggleImpressum, isDropdown}) {
|
*
|
||||||
return <Button className="btn btn-inverted toggle-width"
|
* Note: Do not translate the word "Impressum"! German GDPR needs this.
|
||||||
title="information, Impressum, privacy policy" inverted="true" to="" onClick={(ev) => {toggleImpressum(ev); setNavOpen(false)}}>
|
*/
|
||||||
|
export function ImpressumButton({setNavOpen, toggleImpressum, isDropdown}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return <Button className="btn btn-inverted"
|
||||||
|
title={t("information, Impressum, privacy policy")} inverted="true" to="" onClick={(ev) => {toggleImpressum(ev); setNavOpen(false)}}>
|
||||||
<FontAwesomeIcon icon={faCircleInfo} />
|
<FontAwesomeIcon icon={faCircleInfo} />
|
||||||
{isDropdown && <> Info & Impressum</>}
|
{isDropdown && <> Impressum</>}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PreferencesButton({setNavOpen, togglePreferencesPopup}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return <Button title={t("Preferences")} inverted="true" to="" onClick={() => {togglePreferencesPopup(); setNavOpen(false)}}>
|
||||||
|
<FontAwesomeIcon icon={faGear} /> {t("Preferences")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
|
||||||
|
function GameInfoButton({setNavOpen, toggleInfo}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return <Button className="btn btn-inverted"
|
||||||
|
title={t("Game Info & Credits")} inverted="true" to="" onClick={() => {toggleInfo(); setNavOpen(false)}}>
|
||||||
|
<FontAwesomeIcon icon={faCircleInfo} /> {t("Game Info")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
|
||||||
|
function EraseButton ({setNavOpen, toggleEraseMenu}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return <Button title={t("Clear Progress")} inverted="true" to="" onClick={() => {toggleEraseMenu(); setNavOpen(false)}}>
|
||||||
|
<FontAwesomeIcon icon={faEraser} /> {t("Erase")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
|
||||||
|
function DownloadButton ({setNavOpen, gameId, gameProgress}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return <Button title={t("Download Progress")} inverted="true" to="" onClick={(ev) => {downloadProgress(gameId, gameProgress, ev); setNavOpen(false)}}>
|
||||||
|
<FontAwesomeIcon icon={faDownload} /> {t("Download")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
|
||||||
|
function UploadButton ({setNavOpen, toggleUploadMenu}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return <Button title={t("Load Progress from JSON")} inverted="true" to="" onClick={() => {toggleUploadMenu(); setNavOpen(false)}}>
|
||||||
|
<FontAwesomeIcon icon={faUpload} /> {t("Upload")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** button to go back to welcome page */
|
/** button to go back to welcome page */
|
||||||
function HomeButton({isDropdown}) {
|
function HomeButton({isDropdown}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
return <Button to={`/${gameId}`} inverted="true" title="back to world selection" id="home-btn">
|
return <Button to={`/${gameId}`} inverted="true" title={t("back to world selection")} id="home-btn">
|
||||||
<FontAwesomeIcon icon={faHome} />
|
<FontAwesomeIcon icon={faHome} />
|
||||||
{isDropdown && <> Home</>}
|
{isDropdown && <> {t("Home")}</>}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
|
||||||
|
function LandingPageButton() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return <Button inverted="false" title={t("back to games selection")} to="/">
|
||||||
|
<FontAwesomeIcon icon={faArrowLeft} /> <FontAwesomeIcon icon={faGlobe} />
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,9 +193,10 @@ function HomeButton({isDropdown}) {
|
|||||||
* only displays a button if `setPageNumber` is set.
|
* only displays a button if `setPageNumber` is set.
|
||||||
*/
|
*/
|
||||||
function InventoryButton({pageNumber, setPageNumber}) {
|
function InventoryButton({pageNumber, setPageNumber}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
return (setPageNumber &&
|
return (setPageNumber &&
|
||||||
<Button to="" className="btn btn-inverted toggle-width"
|
<Button to="" className="btn btn-inverted toggle-width"
|
||||||
title={pageNumber ? "close inventory" : "show inventory"}
|
title={pageNumber ? t("close inventory") : t("show inventory")}
|
||||||
inverted="true" onClick={() => {setPageNumber(pageNumber ? 0 : 1)}}>
|
inverted="true" onClick={() => {setPageNumber(pageNumber ? 0 : 1)}}>
|
||||||
<FontAwesomeIcon icon={pageNumber ? faBookOpen : faBook} />
|
<FontAwesomeIcon icon={pageNumber ? faBookOpen : faBook} />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -150,65 +204,59 @@ function InventoryButton({pageNumber, setPageNumber}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** the navigation bar on the welcome page */
|
/** the navigation bar on the welcome page */
|
||||||
export function WelcomeAppBar({pageNumber, setPageNumber, gameInfo, toggleImpressum, toggleEraseMenu, toggleUploadMenu, toggleInfo} : {
|
export function WelcomeAppBar({pageNumber, setPageNumber, gameInfo, toggleImpressum, toggleEraseMenu, toggleUploadMenu, toggleInfo, togglePreferencesPopup} : {
|
||||||
pageNumber: number,
|
pageNumber: number,
|
||||||
setPageNumber: any,
|
setPageNumber: any,
|
||||||
gameInfo: GameInfo,
|
gameInfo: GameInfo,
|
||||||
toggleImpressum: any,
|
toggleImpressum: any,
|
||||||
toggleEraseMenu: any,
|
toggleEraseMenu: any,
|
||||||
toggleUploadMenu: any,
|
toggleUploadMenu: any,
|
||||||
toggleInfo: any
|
toggleInfo: any,
|
||||||
|
togglePreferencesPopup: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const gameProgress = useAppSelector(selectProgress(gameId))
|
const gameProgress = useAppSelector(selectProgress(gameId))
|
||||||
const {mobile} = React.useContext(MobileContext)
|
const {mobile} = React.useContext(PreferencesContext)
|
||||||
const [navOpen, setNavOpen] = React.useState(false)
|
const [navOpen, setNavOpen] = React.useState(false)
|
||||||
|
|
||||||
return <div className="app-bar">
|
return <div className="app-bar">
|
||||||
<div>
|
<div className='app-bar-left'>
|
||||||
<Button inverted="false" title="back to games selection" to="/">
|
<LandingPageButton />
|
||||||
<FontAwesomeIcon icon={faArrowLeft} /> <FontAwesomeIcon icon={faGlobe} />
|
|
||||||
</Button>
|
|
||||||
<span className="app-bar-title"></span>
|
<span className="app-bar-title"></span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{!mobile && <span className="app-bar-title">{gameInfo?.title}</span>}
|
{!mobile && <span className="app-bar-title">{t(gameInfo?.title, {ns: gameId})}</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="nav-btns">
|
<div className="nav-btns">
|
||||||
{mobile && <MobileNavButtons pageNumber={pageNumber} setPageNumber={setPageNumber} />}
|
{mobile && <MobileNavButtons pageNumber={pageNumber} setPageNumber={setPageNumber} />}
|
||||||
<MenuButton navOpen={navOpen} setNavOpen={setNavOpen} />
|
<MenuButton navOpen={navOpen} setNavOpen={setNavOpen} />
|
||||||
</div>
|
</div>
|
||||||
<div className={'menu dropdown' + (navOpen ? '' : ' hidden')}>
|
<div className={'menu dropdown' + (navOpen ? '' : ' hidden')}>
|
||||||
<Button title="Game Info & Credits" inverted="true" to="" onClick={() => {toggleInfo(); setNavOpen(false)}}>
|
<GameInfoButton setNavOpen={setNavOpen} toggleInfo={toggleInfo}/>
|
||||||
<FontAwesomeIcon icon={faCircleInfo} /> Game Info
|
<EraseButton setNavOpen={setNavOpen} toggleEraseMenu={toggleEraseMenu}/>
|
||||||
</Button>
|
<DownloadButton setNavOpen={setNavOpen} gameId={gameId} gameProgress={gameProgress}/>
|
||||||
<Button title="Clear Progress" inverted="true" to="" onClick={() => {toggleEraseMenu(); setNavOpen(false)}}>
|
<UploadButton setNavOpen={setNavOpen} toggleUploadMenu={toggleUploadMenu}/>
|
||||||
<FontAwesomeIcon icon={faEraser} /> Erase
|
<ImpressumButton setNavOpen={setNavOpen} toggleImpressum={toggleImpressum} isDropdown={true} />
|
||||||
</Button>
|
<PreferencesButton setNavOpen={setNavOpen} togglePreferencesPopup={togglePreferencesPopup}/>
|
||||||
<Button title="Download Progress" inverted="true" to="" onClick={(ev) => {downloadProgress(gameId, gameProgress, ev); setNavOpen(false)}}>
|
|
||||||
<FontAwesomeIcon icon={faDownload} /> Download
|
|
||||||
</Button>
|
|
||||||
<Button title="Load Progress from JSON" inverted="true" to="" onClick={() => {toggleUploadMenu(); setNavOpen(false)}}>
|
|
||||||
<FontAwesomeIcon icon={faUpload} /> Upload
|
|
||||||
</Button>
|
|
||||||
<Button title="Impressum, privacy policy" inverted="true" to="" onClick={() => {toggleImpressum(); setNavOpen(false)}}>
|
|
||||||
<FontAwesomeIcon icon={faCircleInfo} /> Impressum
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** the navigation bar in a level */
|
/** the navigation bar in a level */
|
||||||
export function LevelAppBar({isLoading, levelTitle, toggleImpressum, pageNumber=undefined, setPageNumber=undefined} : {
|
export function LevelAppBar({isLoading, levelTitle, toggleImpressum, toggleInfo, togglePreferencesPopup, pageNumber=undefined, setPageNumber=undefined} : {
|
||||||
isLoading: boolean,
|
isLoading: boolean,
|
||||||
levelTitle: string,
|
levelTitle: string,
|
||||||
toggleImpressum: any,
|
toggleImpressum: any,
|
||||||
|
toggleInfo: any,
|
||||||
|
togglePreferencesPopup: any,
|
||||||
pageNumber?: number,
|
pageNumber?: number,
|
||||||
setPageNumber?: any,
|
setPageNumber?: any,
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
|
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
|
||||||
const {mobile} = React.useContext(MobileContext)
|
const {mobile} = React.useContext(PreferencesContext)
|
||||||
const [navOpen, setNavOpen] = React.useState(false)
|
const [navOpen, setNavOpen] = React.useState(false)
|
||||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||||
const completed = useAppSelector(selectCompleted(gameId, worldId, levelId))
|
const completed = useAppSelector(selectCompleted(gameId, worldId, levelId))
|
||||||
@@ -232,14 +280,16 @@ export function LevelAppBar({isLoading, levelTitle, toggleImpressum, pageNumber=
|
|||||||
<PreviousButton setNavOpen={setNavOpen} />
|
<PreviousButton setNavOpen={setNavOpen} />
|
||||||
<HomeButton isDropdown={true} />
|
<HomeButton isDropdown={true} />
|
||||||
<InputModeButton setNavOpen={setNavOpen} isDropdown={true}/>
|
<InputModeButton setNavOpen={setNavOpen} isDropdown={true}/>
|
||||||
|
<GameInfoButton setNavOpen={setNavOpen} toggleInfo={toggleInfo}/>
|
||||||
<ImpressumButton setNavOpen={setNavOpen} toggleImpressum={toggleImpressum} isDropdown={true} />
|
<ImpressumButton setNavOpen={setNavOpen} toggleImpressum={toggleImpressum} isDropdown={true} />
|
||||||
|
<PreferencesButton setNavOpen={setNavOpen} togglePreferencesPopup={togglePreferencesPopup}/>
|
||||||
</div>
|
</div>
|
||||||
</> :
|
</> :
|
||||||
<>
|
<>
|
||||||
{/* DESKTOP VERSION */}
|
{/* DESKTOP VERSION */}
|
||||||
<div>
|
<div className='app-bar-left'>
|
||||||
<HomeButton isDropdown={false} />
|
<HomeButton isDropdown={false} />
|
||||||
<span className="app-bar-title">{worldTitle && `World: ${worldTitle}`}</span>
|
<span className="app-bar-title">{worldTitle && `${t("World")}: ${t(worldTitle, {ns: gameId})}`}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="app-bar-title">{levelTitle}</span>
|
<span className="app-bar-title">{levelTitle}</span>
|
||||||
@@ -248,7 +298,12 @@ export function LevelAppBar({isLoading, levelTitle, toggleImpressum, pageNumber=
|
|||||||
<PreviousButton setNavOpen={setNavOpen} />
|
<PreviousButton setNavOpen={setNavOpen} />
|
||||||
<NextButton worldSize={gameInfo.data?.worldSize[worldId]} difficulty={difficulty} completed={completed} setNavOpen={setNavOpen} />
|
<NextButton worldSize={gameInfo.data?.worldSize[worldId]} difficulty={difficulty} completed={completed} setNavOpen={setNavOpen} />
|
||||||
<InputModeButton setNavOpen={setNavOpen} isDropdown={false}/>
|
<InputModeButton setNavOpen={setNavOpen} isDropdown={false}/>
|
||||||
<ImpressumButton setNavOpen={setNavOpen} toggleImpressum={toggleImpressum} isDropdown={false} />
|
<MenuButton navOpen={navOpen} setNavOpen={setNavOpen}/>
|
||||||
|
</div>
|
||||||
|
<div className={'menu dropdown' + (navOpen ? '' : ' hidden')}>
|
||||||
|
<GameInfoButton setNavOpen={setNavOpen} toggleInfo={toggleInfo}/>
|
||||||
|
<ImpressumButton setNavOpen={setNavOpen} toggleImpressum={toggleImpressum} isDropdown={true} />
|
||||||
|
<PreferencesButton setNavOpen={setNavOpen} togglePreferencesPopup={togglePreferencesPopup}/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,46 @@
|
|||||||
import { GameHint } from "./infoview/rpc_api";
|
import { GameHint, InteractiveGoalsWithHints, ProofState } from "./infoview/rpc_api";
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Markdown from './markdown';
|
import Markdown from './markdown';
|
||||||
|
import { DeletedChatContext, ProofContext } from "./infoview/context";
|
||||||
|
import { lastStepHasErrors } from "./infoview/goals";
|
||||||
|
import { Button } from "./button";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { GameIdContext } from "../app";
|
||||||
|
|
||||||
|
/** Plug-in the variable names in a hint. We do this client-side to prepare
|
||||||
|
* for i18n in the future. i.e. one should be able translate the `rawText`
|
||||||
|
* and have the variables substituted just before displaying.
|
||||||
|
*/
|
||||||
|
function getHintText(hint: GameHint): string {
|
||||||
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
let { t } = useTranslation()
|
||||||
|
if (hint.rawText) {
|
||||||
|
// Replace the variable names used in the hint with the ones used by the player
|
||||||
|
// variable names are marked like `«{g}»` inside the text.
|
||||||
|
return t(hint.rawText, {ns: gameId}).replaceAll(/«\{(.*?)\}»/g, ((_, v) =>
|
||||||
|
// `hint.varNames` contains tuples `[oldName, newName]`
|
||||||
|
(hint.varNames.find(x => x[0] == v))[1]))
|
||||||
|
} else {
|
||||||
|
// hints created in the frontend do not have a `rawText`
|
||||||
|
// TODO: `hint.text` could be removed in theory.
|
||||||
|
return t(hint.text, {ns: gameId})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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}>
|
||||||
<Markdown>{hint.text}</Markdown>
|
<Markdown>{getHintText(hint)}</Markdown>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HiddenHint({hint, step, selected, toggleSelection, lastLevel} : {hint: GameHint, step: number, selected: number, toggleSelection: any, lastLevel?: boolean}) {
|
export function HiddenHint({hint, step, selected, toggleSelection, lastLevel} : {hint: GameHint, step: number, selected: number, toggleSelection: any, lastLevel?: boolean}) {
|
||||||
return <div className={`message warning step-${step}` + (step == selected ? ' selected' : '') + (lastLevel ? ' recent' : '')} onClick={toggleSelection}>
|
return <div className={`message warning step-${step}` + (step == selected ? ' selected' : '') + (lastLevel ? ' recent' : '')} onClick={toggleSelection}>
|
||||||
<Markdown>{hint.text}</Markdown>
|
<Markdown>{getHintText(hint)}</Markdown>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Hints({hints, showHidden, step, selected, toggleSelection, lastLevel} : {hints: GameHint[], showHidden: boolean, step: number, selected: number, toggleSelection: any, lastLevel?: boolean}) {
|
export function Hints({hints, showHidden, step, selected, toggleSelection, lastLevel} : {hints: GameHint[], showHidden: boolean, step: number, selected: number, toggleSelection: any, lastLevel?: boolean}) {
|
||||||
|
if (!hints) {return <></>}
|
||||||
const openHints = hints.filter(hint => !hint.hidden)
|
const openHints = hints.filter(hint => !hint.hidden)
|
||||||
const hiddenHints = hints.filter(hint => hint.hidden)
|
const hiddenHints = hints.filter(hint => hint.hidden)
|
||||||
|
|
||||||
@@ -28,7 +53,7 @@ export function Hints({hints, showHidden, step, selected, toggleSelection, lastL
|
|||||||
|
|
||||||
export function DeletedHint({hint} : {hint: GameHint}) {
|
export function DeletedHint({hint} : {hint: GameHint}) {
|
||||||
return <div className="message information deleted-hint">
|
return <div className="message information deleted-hint">
|
||||||
<Markdown>{hint.text}</Markdown>
|
<Markdown>{getHintText(hint)}</Markdown>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,3 +68,58 @@ 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.
|
||||||
|
* Hidden hints are not filtered.
|
||||||
|
*/
|
||||||
|
export function filterHints(hints: GameHint[], prevHints: GameHint[]): GameHint[] {
|
||||||
|
if (!hints) {
|
||||||
|
return []}
|
||||||
|
else if (!prevHints) {
|
||||||
|
return hints }
|
||||||
|
else {
|
||||||
|
return hints.filter((hint) => hint.hidden ||
|
||||||
|
(prevHints.find(x => (x.text == hint.text && x.hidden == hint.hidden)) === undefined)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function hasHiddenHints(step: InteractiveGoalsWithHints): boolean {
|
||||||
|
return step?.goals[0]?.hints.some((hint) => hint.hidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function MoreHelpButton({selected=null} : {selected?: number}) {
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const {proof, setProof} = React.useContext(ProofContext)
|
||||||
|
const {deletedChat, setDeletedChat, showHelp, setShowHelp} = React.useContext(DeletedChatContext)
|
||||||
|
|
||||||
|
let k = proof?.steps.length ?
|
||||||
|
((selected === null) ? (proof?.steps.length - (lastStepHasErrors(proof) ? 2 : 1)) : selected)
|
||||||
|
: 0
|
||||||
|
|
||||||
|
const activateHiddenHints = (ev) => {
|
||||||
|
// If the last step (`k`) has errors, we want the hidden hints from the
|
||||||
|
// second-to-last step to be affected
|
||||||
|
if (!(proof?.steps.length)) {return}
|
||||||
|
|
||||||
|
// state must not be mutated, therefore we need to clone the set
|
||||||
|
let tmp = new Set(showHelp)
|
||||||
|
if (tmp.has(k)) {
|
||||||
|
tmp.delete(k)
|
||||||
|
} else {
|
||||||
|
tmp.add(k)
|
||||||
|
}
|
||||||
|
setShowHelp(tmp)
|
||||||
|
console.debug(`help: ${Array.from(tmp.values())}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasHiddenHints(proof?.steps[k]) && !showHelp.has(k)) {
|
||||||
|
return <Button to="" onClick={activateHiddenHints}>
|
||||||
|
{t("Show more help!")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,45 +3,59 @@
|
|||||||
*/
|
*/
|
||||||
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 { InteractiveDiagnostic, InteractiveTermGoal } from '@leanprover/infoview-api';
|
import { InteractiveDiagnostic } from '@leanprover/infoview-api';
|
||||||
import { GameHint, InteractiveGoal, InteractiveGoals } from './rpc_api';
|
import { Diagnostic } from 'vscode-languageserver-types'
|
||||||
|
import { GameHint, InteractiveGoal, InteractiveTermGoal,InteractiveGoalsWithHints, ProofState } from './rpc_api';
|
||||||
|
import { PreferencesState } from '../../state/preferences';
|
||||||
|
|
||||||
export const MonacoEditorContext = React.createContext<monaco.editor.IStandaloneCodeEditor>(
|
export const MonacoEditorContext = React.createContext<monaco.editor.IStandaloneCodeEditor>(
|
||||||
null as any)
|
null as any)
|
||||||
|
|
||||||
export type InfoStatus = 'updating' | 'error' | 'ready';
|
export type InfoStatus = 'updating' | 'error' | 'ready';
|
||||||
|
|
||||||
/** One step of the proof */
|
// /** One step of the proof */
|
||||||
export type ProofStep = {
|
// export type ProofStep = {
|
||||||
/** The command in this step */
|
// /** The command in this step */
|
||||||
command : string
|
// command : string
|
||||||
/** List of goals *after* this command */
|
// /** List of goals *after* this command */
|
||||||
goals: InteractiveGoal[] // TODO: Add correct type
|
// goals: InteractiveGoal[] // TODO: Add correct type
|
||||||
/** Story relevant messages */
|
// /** Story relevant messages */
|
||||||
hints: GameHint[] // TODO: Add correct type
|
// hints: GameHint[] // TODO: Add correct type
|
||||||
/** Errors and warnings */
|
// /** Errors and warnings */
|
||||||
errors: InteractiveDiagnostic[] // TODO: Add correct type
|
// errors: InteractiveDiagnostic[] // TODO: Add correct type
|
||||||
}
|
// }
|
||||||
|
|
||||||
/** The context storing the proof step-by-step for the command line mode */
|
/** The context storing the proof step-by-step for the command line mode */
|
||||||
export const ProofContext = React.createContext<{
|
export const ProofContext = React.createContext<{
|
||||||
/** The proof consists of multiple steps that are processed one after the other.
|
/** The proof consists of multiple steps that are processed one after the other.
|
||||||
* In particular multi-line terms like `match`-statements will not be supported.
|
* In particular multi-line terms like `match`-statements will not be supported.
|
||||||
*
|
*
|
||||||
* Note that the first step will always have `null` as command
|
* Note that the first step will always have "" as command
|
||||||
*/
|
*/
|
||||||
proof: ProofStep[],
|
proof: ProofState,
|
||||||
setProof: React.Dispatch<React.SetStateAction<Array<ProofStep>>>
|
setProof: React.Dispatch<React.SetStateAction<ProofState>>
|
||||||
|
/** TODO: Workaround to capture a crash of the gameserver. */
|
||||||
|
interimDiags: Diagnostic[],
|
||||||
|
setInterimDiags: React.Dispatch<React.SetStateAction<Array<Diagnostic>>>
|
||||||
|
/** TODO: Workaround to capture a crash of the gameserver. */
|
||||||
|
crashed: Boolean,
|
||||||
|
setCrashed: React.Dispatch<React.SetStateAction<Boolean>>
|
||||||
}>({
|
}>({
|
||||||
proof: [],
|
proof: {steps: [], diagnostics: [], completed: false, completedWithWarnings: false},
|
||||||
setProof: () => {} // TODO: implement me
|
setProof: () => {},
|
||||||
|
interimDiags: [],
|
||||||
|
setInterimDiags: () => {},
|
||||||
|
crashed: false,
|
||||||
|
setCrashed: () => {}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: Do we still need that?
|
||||||
export interface ProofStateProps {
|
export interface ProofStateProps {
|
||||||
// pos: DocumentPosition;
|
// pos: DocumentPosition;
|
||||||
status: InfoStatus;
|
status: InfoStatus;
|
||||||
messages: InteractiveDiagnostic[];
|
messages: InteractiveDiagnostic[];
|
||||||
goals?: InteractiveGoals;
|
goals?: InteractiveGoalsWithHints;
|
||||||
termGoal?: InteractiveTermGoal;
|
termGoal?: InteractiveTermGoal;
|
||||||
error?: string;
|
error?: string;
|
||||||
// userWidgets: UserWidgetInstance[];
|
// userWidgets: UserWidgetInstance[];
|
||||||
@@ -49,25 +63,34 @@ export interface ProofStateProps {
|
|||||||
// triggerUpdate: () => Promise<void>;
|
// triggerUpdate: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProofStateContext = React.createContext<{
|
// export const ProofStateContext = React.createContext<{
|
||||||
proofState : ProofStateProps,
|
// proofState : ProofStateProps,
|
||||||
setProofState: React.Dispatch<React.SetStateAction<ProofStateProps>>
|
// setProofState: React.Dispatch<React.SetStateAction<ProofStateProps>>
|
||||||
}>({
|
// }>({
|
||||||
proofState : {
|
// proofState : {
|
||||||
status: 'updating',
|
// status: 'updating',
|
||||||
messages: [],
|
// messages: [],
|
||||||
goals: undefined,
|
// goals: undefined,
|
||||||
termGoal: undefined,
|
// termGoal: undefined,
|
||||||
error: undefined},
|
// error: undefined},
|
||||||
setProofState: () => {},
|
// setProofState: () => {},
|
||||||
})
|
// })
|
||||||
|
|
||||||
export const MobileContext = React.createContext<{
|
export interface IPreferencesContext extends PreferencesState{
|
||||||
mobile : boolean,
|
mobile: boolean, // The variables that actually control the page 'layout' can only be changed through layout.
|
||||||
setMobile: React.Dispatch<React.SetStateAction<Boolean>>,
|
setLayout: React.Dispatch<React.SetStateAction<PreferencesState["layout"]>>;
|
||||||
}>({
|
setIsSavePreferences: React.Dispatch<React.SetStateAction<PreferencesState["isSavePreferences"]>>;
|
||||||
mobile : false,
|
setLanguage: React.Dispatch<React.SetStateAction<PreferencesState["language"]>>;
|
||||||
setMobile: () => {},
|
}
|
||||||
|
|
||||||
|
export const PreferencesContext = React.createContext<IPreferencesContext>({
|
||||||
|
mobile: false,
|
||||||
|
layout: "auto",
|
||||||
|
isSavePreferences: false,
|
||||||
|
language: "en",
|
||||||
|
setLayout: () => {},
|
||||||
|
setIsSavePreferences: () => {},
|
||||||
|
setLanguage: () => {},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const WorldLevelIdContext = React.createContext<{
|
export const WorldLevelIdContext = React.createContext<{
|
||||||
@@ -105,13 +128,13 @@ export const InputModeContext = React.createContext<{
|
|||||||
setTypewriterMode: React.Dispatch<React.SetStateAction<boolean>>,
|
setTypewriterMode: React.Dispatch<React.SetStateAction<boolean>>,
|
||||||
typewriterInput: string,
|
typewriterInput: string,
|
||||||
setTypewriterInput: React.Dispatch<React.SetStateAction<string>>,
|
setTypewriterInput: React.Dispatch<React.SetStateAction<string>>,
|
||||||
lockInputMode: boolean,
|
lockEditorMode: boolean,
|
||||||
setLockInputMode: React.Dispatch<React.SetStateAction<boolean>>,
|
setLockEditorMode: React.Dispatch<React.SetStateAction<boolean>>,
|
||||||
}>({
|
}>({
|
||||||
typewriterMode: true,
|
typewriterMode: true,
|
||||||
setTypewriterMode: () => {},
|
setTypewriterMode: () => {},
|
||||||
typewriterInput: "",
|
typewriterInput: "",
|
||||||
setTypewriterInput: () => {},
|
setTypewriterInput: () => {},
|
||||||
lockInputMode: false,
|
lockEditorMode: false,
|
||||||
setLockInputMode: () => {},
|
setLockEditorMode: () => {},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ import { Locations, LocationsContext, SelectableLocation } from '../../../../nod
|
|||||||
import { InteractiveCode } from '../../../../node_modules/lean4-infoview/src/infoview/interactiveCode'
|
import { InteractiveCode } from '../../../../node_modules/lean4-infoview/src/infoview/interactiveCode'
|
||||||
import { WithTooltipOnHover } from '../../../../node_modules/lean4-infoview/src/infoview/tooltips';
|
import { WithTooltipOnHover } from '../../../../node_modules/lean4-infoview/src/infoview/tooltips';
|
||||||
import { InputModeContext } from './context';
|
import { InputModeContext } from './context';
|
||||||
import { InteractiveGoal, InteractiveGoals, InteractiveHypothesisBundle } from './rpc_api';
|
import { InteractiveGoal, InteractiveGoals, InteractiveGoalsWithHints, InteractiveHypothesisBundle, ProofState } from './rpc_api';
|
||||||
|
import { RpcSessionAtPos } from '@leanprover/infoview/*';
|
||||||
|
import { DocumentPosition } from '../../../../node_modules/lean4-infoview/src/infoview/util';
|
||||||
|
import { DiagnosticSeverity } from 'vscode-languageserver-protocol';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
/** Returns true if `h` is inaccessible according to Lean's default name rendering. */
|
/** Returns true if `h` is inaccessible according to Lean's default name rendering. */
|
||||||
function isInaccessibleName(h: string): boolean {
|
function isInaccessibleName(h: string): boolean {
|
||||||
@@ -39,7 +43,11 @@ function goalToString(g: InteractiveGoal): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function goalsToString(goals: InteractiveGoals): string {
|
export function goalsToString(goals: InteractiveGoals): string {
|
||||||
return goals.goals.map(goalToString).join('\n\n')
|
return goals.goals.map(g => goalToString(g)).join('\n\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function goalsWithHintsToString(goals: InteractiveGoalsWithHints): string {
|
||||||
|
return goals.goals.map(g => goalToString(g.goal)).join('\n\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GoalFilterState {
|
interface GoalFilterState {
|
||||||
@@ -127,16 +135,12 @@ interface GoalProps {
|
|||||||
typewriter: boolean
|
typewriter: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProofDisplayProps {
|
|
||||||
proof: string
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the hypotheses, target type and optional case label of a goal according to the
|
* Displays the hypotheses, target type and optional case label of a goal according to the
|
||||||
* provided `filter`. */
|
* provided `filter`. */
|
||||||
export const Goal = React.memo((props: GoalProps) => {
|
export const Goal = React.memo((props: GoalProps) => {
|
||||||
const { goal, filter, showHints, typewriter } = props
|
const { goal, filter, showHints, typewriter } = props
|
||||||
|
let { t } = useTranslation()
|
||||||
|
|
||||||
// TODO: Apparently `goal` can be `undefined`
|
// TODO: Apparently `goal` can be `undefined`
|
||||||
if (!goal) {return <></>}
|
if (!goal) {return <></>}
|
||||||
@@ -150,7 +154,7 @@ export const Goal = React.memo((props: GoalProps) => {
|
|||||||
undefined,
|
undefined,
|
||||||
[locs, goal.mvarId])
|
[locs, goal.mvarId])
|
||||||
const goalLi = <div key={'goal'}>
|
const goalLi = <div key={'goal'}>
|
||||||
<div className="goal-title">Goal: </div>
|
<div className="goal-title">{t("Goal")}:</div>
|
||||||
<LocationsContext.Provider value={goalLocs}>
|
<LocationsContext.Provider value={goalLocs}>
|
||||||
<InteractiveCode fmt={goal.type} />
|
<InteractiveCode fmt={goal.type} />
|
||||||
</LocationsContext.Provider>
|
</LocationsContext.Provider>
|
||||||
@@ -163,24 +167,23 @@ export const Goal = React.memo((props: GoalProps) => {
|
|||||||
// const hints = <Hints hints={goal.hints} key={goal.mvarId} />
|
// const hints = <Hints hints={goal.hints} key={goal.mvarId} />
|
||||||
const objectHyps = hyps.filter(hyp => !hyp.isAssumption)
|
const objectHyps = hyps.filter(hyp => !hyp.isAssumption)
|
||||||
const assumptionHyps = hyps.filter(hyp => hyp.isAssumption)
|
const assumptionHyps = hyps.filter(hyp => hyp.isAssumption)
|
||||||
const {typewriterMode} = React.useContext(InputModeContext)
|
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
{/* {goal.userName && <div><strong className="goal-case">case </strong>{goal.userName}</div>} */}
|
{/* {goal.userName && <div><strong className="goal-case">case </strong>{goal.userName}</div>} */}
|
||||||
{filter.reverse && goalLi}
|
{filter.reverse && goalLi}
|
||||||
{! typewriter && objectHyps.length > 0 &&
|
{! typewriter && objectHyps.length > 0 &&
|
||||||
<div className="hyp-group"><div className="hyp-group-title">Objects:</div>
|
<div className="hyp-group"><div className="hyp-group-title">{t("Objects")}:</div>
|
||||||
{objectHyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)}</div> }
|
{objectHyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)}</div> }
|
||||||
{!typewriter && assumptionHyps.length > 0 &&
|
{!typewriter && assumptionHyps.length > 0 &&
|
||||||
<div className="hyp-group"><div className="hyp-group-title">Assumptions:</div>
|
<div className="hyp-group"><div className="hyp-group-title">{t("Assumptions")}:</div>
|
||||||
{assumptionHyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)}</div> }
|
{assumptionHyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)}</div> }
|
||||||
{/* {typewriter && typewriterMode && <Typewriter />} */}
|
|
||||||
{!filter.reverse && goalLi}
|
{!filter.reverse && goalLi}
|
||||||
{/* {showHints && hints} */}
|
{/* {showHints && hints} */}
|
||||||
</div>
|
</div>
|
||||||
})
|
})
|
||||||
|
|
||||||
export const MainAssumptions = React.memo((props: GoalProps2) => {
|
export const MainAssumptions = React.memo((props: GoalProps2) => {
|
||||||
|
let { t } = useTranslation()
|
||||||
const { goals, filter } = props
|
const { goals, filter } = props
|
||||||
|
|
||||||
const goal = goals[0]
|
const goal = goals[0]
|
||||||
@@ -195,7 +198,7 @@ export const MainAssumptions = React.memo((props: GoalProps2) => {
|
|||||||
[locs, goal.mvarId])
|
[locs, goal.mvarId])
|
||||||
|
|
||||||
const goalLi = <div key={'goal'}>
|
const goalLi = <div key={'goal'}>
|
||||||
<div className="goal-title">Goal: </div>
|
<div className="goal-title">{t("Goal") + ":"}</div>
|
||||||
<LocationsContext.Provider value={goalLocs}>
|
<LocationsContext.Provider value={goalLocs}>
|
||||||
<InteractiveCode fmt={goal.type} />
|
<InteractiveCode fmt={goal.type} />
|
||||||
</LocationsContext.Provider>
|
</LocationsContext.Provider>
|
||||||
@@ -205,25 +208,26 @@ export const MainAssumptions = React.memo((props: GoalProps2) => {
|
|||||||
const assumptionHyps = hyps.filter(hyp => hyp.isAssumption)
|
const assumptionHyps = hyps.filter(hyp => hyp.isAssumption)
|
||||||
|
|
||||||
return <div id="main-assumptions">
|
return <div id="main-assumptions">
|
||||||
<div className="goals-section-title">Current Goal</div>
|
<div className="goals-section-title">{t("Current Goal")}</div>
|
||||||
{filter.reverse && goalLi}
|
{filter.reverse && goalLi}
|
||||||
{ objectHyps.length > 0 &&
|
{ objectHyps.length > 0 &&
|
||||||
<div className="hyp-group"><div className="hyp-group-title">Objects:</div>
|
<div className="hyp-group"><div className="hyp-group-title">{t("Objects") + ":"}</div>
|
||||||
{objectHyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)}</div> }
|
{objectHyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)}</div> }
|
||||||
{ assumptionHyps.length > 0 &&
|
{ assumptionHyps.length > 0 &&
|
||||||
<div className="hyp-group">
|
<div className="hyp-group">
|
||||||
<div className="hyp-group-title">Assumptions:</div>
|
<div className="hyp-group-title">{t("Assumptions") + ":"}</div>
|
||||||
{assumptionHyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)}
|
{assumptionHyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)}
|
||||||
</div> }
|
</div> }
|
||||||
</div>
|
</div>
|
||||||
})
|
})
|
||||||
|
|
||||||
export const OtherGoals = React.memo((props: GoalProps2) => {
|
export const OtherGoals = React.memo((props: GoalProps2) => {
|
||||||
|
let { t } = useTranslation()
|
||||||
const { goals, filter } = props
|
const { goals, filter } = props
|
||||||
return <>
|
return <>
|
||||||
{goals && goals.length > 1 &&
|
{goals && goals.length > 1 &&
|
||||||
<div id="other-goals" className="other-goals">
|
<div id="other-goals" className="other-goals">
|
||||||
<div className="goals-section-title">Further Goals</div>
|
<div className="goals-section-title">{t("Further Goals")}</div>
|
||||||
{goals.slice(1).map((goal, i) =>
|
{goals.slice(1).map((goal, i) =>
|
||||||
<details key={i}>
|
<details key={i}>
|
||||||
<summary>
|
<summary>
|
||||||
@@ -235,36 +239,17 @@ export const OtherGoals = React.memo((props: GoalProps2) => {
|
|||||||
</>
|
</>
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: deprecated
|
|
||||||
export const ProofDisplay = React.memo((props : ProofDisplayProps) => {
|
|
||||||
const { proof } = props
|
|
||||||
const steps = proof.match(/.+/g)
|
|
||||||
return <>
|
|
||||||
{ steps &&
|
|
||||||
<div id="current-proof">
|
|
||||||
<div className="goals-section-title">Proof history</div>
|
|
||||||
<div className="proof-display-wrapper">
|
|
||||||
<div className="proof-display">
|
|
||||||
{steps.map((s) =>
|
|
||||||
<div>{s}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>}
|
|
||||||
</>
|
|
||||||
})
|
|
||||||
|
|
||||||
interface GoalsProps {
|
interface GoalsProps {
|
||||||
goals: InteractiveGoals
|
goals: InteractiveGoalsWithHints
|
||||||
filter: GoalFilterState
|
filter: GoalFilterState
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Goals({ goals, filter }: GoalsProps) {
|
export function Goals({ goals, filter }: GoalsProps) {
|
||||||
if (goals.goals.length === 0) {
|
if (goals.goals.length === 0) {
|
||||||
return <>No goals</>
|
return <></>
|
||||||
} else {
|
} else {
|
||||||
return <>
|
return <>
|
||||||
{goals.goals.map((g, i) => <Goal typewriter={false} key={i} goal={g} filter={filter} />)}
|
{goals.goals.map((g, i) => <Goal typewriter={false} key={i} goal={g.goal} filter={filter} />)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -276,7 +261,7 @@ interface FilteredGoalsProps {
|
|||||||
* When this is `undefined`, the component will not appear at all but will remember its state
|
* When this is `undefined`, the component will not appear at all but will remember its state
|
||||||
* by virtue of still being mounted in the React tree. When it does appear again, the filter
|
* by virtue of still being mounted in the React tree. When it does appear again, the filter
|
||||||
* settings and collapsed state will be as before. */
|
* settings and collapsed state will be as before. */
|
||||||
goals?: InteractiveGoals
|
goals?: InteractiveGoalsWithHints
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -291,7 +276,7 @@ export const FilteredGoals = React.memo(({ headerChildren, goals }: FilteredGoal
|
|||||||
data-id="copy-goal-to-comment"
|
data-id="copy-goal-to-comment"
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (goals) void ec.copyToComment(goalsToString(goals))
|
if (goals) void ec.copyToComment(goalsWithHintsToString(goals))
|
||||||
}}
|
}}
|
||||||
title="copy state to comment" />
|
title="copy state to comment" />
|
||||||
|
|
||||||
@@ -336,3 +321,45 @@ export const FilteredGoals = React.memo(({ headerChildren, goals }: FilteredGoal
|
|||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export function loadGoals(
|
||||||
|
rpcSess: RpcSessionAtPos,
|
||||||
|
uri: string,
|
||||||
|
setProof: React.Dispatch<React.SetStateAction<ProofState>>,
|
||||||
|
setCrashed: React.Dispatch<React.SetStateAction<Boolean>>) {
|
||||||
|
console.info('sending rpc request to load the proof state')
|
||||||
|
|
||||||
|
rpcSess.call('Game.getProofState', DocumentPosition.toTdpp({line: 0, character: 0, uri: uri})).then(
|
||||||
|
(proof : ProofState) => {
|
||||||
|
if (typeof proof !== 'undefined') {
|
||||||
|
console.info(`received a proof state!`)
|
||||||
|
console.log(proof)
|
||||||
|
setProof(proof)
|
||||||
|
setCrashed(false)
|
||||||
|
} else {
|
||||||
|
console.warn('received undefined proof state!')
|
||||||
|
setCrashed(true)
|
||||||
|
// setProof(undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).catch((error) => {
|
||||||
|
setCrashed(true)
|
||||||
|
console.warn(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function lastStepHasErrors (proof : ProofState): boolean {
|
||||||
|
if (!proof?.steps.length) {return false}
|
||||||
|
|
||||||
|
let diags = [...proof.steps[proof.steps.length - 1].diags, ...proof.diagnostics]
|
||||||
|
|
||||||
|
return diags.some(
|
||||||
|
(d) => (d.severity == DiagnosticSeverity.Error ) // || d.severity == DiagnosticSeverity.Warning
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLastStepWithErrors (proof : ProofState, i: number): boolean {
|
||||||
|
if (!proof?.steps.length) {return false}
|
||||||
|
return (i == proof.steps.length - 1) && lastStepHasErrors(proof)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as React from 'react'
|
|||||||
import { CircularProgress } from '@mui/material'
|
import { CircularProgress } from '@mui/material'
|
||||||
import type { Location, Diagnostic } from 'vscode-languageserver-protocol'
|
import type { Location, Diagnostic } from 'vscode-languageserver-protocol'
|
||||||
import { getInteractiveTermGoal, InteractiveDiagnostic, UserWidgetInstance, Widget_getWidgets, RpcSessionAtPos, isRpcError,
|
import { getInteractiveTermGoal, InteractiveDiagnostic, UserWidgetInstance, Widget_getWidgets, RpcSessionAtPos, isRpcError,
|
||||||
RpcErrorCode, getInteractiveDiagnostics, InteractiveTermGoal } from '@leanprover/infoview-api'
|
RpcErrorCode, getInteractiveDiagnostics } from '@leanprover/infoview-api'
|
||||||
import { basename, DocumentPosition, RangeHelpers, useEvent, usePausableState, discardMethodNotFound,
|
import { basename, DocumentPosition, RangeHelpers, useEvent, usePausableState, discardMethodNotFound,
|
||||||
mapRpcError, useAsyncWithTrigger, PausableProps } from '../../../../node_modules/lean4-infoview/src/infoview/util'
|
mapRpcError, useAsyncWithTrigger, PausableProps } from '../../../../node_modules/lean4-infoview/src/infoview/util'
|
||||||
import { ConfigContext, EditorContext, LspDiagnosticsContext, ProgressContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts'
|
import { ConfigContext, EditorContext, LspDiagnosticsContext, ProgressContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts'
|
||||||
@@ -13,9 +13,10 @@ import { RpcContext, useRpcSessionAtPos } from '../../../../node_modules/lean4-i
|
|||||||
import { GoalsLocation, Locations, LocationsContext } from '../../../../node_modules/lean4-infoview/src/infoview/goalLocation'
|
import { GoalsLocation, Locations, LocationsContext } from '../../../../node_modules/lean4-infoview/src/infoview/goalLocation'
|
||||||
|
|
||||||
import { AllMessages, lspDiagToInteractive } from './messages'
|
import { AllMessages, lspDiagToInteractive } from './messages'
|
||||||
import { goalsToString, Goal, MainAssumptions, OtherGoals, ProofDisplay } from './goals'
|
import { goalsToString, Goal, MainAssumptions, OtherGoals } from './goals'
|
||||||
import { InteractiveGoals } from './rpc_api'
|
import { InteractiveTermGoal, InteractiveGoalsWithHints, InteractiveGoals, ProofState } from './rpc_api'
|
||||||
import { MonacoEditorContext, ProofStateProps, InfoStatus, ProofContext } from './context'
|
import { MonacoEditorContext, ProofStateProps, InfoStatus, ProofContext } from './context'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
// TODO: All about pinning could probably be removed
|
// TODO: All about pinning could probably be removed
|
||||||
type InfoKind = 'cursor' | 'pin'
|
type InfoKind = 'cursor' | 'pin'
|
||||||
@@ -83,11 +84,12 @@ interface InfoDisplayContentProps extends PausableProps {
|
|||||||
error?: string
|
error?: string
|
||||||
userWidgets: UserWidgetInstance[]
|
userWidgets: UserWidgetInstance[]
|
||||||
triggerUpdate: () => Promise<void>
|
triggerUpdate: () => Promise<void>
|
||||||
proof? : string
|
proofString? : string
|
||||||
}
|
}
|
||||||
|
|
||||||
const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => {
|
const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => {
|
||||||
const {pos, messages, goals, termGoal, error, userWidgets, triggerUpdate, isPaused, setPaused, proof} = props
|
let { t } = useTranslation()
|
||||||
|
const {pos, messages, goals, termGoal, error, userWidgets, triggerUpdate, isPaused, setPaused, proofString} = props
|
||||||
|
|
||||||
const hasWidget = userWidgets.length > 0
|
const hasWidget = userWidgets.length > 0
|
||||||
const hasError = !!error
|
const hasError = !!error
|
||||||
@@ -114,6 +116,7 @@ const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => {
|
|||||||
|
|
||||||
const goalFilter = { reverse: false, showType: true, showInstance: true, showHiddenAssumption: true, showLetValue: true }
|
const goalFilter = { reverse: false, showType: true, showInstance: true, showHiddenAssumption: true, showLetValue: true }
|
||||||
/* Adding {' '} to manage string literals properly: https://reactjs.org/docs/jsx-in-depth.html#string-literals-1 */
|
/* Adding {' '} to manage string literals properly: https://reactjs.org/docs/jsx-in-depth.html#string-literals-1 */
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
{hasError &&
|
{hasError &&
|
||||||
<div className='error' key='errors'>
|
<div className='error' key='errors'>
|
||||||
@@ -130,14 +133,14 @@ const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => {
|
|||||||
<div>
|
<div>
|
||||||
{ goals && (goals.goals.length > 0
|
{ goals && (goals.goals.length > 0
|
||||||
? <Goal typewriter={true} filter={goalFilter} key='mainGoal' goal={goals.goals[0]} showHints={true} />
|
? <Goal typewriter={true} filter={goalFilter} key='mainGoal' goal={goals.goals[0]} showHints={true} />
|
||||||
: <div className="goals-section-title">No Goals</div>
|
: <div className="goals-section-title">{t("No Goals")}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</LocationsContext.Provider>
|
</LocationsContext.Provider>
|
||||||
{userWidgets.map(widget =>
|
{userWidgets.map(widget =>
|
||||||
<details key={`widget::${widget.id}::${widget.range?.toString()}`} open>
|
<details key={`widget::${widget.id}::${widget.range?.toString()}`} open>
|
||||||
<summary className='mv2 pointer'>{widget.name}</summary>
|
<summary className='mv2 pointer'>{widget.name}</summary>
|
||||||
<PanelWidgetDisplay pos={pos} goals={goals ? goals.goals.map (goal => goal) : []}
|
<PanelWidgetDisplay pos={pos} goals={goals ? goals.goals : []}
|
||||||
termGoal={termGoal} selectedLocations={selectedLocs} widget={widget}/>
|
termGoal={termGoal} selectedLocations={selectedLocs} widget={widget}/>
|
||||||
</details>
|
</details>
|
||||||
)}
|
)}
|
||||||
@@ -149,7 +152,7 @@ const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => {
|
|||||||
{' '}or <a className='link pointer dim' onClick={e => { e.preventDefault(); setPaused(false) }}>resume updating</a>
|
{' '}or <a className='link pointer dim' onClick={e => { e.preventDefault(); setPaused(false) }}>resume updating</a>
|
||||||
{' '}to see information.
|
{' '}to see information.
|
||||||
</span> :
|
</span> :
|
||||||
<><CircularProgress /><div>Loading goal...</div></>)}
|
<><CircularProgress /><div>{t("Loading goal…")}</div></>)}
|
||||||
<AllMessages />
|
<AllMessages />
|
||||||
{/* <LocationsContext.Provider value={locs}>
|
{/* <LocationsContext.Provider value={locs}>
|
||||||
{goals && goals.goals.length > 1 && <div className="goals-section other-goals">
|
{goals && goals.goals.length > 1 && <div className="goals-section other-goals">
|
||||||
@@ -166,6 +169,7 @@ interface InfoDisplayProps {
|
|||||||
pos: DocumentPosition,
|
pos: DocumentPosition,
|
||||||
status: InfoStatus,
|
status: InfoStatus,
|
||||||
messages: InteractiveDiagnostic[],
|
messages: InteractiveDiagnostic[],
|
||||||
|
proof?: ProofState,
|
||||||
goals?: InteractiveGoals,
|
goals?: InteractiveGoals,
|
||||||
termGoal?: InteractiveTermGoal,
|
termGoal?: InteractiveTermGoal,
|
||||||
error?: string,
|
error?: string,
|
||||||
@@ -175,7 +179,7 @@ interface InfoDisplayProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Displays goal state and messages. Can be paused. */
|
/** Displays goal state and messages. Can be paused. */
|
||||||
function InfoDisplay(props0: ProofStateProps & InfoDisplayProps & InfoPinnable) {
|
function InfoDisplay(props0: InfoDisplayProps & InfoPinnable) {
|
||||||
// Used to update the paused state *just once* if it is paused,
|
// Used to update the paused state *just once* if it is paused,
|
||||||
// but a display update is triggered
|
// but a display update is triggered
|
||||||
const [shouldRefresh, setShouldRefresh] = React.useState<boolean>(false)
|
const [shouldRefresh, setShouldRefresh] = React.useState<boolean>(false)
|
||||||
@@ -214,7 +218,7 @@ function InfoDisplay(props0: ProofStateProps & InfoDisplayProps & InfoPinnable)
|
|||||||
{/* <details open> */}
|
{/* <details open> */}
|
||||||
{/* <InfoStatusBar {...props} triggerUpdate={triggerDisplayUpdate} isPaused={isPaused} setPaused={setPaused} /> */}
|
{/* <InfoStatusBar {...props} triggerUpdate={triggerDisplayUpdate} isPaused={isPaused} setPaused={setPaused} /> */}
|
||||||
<div className="vscode-light">
|
<div className="vscode-light">
|
||||||
<InfoDisplayContent {...props} proof={editor.getValue()} triggerUpdate={triggerDisplayUpdate} isPaused={isPaused} setPaused={setPaused} />
|
<InfoDisplayContent {...props} proofString={editor.getValue()} triggerUpdate={triggerDisplayUpdate} isPaused={isPaused} setPaused={setPaused} />
|
||||||
</div>
|
</div>
|
||||||
{/* </details> */}
|
{/* </details> */}
|
||||||
</RpcContext.Provider>
|
</RpcContext.Provider>
|
||||||
@@ -252,7 +256,7 @@ function useIsProcessingAt(p: DocumentPosition): boolean {
|
|||||||
|
|
||||||
function InfoAux(props: InfoProps) {
|
function InfoAux(props: InfoProps) {
|
||||||
|
|
||||||
const proofContext = React.useContext(ProofContext)
|
const { setProof } = React.useContext(ProofContext)
|
||||||
|
|
||||||
const config = React.useContext(ConfigContext)
|
const config = React.useContext(ConfigContext)
|
||||||
|
|
||||||
@@ -290,6 +294,10 @@ function InfoAux(props: InfoProps) {
|
|||||||
// with e.g. a new `pos`.
|
// with e.g. a new `pos`.
|
||||||
type InfoRequestResult = Omit<InfoDisplayProps, 'triggerUpdate'>
|
type InfoRequestResult = Omit<InfoDisplayProps, 'triggerUpdate'>
|
||||||
const [state, triggerUpdateCore] = useAsyncWithTrigger(() => new Promise<InfoRequestResult>((resolve, reject) => {
|
const [state, triggerUpdateCore] = useAsyncWithTrigger(() => new Promise<InfoRequestResult>((resolve, reject) => {
|
||||||
|
|
||||||
|
const proofReq = rpcSess.call('Game.getProofState', DocumentPosition.toTdpp(pos)).catch((error) => {
|
||||||
|
console.warn(error)
|
||||||
|
})
|
||||||
const goalsReq = rpcSess.call('Game.getInteractiveGoals', DocumentPosition.toTdpp(pos))
|
const goalsReq = rpcSess.call('Game.getInteractiveGoals', DocumentPosition.toTdpp(pos))
|
||||||
const termGoalReq = getInteractiveTermGoal(rpcSess, DocumentPosition.toTdpp(pos))
|
const termGoalReq = getInteractiveTermGoal(rpcSess, DocumentPosition.toTdpp(pos))
|
||||||
const widgetsReq = Widget_getWidgets(rpcSess, pos).catch(discardMethodNotFound)
|
const widgetsReq = Widget_getWidgets(rpcSess, pos).catch(discardMethodNotFound)
|
||||||
@@ -308,6 +316,7 @@ function InfoAux(props: InfoProps) {
|
|||||||
pos,
|
pos,
|
||||||
status: 'updating',
|
status: 'updating',
|
||||||
messages: lspDiagsHere.map(lspDiagToInteractive),
|
messages: lspDiagsHere.map(lspDiagToInteractive),
|
||||||
|
proof: undefined,
|
||||||
goals: undefined,
|
goals: undefined,
|
||||||
termGoal: undefined,
|
termGoal: undefined,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
@@ -318,11 +327,12 @@ function InfoAux(props: InfoProps) {
|
|||||||
|
|
||||||
// NB: it is important to await await reqs at once, otherwise
|
// NB: it is important to await await reqs at once, otherwise
|
||||||
// if both throw then one exception becomes unhandled.
|
// if both throw then one exception becomes unhandled.
|
||||||
Promise.all([goalsReq, termGoalReq, widgetsReq, messagesReq]).then(
|
Promise.all([proofReq, goalsReq, termGoalReq, widgetsReq, messagesReq]).then(
|
||||||
([goals, termGoal, userWidgets, messages]) => resolve({
|
([proof, goals, termGoal, userWidgets, messages]) => resolve({
|
||||||
pos,
|
pos,
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
messages,
|
messages,
|
||||||
|
proof : proof as any,
|
||||||
goals: goals as any,
|
goals: goals as any,
|
||||||
termGoal,
|
termGoal,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
@@ -353,6 +363,7 @@ function InfoAux(props: InfoProps) {
|
|||||||
pos,
|
pos,
|
||||||
status: 'error',
|
status: 'error',
|
||||||
messages: lspDiagsHere.map(lspDiagToInteractive),
|
messages: lspDiagsHere.map(lspDiagToInteractive),
|
||||||
|
proof: undefined,
|
||||||
goals: undefined,
|
goals: undefined,
|
||||||
termGoal: undefined,
|
termGoal: undefined,
|
||||||
error: `Error fetching goals: ${errorString}`,
|
error: `Error fetching goals: ${errorString}`,
|
||||||
@@ -389,6 +400,7 @@ function InfoAux(props: InfoProps) {
|
|||||||
pos,
|
pos,
|
||||||
status: 'updating',
|
status: 'updating',
|
||||||
messages: [],
|
messages: [],
|
||||||
|
proof: undefined,
|
||||||
goals: undefined,
|
goals: undefined,
|
||||||
termGoal: undefined,
|
termGoal: undefined,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
@@ -412,6 +424,11 @@ function InfoAux(props: InfoProps) {
|
|||||||
// hintContext.setHints(state.value.goals.goals[0].hints)
|
// hintContext.setHints(state.value.goals.goals[0].hints)
|
||||||
// }
|
// }
|
||||||
setDisplayProps({ ...state.value, triggerUpdate })
|
setDisplayProps({ ...state.value, triggerUpdate })
|
||||||
|
|
||||||
|
// Update the game's proof state
|
||||||
|
console.info('updating proof from editor mode.')
|
||||||
|
setProof(state.value.proof)
|
||||||
|
|
||||||
} else if (state.state === 'rejected' && state.error !== 'retry') {
|
} else if (state.state === 'rejected' && state.error !== 'retry') {
|
||||||
// The code inside `useAsyncWithTrigger` may only ever reject with a `retry` exception.
|
// The code inside `useAsyncWithTrigger` may only ever reject with a `retry` exception.
|
||||||
console.warn('Unreachable code reached with error: ', state.error)
|
console.warn('Unreachable code reached with error: ', state.error)
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import { DidChangeTextDocumentParams, DidCloseTextDocumentParams, TextDocumentCo
|
|||||||
import { EditorContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts';
|
import { EditorContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts';
|
||||||
import { DocumentPosition, Keyed, PositionHelpers, useClientNotificationEffect, useClientNotificationState, useEvent, useEventResult } from '../../../../node_modules/lean4-infoview/src/infoview/util';
|
import { DocumentPosition, Keyed, PositionHelpers, useClientNotificationEffect, useClientNotificationState, useEvent, useEventResult } from '../../../../node_modules/lean4-infoview/src/infoview/util';
|
||||||
import { Info, InfoProps } from './info';
|
import { Info, InfoProps } from './info';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
/** Manages and displays pinned infos, as well as info for the current location. */
|
/** Manages and displays pinned infos, as well as info for the current location. */
|
||||||
export function Infos() {
|
export function Infos() {
|
||||||
|
let { t } = useTranslation()
|
||||||
const ec = React.useContext(EditorContext);
|
const ec = React.useContext(EditorContext);
|
||||||
|
|
||||||
// Update pins when the document changes. In particular, when edits are made
|
// Update pins when the document changes. In particular, when edits are made
|
||||||
@@ -126,6 +128,6 @@ export function Infos() {
|
|||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
{infoProps.map (ps => <Info {...ps} />)}
|
{infoProps.map (ps => <Info {...ps} />)}
|
||||||
{!curPos && <p>Click somewhere in the Lean file to enable the infoview.</p> }
|
{!curPos && <p>{t("Click somewhere in the Lean file to enable the infoview.")}</p> }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,30 +20,35 @@ import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
|
|||||||
|
|
||||||
import { GameIdContext } from '../../app';
|
import { GameIdContext } from '../../app';
|
||||||
import { useAppDispatch, useAppSelector } from '../../hooks';
|
import { useAppDispatch, useAppSelector } from '../../hooks';
|
||||||
import { LevelInfo } from '../../state/api';
|
import { LevelInfo, useGetGameInfoQuery } from '../../state/api';
|
||||||
import { changedInventory, levelCompleted, selectCode, selectCompleted, selectInventory } from '../../state/progress';
|
import { changedInventory, levelCompleted, selectCode, selectCompleted, selectInventory } from '../../state/progress';
|
||||||
import Markdown from '../markdown';
|
import Markdown from '../markdown';
|
||||||
|
|
||||||
import { Infos } from './infos';
|
import { Infos } from './infos';
|
||||||
import { AllMessages, Errors, WithLspDiagnosticsContext } from './messages';
|
import { AllMessages, Errors, WithLspDiagnosticsContext } from './messages';
|
||||||
import { Goal } from './goals';
|
import { Goal, isLastStepWithErrors, lastStepHasErrors, loadGoals } from './goals';
|
||||||
import { DeletedChatContext, InputModeContext, MobileContext, MonacoEditorContext, ProofContext, ProofStep, SelectionContext, WorldLevelIdContext } from './context';
|
import { DeletedChatContext, InputModeContext, PreferencesContext, MonacoEditorContext, ProofContext, SelectionContext, WorldLevelIdContext } from './context';
|
||||||
import { Typewriter, hasErrors, hasInteractiveErrors } from './typewriter';
|
import { Typewriter, getInteractiveDiagsAt, hasErrors, hasInteractiveErrors } from './typewriter';
|
||||||
import { InteractiveDiagnostic } from '@leanprover/infoview/*';
|
import { InteractiveDiagnostic } from '@leanprover/infoview/*';
|
||||||
import { Button } from '../button';
|
import { Button } from '../button';
|
||||||
import { CircularProgress } from '@mui/material';
|
import { CircularProgress } from '@mui/material';
|
||||||
import { GameHint } from './rpc_api';
|
import { GameHint, InteractiveGoalsWithHints, ProofState } from './rpc_api';
|
||||||
import { store } from '../../state/store';
|
import { store } from '../../state/store';
|
||||||
import { Hints } from '../hints';
|
import { Hints, MoreHelpButton, filterHints } from '../hints';
|
||||||
|
import { DocumentPosition } from '../../../../node_modules/lean4-infoview/src/infoview/util';
|
||||||
|
import { DiagnosticSeverity } from 'vscode-languageclient';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
|
||||||
/** Wrapper for the two editors. It is important that the `div` with `codeViewRef` is
|
/** Wrapper for the two editors. It is important that the `div` with `codeViewRef` is
|
||||||
* always present, or the monaco editor cannot start.
|
* always present, or the monaco editor cannot start.
|
||||||
*/
|
*/
|
||||||
export function DualEditor({ level, codeviewRef, levelId, worldId, worldSize }) {
|
export function DualEditor({ level, codeviewRef, levelId, worldId, worldSize }) {
|
||||||
const ec = React.useContext(EditorContext)
|
const ec = React.useContext(EditorContext)
|
||||||
const { typewriterMode } = React.useContext(InputModeContext)
|
const { typewriterMode, lockEditorMode } = React.useContext(InputModeContext)
|
||||||
return <>
|
return <>
|
||||||
<div className={typewriterMode ? 'hidden' : ''}>
|
<div className={(typewriterMode && !lockEditorMode) ? 'hidden' : ''}>
|
||||||
<ExerciseStatement data={level} showLeanStatement={true} />
|
<ExerciseStatement data={level} showLeanStatement={true} />
|
||||||
<div ref={codeviewRef} className={'codeview'}></div>
|
<div ref={codeviewRef} className={'codeview'}></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,15 +64,14 @@ export function DualEditor({ level, codeviewRef, levelId, worldId, worldSize })
|
|||||||
function DualEditorMain({ worldId, levelId, level, worldSize }: { worldId: string, levelId: number, level: LevelInfo, worldSize: number }) {
|
function DualEditorMain({ worldId, levelId, level, worldSize }: { worldId: string, levelId: number, level: LevelInfo, worldSize: number }) {
|
||||||
const ec = React.useContext(EditorContext)
|
const ec = React.useContext(EditorContext)
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const { typewriterMode } = React.useContext(InputModeContext)
|
const { typewriterMode, lockEditorMode } = React.useContext(InputModeContext)
|
||||||
|
|
||||||
|
const {proof, setProof} = React.useContext(ProofContext)
|
||||||
|
|
||||||
// Mark level as completed when server gives notification
|
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
useServerNotificationEffect(
|
|
||||||
'$/game/completed',
|
React.useEffect(() => {
|
||||||
(params: any) => {
|
if (proof?.completed) {
|
||||||
if (ec.events.changedCursorLocation.current &&
|
|
||||||
ec.events.changedCursorLocation.current.uri === params.uri) {
|
|
||||||
dispatch(levelCompleted({ game: gameId, world: worldId, level: levelId }))
|
dispatch(levelCompleted({ game: gameId, world: worldId, level: levelId }))
|
||||||
|
|
||||||
// On completion, add the names of all new items to the local storage
|
// On completion, add the names of all new items to the local storage
|
||||||
@@ -88,9 +92,9 @@ function DualEditorMain({ worldId, levelId, level, worldSize }: { worldId: strin
|
|||||||
let newInv = [...inv, ...newTiles].filter((item, i, array) => array.indexOf(item) == i)
|
let newInv = [...inv, ...newTiles].filter((item, i, array) => array.indexOf(item) == i)
|
||||||
|
|
||||||
dispatch(changedInventory({ game: gameId, inventory: newInv }))
|
dispatch(changedInventory({ game: gameId, inventory: newInv }))
|
||||||
|
|
||||||
}
|
}
|
||||||
}, [level]
|
}, [proof, level])
|
||||||
)
|
|
||||||
|
|
||||||
/* Set up updates to the global infoview state on editor events. */
|
/* Set up updates to the global infoview state on editor events. */
|
||||||
const config = useEventResult(ec.events.changedInfoviewConfig) ?? defaultInfoviewConfig;
|
const config = useEventResult(ec.events.changedInfoviewConfig) ?? defaultInfoviewConfig;
|
||||||
@@ -110,7 +114,7 @@ function DualEditorMain({ worldId, levelId, level, worldSize }: { worldId: strin
|
|||||||
<WithRpcSessions>
|
<WithRpcSessions>
|
||||||
<WithLspDiagnosticsContext>
|
<WithLspDiagnosticsContext>
|
||||||
<ProgressContext.Provider value={allProgress}>
|
<ProgressContext.Provider value={allProgress}>
|
||||||
{typewriterMode ?
|
{(typewriterMode && !lockEditorMode) ?
|
||||||
<TypewriterInterfaceWrapper world={worldId} level={levelId} data={level} worldSize={worldSize}/>
|
<TypewriterInterfaceWrapper world={worldId} level={levelId} data={level} worldSize={worldSize}/>
|
||||||
:
|
:
|
||||||
<Main key={`${worldId}/${levelId}`} world={worldId} level={levelId} data={level} />
|
<Main key={`${worldId}/${levelId}`} world={worldId} level={levelId} data={level} />
|
||||||
@@ -132,12 +136,15 @@ function DualEditorMain({ worldId, levelId, level, worldSize }: { worldId: strin
|
|||||||
* If `showLeanStatement` is true, it will additionally display the lean code.
|
* If `showLeanStatement` is true, it will additionally display the lean code.
|
||||||
*/
|
*/
|
||||||
function ExerciseStatement({ data, showLeanStatement = false }) {
|
function ExerciseStatement({ data, showLeanStatement = false }) {
|
||||||
|
let { t } = useTranslation()
|
||||||
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
|
||||||
if (!(data?.descrText || data?.descrFormat)) { return <></> }
|
if (!(data?.descrText || data?.descrFormat)) { return <></> }
|
||||||
return <>
|
return <>
|
||||||
<div className="exercise-statement">
|
<div className="exercise-statement">
|
||||||
{data?.descrText &&
|
{data?.descrText &&
|
||||||
<Markdown>
|
<Markdown>
|
||||||
{(data?.displayName ? `**Theorem** \`${data?.displayName}\`: ` : '') + data?.descrText}
|
{(data?.displayName ? `**Theorem** \`${data?.displayName}\`: ` : '') + t(data?.descrText, {ns: gameId})}
|
||||||
</Markdown>
|
</Markdown>
|
||||||
}
|
}
|
||||||
{data?.descrFormat && showLeanStatement &&
|
{data?.descrFormat && showLeanStatement &&
|
||||||
@@ -150,13 +157,27 @@ function ExerciseStatement({ data, showLeanStatement = false }) {
|
|||||||
// TODO: This is only used in `EditorInterface`
|
// TODO: This is only used in `EditorInterface`
|
||||||
// while `TypewriterInterface` has this copy-pasted in.
|
// while `TypewriterInterface` has this copy-pasted in.
|
||||||
export function Main(props: { world: string, level: number, data: LevelInfo}) {
|
export function Main(props: { world: string, level: number, data: LevelInfo}) {
|
||||||
|
let { t } = useTranslation()
|
||||||
const ec = React.useContext(EditorContext);
|
const ec = React.useContext(EditorContext);
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
|
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
|
||||||
|
|
||||||
const completed = useAppSelector(selectCompleted(gameId, props.world, props.level))
|
const { proof, setProof } = React.useContext(ProofContext)
|
||||||
|
const {selectedStep, setSelectedStep} = React.useContext(SelectionContext)
|
||||||
|
const { setDeletedChat, showHelp, setShowHelp } = React.useContext(DeletedChatContext)
|
||||||
|
|
||||||
console.debug(`template: ${props.data.template}`)
|
|
||||||
|
function toggleSelection(line: number) {
|
||||||
|
return (ev) => {
|
||||||
|
console.debug('toggled selection')
|
||||||
|
if (selectedStep == line) {
|
||||||
|
setSelectedStep(undefined)
|
||||||
|
} else {
|
||||||
|
setSelectedStep(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.debug(`template: ${props.data?.template}`)
|
||||||
|
|
||||||
// React.useEffect (() => {
|
// React.useEffect (() => {
|
||||||
// if (props.data.template) {
|
// if (props.data.template) {
|
||||||
@@ -182,6 +203,19 @@ export function Main(props: { world: string, level: number, data: LevelInfo}) {
|
|||||||
|
|
||||||
const curUri = useEventResult(ec.events.changedCursorLocation, loc => loc?.uri);
|
const curUri = useEventResult(ec.events.changedCursorLocation, loc => loc?.uri);
|
||||||
|
|
||||||
|
const curPos: DocumentPosition | undefined =
|
||||||
|
useEventResult(ec.events.changedCursorLocation, loc => loc ? { uri: loc.uri, ...loc.range.start } : undefined)
|
||||||
|
|
||||||
|
// Effect when the cursor changes in the editor
|
||||||
|
React.useEffect(() => {
|
||||||
|
// TODO: this is a bit of a hack and will yield unexpected behaviour if lines
|
||||||
|
// are indented.
|
||||||
|
let newPos = curPos?.line + (curPos?.character == 0 ? 0 : 1)
|
||||||
|
|
||||||
|
// scroll the chat along
|
||||||
|
setSelectedStep(newPos)
|
||||||
|
}, [curPos])
|
||||||
|
|
||||||
useClientNotificationEffect(
|
useClientNotificationEffect(
|
||||||
'textDocument/didClose',
|
'textDocument/didClose',
|
||||||
(params: DidCloseTextDocumentParams) => {
|
(params: DidCloseTextDocumentParams) => {
|
||||||
@@ -201,13 +235,22 @@ export function Main(props: { world: string, level: number, data: LevelInfo}) {
|
|||||||
// that we want to persist.
|
// that we want to persist.
|
||||||
let ret
|
let ret
|
||||||
if (!serverVersion) {
|
if (!serverVersion) {
|
||||||
ret = <p>Waiting for Lean server to start...</p>
|
ret = <p>{t("Waiting for Lean server to start…")}</p>
|
||||||
} else if (serverStoppedResult) {
|
} else if (serverStoppedResult) {
|
||||||
ret = <div><p>{serverStoppedResult.message}</p><p className="error">{serverStoppedResult.reason}</p></div>
|
ret = <div><p>{serverStoppedResult.message}</p><p className="error">{serverStoppedResult.reason}</p></div>
|
||||||
} else {
|
} else {
|
||||||
ret = <div className="infoview vscode-light">
|
ret = <div className="infoview vscode-light">
|
||||||
{completed && <div className="level-completed">Level completed! 🎉</div>}
|
{proof?.completedWithWarnings &&
|
||||||
|
<div className="level-completed">
|
||||||
|
{proof?.completed ? t("Level completed! 🎉") : t("Level completed with warnings 🎭")}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<Infos />
|
<Infos />
|
||||||
|
<Hints hints={proof?.steps[curPos?.line]?.goals[0]?.hints}
|
||||||
|
showHidden={showHelp.has(curPos?.line)} step={curPos?.line}
|
||||||
|
selected={selectedStep} toggleSelection={toggleSelection(curPos?.line)}
|
||||||
|
lastLevel={curPos?.line == proof?.steps.length - 1}/>
|
||||||
|
<MoreHelpButton selected={curPos?.line}/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,15 +266,26 @@ const goalFilter = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The display of a single entered lean command */
|
/** The display of a single entered lean command */
|
||||||
function Command({ command, deleteProof }: { command: string, deleteProof: any }) {
|
function Command({ proof, i, deleteProof }: { proof: ProofState, i: number, deleteProof: any }) {
|
||||||
|
let {t} = useTranslation()
|
||||||
|
|
||||||
// The first step will always have an empty command
|
// The first step will always have an empty command
|
||||||
if (!command) { return <></> }
|
if (!proof?.steps[i]?.command) { return <></> }
|
||||||
|
|
||||||
|
if (isLastStepWithErrors(proof, i)) {
|
||||||
|
// If the last step has errors, we display the command in a different style
|
||||||
|
// indicating that it will be removed on the next try.
|
||||||
|
return <div className="failed-command">
|
||||||
|
<i>{t("Failed command")}</i>: {proof?.steps[i].command}
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
return <div className="command">
|
return <div className="command">
|
||||||
<div className="command-text">{command}</div>
|
<div className="command-text">{proof?.steps[i].command}</div>
|
||||||
<Button to="" className="undo-button btn btn-inverted" title="Retry proof from here" onClick={deleteProof}>
|
<Button to="" className="undo-button btn btn-inverted" title={t("Retry proof from here")} onClick={deleteProof}>
|
||||||
<FontAwesomeIcon icon={faDeleteLeft} /> Retry
|
<FontAwesomeIcon icon={faDeleteLeft} /> {t("Retry")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// const MessageView = React.memo(({uri, diag}: MessageViewProps) => {
|
// const MessageView = React.memo(({uri, diag}: MessageViewProps) => {
|
||||||
@@ -286,21 +340,25 @@ function Command({ command, deleteProof }: { command: string, deleteProof: any }
|
|||||||
// }, fastIsEqual)
|
// }, fastIsEqual)
|
||||||
|
|
||||||
/** The tabs of goals that lean ahs after the command of this step has been processed */
|
/** The tabs of goals that lean ahs after the command of this step has been processed */
|
||||||
function GoalsTabs({ proofStep, last, onClick, onGoalChange=(n)=>{}}: { proofStep: ProofStep, last : boolean, onClick? : any, onGoalChange?: (n?: number) => void }) {
|
function GoalsTabs({ proofStep, last, onClick, onGoalChange=(n)=>{}}: { proofStep: InteractiveGoalsWithHints, last : boolean, onClick? : any, onGoalChange?: (n?: number) => void }) {
|
||||||
|
let { t } = useTranslation()
|
||||||
const [selectedGoal, setSelectedGoal] = React.useState<number>(0)
|
const [selectedGoal, setSelectedGoal] = React.useState<number>(0)
|
||||||
|
|
||||||
|
if (proofStep.goals.length == 0) {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
|
||||||
return <div className="goal-tabs" onClick={onClick}>
|
return <div className="goal-tabs" onClick={onClick}>
|
||||||
<div className={`tab-bar ${last ? 'current' : ''}`}>
|
<div className={`tab-bar ${last ? 'current' : ''}`}>
|
||||||
{proofStep.goals.map((goal, i) => (
|
{proofStep.goals.map((goal, i) => (
|
||||||
// TODO: Should not use index as key.
|
// TODO: Should not use index as key.
|
||||||
<div key={`proof-goal-${i}`} className={`tab ${i == (selectedGoal) ? "active" : ""}`} onClick={(ev) => { onGoalChange(i); setSelectedGoal(i); ev.stopPropagation() }}>
|
<div key={`proof-goal-${i}`} className={`tab ${i == (selectedGoal) ? "active" : ""}`} onClick={(ev) => { onGoalChange(i); setSelectedGoal(i); ev.stopPropagation() }}>
|
||||||
{i ? `Goal ${i + 1}` : "Active Goal"}
|
{i ? t("Goal") + ` ${i + 1}` : t("Active Goal")}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="goal-tab vscode-light">
|
<div className="goal-tab vscode-light">
|
||||||
<Goal typewriter={false} filter={goalFilter} goal={proofStep.goals[selectedGoal]} />
|
<Goal typewriter={false} filter={goalFilter} goal={proofStep.goals[selectedGoal]?.goal} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -340,21 +398,27 @@ export function TypewriterInterfaceWrapper(props: { world: string, level: number
|
|||||||
|
|
||||||
/** The interface in command line mode */
|
/** The interface in command line mode */
|
||||||
export function TypewriterInterface({props}) {
|
export function TypewriterInterface({props}) {
|
||||||
|
let { t } = useTranslation()
|
||||||
const ec = React.useContext(EditorContext)
|
const ec = React.useContext(EditorContext)
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const editor = React.useContext(MonacoEditorContext)
|
const editor = React.useContext(MonacoEditorContext)
|
||||||
const model = editor.getModel()
|
const model = editor.getModel()
|
||||||
const uri = model.uri.toString()
|
const uri = model.uri.toString()
|
||||||
|
|
||||||
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||||
|
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
|
||||||
|
let image: string = gameInfo.data?.worlds.nodes[worldId].image
|
||||||
|
|
||||||
|
|
||||||
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(PreferencesContext)
|
||||||
const { proof } = React.useContext(ProofContext)
|
const { proof, setProof, crashed, setCrashed, interimDiags } = React.useContext(ProofContext)
|
||||||
const { setTypewriterInput } = React.useContext(InputModeContext)
|
const { setTypewriterInput } = React.useContext(InputModeContext)
|
||||||
const { selectedStep, setSelectedStep } = React.useContext(SelectionContext)
|
const { selectedStep, setSelectedStep } = React.useContext(SelectionContext)
|
||||||
|
|
||||||
const proofPanelRef = React.useRef<HTMLDivElement>(null)
|
const proofPanelRef = React.useRef<HTMLDivElement>(null)
|
||||||
const completed = useAppSelector(selectCompleted(gameId, props.world, props.level))
|
|
||||||
// const config = useEventResult(ec.events.changedInfoviewConfig) ?? defaultInfoviewConfig;
|
// const config = useEventResult(ec.events.changedInfoviewConfig) ?? defaultInfoviewConfig;
|
||||||
// const curUri = useEventResult(ec.events.changedCursorLocation, loc => loc?.uri);
|
// const curUri = useEventResult(ec.events.changedCursorLocation, loc => loc?.uri);
|
||||||
|
|
||||||
@@ -366,12 +430,17 @@ export function TypewriterInterface({props}) {
|
|||||||
function deleteProof(line: number) {
|
function deleteProof(line: number) {
|
||||||
return (ev) => {
|
return (ev) => {
|
||||||
let deletedChat: Array<GameHint> = []
|
let deletedChat: Array<GameHint> = []
|
||||||
proof.slice(line).map((step, i) => {
|
proof?.steps.slice(line).map((step, i) => {
|
||||||
|
let filteredHints = filterHints(step.goals[0]?.hints, proof?.steps[i-1]?.goals[0]?.hints)
|
||||||
|
|
||||||
// Only add these hidden hints to the deletion stack which were visible
|
// Only add these hidden hints to the deletion stack which were visible
|
||||||
deletedChat = [...deletedChat, ...step.hints.filter(hint => (!hint.hidden || showHelp.has(line + i)))]
|
deletedChat = [...deletedChat, ...filteredHints.filter(hint => (!hint.hidden || showHelp.has(line + i)))]
|
||||||
})
|
})
|
||||||
setDeletedChat(deletedChat)
|
setDeletedChat(deletedChat)
|
||||||
|
|
||||||
|
// delete showHelp for deleted steps
|
||||||
|
setShowHelp(new Set(Array.from(showHelp).filter(i => i < line - 1)))
|
||||||
|
|
||||||
editor.executeEdits("typewriter", [{
|
editor.executeEdits("typewriter", [{
|
||||||
range: monaco.Selection.fromPositions(
|
range: monaco.Selection.fromPositions(
|
||||||
{ lineNumber: line, column: 1 },
|
{ lineNumber: line, column: 1 },
|
||||||
@@ -381,7 +450,9 @@ export function TypewriterInterface({props}) {
|
|||||||
forceMoveMarkers: false
|
forceMoveMarkers: false
|
||||||
}])
|
}])
|
||||||
setSelectedStep(undefined)
|
setSelectedStep(undefined)
|
||||||
setTypewriterInput(proof[line].command)
|
setTypewriterInput(proof?.steps[line].command)
|
||||||
|
// Reload proof on deleting
|
||||||
|
loadGoals(rpcSess, uri, setProof, setCrashed)
|
||||||
ev.stopPropagation()
|
ev.stopPropagation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -401,7 +472,7 @@ export function TypewriterInterface({props}) {
|
|||||||
|
|
||||||
// Scroll to the end of the proof if it is updated.
|
// Scroll to the end of the proof if it is updated.
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (proof?.length > 1) {
|
if (proof?.steps.length > 1) {
|
||||||
proofPanelRef.current?.lastElementChild?.scrollIntoView() //scrollTo(0,0)
|
proofPanelRef.current?.lastElementChild?.scrollIntoView() //scrollTo(0,0)
|
||||||
} else {
|
} else {
|
||||||
proofPanelRef.current?.scrollTo(0,0)
|
proofPanelRef.current?.scrollTo(0,0)
|
||||||
@@ -422,81 +493,94 @@ export function TypewriterInterface({props}) {
|
|||||||
}
|
}
|
||||||
}, [selectedStep])
|
}, [selectedStep])
|
||||||
|
|
||||||
// TODO: This about hidden hints is all copied from `level.tsx`. Can we move that into `hints.tsx`?
|
// TODO: superfluous, can be replaced with `withErr` from above
|
||||||
|
let lastStepErrors = proof?.steps.length ? hasInteractiveErrors(getInteractiveDiagsAt(proof, proof?.steps.length)) : false
|
||||||
|
|
||||||
// If the last step has errors, we want to treat it as if it is part of the second-to-last step
|
|
||||||
let k = proof.length - 1
|
|
||||||
let withErr = hasInteractiveErrors(proof[k]?.errors) ? 1 : 0
|
|
||||||
|
|
||||||
const activateHiddenHints = (ev) => {
|
useServerNotificationEffect("$/game/loading", (params : any) => {
|
||||||
// If the last step (`k`) has errors, we want the hidden hints from the
|
if (params.kind == "loadConstants") {
|
||||||
// second-to-last step to be affected
|
setLoadingProgress(params.counter/100*50)
|
||||||
if (!(proof.length)) {return}
|
} else if (params.kind == "finalizeExtensions") {
|
||||||
|
setLoadingProgress(50 + params.counter/150*50)
|
||||||
// state must not be mutated, therefore we need to clone the set
|
|
||||||
let tmp = new Set(showHelp)
|
|
||||||
if (tmp.has(k - withErr)) {
|
|
||||||
tmp.delete(k - withErr)
|
|
||||||
} else {
|
} else {
|
||||||
tmp.add(k - withErr)
|
console.error(`Unknown loading kind: ${params.kind}`)
|
||||||
}
|
}
|
||||||
setShowHelp(tmp)
|
})
|
||||||
console.debug(`help: ${Array.from(tmp.values())}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasHiddenHints(i : number): boolean {
|
|
||||||
let step = proof[i]
|
|
||||||
|
|
||||||
// For example if the proof isn't loaded yet
|
|
||||||
if(!step) {return false}
|
|
||||||
|
|
||||||
return step.hints.some((hint) => hint.hidden)
|
|
||||||
}
|
|
||||||
|
|
||||||
let lastStepErrors = proof.length ? hasInteractiveErrors(proof[proof.length - 1].errors) : false
|
|
||||||
|
|
||||||
return <div className="typewriter-interface">
|
return <div className="typewriter-interface">
|
||||||
<RpcContext.Provider value={rpcSess}>
|
<RpcContext.Provider value={rpcSess}>
|
||||||
<div className="content">
|
<div className="content">
|
||||||
|
<div className='world-image-container empty'>
|
||||||
|
{image &&
|
||||||
|
<img className="contain" src={path.join("data", gameId, image)} alt="" />
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
<div className="tmp-pusher">
|
<div className="tmp-pusher">
|
||||||
|
{/* <div className="world-image-container empty">
|
||||||
|
|
||||||
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
<div className='proof' ref={proofPanelRef}>
|
<div className='proof' ref={proofPanelRef}>
|
||||||
<ExerciseStatement data={props.data} />
|
<ExerciseStatement data={props.data} />
|
||||||
{proof.length ?
|
{crashed ? <div>
|
||||||
<>
|
<p className="crashed_message">{t("Crashed! Go to editor mode and fix your proof! Last server response:")}</p>
|
||||||
{proof.map((step, i) => {
|
{interimDiags.map(diag => {
|
||||||
if (i == proof.length - 1 && lastStepErrors) {
|
const severityClass = diag.severity ? {
|
||||||
// if the last command contains an error, we only display the errors but not the
|
[DiagnosticSeverity.Error]: 'error',
|
||||||
// entered command as it is still present in the command line.
|
[DiagnosticSeverity.Warning]: 'warning',
|
||||||
// TODO: Should not use index as key.
|
[DiagnosticSeverity.Information]: 'information',
|
||||||
return <div key={`proof-step-${i}`}>
|
[DiagnosticSeverity.Hint]: 'hint',
|
||||||
<Errors errors={step.errors} typewriterMode={true} />
|
}[diag.severity] : '';
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
<div className={`${severityClass} ml1 message`}>
|
||||||
|
<p className="mv2">{t("Line")} {diag.range.start.line}, {t("Character")} {diag.range.start.character}</p>
|
||||||
|
<pre className="font-code pre-wrap">
|
||||||
|
{diag.message}
|
||||||
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
} else {
|
</div>
|
||||||
|
})}
|
||||||
|
|
||||||
|
</div> : proof?.steps.length ?
|
||||||
|
<>
|
||||||
|
{proof?.steps.map((step, i) => {
|
||||||
|
let filteredHints = filterHints(step.goals[0]?.hints, proof?.steps[i-1]?.goals[0]?.hints)
|
||||||
|
|
||||||
|
// if (i == proof?.steps.length - 1 && hasInteractiveErrors(step.diags)) {
|
||||||
|
// // if the last command contains an error, we only display the errors but not the
|
||||||
|
// // entered command as it is still present in the command line.
|
||||||
|
// // TODO: Should not use index as key.
|
||||||
|
// return <div key={`proof-step-${i}`} className={`step step-${i}`}>
|
||||||
|
// <Errors errors={step.diags} typewriterMode={true} />
|
||||||
|
// </div>
|
||||||
|
// } else {
|
||||||
return <div key={`proof-step-${i}`} className={`step step-${i}` + (selectedStep == i ? ' selected' : '')}>
|
return <div key={`proof-step-${i}`} className={`step step-${i}` + (selectedStep == i ? ' selected' : '')}>
|
||||||
<Command command={step.command} deleteProof={deleteProof(i)} />
|
<Command proof={proof} i={i} deleteProof={deleteProof(i)} />
|
||||||
<Errors errors={step.errors} typewriterMode={true} />
|
<Errors errors={step.diags} typewriterMode={true} />
|
||||||
{mobile && i == 0 && props.data?.introduction &&
|
{mobile && i == 0 && props.data?.introduction &&
|
||||||
<div className={`message information step-0${selectedStep === 0 ? ' selected' : ''}`} onClick={toggleSelectStep(0)}>
|
<div className={`message information step-0${selectedStep === 0 ? ' selected' : ''}`} onClick={toggleSelectStep(0)}>
|
||||||
<Markdown>{props.data?.introduction}</Markdown>
|
<Markdown>{props.data?.introduction}</Markdown>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{mobile && <>
|
{mobile &&
|
||||||
<Hints key={`hints-${i}`}
|
<Hints key={`hints-${i}`}
|
||||||
hints={step.hints} showHidden={showHelp.has(i)} step={i}
|
hints={filteredHints} showHidden={showHelp.has(i)} step={i}
|
||||||
selected={selectedStep} toggleSelection={toggleSelectStep(i)}/>
|
selected={selectedStep} toggleSelection={toggleSelectStep(i)}/>
|
||||||
{i == proof.length - 1 && hasHiddenHints(proof.length - 1) && !showHelp.has(k - withErr) &&
|
|
||||||
<Button className="btn btn-help" to="" onClick={activateHiddenHints}>
|
|
||||||
Show more help!
|
|
||||||
</Button>
|
|
||||||
}
|
}
|
||||||
</>
|
{/* <GoalsTabs proofStep={step} last={i == proof?.steps.length - (lastStepErrors ? 2 : 1)} onClick={toggleSelectStep(i)} onGoalChange={i == proof?.steps.length - 1 - withErr ? (n) => setDisableInput(n > 0) : (n) => {}}/> */}
|
||||||
|
{!(isLastStepWithErrors(proof, i)) &&
|
||||||
|
<GoalsTabs proofStep={step} last={i == proof?.steps.length - (lastStepHasErrors(proof) ? 2 : 1)} onClick={toggleSelectStep(i)} onGoalChange={i == proof?.steps.length - (lastStepHasErrors(proof) ? 2 : 1) ? (n) => setDisableInput(n > 0) : (n) => {}}/>
|
||||||
}
|
}
|
||||||
<GoalsTabs proofStep={step} last={i == proof.length - (lastStepErrors ? 2 : 1)} onClick={toggleSelectStep(i)} onGoalChange={i == proof.length - 1 - withErr ? (n) => setDisableInput(n > 0) : (n) => {}}/>
|
{mobile && i == proof?.steps.length - 1 &&
|
||||||
|
<MoreHelpButton />
|
||||||
|
}
|
||||||
|
|
||||||
{/* Show a message that there are no goals left */}
|
{/* Show a message that there are no goals left */}
|
||||||
{!step.goals.length && (
|
{/* {!step.goals.length && (
|
||||||
<div className="message information">
|
<div className="message information">
|
||||||
{completed ?
|
{proof?.completed ?
|
||||||
<p>Level completed! 🎉</p> :
|
<p>Level completed! 🎉</p> :
|
||||||
<p>
|
<p>
|
||||||
<b>no goals left</b><br />
|
<b>no goals left</b><br />
|
||||||
@@ -504,15 +588,21 @@ export function TypewriterInterface({props}) {
|
|||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
})}
|
//}
|
||||||
{mobile && completed &&
|
)}
|
||||||
<div className="button-row">
|
{proof?.diagnostics.length > 0 &&
|
||||||
|
<div key={`proof-step-remaining`} className="step step-remaining">
|
||||||
|
<Errors errors={proof?.diagnostics} typewriterMode={true} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{mobile && proof?.completed &&
|
||||||
|
<div className="button-row mobile">
|
||||||
{props.level >= props.worldSize ?
|
{props.level >= props.worldSize ?
|
||||||
<Button to={`/${gameId}`}>
|
<Button to={`/${gameId}`}>
|
||||||
<FontAwesomeIcon icon={faHome} /> Leave World
|
<FontAwesomeIcon icon={faHome} /> {t("Leave World")}
|
||||||
</Button>
|
</Button>
|
||||||
:
|
:
|
||||||
<Button to={`/${gameId}/world/${props.world}/level/${props.level + 1}`}>
|
<Button to={`/${gameId}/world/${props.world}/level/${props.level + 1}`}>
|
||||||
@@ -521,11 +611,14 @@ export function TypewriterInterface({props}) {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</> : <CircularProgress />
|
</> : <CircularProgress variant="determinate" value={100*(1 - 1.024 ** (- loadingProgress))} />
|
||||||
|
// note: since we don't know the total number of files,
|
||||||
|
// we use a function which strictly monotonely increases towards `100` as `x → ∞`
|
||||||
|
// The base is chosen at random s.t. we get roughly 91% for `x = 100`.
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Typewriter hidden={!withErr && proof[proof.length - 1]?.goals.length == 0} disabled={disableInput || !proof.length}/>
|
<Typewriter disabled={disableInput || !proof?.steps.length}/>
|
||||||
</RpcContext.Provider>
|
</RpcContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { InteractiveMessage } from '../../../../node_modules/lean4-infoview/src/
|
|||||||
import { RpcContext, useRpcSessionAtPos } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions'
|
import { RpcContext, useRpcSessionAtPos } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions'
|
||||||
|
|
||||||
import { InputModeContext } from './context'
|
import { InputModeContext } from './context'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
interface MessageViewProps {
|
interface MessageViewProps {
|
||||||
uri: DocumentUri;
|
uri: DocumentUri;
|
||||||
@@ -79,7 +80,7 @@ const MessageView = React.memo(({uri, diag}: MessageViewProps) => {
|
|||||||
message = diag.message
|
message = diag.message
|
||||||
}
|
}
|
||||||
|
|
||||||
const { typewriterMode } = React.useContext(InputModeContext)
|
const { typewriterMode, lockEditorMode } = React.useContext(InputModeContext)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// <details open>
|
// <details open>
|
||||||
@@ -98,7 +99,7 @@ const MessageView = React.memo(({uri, diag}: MessageViewProps) => {
|
|||||||
// </span>
|
// </span>
|
||||||
// </summary>
|
// </summary>
|
||||||
<div className={severityClass + ' ml1 message'}>
|
<div className={severityClass + ' ml1 message'}>
|
||||||
{!typewriterMode && <p className="mv2">{title}</p>}
|
{!(typewriterMode && !lockEditorMode) && <p className="mv2">{title}</p>}
|
||||||
<pre className="font-code pre-wrap">
|
<pre className="font-code pre-wrap">
|
||||||
<InteractiveMessage fmt={message} />
|
<InteractiveMessage fmt={message} />
|
||||||
</pre>
|
</pre>
|
||||||
@@ -194,17 +195,26 @@ export function AllMessages() {
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</summary> */}
|
</summary> */}
|
||||||
<AllMessagesBody uri={curPos.uri} key={curPos.uri} messages={iDiags0} />
|
<AllMessagesBody uri={curPos.uri} key={curPos.uri} messages={iDiags0} curPos={curPos} />
|
||||||
{/* </Details> */}
|
{/* </Details> */}
|
||||||
</RpcContext.Provider>
|
</RpcContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** We factor out the body of {@link AllMessages} which lazily fetches its contents only when expanded. */
|
/** We factor out the body of {@link AllMessages} which lazily fetches its contents only when expanded. */
|
||||||
function AllMessagesBody({uri, messages}: {uri: DocumentUri, messages: () => Promise<InteractiveDiagnostic[]>}) {
|
function AllMessagesBody({uri, curPos, messages}: {uri: DocumentUri, curPos: DocumentPosition | undefined , messages: () => Promise<InteractiveDiagnostic[]>}) {
|
||||||
|
let { t } = useTranslation()
|
||||||
const [msgs, setMsgs] = React.useState<InteractiveDiagnostic[] | undefined>(undefined)
|
const [msgs, setMsgs] = React.useState<InteractiveDiagnostic[] | undefined>(undefined)
|
||||||
React.useEffect(() => { void messages().then(setMsgs) }, [messages])
|
React.useEffect(() => { void messages().then(
|
||||||
if (msgs === undefined) return <div>Loading messages...</div>
|
msgs => setMsgs(msgs.filter(
|
||||||
|
(d)=>{
|
||||||
|
//console.log(`message start: ${d.range.start.line}. CurPos: ${curPos.line}`)
|
||||||
|
|
||||||
|
// Only show the messages from the line where the cursor is.
|
||||||
|
return d.range.start.line == curPos.line
|
||||||
|
}))
|
||||||
|
) }, [messages, curPos])
|
||||||
|
if (msgs === undefined) return <div>{t("Loading messages…")}</div>
|
||||||
else return <MessagesList uri={uri} messages={msgs}/>
|
else return <MessagesList uri={uri} messages={msgs}/>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,46 +3,82 @@
|
|||||||
*
|
*
|
||||||
* This file is based on `vscode-lean4/vscode-lean4/src/rpcApi.ts`
|
* This file is based on `vscode-lean4/vscode-lean4/src/rpcApi.ts`
|
||||||
*/
|
*/
|
||||||
import { ContextInfo, FVarId, CodeWithInfos, MVarId } from '@leanprover/infoview-api';
|
import type { Range } from 'vscode-languageserver-protocol';
|
||||||
|
import type { ContextInfo, FVarId, CodeWithInfos, MVarId } from '@leanprover/infoview-api';
|
||||||
export interface GameHint {
|
import { InteractiveDiagnostic, TermInfo } from '@leanprover/infoview/*';
|
||||||
text: string;
|
import type { Diagnostic } from 'vscode-languageserver-protocol';
|
||||||
hidden: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InteractiveHypothesisBundle {
|
export interface InteractiveHypothesisBundle {
|
||||||
/** The pretty names of the variables in the bundle. Anonymous names are rendered
|
/** The pretty names of the variables in the bundle. Anonymous names are rendered
|
||||||
* as `"[anonymous]"` whereas inaccessible ones have a `✝` appended at the end.
|
* as `"[anonymous]"` whereas inaccessible ones have a `✝` appended at the end.
|
||||||
* Use `InteractiveHypothesisBundle_nonAnonymousNames` to filter anonymouse ones out. */
|
* Use `InteractiveHypothesisBundle_nonAnonymousNames` to filter anonymouse ones out. */
|
||||||
names: string[];
|
names: string[];
|
||||||
/** Present since server version 1.1.2. */
|
|
||||||
fvarIds?: FVarId[];
|
fvarIds?: FVarId[];
|
||||||
type: CodeWithInfos;
|
type: CodeWithInfos;
|
||||||
val?: CodeWithInfos;
|
val?: CodeWithInfos;
|
||||||
isInstance?: boolean;
|
isInstance?: boolean;
|
||||||
isType?: boolean;
|
isType?: boolean;
|
||||||
isAssumption?: boolean;
|
|
||||||
isInserted?: boolean;
|
isInserted?: boolean;
|
||||||
isRemoved?: boolean;
|
isRemoved?: boolean;
|
||||||
|
isAssumption?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InteractiveGoalCore {
|
export interface InteractiveGoalCore {
|
||||||
hyps: InteractiveHypothesisBundle[];
|
hyps: InteractiveHypothesisBundle[];
|
||||||
type: CodeWithInfos;
|
type: CodeWithInfos;
|
||||||
/** Present since server version 1.1.2. */
|
|
||||||
ctx?: ContextInfo;
|
ctx?: ContextInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InteractiveGoal extends InteractiveGoalCore {
|
export interface InteractiveGoal extends InteractiveGoalCore {
|
||||||
userName?: string;
|
userName?: string;
|
||||||
goalPrefix?: string;
|
goalPrefix?: string;
|
||||||
/** Present since server version 1.1.2. */
|
|
||||||
mvarId?: MVarId;
|
mvarId?: MVarId;
|
||||||
isInserted?: boolean;
|
isInserted?: boolean;
|
||||||
isRemoved?: boolean;
|
isRemoved?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InteractiveGoals extends InteractiveGoalCore {
|
||||||
|
goals: InteractiveGoals[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InteractiveTermGoal extends InteractiveGoalCore {
|
||||||
|
range?: Range;
|
||||||
|
term?: TermInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GameHint {
|
||||||
|
text: string;
|
||||||
|
hidden: boolean;
|
||||||
|
rawText: string;
|
||||||
|
varNames: string[][]; // in Lean: `Array (Name × Name)`
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InteractiveGoalWithHints {
|
||||||
|
goal: InteractiveGoal;
|
||||||
hints: GameHint[];
|
hints: GameHint[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InteractiveGoals {
|
export interface InteractiveGoalsWithHints {
|
||||||
goals: InteractiveGoal[];
|
goals: InteractiveGoalWithHints[];
|
||||||
|
command: string;
|
||||||
|
diags: InteractiveDiagnostic[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The proof state as it is received from the server.
|
||||||
|
* Per proof step of the tactic proof, there is one `InteractiveGoalWithHints[]`.
|
||||||
|
*/
|
||||||
|
export interface ProofState {
|
||||||
|
/** The proof steps. step 0 is the state at the beginning of the proof. step one
|
||||||
|
* contains the goal after the first line has been evaluated.
|
||||||
|
*
|
||||||
|
* In particular `step[i]` is the proof step at the beginning of line `i` in vscode.
|
||||||
|
*/
|
||||||
|
steps: InteractiveGoalsWithHints[];
|
||||||
|
/** The remaining diagnostics that are not in the steps. Usually this should only
|
||||||
|
* be the "unsolved goals" message, I believe.
|
||||||
|
*/
|
||||||
|
diagnostics : InteractiveDiagnostic[];
|
||||||
|
completed : Boolean;
|
||||||
|
completedWithWarnings : Boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { faWandMagicSparkles } from '@fortawesome/free-solid-svg-icons'
|
|||||||
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 { Registry } from 'monaco-textmate' // peer dependency
|
import { Registry } from 'monaco-textmate' // peer dependency
|
||||||
import { wireTmGrammars } from 'monaco-editor-textmate'
|
import { wireTmGrammars } from 'monaco-editor-textmate'
|
||||||
import { DiagnosticSeverity, PublishDiagnosticsParams } from 'vscode-languageserver-protocol';
|
import { DiagnosticSeverity, PublishDiagnosticsParams, DocumentUri } from 'vscode-languageserver-protocol';
|
||||||
import { useServerNotificationEffect } from '../../../../node_modules/lean4-infoview/src/infoview/util';
|
import { useServerNotificationEffect } from '../../../../node_modules/lean4-infoview/src/infoview/util';
|
||||||
import { AbbreviationRewriter } from 'lean4web/client/src/editor/abbreviation/rewriter/AbbreviationRewriter';
|
import { AbbreviationRewriter } from 'lean4web/client/src/editor/abbreviation/rewriter/AbbreviationRewriter';
|
||||||
import { AbbreviationProvider } from 'lean4web/client/src/editor/abbreviation/AbbreviationProvider';
|
import { AbbreviationProvider } from 'lean4web/client/src/editor/abbreviation/AbbreviationProvider';
|
||||||
@@ -13,13 +13,19 @@ import * as leanSyntax from 'lean4web/client/src/syntaxes/lean.json'
|
|||||||
import * as leanMarkdownSyntax from 'lean4web/client/src/syntaxes/lean-markdown.json'
|
import * as leanMarkdownSyntax from 'lean4web/client/src/syntaxes/lean-markdown.json'
|
||||||
import * as codeblockSyntax from 'lean4web/client/src/syntaxes/codeblock.json'
|
import * as codeblockSyntax from 'lean4web/client/src/syntaxes/codeblock.json'
|
||||||
import languageConfig from 'lean4/language-configuration.json';
|
import languageConfig from 'lean4/language-configuration.json';
|
||||||
import { InteractiveDiagnostic, getInteractiveDiagnostics } from '@leanprover/infoview-api';
|
import { InteractiveDiagnostic, RpcSessionAtPos, getInteractiveDiagnostics } from '@leanprover/infoview-api';
|
||||||
import { Diagnostic } from 'vscode-languageserver-types';
|
import { Diagnostic } from 'vscode-languageserver-types';
|
||||||
import { DocumentPosition } from '../../../../node_modules/lean4-infoview/src/infoview/util';
|
import { DocumentPosition } from '../../../../node_modules/lean4-infoview/src/infoview/util';
|
||||||
import { RpcContext } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions';
|
import { RpcContext } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions';
|
||||||
import { DeletedChatContext, InputModeContext, MonacoEditorContext, ProofContext, ProofStep } from './context'
|
import { DeletedChatContext, InputModeContext, MonacoEditorContext, ProofContext } from './context'
|
||||||
import { goalsToString } from './goals'
|
import { goalsToString, lastStepHasErrors, loadGoals } from './goals'
|
||||||
import { GameHint, InteractiveGoals } from './rpc_api'
|
import { GameHint, ProofState } from './rpc_api'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
export interface GameDiagnosticsParams {
|
||||||
|
uri: DocumentUri;
|
||||||
|
diagnostics: Diagnostic[];
|
||||||
|
}
|
||||||
|
|
||||||
/* We register a new language `leancmd` that looks like lean4, but does not use the lsp server. */
|
/* We register a new language `leancmd` that looks like lean4, but does not use the lsp server. */
|
||||||
|
|
||||||
@@ -64,7 +70,8 @@ config.autoClosingPairs = config.autoClosingPairs.map(
|
|||||||
monaco.languages.setLanguageConfiguration('lean4cmd', config);
|
monaco.languages.setLanguageConfiguration('lean4cmd', config);
|
||||||
|
|
||||||
/** The input field */
|
/** The input field */
|
||||||
export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boolean}) {
|
export function Typewriter({disabled}: {disabled?: boolean}) {
|
||||||
|
let { t } = useTranslation()
|
||||||
|
|
||||||
/** Reference to the hidden multi-line editor */
|
/** Reference to the hidden multi-line editor */
|
||||||
const editor = React.useContext(MonacoEditorContext)
|
const editor = React.useContext(MonacoEditorContext)
|
||||||
@@ -79,109 +86,13 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
|
|||||||
const inputRef = useRef()
|
const inputRef = useRef()
|
||||||
|
|
||||||
// The context storing all information about the current proof
|
// The context storing all information about the current proof
|
||||||
const {proof, setProof} = React.useContext(ProofContext)
|
const {proof, setProof, interimDiags, setInterimDiags, setCrashed} = React.useContext(ProofContext)
|
||||||
|
|
||||||
// state to store the last batch of deleted messages
|
// state to store the last batch of deleted messages
|
||||||
const {setDeletedChat} = React.useContext(DeletedChatContext)
|
const {setDeletedChat} = React.useContext(DeletedChatContext)
|
||||||
|
|
||||||
const rpcSess = React.useContext(RpcContext)
|
const rpcSess = React.useContext(RpcContext)
|
||||||
|
|
||||||
/** Load all goals an messages of the current proof (line-by-line) and save
|
|
||||||
* the retrieved information into context (`ProofContext`)
|
|
||||||
*/
|
|
||||||
const loadAllGoals = React.useCallback(() => {
|
|
||||||
|
|
||||||
let goalCalls = []
|
|
||||||
let msgCalls = []
|
|
||||||
|
|
||||||
// For each line of code ask the server for the goals and the messages on this line
|
|
||||||
for (let i = 0; i < model.getLineCount(); i++) {
|
|
||||||
goalCalls.push(
|
|
||||||
rpcSess.call('Game.getInteractiveGoals', DocumentPosition.toTdpp({line: i, character: 0, uri: uri}))
|
|
||||||
)
|
|
||||||
msgCalls.push(
|
|
||||||
getInteractiveDiagnostics(rpcSess, {start: i, end: i+1}).catch((error) => {console.debug("promise broken")})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all these requests to be processed before saving the results
|
|
||||||
Promise.all(goalCalls).then((steps : InteractiveGoals[]) => {
|
|
||||||
Promise.all(msgCalls).then((diagnostics : [InteractiveDiagnostic[]]) => {
|
|
||||||
let tmpProof : ProofStep[] = []
|
|
||||||
|
|
||||||
let goalCount = 0
|
|
||||||
|
|
||||||
steps.map((goals, i) => {
|
|
||||||
// The first step has an empty command and therefore also no error messages
|
|
||||||
// Usually there is a newline at the end of the editors content, so we need to
|
|
||||||
// display diagnostics from potentally two lines in the last step.
|
|
||||||
let messages = i ? (i == steps.length - 1 ? diagnostics.slice(i-1).flat() : diagnostics[i-1]) : []
|
|
||||||
|
|
||||||
// Filter out the 'unsolved goals' message
|
|
||||||
messages = messages.filter((msg) => {
|
|
||||||
return !("append" in msg.message &&
|
|
||||||
"text" in msg.message.append[0] &&
|
|
||||||
msg.message.append[0].text === "unsolved goals")
|
|
||||||
})
|
|
||||||
|
|
||||||
if (typeof goals == 'undefined') {
|
|
||||||
tmpProof.push({
|
|
||||||
command: i ? model.getLineContent(i) : '',
|
|
||||||
goals: [],
|
|
||||||
hints: [],
|
|
||||||
errors: messages
|
|
||||||
} as ProofStep)
|
|
||||||
console.debug('goals is undefined')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the number of goals reduce, show a message
|
|
||||||
if (goals.goals.length && goalCount > goals.goals.length) {
|
|
||||||
messages.unshift({
|
|
||||||
range: {
|
|
||||||
start: {
|
|
||||||
line: i-1,
|
|
||||||
character: 0,
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
line: i-1,
|
|
||||||
character: 0,
|
|
||||||
}},
|
|
||||||
severity: DiagnosticSeverity.Information,
|
|
||||||
message: {
|
|
||||||
text: 'intermediate goal solved 🎉'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
goalCount = goals.goals.length
|
|
||||||
|
|
||||||
// with no goals there will be no hints.
|
|
||||||
let hints : GameHint[] = goals.goals.length ? goals.goals[0].hints : []
|
|
||||||
|
|
||||||
console.debug(`Command (${i}): `, i ? model.getLineContent(i) : '')
|
|
||||||
console.debug(`Goals: (${i}): `, goalsToString(goals)) //
|
|
||||||
console.debug(`Hints: (${i}): `, hints)
|
|
||||||
console.debug(`Errors: (${i}): `, messages)
|
|
||||||
|
|
||||||
tmpProof.push({
|
|
||||||
// the command of the line above. Note that `getLineContent` starts counting
|
|
||||||
// at `1` instead of `zero`. The first ProofStep will have an empty command.
|
|
||||||
command: i ? model.getLineContent(i) : '',
|
|
||||||
// TODO: store correct data
|
|
||||||
goals: goals.goals,
|
|
||||||
// only need the hints of the active goals in chat
|
|
||||||
hints: hints,
|
|
||||||
// errors and messages from the server
|
|
||||||
errors: messages
|
|
||||||
} as ProofStep)
|
|
||||||
|
|
||||||
})
|
|
||||||
// Save the proof to the context
|
|
||||||
setProof(tmpProof)
|
|
||||||
}).catch((error) => {console.debug("promise broken")})
|
|
||||||
}).catch((error) => {console.debug("promise broken")})
|
|
||||||
}, [editor, rpcSess, uri, model])
|
|
||||||
|
|
||||||
// Run the command
|
// Run the command
|
||||||
const runCommand = React.useCallback(() => {
|
const runCommand = React.useCallback(() => {
|
||||||
if (processing) {return}
|
if (processing) {return}
|
||||||
@@ -201,6 +112,8 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
|
|||||||
forceMoveMarkers: false
|
forceMoveMarkers: false
|
||||||
}])
|
}])
|
||||||
setTypewriterInput('')
|
setTypewriterInput('')
|
||||||
|
// Load proof after executing edits
|
||||||
|
loadGoals(rpcSess, uri, setProof, setCrashed)
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.setPosition(pos)
|
editor.setPosition(pos)
|
||||||
@@ -212,9 +125,15 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
|
|||||||
}
|
}
|
||||||
}, [typewriterInput])
|
}, [typewriterInput])
|
||||||
|
|
||||||
|
/* Load proof on start/switching to typewriter */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (proof.length && hasInteractiveErrors(proof[proof.length - 1].errors)) {
|
loadGoals(rpcSess, uri, setProof, setCrashed)
|
||||||
setTypewriterInput(proof[proof.length - 1].command)
|
}, [])
|
||||||
|
|
||||||
|
/** If the last step has an error, add the command to the typewriter. */
|
||||||
|
useEffect(() => {
|
||||||
|
if (lastStepHasErrors(proof)) {
|
||||||
|
setTypewriterInput(proof?.steps[proof?.steps.length - 1].command)
|
||||||
}
|
}
|
||||||
}, [proof])
|
}, [proof])
|
||||||
|
|
||||||
@@ -222,7 +141,14 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
|
|||||||
useServerNotificationEffect('textDocument/publishDiagnostics', (params: PublishDiagnosticsParams) => {
|
useServerNotificationEffect('textDocument/publishDiagnostics', (params: PublishDiagnosticsParams) => {
|
||||||
if (params.uri == uri) {
|
if (params.uri == uri) {
|
||||||
setProcessing(false)
|
setProcessing(false)
|
||||||
loadAllGoals()
|
|
||||||
|
console.log('Received lean diagnostics')
|
||||||
|
console.log(params.diagnostics)
|
||||||
|
setInterimDiags(params.diagnostics)
|
||||||
|
|
||||||
|
//loadGoals(rpcSess, uri, setProof)
|
||||||
|
|
||||||
|
// TODO: loadAllGoals()
|
||||||
if (!hasErrors(params.diagnostics)) {
|
if (!hasErrors(params.diagnostics)) {
|
||||||
//setTypewriterInput("")
|
//setTypewriterInput("")
|
||||||
editor.setPosition(editor.getModel().getFullModelRange().getEndPosition())
|
editor.setPosition(editor.getModel().getFullModelRange().getEndPosition())
|
||||||
@@ -236,6 +162,15 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
|
|||||||
// loadAllGoals()
|
// loadAllGoals()
|
||||||
}, [uri]);
|
}, [uri]);
|
||||||
|
|
||||||
|
// // React when answer from the server comes back
|
||||||
|
// useServerNotificationEffect('$/game/publishDiagnostics', (params: GameDiagnosticsParams) => {
|
||||||
|
// console.log('Received game diagnostics')
|
||||||
|
// console.log(`diag. uri : ${params.uri}`)
|
||||||
|
// console.log(params.diagnostics)
|
||||||
|
|
||||||
|
// }, [uri]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const myEditor = monaco.editor.create(inputRef.current!, {
|
const myEditor = monaco.editor.create(inputRef.current!, {
|
||||||
value: typewriterInput,
|
value: typewriterInput,
|
||||||
@@ -306,7 +241,8 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.debug(`time to update: ${uri} \n ${rpcSess}`)
|
console.debug(`time to update: ${uri} \n ${rpcSess}`)
|
||||||
console.debug(rpcSess)
|
console.debug(rpcSess)
|
||||||
loadAllGoals()
|
// console.debug('LOAD ALL GOALS')
|
||||||
|
// TODO: loadAllGoals()
|
||||||
}, [rpcSess])
|
}, [rpcSess])
|
||||||
|
|
||||||
/** Process the entered command */
|
/** Process the entered command */
|
||||||
@@ -315,13 +251,14 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
|
|||||||
runCommand()
|
runCommand()
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={`typewriter${hidden ? ' hidden' : ''}${disabled ? ' disabled' : ''}`}>
|
// do not display if the proof is completed (with potential warnings still present)
|
||||||
|
return <div className={`typewriter${proof?.completedWithWarnings ? ' hidden' : ''}${disabled ? ' disabled' : ''}`}>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="typewriter-input-wrapper">
|
<div className="typewriter-input-wrapper">
|
||||||
<div ref={inputRef} className="typewriter-input" />
|
<div ref={inputRef} className="typewriter-input" />
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" disabled={processing} className="btn btn-inverted">
|
<button type="submit" disabled={processing} className="btn btn-inverted">
|
||||||
<FontAwesomeIcon icon={faWandMagicSparkles} /> Execute
|
<FontAwesomeIcon icon={faWandMagicSparkles} /> {t("Execute")}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -343,3 +280,14 @@ export function hasInteractiveErrors (diags: InteractiveDiagnostic[]) {
|
|||||||
(d) => (d.severity == DiagnosticSeverity.Error ) // || d.severity == DiagnosticSeverity.Warning
|
(d) => (d.severity == DiagnosticSeverity.Error ) // || d.severity == DiagnosticSeverity.Warning
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getInteractiveDiagsAt (proof: ProofState, k : number) {
|
||||||
|
if (k == 0) {
|
||||||
|
return []
|
||||||
|
} else if (k >= proof?.steps.length-1) {
|
||||||
|
// TODO: Do we need that?
|
||||||
|
return proof?.diagnostics.filter(msg => msg.range.start.line >= proof?.steps.length-1)
|
||||||
|
} else {
|
||||||
|
return proof?.diagnostics.filter(msg => msg.range.start.line == k-1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,48 +2,55 @@ import * as React from 'react';
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import '../css/inventory.css'
|
import '../css/inventory.css'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faLock, faBan } from '@fortawesome/free-solid-svg-icons'
|
import { faLock, faBan, faCheck } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { faClipboard } from '@fortawesome/free-regular-svg-icons'
|
||||||
import { GameIdContext } from '../app';
|
import { GameIdContext } from '../app';
|
||||||
import Markdown from './markdown';
|
import Markdown from './markdown';
|
||||||
import { useLoadDocQuery, InventoryTile, LevelInfo, InventoryOverview, useLoadInventoryOverviewQuery } from '../state/api';
|
import { useLoadDocQuery, InventoryTile, LevelInfo, InventoryOverview, useLoadInventoryOverviewQuery } 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';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
|
||||||
export function Inventory({levelInfo, openDoc, enableAll=false} :
|
export function Inventory({levelInfo, openDoc, lemmaTab, setLemmaTab, enableAll=false} :
|
||||||
{
|
{
|
||||||
levelInfo: LevelInfo|InventoryOverview,
|
levelInfo: LevelInfo|InventoryOverview,
|
||||||
openDoc: (props: {name: string, type: string}) => void,
|
openDoc: (props: {name: string, type: string}) => void,
|
||||||
|
lemmaTab: any,
|
||||||
|
setLemmaTab: any,
|
||||||
enableAll?: boolean,
|
enableAll?: boolean,
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inventory">
|
<div className="inventory">
|
||||||
{/* TODO: Click on Tactic: show info
|
{/* TODO: Click on Tactic: show info
|
||||||
TODO: click on paste icon -> paste into command line */}
|
TODO: click on paste icon -> paste into command line */}
|
||||||
<h2>Tactics</h2>
|
<h2>{t("Tactics")}</h2>
|
||||||
{levelInfo?.tactics &&
|
{levelInfo?.tactics &&
|
||||||
<InventoryList items={levelInfo?.tactics} docType="Tactic" openDoc={openDoc} enableAll={enableAll}/>
|
<InventoryList items={levelInfo?.tactics} docType="Tactic" openDoc={openDoc} enableAll={enableAll}/>
|
||||||
}
|
}
|
||||||
<h2>Definitions</h2>
|
<h2>{t("Definitions")}</h2>
|
||||||
{levelInfo?.definitions &&
|
{levelInfo?.definitions &&
|
||||||
<InventoryList items={levelInfo?.definitions} docType="Definition" openDoc={openDoc} enableAll={enableAll}/>
|
<InventoryList items={levelInfo?.definitions} docType="Definition" openDoc={openDoc} enableAll={enableAll}/>
|
||||||
}
|
}
|
||||||
<h2>Theorems</h2>
|
<h2>{t("Theorems")}</h2>
|
||||||
{levelInfo?.lemmas &&
|
{levelInfo?.lemmas &&
|
||||||
<InventoryList items={levelInfo?.lemmas} docType="Lemma" openDoc={openDoc} defaultTab={levelInfo?.lemmaTab} level={levelInfo} enableAll={enableAll}/>
|
<InventoryList items={levelInfo?.lemmas} docType="Lemma" openDoc={openDoc} level={levelInfo} enableAll={enableAll} tab={lemmaTab} setTab={setLemmaTab}/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function InventoryList({items, docType, openDoc, defaultTab=null, level=undefined, enableAll=false} :
|
function InventoryList({items, docType, openDoc, tab=null, setTab=undefined, level=undefined, enableAll=false} :
|
||||||
{
|
{
|
||||||
items: InventoryTile[],
|
items: InventoryTile[],
|
||||||
docType: string,
|
docType: string,
|
||||||
openDoc(props: {name: string, type: string}): void,
|
openDoc(props: {name: string, type: string}): void,
|
||||||
defaultTab? : string,
|
tab?: any,
|
||||||
level? : LevelInfo|InventoryOverview,
|
setTab?: any,
|
||||||
|
level?: LevelInfo|InventoryOverview,
|
||||||
enableAll?: boolean,
|
enableAll?: boolean,
|
||||||
}) {
|
}) {
|
||||||
// TODO: `level` is only used in the `useEffect` below to check if a new level has
|
// TODO: `level` is only used in the `useEffect` below to check if a new level has
|
||||||
@@ -59,8 +66,6 @@ function InventoryList({items, docType, openDoc, defaultTab=null, level=undefine
|
|||||||
}
|
}
|
||||||
const categories = Array.from(categorySet).sort()
|
const categories = Array.from(categorySet).sort()
|
||||||
|
|
||||||
const [tab, setTab] = useState(defaultTab)
|
|
||||||
|
|
||||||
// Add inventory items from local store as unlocked.
|
// Add inventory items from local store as unlocked.
|
||||||
// Items are unlocked if they are in the local store, or if the server says they should be
|
// Items are unlocked if they are in the local store, or if the server says they should be
|
||||||
// given the dependency graph. (OR-connection) (TODO: maybe add different logic for different
|
// given the dependency graph. (OR-connection) (TODO: maybe add different logic for different
|
||||||
@@ -68,13 +73,6 @@ function InventoryList({items, docType, openDoc, defaultTab=null, level=undefine
|
|||||||
let inv: string[] = selectInventory(gameId)(store.getState())
|
let inv: string[] = selectInventory(gameId)(store.getState())
|
||||||
let modifiedItems : InventoryTile[] = items.map(tile => inv.includes(tile.name) ? {...tile, locked: false} : tile)
|
let modifiedItems : InventoryTile[] = items.map(tile => inv.includes(tile.name) ? {...tile, locked: false} : tile)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// If the level specifies `LemmaTab "Nat"`, we switch to this tab on loading.
|
|
||||||
// `defaultTab` is `null` or `undefined` otherwise, in which case we don't want to switch.
|
|
||||||
if (defaultTab) {
|
|
||||||
setTab(defaultTab)
|
|
||||||
}}, [level])
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
{categories.length > 1 &&
|
{categories.length > 1 &&
|
||||||
<div className="tab-bar">
|
<div className="tab-bar">
|
||||||
@@ -86,24 +84,29 @@ function InventoryList({items, docType, openDoc, defaultTab=null, level=undefine
|
|||||||
{[...modifiedItems].sort(
|
{[...modifiedItems].sort(
|
||||||
// For lemas, sort entries `available > disabled > locked`
|
// For lemas, sort entries `available > disabled > locked`
|
||||||
// otherwise alphabetically
|
// otherwise alphabetically
|
||||||
(x, y) => +(docType == "Lemma") * (+x.locked - +y.locked || +x.disabled - +y.disabled)
|
(x, y) => +(docType == "Lemma") * (+x.locked - +y.locked || +x.disabled - +y.disabled) || x.displayName.localeCompare(y.displayName)
|
||||||
).filter(item => !item.hidden && ((tab ?? categories[0]) == item.category)).map((item, i) => {
|
).filter(item => !item.hidden && ((tab ?? categories[0]) == item.category)).map((item, i) => {
|
||||||
return <InventoryItem key={`${item.category}-${item.name}`}
|
return <InventoryItem key={`${item.category}-${item.name}`}
|
||||||
|
item={item}
|
||||||
showDoc={() => {openDoc({name: item.name, type: docType})}}
|
showDoc={() => {openDoc({name: item.name, type: docType})}}
|
||||||
name={item.name} displayName={item.displayName} locked={difficulty > 0 ? item.locked : false}
|
name={item.name} displayName={item.displayName} locked={difficulty > 0 ? item.locked : false}
|
||||||
disabled={item.disabled} newly={item.new} enableAll={enableAll}/>
|
disabled={item.disabled} newly={item.new} enableAll={enableAll} />
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
function InventoryItem({name, displayName, locked, disabled, newly, showDoc, enableAll=false}) {
|
function InventoryItem({item, name, displayName, locked, disabled, newly, showDoc, enableAll=false}) {
|
||||||
const icon = locked ? <FontAwesomeIcon icon={faLock} /> :
|
const icon = locked ? <FontAwesomeIcon icon={faLock} /> :
|
||||||
disabled ? <FontAwesomeIcon icon={faBan} /> : ""
|
disabled ? <FontAwesomeIcon icon={faBan} /> : item.st
|
||||||
const className = locked ? "locked" : disabled ? "disabled" : newly ? "new" : ""
|
const className = locked ? "locked" : disabled ? "disabled" : newly ? "new" : ""
|
||||||
const title = locked ? "Not unlocked yet" :
|
// Note: This is somewhat a hack as the statement of lemmas comes currently in the form
|
||||||
disabled ? "Not available in this level" : ""
|
// `Namespace.statement_name (x y : Nat) : some type`
|
||||||
|
const title = locked ? t("Not unlocked yet") :
|
||||||
|
disabled ? t("Not available in this level") : (item.altTitle ? item.altTitle.substring(item.altTitle.indexOf(' ') + 1) : '')
|
||||||
|
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (enableAll || !locked) {
|
if (enableAll || !locked) {
|
||||||
@@ -111,7 +114,21 @@ function InventoryItem({name, displayName, locked, disabled, newly, showDoc, ena
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={`item ${className}${enableAll ? ' enabled' : ''}`} onClick={handleClick} title={title}>{icon} {displayName}</div>
|
const copyItemName = (ev) => {
|
||||||
|
navigator.clipboard.writeText(displayName)
|
||||||
|
setCopied(true)
|
||||||
|
setInterval(() => {
|
||||||
|
setCopied(false)
|
||||||
|
}, 3000);
|
||||||
|
ev.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={`item ${className}${enableAll ? ' enabled' : ''}`} onClick={handleClick} title={title}>
|
||||||
|
{icon} {displayName}
|
||||||
|
<div className="copy-button" onClick={copyItemName}>
|
||||||
|
{copied ? <FontAwesomeIcon icon={faCheck} /> : <FontAwesomeIcon icon={faClipboard} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Documentation({name, type, handleClose}) {
|
export function Documentation({name, type, handleClose}) {
|
||||||
@@ -123,7 +140,7 @@ export function Documentation({name, type, handleClose}) {
|
|||||||
<h1 className="doc">{doc.data?.displayName}</h1>
|
<h1 className="doc">{doc.data?.displayName}</h1>
|
||||||
<p><code>{doc.data?.statement}</code></p>
|
<p><code>{doc.data?.statement}</code></p>
|
||||||
{/* <code>docstring: {doc.data?.docstring}</code> */}
|
{/* <code>docstring: {doc.data?.docstring}</code> */}
|
||||||
<Markdown>{doc.data?.content}</Markdown>
|
<Markdown>{t(doc.data?.content, {ns: gameId})}</Markdown>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,16 +148,25 @@ export function Documentation({name, type, handleClose}) {
|
|||||||
export function InventoryPanel({levelInfo, visible = true}) {
|
export function InventoryPanel({levelInfo, visible = true}) {
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
|
||||||
|
const [lemmaTab, setLemmaTab] = useState(levelInfo?.lemmaTab)
|
||||||
|
|
||||||
// The inventory is overlayed by the doc entry of a clicked item
|
// The inventory is overlayed by the doc entry of a clicked item
|
||||||
const [inventoryDoc, setInventoryDoc] = useState<{name: string, type: string}>(null)
|
const [inventoryDoc, setInventoryDoc] = useState<{name: string, type: string}>(null)
|
||||||
// Set `inventoryDoc` to `null` to close the doc
|
// Set `inventoryDoc` to `null` to close the doc
|
||||||
function closeInventoryDoc() {setInventoryDoc(null)}
|
function closeInventoryDoc() {setInventoryDoc(null)}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// If the level specifies `LemmaTab "Nat"`, we switch to this tab on loading.
|
||||||
|
// `defaultTab` is `null` or `undefined` otherwise, in which case we don't want to switch.
|
||||||
|
if (levelInfo?.lemmaTab) {
|
||||||
|
setLemmaTab(levelInfo?.lemmaTab)
|
||||||
|
}}, [levelInfo])
|
||||||
|
|
||||||
return <div className={`column inventory-panel ${visible ? '' : 'hidden'}`}>
|
return <div className={`column inventory-panel ${visible ? '' : 'hidden'}`}>
|
||||||
{inventoryDoc ?
|
{inventoryDoc ?
|
||||||
<Documentation name={inventoryDoc.name} type={inventoryDoc.type} handleClose={closeInventoryDoc}/>
|
<Documentation name={inventoryDoc.name} type={inventoryDoc.type} handleClose={closeInventoryDoc}/>
|
||||||
:
|
:
|
||||||
<Inventory levelInfo={levelInfo} openDoc={setInventoryDoc} enableAll={true}/>
|
<Inventory levelInfo={levelInfo} openDoc={setInventoryDoc} enableAll={true} lemmaTab={lemmaTab} setLemmaTab={setLemmaTab}/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useNavigate, Link } from "react-router-dom";
|
import { useNavigate, Link } from "react-router-dom";
|
||||||
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import '@fontsource/roboto/300.css';
|
import '@fontsource/roboto/300.css';
|
||||||
import '@fontsource/roboto/400.css';
|
import '@fontsource/roboto/400.css';
|
||||||
@@ -7,19 +8,18 @@ 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 = {
|
import { PreferencesPopup } from './popup/preferences';
|
||||||
'Dutch': '🇳🇱',
|
import { ImpressumButton, MenuButton, PreferencesButton } from './app_bar';
|
||||||
'English': '🇬🇧',
|
import ReactCountryFlag from 'react-country-flag';
|
||||||
'French': '🇫🇷',
|
import lean4gameConfig from '../config.json'
|
||||||
'German': '🇩🇪',
|
import i18next from 'i18next';
|
||||||
'Italian': '🇮🇹',
|
|
||||||
}
|
|
||||||
|
|
||||||
function GithubIcon({url='https://github.com'}) {
|
function GithubIcon({url='https://github.com'}) {
|
||||||
|
|
||||||
@@ -32,47 +32,49 @@ function GithubIcon({url='https://github.com'}) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
function GameTile({
|
function Tile({gameId, data}: {gameId: string, data: GameTile|undefined}) {
|
||||||
title,
|
let { t } = useTranslation()
|
||||||
gameId,
|
|
||||||
intro, // Catchy intro phrase.
|
|
||||||
image=null,
|
|
||||||
worlds='?',
|
|
||||||
levels='?',
|
|
||||||
prereq='–', // Optional list of games that this game builds on. Use markdown.
|
|
||||||
description, // Longer description. Supports Markdown.
|
|
||||||
language}) {
|
|
||||||
|
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
const routeChange = () =>{
|
const routeChange = () =>{
|
||||||
navigate(gameId);
|
navigate(gameId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof data === 'undefined') {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
|
||||||
return <div className="game" onClick={routeChange}>
|
return <div className="game" onClick={routeChange}>
|
||||||
<div className="wrapper">
|
<div className="wrapper">
|
||||||
<div className="title">{title}</div>
|
<div className="title">{t(data.title, { ns: gameId })}</div>
|
||||||
<div className="short-description">{intro}
|
<div className="short-description">{t(data.short, { ns: gameId })}
|
||||||
</div>
|
</div>
|
||||||
{ image ? <img className="image" src={image} alt="" /> : <div className="image"/> }
|
{ data.image ? <img className="image" src={path.join("data", gameId, data.image)} alt="" /> : <div className="image"/> }
|
||||||
<div className="long description"><Markdown>{description}</Markdown></div>
|
<div className="long description"><Markdown>{t(data.long, { ns: gameId })}</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.">{t("Prerequisites")}</td>
|
||||||
<td><Markdown>{prereq}</Markdown></td>
|
<td><Markdown>{t(data.prerequisites.join(', '), { ns: gameId })}</Markdown></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Worlds</td>
|
<td>{t("Worlds")}</td>
|
||||||
<td>{worlds}</td>
|
<td>{data.worlds}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Levels</td>
|
<td>{t("Levels")}</td>
|
||||||
<td>{levels}</td>
|
<td>{data.levels}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Language</td>
|
<td>{t("Language")}</td>
|
||||||
<td title={`in ${language}`}>{flag[language]}</td>
|
|
||||||
|
<td>
|
||||||
|
{data.languages.map((lang) => {
|
||||||
|
let langOpt = lean4gameConfig.languages.find((e) => e.iso == lang)
|
||||||
|
return <ReactCountryFlag key={`flag-${lang}`} title={langOpt?.name} countryCode={langOpt?.flag} className="emojiFlag"/>
|
||||||
|
|
||||||
|
})}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -84,114 +86,133 @@ function LandingPage() {
|
|||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [impressum, setImpressum] = React.useState(false);
|
const [impressumPopup, setImpressumPopup] = React.useState(false);
|
||||||
const openImpressum = () => setImpressum(true);
|
const [preferencesPopup, setPreferencesPopup] = React.useState(false);
|
||||||
const closeImpressum = () => setImpressum(false);
|
const [navOpen, setNavOpen] = React.useState(false);
|
||||||
|
const openImpressum = () => setImpressumPopup(true);
|
||||||
|
const closeImpressum = () => setImpressumPopup(false);
|
||||||
|
const toggleImpressum = () => setImpressumPopup(!impressumPopup);
|
||||||
|
const closePreferencesPopup = () => setPreferencesPopup(false);
|
||||||
|
const togglePreferencesPopup = () => setPreferencesPopup(!preferencesPopup);
|
||||||
|
|
||||||
|
const { t, i18n } = useTranslation()
|
||||||
|
|
||||||
|
// Load the namespaces of all games
|
||||||
|
// TODO: should `allGames` contain game-ids starting with `g/`?
|
||||||
|
i18next.loadNamespaces(lean4gameConfig.allGames.map(id => `g/${id}`))
|
||||||
|
|
||||||
|
let allTiles = lean4gameConfig.allGames.map((gameId) => {
|
||||||
|
let q = useGetGameInfoQuery({game: `g/${gameId}`})
|
||||||
|
|
||||||
|
// if (q.isError) {
|
||||||
|
// if (q.error?.originalStatus === 404) {
|
||||||
|
// // Handle 404 error
|
||||||
|
// console.log('File not found');
|
||||||
|
// } else {
|
||||||
|
// // Suppress additional console.error messages
|
||||||
|
// console.error(q.error);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
return q.data?.tile
|
||||||
|
})
|
||||||
|
|
||||||
return <div className="landing-page">
|
return <div className="landing-page">
|
||||||
<header style={{backgroundImage: `url(${bgImage})`}}>
|
<header style={{backgroundImage: `url(${bgImage})`}}>
|
||||||
<nav>
|
<nav className="landing-page-nav">
|
||||||
<GithubIcon url="https://github.com/leanprover-community/lean4game"/>
|
<GithubIcon url="https://github.com/leanprover-community/lean4game"/>
|
||||||
|
<MenuButton navOpen={navOpen} setNavOpen={setNavOpen}/>
|
||||||
|
<div className={'menu dropdown' + (navOpen ? '' : ' hidden')}>
|
||||||
|
<ImpressumButton setNavOpen={setNavOpen} toggleImpressum={toggleImpressum} isDropdown={true} />
|
||||||
|
<PreferencesButton setNavOpen={setNavOpen} togglePreferencesPopup={togglePreferencesPopup}/>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div id="main-title">
|
<div id="main-title">
|
||||||
<h1>Lean Game Server</h1>
|
<h1>{t("Lean Game Server")}</h1>
|
||||||
<p>
|
<p>
|
||||||
|
<Trans>
|
||||||
A repository of learning games for the
|
A repository of learning games for the
|
||||||
proof assistant <a target="_blank" href="https://leanprover-community.github.io/">Lean</a> <i>(Lean 4)</i> and
|
proof assistant <a target="_blank" href="https://leanprover-community.github.io/">Lean</a> <i>(Lean 4)</i> and
|
||||||
its mathematical library <a target="_blank" href="https://github.com/leanprover-community/mathlib4">mathlib</a>
|
its mathematical library <a target="_blank" href="https://github.com/leanprover-community/mathlib4">mathlib</a>
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="game-list">
|
<div className="game-list">
|
||||||
|
{allTiles.filter(x => x != null).length == 0 ?
|
||||||
<GameTile
|
<p>
|
||||||
title="Natural Number Game"
|
<Trans>
|
||||||
gameId="g/hhu-adam/NNG4"
|
No Games loaded. Use <a>http://localhost:3000/#/g/local/FOLDER</a> to open a
|
||||||
intro="The classical introduction game for Lean."
|
game directly from a local folder.
|
||||||
description="In this game you recreate the natural numbers $\mathbb{N}$ from the Peano axioms,
|
</Trans>
|
||||||
learning the basics about theorem proving in Lean.
|
</p>
|
||||||
|
: lean4gameConfig.allGames.map((id, i) => (
|
||||||
This is a good first introduction to Lean!"
|
<Tile
|
||||||
worlds="4"
|
key={id}
|
||||||
levels="30"
|
gameId={`g/${id}`}
|
||||||
language="English"
|
data={allTiles[i]}
|
||||||
/>
|
|
||||||
|
|
||||||
<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>{t("Development notes")}</h2>
|
||||||
|
<Trans>
|
||||||
<p>
|
<p>
|
||||||
As this server runs lean on our university machines, it has a limited capacity.
|
As this server runs lean on our university machines, it has a limited capacity.
|
||||||
Our current estimate is about 55 copies of the NNG or 25 copies of games importing
|
Our current estimate is about 70 simultaneous games.
|
||||||
mathlib. We hope to address this limitation in the future.
|
We hope to address and test this limitation better in the future.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Most aspects of the games and the infrastructure are still in development. Feel free to
|
Most aspects of the games and the infrastructure are still in development. Feel free to
|
||||||
file a <a target="_blank" href="https://github.com/leanprover-community/lean4game/issues">GitHub Issue</a> about
|
file a <a target="_blank" href="https://github.com/leanprover-community/lean4game/issues">GitHub Issue</a> about
|
||||||
any problems you experience!
|
any problems you experience!
|
||||||
</p>
|
</p>
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<div className="wrapper">
|
<div className="wrapper">
|
||||||
<h2>Adding new games</h2>
|
<h2>{t("Adding new games")}</h2>
|
||||||
|
<Trans>
|
||||||
<p>
|
<p>
|
||||||
If you are considering writing your own game, you should use
|
If you are considering writing your own game, you should use
|
||||||
the <a target="_blank" href="https://github.com/hhu-adam/NNG4">NNG Github Repo</a> as
|
the <a target="_blank" href="https://github.com/hhu-adam/GameSkeleton">GameSkeleton Github Repo</a> as
|
||||||
a template.
|
a template and read <a target="_blank" href="https://github.com/leanprover-community/lean4game/">How to Create a Game</a>.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
There is an option to load and run your own games directly on the server,
|
You can directly load your games into the server and play it using
|
||||||
instructions are in the NNG repo. Since this is still in development we'd like to
|
the correct URL. The <a target="_blank" href="https://github.com/leanprover-community/lean4game/">instructions above</a> also
|
||||||
encourage you to contact us for support creating your own game. The documentation is
|
explain the details for how to load your game to the server.
|
||||||
not polished yet.
|
|
||||||
|
We'd like to encourage you to contact us if you have any questions.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
To add games to this main page, you should get in contact as
|
Featured games on this page are added manually.
|
||||||
games will need to be added manually.
|
Please get in contact and we'll happily add yours.
|
||||||
</p>
|
</p>
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<div className="wrapper">
|
<div className="wrapper">
|
||||||
<h2>Funding</h2>
|
<h2>{t("Funding")}</h2>
|
||||||
<p>
|
<p>
|
||||||
|
<Trans>
|
||||||
This server has been developed as part of the
|
This server has been developed as part of the
|
||||||
project <a target="_blank" href="https://hhu-adam.github.io">ADAM : Anticipating the Digital Age of Mathematics</a> at
|
project <a target="_blank" href="https://hhu-adam.github.io">ADAM : Anticipating the Digital Age of Mathematics</a> at
|
||||||
Heinrich-Heine-Universität in Düsseldorf.
|
Heinrich-Heine-Universität in Düsseldorf.
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<footer>
|
<footer>
|
||||||
|
{/* Do not translate "Impressum", it's needed for German GDPR */}
|
||||||
<a className="link" onClick={openImpressum}>Impressum</a>
|
<a className="link" onClick={openImpressum}>Impressum</a>
|
||||||
{impressum? <PrivacyPolicyPopup handleClose={closeImpressum} />: null}
|
{impressumPopup? <PrivacyPolicyPopup handleClose={closeImpressum} />: null}
|
||||||
|
{preferencesPopup ? <PreferencesPopup handleClose={closePreferencesPopup} /> : null}
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
{/* <PrivacyPolicy/> */}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+214
-168
@@ -16,24 +16,25 @@ import { InfoviewApi } from '@leanprover/infoview'
|
|||||||
import { EditorContext } from '../../../node_modules/lean4-infoview/src/infoview/contexts'
|
import { EditorContext } from '../../../node_modules/lean4-infoview/src/infoview/contexts'
|
||||||
import { EditorConnection, EditorEvents } from '../../../node_modules/lean4-infoview/src/infoview/editorConnection'
|
import { EditorConnection, EditorEvents } from '../../../node_modules/lean4-infoview/src/infoview/editorConnection'
|
||||||
import { EventEmitter } from '../../../node_modules/lean4-infoview/src/infoview/event'
|
import { EventEmitter } from '../../../node_modules/lean4-infoview/src/infoview/event'
|
||||||
|
import { Diagnostic } from 'vscode-languageserver-types'
|
||||||
|
|
||||||
import { GameIdContext } from '../app'
|
import { GameIdContext } from '../app'
|
||||||
import { ConnectionContext, connection, useLeanClient } from '../connection'
|
|
||||||
import { useAppDispatch, useAppSelector } from '../hooks'
|
import { useAppDispatch, useAppSelector } from '../hooks'
|
||||||
import { useGetGameInfoQuery, useLoadInventoryOverviewQuery, useLoadLevelQuery } from '../state/api'
|
import { useGetGameInfoQuery, useLoadInventoryOverviewQuery, useLoadLevelQuery } from '../state/api'
|
||||||
import { changedSelection, codeEdited, selectCode, selectSelections, selectCompleted, helpEdited,
|
import { changedSelection, codeEdited, selectCode, selectSelections, selectCompleted, helpEdited,
|
||||||
selectHelp, selectDifficulty, selectInventory } from '../state/progress'
|
selectHelp, selectDifficulty, selectInventory, selectTypewriterMode, changeTypewriterMode } from '../state/progress'
|
||||||
import { store } from '../state/store'
|
import { store } from '../state/store'
|
||||||
import { Button } from './button'
|
import { Button } from './button'
|
||||||
import Markdown from './markdown'
|
import Markdown from './markdown'
|
||||||
import {InventoryPanel} from './inventory'
|
import {InventoryPanel} from './inventory'
|
||||||
import { hasInteractiveErrors } from './infoview/typewriter'
|
import { hasInteractiveErrors } from './infoview/typewriter'
|
||||||
import { DeletedChatContext, InputModeContext, MobileContext, MonacoEditorContext,
|
import { DeletedChatContext, InputModeContext, PreferencesContext, MonacoEditorContext,
|
||||||
ProofContext, ProofStep, SelectionContext, WorldLevelIdContext } from './infoview/context'
|
ProofContext, SelectionContext, WorldLevelIdContext } from './infoview/context'
|
||||||
import { DualEditor } from './infoview/main'
|
import { DualEditor } from './infoview/main'
|
||||||
import { GameHint } from './infoview/rpc_api'
|
import { GameHint, InteractiveGoalsWithHints, ProofState } from './infoview/rpc_api'
|
||||||
import { DeletedHints, Hint, Hints } from './hints'
|
import { DeletedHints, Hint, Hints, MoreHelpButton, filterHints } from './hints'
|
||||||
import { PrivacyPolicyPopup } from './popup/privacy_policy'
|
import { PrivacyPolicyPopup } from './popup/privacy_policy'
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
import '@fontsource/roboto/300.css'
|
import '@fontsource/roboto/300.css'
|
||||||
import '@fontsource/roboto/400.css'
|
import '@fontsource/roboto/400.css'
|
||||||
@@ -43,30 +44,61 @@ import 'lean4web/client/src/editor/infoview.css'
|
|||||||
import 'lean4web/client/src/editor/vscode.css'
|
import 'lean4web/client/src/editor/vscode.css'
|
||||||
import '../css/level.css'
|
import '../css/level.css'
|
||||||
import { LevelAppBar } from './app_bar'
|
import { LevelAppBar } from './app_bar'
|
||||||
|
import { LeanClient } from 'lean4web/client/src/editor/leanclient'
|
||||||
|
import { DisposingWebSocketMessageReader } from 'lean4web/client/src/reader'
|
||||||
|
import { WebSocketMessageWriter, toSocket } from 'vscode-ws-jsonrpc'
|
||||||
|
import { IConnectionProvider } from 'monaco-languageclient'
|
||||||
|
import { monacoSetup } from 'lean4web/client/src/monacoSetup'
|
||||||
|
import { onigasmH } from 'onigasm/lib/onigasmH'
|
||||||
|
import { isLastStepWithErrors, lastStepHasErrors } from './infoview/goals'
|
||||||
|
import { InfoPopup } from './popup/game_info'
|
||||||
|
import { PreferencesPopup } from './popup/preferences'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import i18next from 'i18next'
|
||||||
|
|
||||||
|
|
||||||
|
monacoSetup()
|
||||||
|
|
||||||
function Level() {
|
function Level() {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const levelId = parseInt(params.levelId)
|
const levelId = parseInt(params.levelId)
|
||||||
const worldId = params.worldId
|
const worldId = params.worldId
|
||||||
// useLoadWorldFiles(worldId)
|
|
||||||
|
|
||||||
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
|
||||||
|
// Load the namespace of the game
|
||||||
|
i18next.loadNamespaces(gameId).catch(err => {
|
||||||
|
console.warn(`translations for ${gameId} do not exist.`)
|
||||||
|
})
|
||||||
|
|
||||||
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||||
|
|
||||||
|
// pop-ups
|
||||||
const [impressum, setImpressum] = React.useState(false)
|
const [impressum, setImpressum] = React.useState(false)
|
||||||
|
const [info, setInfo] = React.useState(false)
|
||||||
|
const [preferencesPopup, setPreferencesPopup] = React.useState(false)
|
||||||
|
|
||||||
const closeImpressum = () => {
|
function closeImpressum() {setImpressum(false)}
|
||||||
setImpressum(false)
|
function closeInfo() {setInfo(false)}
|
||||||
}
|
function closePreferencesPopup() {setPreferencesPopup(false)}
|
||||||
|
function toggleImpressum() {setImpressum(!impressum)}
|
||||||
|
function toggleInfo() {setInfo(!info)}
|
||||||
|
function togglePreferencesPopup() {setPreferencesPopup(!preferencesPopup)}
|
||||||
|
|
||||||
return <WorldLevelIdContext.Provider value={{worldId, levelId}}>
|
return <WorldLevelIdContext.Provider value={{worldId, levelId}}>
|
||||||
{levelId == 0 ?
|
{levelId == 0 ?
|
||||||
<Introduction impressum={impressum} setImpressum={setImpressum} /> :
|
<Introduction impressum={impressum} setImpressum={setImpressum} toggleInfo={toggleInfo} togglePreferencesPopup={togglePreferencesPopup} /> :
|
||||||
<PlayableLevel key={`${worldId}/${levelId}`} impressum={impressum} setImpressum={setImpressum} />}
|
<PlayableLevel key={`${worldId}/${levelId}`} impressum={impressum} setImpressum={setImpressum} toggleInfo={toggleInfo} togglePreferencesPopup={togglePreferencesPopup}/>}
|
||||||
{impressum ? <PrivacyPolicyPopup handleClose={closeImpressum} /> : null}
|
{impressum ? <PrivacyPolicyPopup handleClose={closeImpressum} /> : null}
|
||||||
|
{info ? <InfoPopup info={gameInfo.data?.info} handleClose={closeInfo}/> : null}
|
||||||
|
{preferencesPopup ? <PreferencesPopup handleClose={closePreferencesPopup} /> : null}
|
||||||
</WorldLevelIdContext.Provider>
|
</WorldLevelIdContext.Provider>
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChatPanel({lastLevel}) {
|
function ChatPanel({lastLevel, visible = true}) {
|
||||||
|
let { t } = useTranslation()
|
||||||
const chatRef = useRef<HTMLDivElement>(null)
|
const chatRef = useRef<HTMLDivElement>(null)
|
||||||
const {mobile} = useContext(MobileContext)
|
const {mobile} = useContext(PreferencesContext)
|
||||||
const gameId = useContext(GameIdContext)
|
const gameId = useContext(GameIdContext)
|
||||||
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
||||||
const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
||||||
@@ -75,9 +107,7 @@ function ChatPanel({lastLevel}) {
|
|||||||
const {selectedStep, setSelectedStep} = useContext(SelectionContext)
|
const {selectedStep, setSelectedStep} = useContext(SelectionContext)
|
||||||
const completed = useAppSelector(selectCompleted(gameId, worldId, levelId))
|
const completed = useAppSelector(selectCompleted(gameId, worldId, levelId))
|
||||||
|
|
||||||
// If the last step has errors, we want to treat it as if it is part of the second-to-last step
|
let k = proof?.steps.length ? proof?.steps.length - (lastStepHasErrors(proof) ? 2 : 1) : 0
|
||||||
let k = proof.length - 1
|
|
||||||
let withErr = hasInteractiveErrors(proof[k]?.errors) ? 1 : 0
|
|
||||||
|
|
||||||
function toggleSelection(line: number) {
|
function toggleSelection(line: number) {
|
||||||
return (ev) => {
|
return (ev) => {
|
||||||
@@ -90,29 +120,6 @@ function ChatPanel({lastLevel}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasHiddenHints(i : number): boolean {
|
|
||||||
let step = proof[i]
|
|
||||||
// For example if the proof isn't loaded yet
|
|
||||||
if(!step) {return false}
|
|
||||||
return step.hints.some((hint) => hint.hidden)
|
|
||||||
}
|
|
||||||
|
|
||||||
const activateHiddenHints = (ev) => {
|
|
||||||
// If the last step (`k`) has errors, we want the hidden hints from the
|
|
||||||
// second-to-last step to be affected
|
|
||||||
if (!(proof.length)) {return}
|
|
||||||
|
|
||||||
// state must not be mutated, therefore we need to clone the set
|
|
||||||
let tmp = new Set(showHelp)
|
|
||||||
if (tmp.has(k - withErr)) {
|
|
||||||
tmp.delete(k - withErr)
|
|
||||||
} else {
|
|
||||||
tmp.add(k - withErr)
|
|
||||||
}
|
|
||||||
setShowHelp(tmp)
|
|
||||||
console.debug(`help: ${Array.from(tmp.values())}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// TODO: For some reason this is always called twice
|
// TODO: For some reason this is always called twice
|
||||||
console.debug('scroll chat')
|
console.debug('scroll chat')
|
||||||
@@ -136,57 +143,64 @@ function ChatPanel({lastLevel}) {
|
|||||||
// // chatRef.current!.scrollTo(0,0)
|
// // chatRef.current!.scrollTo(0,0)
|
||||||
// }, [gameId, worldId, levelId])
|
// }, [gameId, worldId, levelId])
|
||||||
|
|
||||||
let introText: Array<string> = level?.data?.introduction.split(/\n(\s*\n)+/)
|
let introText: Array<string> = t(level?.data?.introduction, {ns: gameId}).split(/\n(\s*\n)+/)
|
||||||
|
|
||||||
return <div className="chat-panel">
|
return <div className={`chat-panel ${visible ? '' : 'hidden'}`}>
|
||||||
<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, rawText: t, varNames: []}} step={0} selected={selectedStep} toggleSelection={toggleSelection(0)} />
|
||||||
))}
|
))}
|
||||||
{proof.map((step, i) => {
|
{proof?.steps.map((step, i) => {
|
||||||
// It the last step has errors, it will have the same hints
|
let filteredHints = filterHints(step.goals[0]?.hints, proof?.steps[i-1]?.goals[0]?.hints)
|
||||||
// as the second-to-last step. Therefore we should not display them.
|
if (step.goals.length > 0 && !isLastStepWithErrors(proof, i)) {
|
||||||
if (!(i == proof.length - 1 && withErr)) {
|
|
||||||
// TODO: Should not use index as key.
|
|
||||||
return <Hints key={`hints-${i}`}
|
return <Hints key={`hints-${i}`}
|
||||||
hints={step.hints} showHidden={showHelp.has(i)} step={i}
|
hints={filteredHints} showHidden={showHelp.has(i)} step={i}
|
||||||
selected={selectedStep} toggleSelection={toggleSelection(i)} lastLevel={i == proof.length - 1}/>
|
selected={selectedStep} toggleSelection={toggleSelection(i)} lastLevel={i == proof?.steps.length - 1}/>
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{/* {modifiedHints.map((step, i) => {
|
||||||
|
// It the last step has errors, it will have the same hints
|
||||||
|
// as the second-to-last step. Therefore we should not display them.
|
||||||
|
if (!(i == proof?.steps.length - 1 && withErr)) {
|
||||||
|
// TODO: Should not use index as key.
|
||||||
|
return <Hints key={`hints-${i}`}
|
||||||
|
hints={step} showHidden={showHelp.has(i)} step={i}
|
||||||
|
selected={selectedStep} toggleSelection={toggleSelection(i)} lastLevel={i == proof?.steps.length - 1}/>
|
||||||
|
}
|
||||||
|
})} */}
|
||||||
<DeletedHints hints={deletedChat}/>
|
<DeletedHints hints={deletedChat}/>
|
||||||
{completed &&
|
{proof?.completed &&
|
||||||
<>
|
<>
|
||||||
<div className={`message information recent step-${k}${selectedStep == k ? ' selected' : ''}`} onClick={toggleSelection(k)}>
|
<div className={`message information recent step-${k}${selectedStep == k ? ' selected' : ''}`} onClick={toggleSelection(k)}>
|
||||||
Level completed! 🎉
|
{t("Level completed! 🎉")}
|
||||||
</div>
|
</div>
|
||||||
{level?.data?.conclusion?.trim() &&
|
{level?.data?.conclusion?.trim() &&
|
||||||
<div className={`message information recent step-${k}${selectedStep == k ? ' selected' : ''}`} onClick={toggleSelection(k)}>
|
<div className={`message information recent step-${k}${selectedStep == k ? ' selected' : ''}`} onClick={toggleSelection(k)}>
|
||||||
<Markdown>{level?.data?.conclusion}</Markdown>
|
<Markdown>{t(level?.data?.conclusion, {ns: gameId})}</Markdown>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div className="button-row">
|
<div className="button-row">
|
||||||
{completed && (lastLevel ?
|
{proof?.completed && (lastLevel ?
|
||||||
<Button to={`/${gameId}`}>
|
<Button to={`/${gameId}`}>
|
||||||
<FontAwesomeIcon icon={faHome} /> Leave World
|
<FontAwesomeIcon icon={faHome} /> {t("Leave World")}
|
||||||
</Button> :
|
</Button> :
|
||||||
<Button to={`/${gameId}/world/${worldId}/level/${levelId + 1}`}>
|
<Button to={`/${gameId}/world/${worldId}/level/${levelId + 1}`}>
|
||||||
Next <FontAwesomeIcon icon={faArrowRight} />
|
{t("Next")} <FontAwesomeIcon icon={faArrowRight} />
|
||||||
</Button>)
|
</Button>)
|
||||||
}
|
}
|
||||||
{hasHiddenHints(proof.length - 1) && !showHelp.has(k - withErr) &&
|
<MoreHelpButton />
|
||||||
<Button to="" onClick={activateHiddenHints}>
|
|
||||||
Show more help!
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
function ExercisePanel({codeviewRef, visible=true}) {
|
|
||||||
|
function ExercisePanel({codeviewRef, visible=true}: {codeviewRef: React.MutableRefObject<HTMLDivElement>, visible?: boolean}) {
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
||||||
const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
||||||
@@ -198,22 +212,30 @@ function ExercisePanel({codeviewRef, visible=true}) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
function PlayableLevel({impressum, setImpressum}) {
|
function PlayableLevel({impressum, setImpressum, toggleInfo, togglePreferencesPopup}) {
|
||||||
|
let { t } = useTranslation()
|
||||||
const codeviewRef = useRef<HTMLDivElement>(null)
|
const codeviewRef = useRef<HTMLDivElement>(null)
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
||||||
const {mobile} = React.useContext(MobileContext)
|
const {mobile} = React.useContext(PreferencesContext)
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
const difficulty = useSelector(selectDifficulty(gameId))
|
|
||||||
const initialCode = useAppSelector(selectCode(gameId, worldId, levelId))
|
const initialCode = useAppSelector(selectCode(gameId, worldId, levelId))
|
||||||
const initialSelections = useAppSelector(selectSelections(gameId, worldId, levelId))
|
const initialSelections = useAppSelector(selectSelections(gameId, worldId, levelId))
|
||||||
const inventory: Array<String> = useSelector(selectInventory(gameId))
|
|
||||||
|
const typewriterMode = useSelector(selectTypewriterMode(gameId))
|
||||||
|
const 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})
|
||||||
|
|
||||||
// The state variables for the `ProofContext`
|
// The state variables for the `ProofContext`
|
||||||
const [proof, setProof] = useState<Array<ProofStep>>([])
|
const [proof, setProof] = useState<ProofState>({steps: [], diagnostics: [], completed: false, completedWithWarnings: false})
|
||||||
|
const [interimDiags, setInterimDiags] = useState<Array<Diagnostic>>([])
|
||||||
|
const [isCrashed, setIsCrashed] = useState<Boolean>(false)
|
||||||
|
|
||||||
|
|
||||||
// When deleting the proof, we want to keep to old messages around until
|
// When deleting the proof, we want to keep to old messages around until
|
||||||
// a new proof has been entered. e.g. to consult messages coming from dead ends
|
// a new proof has been entered. e.g. to consult messages coming from dead ends
|
||||||
const [deletedChat, setDeletedChat] = useState<Array<GameHint>>([])
|
const [deletedChat, setDeletedChat] = useState<Array<GameHint>>([])
|
||||||
@@ -221,12 +243,11 @@ function PlayableLevel({impressum, setImpressum}) {
|
|||||||
const [showHelp, setShowHelp] = useState<Set<number>>(new Set())
|
const [showHelp, setShowHelp] = useState<Set<number>>(new Set())
|
||||||
// Only for mobile layout
|
// Only for mobile layout
|
||||||
const [pageNumber, setPageNumber] = useState(0)
|
const [pageNumber, setPageNumber] = useState(0)
|
||||||
const [typewriterMode, setTypewriterMode] = useState(true)
|
|
||||||
// set to true to prevent switching between typewriter and editor
|
// set to true to prevent switching between typewriter and editor
|
||||||
const [lockInputMode, setLockInputMode] = useState(false)
|
const [lockEditorMode, setLockEditorMode] = 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)}
|
||||||
@@ -284,16 +305,11 @@ function PlayableLevel({impressum, setImpressum}) {
|
|||||||
// a hint at the beginning of the proof...
|
// a hint at the beginning of the proof...
|
||||||
const [selectedStep, setSelectedStep] = useState<number>()
|
const [selectedStep, setSelectedStep] = useState<number>()
|
||||||
|
|
||||||
// if the user inventory changes, notify the server
|
|
||||||
useEffect(() => {
|
|
||||||
let leanClient = connection.getLeanClient(gameId)
|
|
||||||
leanClient.sendNotification('$/game/setInventory', {inventory: inventory, difficulty: difficulty})
|
|
||||||
}, [inventory])
|
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
// Lock editor mode
|
// Lock editor mode
|
||||||
if (level?.data?.template) {
|
if (level?.data?.template) {
|
||||||
setTypewriterMode(false)
|
setLockEditorMode(true)
|
||||||
|
|
||||||
if (editor) {
|
if (editor) {
|
||||||
let code = editor.getModel().getLinesContent()
|
let code = editor.getModel().getLinesContent()
|
||||||
@@ -321,7 +337,7 @@ function PlayableLevel({impressum, setImpressum}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setTypewriterMode(true)
|
setLockEditorMode(false)
|
||||||
}
|
}
|
||||||
}, [level, levelId, worldId, gameId, editor])
|
}, [level, levelId, worldId, gameId, editor])
|
||||||
|
|
||||||
@@ -336,7 +352,7 @@ function PlayableLevel({impressum, setImpressum}) {
|
|||||||
}, [gameId, worldId, levelId])
|
}, [gameId, worldId, levelId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!typewriterMode) {
|
if (!(typewriterMode && !lockEditorMode) && editor) {
|
||||||
// Delete last input attempt from command line
|
// Delete last input attempt from command line
|
||||||
editor.executeEdits("typewriter", [{
|
editor.executeEdits("typewriter", [{
|
||||||
range: editor.getSelection(),
|
range: editor.getSelection(),
|
||||||
@@ -345,19 +361,19 @@ function PlayableLevel({impressum, setImpressum}) {
|
|||||||
}]);
|
}]);
|
||||||
editor.focus()
|
editor.focus()
|
||||||
}
|
}
|
||||||
}, [typewriterMode])
|
}, [typewriterMode, lockEditorMode])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Forget whether hidden hints are displayed for steps that don't exist yet
|
// Forget whether hidden hints are displayed for steps that don't exist yet
|
||||||
if (proof.length) {
|
if (proof?.steps.length) {
|
||||||
console.debug(Array.from(showHelp))
|
console.debug(Array.from(showHelp))
|
||||||
setShowHelp(new Set(Array.from(showHelp).filter(i => (i < proof.length))))
|
setShowHelp(new Set(Array.from(showHelp).filter(i => (i < proof?.steps.length))))
|
||||||
}
|
}
|
||||||
}, [proof])
|
}, [proof])
|
||||||
|
|
||||||
// save showed help in store
|
// save showed help in store
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (proof.length) {
|
if (proof?.steps.length) {
|
||||||
console.debug(`showHelp:\n ${showHelp}`)
|
console.debug(`showHelp:\n ${showHelp}`)
|
||||||
dispatch(helpEdited({game: gameId, world: worldId, level: levelId, help: Array.from(showHelp)}))
|
dispatch(helpEdited({game: gameId, world: worldId, level: levelId, help: Array.from(showHelp)}))
|
||||||
}
|
}
|
||||||
@@ -365,7 +381,7 @@ function PlayableLevel({impressum, setImpressum}) {
|
|||||||
|
|
||||||
// Effect when command line mode gets enabled
|
// Effect when command line mode gets enabled
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editor && typewriterMode) {
|
if (onigasmH && editor && (typewriterMode && !lockEditorMode)) {
|
||||||
let code = editor.getModel().getLinesContent().filter(line => line.trim())
|
let code = editor.getModel().getLinesContent().filter(line => line.trim())
|
||||||
editor.executeEdits("typewriter", [{
|
editor.executeEdits("typewriter", [{
|
||||||
range: editor.getModel().getFullModelRange(),
|
range: editor.getModel().getFullModelRange(),
|
||||||
@@ -388,22 +404,25 @@ function PlayableLevel({impressum, setImpressum}) {
|
|||||||
// editor.setSelection(monaco.Selection.fromPositions(endPos, endPos))
|
// editor.setSelection(monaco.Selection.fromPositions(endPos, endPos))
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
}, [editor, typewriterMode])
|
}, [editor, typewriterMode, lockEditorMode, onigasmH == null])
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<div style={level.isLoading ? null : {display: "none"}} className="app-content loading"><CircularProgress /></div>
|
<div style={level.isLoading ? null : {display: "none"}} className="app-content loading"><CircularProgress /></div>
|
||||||
<DeletedChatContext.Provider value={{deletedChat, setDeletedChat, showHelp, setShowHelp}}>
|
<DeletedChatContext.Provider value={{deletedChat, setDeletedChat, showHelp, setShowHelp}}>
|
||||||
<SelectionContext.Provider value={{selectedStep, setSelectedStep}}>
|
<SelectionContext.Provider value={{selectedStep, setSelectedStep}}>
|
||||||
<InputModeContext.Provider value={{typewriterMode, setTypewriterMode, typewriterInput, setTypewriterInput, lockInputMode, setLockInputMode}}>
|
<InputModeContext.Provider value={{typewriterMode, setTypewriterMode, typewriterInput, setTypewriterInput, lockEditorMode, setLockEditorMode}}>
|
||||||
<ProofContext.Provider value={{proof, setProof}}>
|
<ProofContext.Provider value={{proof, setProof, interimDiags, setInterimDiags, crashed: isCrashed, setCrashed: setIsCrashed}}>
|
||||||
<EditorContext.Provider value={editorConnection}>
|
<EditorContext.Provider value={editorConnection}>
|
||||||
<MonacoEditorContext.Provider value={editor}>
|
<MonacoEditorContext.Provider value={editor}>
|
||||||
<LevelAppBar
|
<LevelAppBar
|
||||||
pageNumber={pageNumber} setPageNumber={setPageNumber}
|
pageNumber={pageNumber} setPageNumber={setPageNumber}
|
||||||
isLoading={level.isLoading}
|
isLoading={level.isLoading}
|
||||||
levelTitle={`${mobile ? '' : 'Level '}${levelId} / ${gameInfo.data?.worldSize[worldId]}` +
|
levelTitle={(mobile ? "" : t("Level")) + ` ${levelId} / ${gameInfo.data?.worldSize[worldId]}` +
|
||||||
(level?.data?.title && ` : ${level?.data?.title}`)}
|
(level?.data?.title && ` : ${t(level?.data?.title, {ns: gameId})}`)}
|
||||||
toggleImpressum={toggleImpressum} />
|
toggleImpressum={toggleImpressum}
|
||||||
|
toggleInfo={toggleInfo}
|
||||||
|
togglePreferencesPopup={togglePreferencesPopup}
|
||||||
|
/>
|
||||||
{mobile?
|
{mobile?
|
||||||
// TODO: This is copied from the `Split` component below...
|
// TODO: This is copied from the `Split` component below...
|
||||||
<>
|
<>
|
||||||
@@ -432,23 +451,25 @@ function PlayableLevel({impressum, setImpressum}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function IntroductionPanel({gameInfo}) {
|
function IntroductionPanel({gameInfo}) {
|
||||||
|
let { t } = useTranslation()
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {worldId} = useContext(WorldLevelIdContext)
|
const {worldId} = useContext(WorldLevelIdContext)
|
||||||
|
const {mobile} = React.useContext(PreferencesContext)
|
||||||
|
|
||||||
let text: Array<string> = gameInfo.data?.worlds.nodes[worldId].introduction.split(/\n(\s*\n)+/)
|
let text: Array<string> = t(gameInfo.data?.worlds.nodes[worldId].introduction, {ns: gameId}).split(/\n(\s*\n)+/)
|
||||||
|
|
||||||
return <div className="chat-panel">
|
return <div className="chat-panel">
|
||||||
<div className="chat">
|
<div className="chat">
|
||||||
{text?.filter(t => t.trim()).map(((t, i) =>
|
{text?.filter(t => t.trim()).map(((t, i) =>
|
||||||
<Hint key={`intro-p-${i}`}
|
<Hint key={`intro-p-${i}`}
|
||||||
hint={{text: t, hidden: false}} step={0} selected={null} toggleSelection={undefined} />
|
hint={{text: t, hidden: false, rawText: t, varNames: []}} step={0} selected={null} toggleSelection={undefined} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="button-row">
|
<div className={`button-row${mobile ? ' mobile' : ''}`}>
|
||||||
{gameInfo.data?.worldSize[worldId] == 0 ?
|
{gameInfo.data?.worldSize[worldId] == 0 ?
|
||||||
<Button to={`/${gameId}`}><FontAwesomeIcon icon={faHome} /></Button> :
|
<Button to={`/${gameId}`}><FontAwesomeIcon icon={faHome} /></Button> :
|
||||||
<Button to={`/${gameId}/world/${worldId}/level/1`}>
|
<Button to={`/${gameId}/world/${worldId}/level/1`}>
|
||||||
Start <FontAwesomeIcon icon={faArrowRight} />
|
{t("Start")} <FontAwesomeIcon icon={faArrowRight} />
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -458,20 +479,27 @@ function IntroductionPanel({gameInfo}) {
|
|||||||
export default Level
|
export default Level
|
||||||
|
|
||||||
/** The site with the introduction text of a world */
|
/** The site with the introduction text of a world */
|
||||||
function Introduction({impressum, setImpressum}) {
|
function Introduction({impressum, setImpressum, toggleInfo, togglePreferencesPopup}) {
|
||||||
|
let { t } = useTranslation()
|
||||||
|
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {mobile} = useContext(MobileContext)
|
const {mobile} = useContext(PreferencesContext)
|
||||||
|
|
||||||
const inventory = useLoadInventoryOverviewQuery({game: gameId})
|
const inventory = useLoadInventoryOverviewQuery({game: gameId})
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<LevelAppBar isLoading={gameInfo.isLoading} levelTitle="Introduction" toggleImpressum={toggleImpressum}/>
|
<LevelAppBar isLoading={gameInfo.isLoading} levelTitle={t("Introduction")} toggleImpressum={toggleImpressum} toggleInfo={toggleInfo} togglePreferencesPopup={togglePreferencesPopup}/>
|
||||||
{gameInfo.isLoading ?
|
{gameInfo.isLoading ?
|
||||||
<div className="app-content loading"><CircularProgress /></div>
|
<div className="app-content loading"><CircularProgress /></div>
|
||||||
: mobile ?
|
: mobile ?
|
||||||
@@ -479,7 +507,12 @@ function Introduction({impressum, setImpressum}) {
|
|||||||
:
|
:
|
||||||
<Split minSize={0} snapOffset={200} sizes={[25, 50, 25]} className={`app-content level`}>
|
<Split minSize={0} snapOffset={200} sizes={[25, 50, 25]} className={`app-content level`}>
|
||||||
<IntroductionPanel gameInfo={gameInfo} />
|
<IntroductionPanel gameInfo={gameInfo} />
|
||||||
<div className="world-image-container empty"></div>
|
<div className="world-image-container empty center">
|
||||||
|
{image &&
|
||||||
|
<img className="contain" src={path.join("data", gameId, image)} alt="" />
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
<InventoryPanel levelInfo={inventory?.data} />
|
<InventoryPanel levelInfo={inventory?.data} />
|
||||||
</Split>
|
</Split>
|
||||||
}
|
}
|
||||||
@@ -512,21 +545,32 @@ function Introduction({impressum, setImpressum}) {
|
|||||||
|
|
||||||
function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChangeContent, onDidChangeSelection) {
|
function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChangeContent, onDidChangeSelection) {
|
||||||
|
|
||||||
const connection = React.useContext(ConnectionContext)
|
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
||||||
|
|
||||||
|
|
||||||
const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor|null>(null)
|
const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor|null>(null)
|
||||||
const [infoProvider, setInfoProvider] = useState<null|InfoProvider>(null)
|
const [infoProvider, setInfoProvider] = useState<null|InfoProvider>(null)
|
||||||
const [infoviewApi, setInfoviewApi] = useState<null|InfoviewApi>(null)
|
|
||||||
const [editorConnection, setEditorConnection] = useState<null|EditorConnection>(null)
|
const [editorConnection, setEditorConnection] = useState<null|EditorConnection>(null)
|
||||||
|
|
||||||
// Create Editor
|
const uriStr = `file:///${worldId}/${levelId}`
|
||||||
|
const uri = monaco.Uri.parse(uriStr)
|
||||||
|
|
||||||
|
const inventory: Array<String> = useSelector(selectInventory(gameId))
|
||||||
|
const difficulty: number = useSelector(selectDifficulty(gameId))
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const model = monaco.editor.createModel(initialCode ?? '', 'lean4', uri)
|
||||||
|
if (onDidChangeContent) {
|
||||||
|
model.onDidChangeContent(() => onDidChangeContent(model.getValue()))
|
||||||
|
}
|
||||||
|
|
||||||
const editor = monaco.editor.create(codeviewRef.current!, {
|
const editor = monaco.editor.create(codeviewRef.current!, {
|
||||||
|
model,
|
||||||
glyphMargin: true,
|
glyphMargin: true,
|
||||||
quickSuggestions: false,
|
quickSuggestions: false,
|
||||||
|
lineDecorationsWidth: 5,
|
||||||
|
folding: false,
|
||||||
|
lineNumbers: 'on',
|
||||||
lightbulb: {
|
lightbulb: {
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
@@ -538,11 +582,63 @@ function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChange
|
|||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
lineNumbersMinChars: 3,
|
lineNumbersMinChars: 3,
|
||||||
|
tabSize: 2,
|
||||||
'semanticHighlighting.enabled': true,
|
'semanticHighlighting.enabled': true,
|
||||||
theme: 'vs-code-theme-converted'
|
theme: 'vs-code-theme-converted'
|
||||||
})
|
})
|
||||||
|
if (onDidChangeSelection) {
|
||||||
|
editor.onDidChangeCursorSelection(() => onDidChangeSelection(editor.getSelections()))
|
||||||
|
}
|
||||||
|
if (initialSelections) {
|
||||||
|
console.debug("Initial Selection: ", initialSelections)
|
||||||
|
// BUG: Somehow I get an `invalid arguments` bug here
|
||||||
|
// editor.setSelections(initialSelections)
|
||||||
|
}
|
||||||
|
setEditor(editor)
|
||||||
|
const abbrevRewriter = new AbbreviationRewriter(new AbbreviationProvider(), model, editor)
|
||||||
|
|
||||||
const infoProvider = new InfoProvider(connection.getLeanClient(gameId))
|
const socketUrl = ((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + '/websocket/' + gameId
|
||||||
|
|
||||||
|
const connectionProvider : IConnectionProvider = {
|
||||||
|
get: async () => {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
console.log(`connecting ${socketUrl}`)
|
||||||
|
const websocket = new WebSocket(socketUrl)
|
||||||
|
websocket.addEventListener('error', (ev) => {
|
||||||
|
reject(ev)
|
||||||
|
})
|
||||||
|
websocket.addEventListener('message', (msg) => {
|
||||||
|
// console.log(msg.data)
|
||||||
|
})
|
||||||
|
websocket.addEventListener('open', () => {
|
||||||
|
const socket = toSocket(websocket)
|
||||||
|
const reader = new DisposingWebSocketMessageReader(socket)
|
||||||
|
const writer = new WebSocketMessageWriter(socket)
|
||||||
|
resolve({
|
||||||
|
reader,
|
||||||
|
writer
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Following `vscode-lean4/webview/index.ts`
|
||||||
|
const client = new LeanClient(connectionProvider, showRestartMessage, {inventory, difficulty})
|
||||||
|
const infoProvider = new InfoProvider(client)
|
||||||
|
// const div: HTMLElement = infoviewRef.current!
|
||||||
|
const imports = {
|
||||||
|
'@leanprover/infoview': `${window.location.origin}/index.production.min.js`,
|
||||||
|
'react': `${window.location.origin}/react.production.min.js`,
|
||||||
|
'react/jsx-runtime': `${window.location.origin}/react-jsx-runtime.production.min.js`,
|
||||||
|
'react-dom': `${window.location.origin}/react-dom.production.min.js`,
|
||||||
|
'react-popper': `${window.location.origin}/react-popper.production.min.js`
|
||||||
|
}
|
||||||
|
// loadRenderInfoview(imports, [infoProvider.getApi(), div], setInfoviewApi)
|
||||||
|
setInfoProvider(infoProvider)
|
||||||
|
|
||||||
|
// TODO: it looks like we get errors "File Changed" here.
|
||||||
|
client.restart("Lean4Game")
|
||||||
|
|
||||||
const editorApi = infoProvider.getApi()
|
const editorApi = infoProvider.getApi()
|
||||||
|
|
||||||
@@ -587,77 +683,27 @@ function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChange
|
|||||||
|
|
||||||
setEditor(editor)
|
setEditor(editor)
|
||||||
setInfoProvider(infoProvider)
|
setInfoProvider(infoProvider)
|
||||||
setInfoviewApi(infoviewApi)
|
|
||||||
|
|
||||||
return () => { infoProvider.dispose(); editor.dispose() }
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const {leanClient, leanClientStarted} = useLeanClient(gameId)
|
|
||||||
const uriStr = `file:///${worldId}/${levelId}`
|
|
||||||
const uri = monaco.Uri.parse(uriStr)
|
|
||||||
|
|
||||||
// Create model when level changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (editor && leanClientStarted) {
|
|
||||||
|
|
||||||
let model = monaco.editor.getModel(uri)
|
|
||||||
if (!model) {
|
|
||||||
model = monaco.editor.createModel(initialCode, 'lean4', uri)
|
|
||||||
}
|
|
||||||
model.onDidChangeContent(() => onDidChangeContent(model.getValue()))
|
|
||||||
editor.onDidChangeCursorSelection(() => onDidChangeSelection(editor.getSelections()))
|
|
||||||
editor.setModel(model)
|
|
||||||
if (initialSelections) {
|
|
||||||
console.debug("Initial Selection: ", initialSelections)
|
|
||||||
// BUG: Somehow I get an `invalid arguments` bug here
|
|
||||||
// editor.setSelections(initialSelections)
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
editorConnection.api.sendClientNotification(uriStr, "textDocument/didClose", {textDocument: {uri: uriStr}})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [editor, levelId, connection, leanClientStarted])
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (editor && leanClientStarted) {
|
|
||||||
|
|
||||||
let model = monaco.editor.getModel(uri)
|
|
||||||
infoviewApi.serverRestarted(leanClient.initializeResult)
|
|
||||||
|
|
||||||
infoProvider.openPreview(editor, infoviewApi)
|
infoProvider.openPreview(editor, infoviewApi)
|
||||||
|
const taskgutter = new LeanTaskGutter(infoProvider.client, editor)
|
||||||
|
|
||||||
const taskGutter = new LeanTaskGutter(infoProvider.client, editor)
|
// TODO:
|
||||||
const abbrevRewriter = new AbbreviationRewriter(new AbbreviationProvider(), model, editor)
|
// setRestart(() => restart)
|
||||||
|
|
||||||
return () => { abbrevRewriter.dispose(); taskGutter.dispose(); }
|
return () => {
|
||||||
|
editor.dispose();
|
||||||
|
model.dispose();
|
||||||
|
abbrevRewriter.dispose();
|
||||||
|
taskgutter.dispose();
|
||||||
|
infoProvider.dispose();
|
||||||
|
client.dispose();
|
||||||
|
}
|
||||||
|
}, [gameId, worldId, levelId])
|
||||||
|
|
||||||
|
const showRestartMessage = () => {
|
||||||
|
// setRestartMessage(true)
|
||||||
|
console.log("TODO: SHOW RESTART MESSAGE")
|
||||||
}
|
}
|
||||||
}, [editor, connection, leanClientStarted])
|
|
||||||
|
|
||||||
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])
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useAppDispatch } from '../../hooks'
|
|||||||
import { deleteProgress, selectProgress } from '../../state/progress'
|
import { deleteProgress, selectProgress } from '../../state/progress'
|
||||||
import { downloadFile } from '../world_tree'
|
import { downloadFile } from '../world_tree'
|
||||||
import { Button } from '../button'
|
import { Button } from '../button'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
/** download the current progress (i.e. what's saved in the browser store) */
|
/** download the current progress (i.e. what's saved in the browser store) */
|
||||||
export function downloadProgress(gameId: string, gameProgress: any, ev: React.MouseEvent) {
|
export function downloadProgress(gameId: string, gameProgress: any, ev: React.MouseEvent) {
|
||||||
@@ -25,6 +26,7 @@ export function downloadProgress(gameId: string, gameProgress: any, ev: React.Mo
|
|||||||
* controlled by the containing element.
|
* controlled by the containing element.
|
||||||
*/
|
*/
|
||||||
export function ErasePopup ({handleClose}) {
|
export function ErasePopup ({handleClose}) {
|
||||||
|
let { t } = useTranslation()
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const gameProgress = useSelector(selectProgress(gameId))
|
const gameProgress = useSelector(selectProgress(gameId))
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
@@ -43,17 +45,17 @@ export function ErasePopup ({handleClose}) {
|
|||||||
<div className="modal-backdrop" onClick={handleClose} />
|
<div className="modal-backdrop" onClick={handleClose} />
|
||||||
<div className="modal">
|
<div className="modal">
|
||||||
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
||||||
<h2>Delete Progress?</h2>
|
<h2>{t("Delete Progress?")}</h2>
|
||||||
|
<Trans>
|
||||||
<p>Do you want to delete your saved progress irreversibly?</p>
|
<p>Do you want to delete your saved progress irreversibly?</p>
|
||||||
<p>
|
<p>
|
||||||
(This deletes your proofs and your collected inventory.
|
(This deletes your proofs and your collected inventory.
|
||||||
Saves from other games are not deleted.)
|
Saves from other games are not deleted.)
|
||||||
</p>
|
</p>
|
||||||
|
</Trans>
|
||||||
<Button onClick={eraseProgress} to="">Delete</Button>
|
<Button onClick={eraseProgress} to="">{t("Delete")}</Button>
|
||||||
<Button onClick={downloadAndErase} to="">Download & Delete</Button>
|
<Button onClick={downloadAndErase} to="">{t("Download & Delete")}</Button>
|
||||||
<Button onClick={handleClose} to="">Cancel</Button>
|
<Button onClick={handleClose} to="">{t("Cancel")}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Typography } from '@mui/material'
|
import { Typography } from '@mui/material'
|
||||||
import Markdown from '../markdown'
|
import Markdown from '../markdown'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { GameIdContext } from '../../app'
|
||||||
|
|
||||||
/** Pop-up that is displaying the Game Info.
|
/** Pop-up that is displaying the Game Info.
|
||||||
*
|
*
|
||||||
@@ -11,12 +13,15 @@ import Markdown from '../markdown'
|
|||||||
* controlled by the containing element.
|
* controlled by the containing element.
|
||||||
*/
|
*/
|
||||||
export function InfoPopup ({info, handleClose}: {info: string, handleClose: () => void}) {
|
export function InfoPopup ({info, handleClose}: {info: string, handleClose: () => void}) {
|
||||||
|
let { t } = useTranslation()
|
||||||
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
|
||||||
return <div className="modal-wrapper">
|
return <div className="modal-wrapper">
|
||||||
<div className="modal-backdrop" onClick={handleClose} />
|
<div className="modal-backdrop" onClick={handleClose} />
|
||||||
<div className="modal">
|
<div className="modal">
|
||||||
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
||||||
<Typography variant="body1" component="div" className="welcome-text">
|
<Typography variant="body1" component="div" className="welcome-text">
|
||||||
<Markdown>{info}</Markdown>
|
<Markdown>{t(info, {ns: gameId})}</Markdown>
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import { Input, MenuItem, Select, SelectChangeEvent, Typography } from '@mui/material'
|
||||||
|
import Markdown from '../markdown'
|
||||||
|
import { Switch, Button, ButtonGroup } from '@mui/material';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Slider from '@mui/material/Slider';
|
||||||
|
import lean4gameConfig from '../../config.json'
|
||||||
|
|
||||||
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
|
||||||
|
import { IPreferencesContext, PreferencesContext } from "../infoview/context"
|
||||||
|
import ReactCountryFlag from 'react-country-flag';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export function PreferencesPopup({ handleClose }: { handleClose: () => void }) {
|
||||||
|
let { t } = useTranslation()
|
||||||
|
const {layout, isSavePreferences, language, setLayout, setIsSavePreferences, setLanguage} = React.useContext(PreferencesContext)
|
||||||
|
|
||||||
|
const marks = [
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
label: t('Mobile'),
|
||||||
|
key: "mobile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 1,
|
||||||
|
label: t('Auto'),
|
||||||
|
key: "auto"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 2,
|
||||||
|
label: t('Desktop'),
|
||||||
|
key: "desktop"
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handlerChangeLayout = (_: Event, value: number) => {
|
||||||
|
setLayout(marks[value].key as IPreferencesContext["layout"])
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlerChangeLanguage = (ev: SelectChangeEvent<string>) => {
|
||||||
|
setLanguage(ev.target.value as IPreferencesContext["language"])
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="modal-wrapper">
|
||||||
|
<div className="modal-backdrop" onClick={handleClose} />
|
||||||
|
<div className="modal">
|
||||||
|
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
||||||
|
<Typography variant="body1" component="div" className="settings">
|
||||||
|
<div className='preferences-category'>
|
||||||
|
<div className='category-title'>
|
||||||
|
<h3>{t("Language")}</h3>
|
||||||
|
</div>
|
||||||
|
<div className='preferences-item first leave-left-gap'>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Box sx={{ width: 300 }}>
|
||||||
|
<Select
|
||||||
|
value={language}
|
||||||
|
label={t("Language")}
|
||||||
|
onChange={handlerChangeLanguage}>
|
||||||
|
{lean4gameConfig.languages.map(lang => {return <MenuItem key={`menu-item-lang-${lang.iso}`} value={lang.iso}><ReactCountryFlag countryCode={lang.flag}/> {lang.name}</MenuItem>})}
|
||||||
|
</Select>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
label=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='preferences-category'>
|
||||||
|
<div className='category-title'>
|
||||||
|
<h3>{t("Layout")}</h3>
|
||||||
|
</div>
|
||||||
|
<div className='preferences-item first leave-left-gap'>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Box sx={{ width: 300 }}>
|
||||||
|
<Slider
|
||||||
|
aria-label={t("Always visible")}
|
||||||
|
value={marks.find(item => item.key === layout).value}
|
||||||
|
step={1}
|
||||||
|
marks={marks}
|
||||||
|
max={2}
|
||||||
|
sx={{
|
||||||
|
'& .MuiSlider-track': { display: 'none', },
|
||||||
|
}}
|
||||||
|
onChange={handlerChangeLayout}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
label=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='preferences-category tail-category'>
|
||||||
|
<div className='preferences-item'>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={isSavePreferences}
|
||||||
|
onChange={() => setIsSavePreferences(!isSavePreferences)}
|
||||||
|
name="checked"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={t("Save my settings (in the browser store)")}
|
||||||
|
labelPlacement="end"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ import * as React from 'react'
|
|||||||
*
|
*
|
||||||
* `handleClose` is the function to close it again because it's open/closed state is
|
* `handleClose` is the function to close it again because it's open/closed state is
|
||||||
* controlled by the containing element.
|
* controlled by the containing element.
|
||||||
|
*
|
||||||
|
* Note: Do not translate the Impressum!
|
||||||
*/
|
*/
|
||||||
export function PrivacyPolicyPopup ({handleClose}: {handleClose: () => void}) {
|
export function PrivacyPolicyPopup ({handleClose}: {handleClose: () => void}) {
|
||||||
return <div className="privacy-policy modal-wrapper">
|
return <div className="privacy-policy modal-wrapper">
|
||||||
@@ -57,19 +59,3 @@ export function PrivacyPolicyPopup ({handleClose}: {handleClose: () => void}) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PrivacyPolicy: React.FC = () => {
|
|
||||||
const [open, setOpen] = React.useState(false)
|
|
||||||
const handleOpen = () => setOpen(true)
|
|
||||||
const handleClose = () => setOpen(false)
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="privacy" onClick={handleOpen} title="Privacy Policy & Impressum">
|
|
||||||
<FontAwesomeIcon icon={faShield} />
|
|
||||||
<p className="p1">legal</p>
|
|
||||||
<p className="p2">notes</p>
|
|
||||||
</div>
|
|
||||||
{open ? <PrivacyPolicyPopup handleClose={handleClose} /> : null}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* @fileOverview
|
* @fileOverview
|
||||||
*/
|
*/
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
/** Pop-up that is displayed when opening the help explaining the game rules.
|
/** Pop-up that is displayed when opening the help explaining the game rules.
|
||||||
*
|
*
|
||||||
@@ -9,11 +10,14 @@ import * as React from 'react'
|
|||||||
* controlled by the containing element.
|
* controlled by the containing element.
|
||||||
*/
|
*/
|
||||||
export function RulesHelpPopup ({handleClose}: {handleClose: () => void}) {
|
export function RulesHelpPopup ({handleClose}: {handleClose: () => void}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return <div className="privacy-policy modal-wrapper">
|
return <div className="privacy-policy modal-wrapper">
|
||||||
<div className="modal-backdrop" onClick={handleClose} />
|
<div className="modal-backdrop" onClick={handleClose} />
|
||||||
<div className="modal">
|
<div className="modal">
|
||||||
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
||||||
<h2>Game Rules</h2>
|
<h2>{t("Game Rules")}</h2>
|
||||||
|
<Trans>
|
||||||
<p>
|
<p>
|
||||||
Game rules determine if it is allowed to skip levels and if the games runs checks to only
|
Game rules determine if it is allowed to skip levels and if the games runs checks to only
|
||||||
allow unlocked tactics and theorems in proofs.
|
allow unlocked tactics and theorems in proofs.
|
||||||
@@ -24,27 +28,28 @@ export function RulesHelpPopup ({handleClose}: {handleClose: () => void}) {
|
|||||||
if you unlock <code>simp</code> in a level, you can use it henceforth in any level.
|
if you unlock <code>simp</code> in a level, you can use it henceforth in any level.
|
||||||
</p>
|
</p>
|
||||||
<p>The options are:</p>
|
<p>The options are:</p>
|
||||||
|
</Trans>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col"></th>
|
<th scope="col"></th>
|
||||||
<th scope="col">levels</th>
|
<th scope="col">{t("levels")}</th>
|
||||||
<th scope="col">tactics</th>
|
<th scope="col">{t("tactics")}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">regular</th>
|
<th scope="row">{t("regular")}</th>
|
||||||
<td>🔐</td>
|
<td>🔐</td>
|
||||||
<td>🔐</td>
|
<td>🔐</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">relaxed</th>
|
<th scope="row">{t("relaxed")}</th>
|
||||||
<td>🔓</td>
|
<td>🔓</td>
|
||||||
<td>🔐</td>
|
<td>🔐</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">none</th>
|
<th scope="row">{t("none")}</th>
|
||||||
<td>🔓</td>
|
<td>🔓</td>
|
||||||
<td>🔓</td>
|
<td>🔓</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useAppDispatch } from '../../hooks'
|
|||||||
import { GameProgressState, loadProgress, selectProgress } from '../../state/progress'
|
import { GameProgressState, loadProgress, selectProgress } from '../../state/progress'
|
||||||
import { downloadFile } from '../world_tree'
|
import { downloadFile } from '../world_tree'
|
||||||
import { Button } from '../button'
|
import { Button } from '../button'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
/** Pop-up that is displaying the Game Info.
|
/** Pop-up that is displaying the Game Info.
|
||||||
*
|
*
|
||||||
@@ -15,6 +16,8 @@ import { Button } from '../button'
|
|||||||
* controlled by the containing element.
|
* controlled by the containing element.
|
||||||
*/
|
*/
|
||||||
export function UploadPopup ({handleClose}) {
|
export function UploadPopup ({handleClose}) {
|
||||||
|
let { t } = useTranslation()
|
||||||
|
|
||||||
const [file, setFile] = React.useState<File>();
|
const [file, setFile] = React.useState<File>();
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const gameProgress = useSelector(selectProgress(gameId))
|
const gameProgress = useSelector(selectProgress(gameId))
|
||||||
@@ -54,17 +57,19 @@ export function UploadPopup ({handleClose}) {
|
|||||||
<div className="modal-backdrop" onClick={handleClose} />
|
<div className="modal-backdrop" onClick={handleClose} />
|
||||||
<div className="modal">
|
<div className="modal">
|
||||||
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
||||||
<h2>Upload Saved Progress</h2>
|
<h2>{t("Upload Saved Progress")}</h2>
|
||||||
|
<Trans>
|
||||||
<p>Select a JSON file with the saved game progress to load your progress.</p>
|
<p>Select a JSON file with the saved game progress to load your progress.</p>
|
||||||
|
|
||||||
<p><b>Warning:</b> This will delete your current game progress!
|
<p><b>Warning:</b> This will delete your current game progress!
|
||||||
Consider <a className="download-link" onClick={downloadProgress} >downloading your current progress</a> first!</p>
|
Consider <a className="download-link" onClick={downloadProgress} >downloading your current progress</a> first!
|
||||||
|
</p>
|
||||||
|
</Trans>
|
||||||
<p>
|
<p>
|
||||||
<input type="file" onChange={handleFileChange}/>
|
<input type="file" onChange={handleFileChange}/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button to="" onClick={uploadProgress}>Load selected file</Button>
|
<Button to="" onClick={uploadProgress}>{t("Load selected file")}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,38 +10,44 @@ import { useAppDispatch, useAppSelector } from '../hooks'
|
|||||||
import { changedOpenedIntro, selectOpenedIntro } from '../state/progress'
|
import { changedOpenedIntro, selectOpenedIntro } from '../state/progress'
|
||||||
import { useGetGameInfoQuery, useLoadInventoryOverviewQuery } from '../state/api'
|
import { useGetGameInfoQuery, useLoadInventoryOverviewQuery } from '../state/api'
|
||||||
import { Button } from './button'
|
import { Button } from './button'
|
||||||
import { MobileContext } from './infoview/context'
|
import { PreferencesContext } from './infoview/context'
|
||||||
import { 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'
|
||||||
import { RulesHelpPopup } from './popup/rules_help'
|
import { RulesHelpPopup } from './popup/rules_help'
|
||||||
import { UploadPopup } from './popup/upload'
|
import { UploadPopup } from './popup/upload'
|
||||||
|
import { PreferencesPopup} from "./popup/preferences"
|
||||||
import { WorldTreePanel } from './world_tree'
|
import { WorldTreePanel } from './world_tree'
|
||||||
|
|
||||||
import '../css/welcome.css'
|
import '../css/welcome.css'
|
||||||
import { WelcomeAppBar } from './app_bar'
|
import { WelcomeAppBar } from './app_bar'
|
||||||
import { Hint } from './hints'
|
import { Hint } from './hints'
|
||||||
|
import i18next from 'i18next'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
|
||||||
/** the panel showing the game's introduction text */
|
/** the panel showing the game's introduction text */
|
||||||
function IntroductionPanel({introduction, setPageNumber}: {introduction: string, setPageNumber}) {
|
function IntroductionPanel({introduction, setPageNumber}: {introduction: string, setPageNumber}) {
|
||||||
const {mobile} = React.useContext(MobileContext)
|
const {mobile} = React.useContext(PreferencesContext)
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
|
||||||
|
let { t } = useTranslation()
|
||||||
|
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
// TODO: I left the setup for splitting up the introduction in place, but if it's not needed
|
// TODO: I left the setup for splitting up the introduction in place, but if it's not needed
|
||||||
// then this can be simplified.
|
// then this can be simplified.
|
||||||
|
|
||||||
// let text: Array<string> = introduction.split(/\n(\s*\n)+/)
|
// let text: Array<string> = introduction.split(/\n(\s*\n)+/)
|
||||||
let text: Array<string> = introduction ? [introduction] : []
|
let text: Array<string> = introduction ? [t(introduction, {ns : gameId})] : []
|
||||||
|
|
||||||
return <div className="column chat-panel">
|
return <div className="column chat-panel">
|
||||||
<div className="chat">
|
<div className="chat">
|
||||||
{text?.map(((t, i) =>
|
{text?.map(((t, i) =>
|
||||||
t.trim() ?
|
t.trim() ?
|
||||||
<Hint key={`intro-p-${i}`}
|
<Hint key={`intro-p-${i}`}
|
||||||
hint={{text: t, hidden: false}}
|
hint={{text: t, hidden: false, rawText: t, varNames: []}}
|
||||||
step={0} selected={null} toggleSelection={undefined} />
|
step={0} selected={null} toggleSelection={undefined} />
|
||||||
: <></>
|
: <></>
|
||||||
))}
|
))}
|
||||||
@@ -63,7 +69,13 @@ function IntroductionPanel({introduction, setPageNumber}: {introduction: string,
|
|||||||
/** main page of the game showing among others the tree of worlds/levels */
|
/** main page of the game showing among others the tree of worlds/levels */
|
||||||
function Welcome() {
|
function Welcome() {
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {mobile} = React.useContext(MobileContext)
|
|
||||||
|
// Load the namespace of the game
|
||||||
|
i18next.loadNamespaces(gameId)
|
||||||
|
|
||||||
|
const {mobile} = React.useContext(PreferencesContext)
|
||||||
|
const {layout, isSavePreferences, language, setLayout, setIsSavePreferences, setLanguage} = React.useContext(PreferencesContext)
|
||||||
|
|
||||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||||
const inventory = useLoadInventoryOverviewQuery({game: gameId})
|
const inventory = useLoadInventoryOverviewQuery({game: gameId})
|
||||||
|
|
||||||
@@ -77,15 +89,19 @@ function Welcome() {
|
|||||||
const [info, setInfo] = React.useState(false)
|
const [info, setInfo] = React.useState(false)
|
||||||
const [rulesHelp, setRulesHelp] = React.useState(false)
|
const [rulesHelp, setRulesHelp] = React.useState(false)
|
||||||
const [uploadMenu, setUploadMenu] = React.useState(false)
|
const [uploadMenu, setUploadMenu] = React.useState(false)
|
||||||
|
const [preferencesPopup, setPreferencesPopup] = React.useState(false)
|
||||||
|
|
||||||
function closeEraseMenu() {setEraseMenu(false)}
|
function closeEraseMenu() {setEraseMenu(false)}
|
||||||
function closeImpressum() {setImpressum(false)}
|
function closeImpressum() {setImpressum(false)}
|
||||||
function closeInfo() {setInfo(false)}
|
function closeInfo() {setInfo(false)}
|
||||||
function closeRulesHelp() {setRulesHelp(false)}
|
function closeRulesHelp() {setRulesHelp(false)}
|
||||||
function closeUploadMenu() {setUploadMenu(false)}
|
function closeUploadMenu() {setUploadMenu(false)}
|
||||||
|
function closePreferencesPopup() {setPreferencesPopup(false)}
|
||||||
function toggleEraseMenu() {setEraseMenu(!eraseMenu)}
|
function toggleEraseMenu() {setEraseMenu(!eraseMenu)}
|
||||||
function toggleImpressum() {setImpressum(!impressum)}
|
function toggleImpressum() {setImpressum(!impressum)}
|
||||||
function toggleInfo() {setInfo(!info)}
|
function toggleInfo() {setInfo(!info)}
|
||||||
function toggleUploadMenu() {setUploadMenu(!uploadMenu)}
|
function toggleUploadMenu() {setUploadMenu(!uploadMenu)}
|
||||||
|
function togglePreferencesPopup() {setPreferencesPopup(!preferencesPopup)}
|
||||||
|
|
||||||
// set the window title
|
// set the window title
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -101,7 +117,7 @@ function Welcome() {
|
|||||||
: <>
|
: <>
|
||||||
<WelcomeAppBar pageNumber={pageNumber} setPageNumber={setPageNumber} gameInfo={gameInfo.data} toggleImpressum={toggleImpressum}
|
<WelcomeAppBar pageNumber={pageNumber} setPageNumber={setPageNumber} gameInfo={gameInfo.data} toggleImpressum={toggleImpressum}
|
||||||
toggleEraseMenu={toggleEraseMenu} toggleUploadMenu={toggleUploadMenu}
|
toggleEraseMenu={toggleEraseMenu} toggleUploadMenu={toggleUploadMenu}
|
||||||
toggleInfo={toggleInfo} />
|
toggleInfo={toggleInfo} togglePreferencesPopup={togglePreferencesPopup}/>
|
||||||
<div className="app-content">
|
<div className="app-content">
|
||||||
{ mobile ?
|
{ mobile ?
|
||||||
<div className="welcome mobile">
|
<div className="welcome mobile">
|
||||||
@@ -128,6 +144,7 @@ function Welcome() {
|
|||||||
{eraseMenu? <ErasePopup handleClose={closeEraseMenu}/> : null}
|
{eraseMenu? <ErasePopup handleClose={closeEraseMenu}/> : null}
|
||||||
{uploadMenu? <UploadPopup handleClose={closeUploadMenu}/> : null}
|
{uploadMenu? <UploadPopup handleClose={closeUploadMenu}/> : null}
|
||||||
{info ? <InfoPopup info={gameInfo.data?.info} handleClose={closeInfo}/> : null}
|
{info ? <InfoPopup info={gameInfo.data?.info} handleClose={closeInfo}/> : null}
|
||||||
|
{preferencesPopup ? <PreferencesPopup handleClose={closePreferencesPopup} /> : null}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import { selectDifficulty, changedDifficulty, selectCompleted } from '../state/p
|
|||||||
import { store } from '../state/store'
|
import { store } from '../state/store'
|
||||||
|
|
||||||
import '../css/world_tree.css'
|
import '../css/world_tree.css'
|
||||||
|
import { PreferencesContext } from './infoview/context'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
// Settings for the world tree
|
// Settings for the world tree
|
||||||
cytoscape.use( klay )
|
cytoscape.use( klay )
|
||||||
@@ -111,6 +113,7 @@ export function WorldIcon({world, title, position, completedLevels, difficulty,
|
|||||||
difficulty: number,
|
difficulty: number,
|
||||||
worldSize: number
|
worldSize: number
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
// See level icons. Match radius computed there minus `1.2*r`
|
// See level icons. Match radius computed there minus `1.2*r`
|
||||||
const N = Math.max(worldSize, NMIN)
|
const N = Math.max(worldSize, NMIN)
|
||||||
@@ -149,7 +152,7 @@ export function WorldIcon({world, title, position, completedLevels, difficulty,
|
|||||||
width={1.42*R} height={1.42*R} transform={"translate("+ -.71*R +","+ -.71*R +")"}>
|
width={1.42*R} height={1.42*R} transform={"translate("+ -.71*R +","+ -.71*R +")"}>
|
||||||
<div className={unlocked && !completed ? "playable-world" : ''}>
|
<div className={unlocked && !completed ? "playable-world" : ''}>
|
||||||
<p className="world-title" style={{fontSize: fontSize + "px"}}>
|
<p className="world-title" style={{fontSize: fontSize + "px"}}>
|
||||||
{title ? title : world}
|
{title ? t(title, {ns: gameId}) : world}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</foreignObject>
|
</foreignObject>
|
||||||
@@ -160,7 +163,7 @@ export function WorldIcon({world, title, position, completedLevels, difficulty,
|
|||||||
>
|
>
|
||||||
<div className='world-label' style={{backgroundColor: completed ? darkgreen : unlocked ? darkblue : darkgrey}}>
|
<div className='world-label' style={{backgroundColor: completed ? darkgreen : unlocked ? darkblue : darkgrey}}>
|
||||||
<p className='world-title' style={{fontSize: MINFONT + "px"}}>
|
<p className='world-title' style={{fontSize: MINFONT + "px"}}>
|
||||||
{title ? title : world}
|
{title ? t(title, {ns: gameId}) : world}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</foreignObject>}
|
</foreignObject>}
|
||||||
@@ -194,26 +197,29 @@ export const downloadFile = ({ data, fileName, fileType } :
|
|||||||
|
|
||||||
/** The menu that is shown next to the world selection graph */
|
/** The menu that is shown next to the world selection graph */
|
||||||
export function WorldSelectionMenu({rulesHelp, setRulesHelp}) {
|
export function WorldSelectionMenu({rulesHelp, setRulesHelp}) {
|
||||||
|
const { t, i18n } = useTranslation()
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const difficulty = useSelector(selectDifficulty(gameId))
|
const difficulty = useSelector(selectDifficulty(gameId))
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
const { mobile } = React.useContext(PreferencesContext)
|
||||||
|
|
||||||
|
|
||||||
function label(x : number) {
|
function label(x : number) {
|
||||||
return x == 0 ? 'none' : x == 1 ? 'relaxed' : 'regular'
|
return x == 0 ? t("none") : x == 1 ? t("relaxed") : t("regular")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return <nav className="world-selection-menu">
|
return <nav className={`world-selection-menu${mobile ? '' : ' desktop'}`}>
|
||||||
<div className="slider-wrap">
|
<div className="slider-wrap">
|
||||||
<span className="difficulty-label">Rules
|
<span className="difficulty-label">{t("Rules")}
|
||||||
<FontAwesomeIcon icon={rulesHelp ? faXmark : faCircleQuestion} className='helpButton' onClick={() => (setRulesHelp(!rulesHelp))}/>
|
<FontAwesomeIcon icon={rulesHelp ? faXmark : faCircleQuestion} className='helpButton' onClick={() => (setRulesHelp(!rulesHelp))}/>
|
||||||
</span>
|
</span>
|
||||||
<Slider
|
<Slider
|
||||||
orientation="vertical"
|
orientation="vertical"
|
||||||
title="Game Rules"
|
title={t("Game Rules")}
|
||||||
min={0} max={2}
|
min={0} max={2}
|
||||||
aria-label="Game Rules"
|
aria-label={t("Game Rules")}
|
||||||
defaultValue={difficulty}
|
value={difficulty}
|
||||||
marks={[
|
marks={[
|
||||||
{value: 0, label: label(0)},
|
{value: 0, label: label(0)},
|
||||||
{value: 1, label: label(1)},
|
{value: 1, label: label(1)},
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"allGames": [
|
||||||
|
"leanprover-community/nng4",
|
||||||
|
"hhu-adam/robo",
|
||||||
|
"djvelleman/stg4",
|
||||||
|
"trequetrum/lean4game-logic"
|
||||||
|
],
|
||||||
|
|
||||||
|
"languages": [
|
||||||
|
{
|
||||||
|
"iso": "en",
|
||||||
|
"flag": "GB",
|
||||||
|
"name": "English"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iso": "de",
|
||||||
|
"flag": "DE",
|
||||||
|
"name": "Deutsch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iso": "zh",
|
||||||
|
"flag": "CN",
|
||||||
|
"name": "中文"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileOverview todo
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
|
|
||||||
import { LeanClient } from 'lean4web/client/src/editor/leanclient';
|
|
||||||
|
|
||||||
export class Connection {
|
|
||||||
private game: string = undefined // We only keep a connection to a single game at a time
|
|
||||||
private leanClient: LeanClient = null
|
|
||||||
|
|
||||||
getLeanClient(game): LeanClient {
|
|
||||||
if (this.game !== game) {
|
|
||||||
if (this.leanClient) {
|
|
||||||
this.leanClient.stop() // Stop previous Lean client
|
|
||||||
}
|
|
||||||
this.game = game
|
|
||||||
// Start a new Lean client for the new `gameId`.
|
|
||||||
const socketUrl = ((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + '/websocket/' + game
|
|
||||||
const uri = monaco.Uri.parse('file:///')
|
|
||||||
this.leanClient = new LeanClient(socketUrl, undefined, uri, () => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.leanClient
|
|
||||||
}
|
|
||||||
|
|
||||||
/** If not already started, starts the Lean client. resolves the returned promise as soon as a
|
|
||||||
* Lean client is running.
|
|
||||||
*/
|
|
||||||
startLeanClient = (game) => {
|
|
||||||
return new Promise<LeanClient>((resolve) => {
|
|
||||||
const leanClient = this.getLeanClient(game)
|
|
||||||
if (leanClient.isRunning()) {
|
|
||||||
resolve(leanClient)
|
|
||||||
} else {
|
|
||||||
if (!leanClient.isStarted()) {
|
|
||||||
leanClient.start()
|
|
||||||
}
|
|
||||||
leanClient.restarted(() => {
|
|
||||||
// This keep alive message is not recognized by the server,
|
|
||||||
// but it makes sure that the websocket connection does not
|
|
||||||
// time out after 60 seconds.
|
|
||||||
setInterval(() => {leanClient.sendNotification('$/keepAlive', {}) }, 5000)
|
|
||||||
resolve(leanClient)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const connection = new Connection()
|
|
||||||
|
|
||||||
export const ConnectionContext = React.createContext(null);
|
|
||||||
|
|
||||||
export const useLeanClient = (gameId) => {
|
|
||||||
const leanClient = connection.getLeanClient(gameId)
|
|
||||||
const [leanClientStarted, setLeanClientStarted] = React.useState(leanClient.isStarted())
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const t1 = leanClient.restarted(() => { console.log("START"); setLeanClientStarted(true) })
|
|
||||||
const t2 = leanClient.stopped(() => { console.log("STOP"); setLeanClientStarted(false) })
|
|
||||||
|
|
||||||
return () => {t1.dispose(); t2.dispose()}
|
|
||||||
}, [leanClient, setLeanClientStarted])
|
|
||||||
|
|
||||||
return {leanClientStarted, leanClient}
|
|
||||||
}
|
|
||||||
@@ -105,11 +105,18 @@ em {
|
|||||||
position: relative;
|
position: relative;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
padding: 1.1em;
|
padding: 1.1em;
|
||||||
filter: drop-shadow(0 0 5px rgba(0,0,0,0.5));
|
filter: drop-shadow(0 0 5px rgba(0,0,0,0.5));
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-bar > .app-bar-left{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
.app-bar-title, .app-bar-subtitle {
|
.app-bar-title, .app-bar-subtitle {
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|||||||
@@ -41,6 +41,13 @@
|
|||||||
.level-completed {
|
.level-completed {
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
padding-left: .5em;
|
||||||
|
padding-right: .5em;
|
||||||
|
padding-top: .2em;
|
||||||
|
padding-bottom: .2em;
|
||||||
|
border-radius: .5em;
|
||||||
|
background-color: #eee;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.typewriter {
|
.typewriter {
|
||||||
@@ -188,6 +195,18 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.exercise .failed-command {
|
||||||
|
background-color: #eee;
|
||||||
|
padding: .5em;
|
||||||
|
border-radius: .2em;
|
||||||
|
/* TODO: It seems my browsers merge the margings of the proof steps,
|
||||||
|
so that it only shows once 0.5rem instead of twice. Thus have 1.5 here now.
|
||||||
|
*/
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
.exercise .command-text {
|
.exercise .command-text {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
@@ -199,3 +218,10 @@
|
|||||||
.undo-button {
|
.undo-button {
|
||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.crashed_message {
|
||||||
|
color: #D8000C;
|
||||||
|
font-weight: bold;
|
||||||
|
padding-left: .5em;
|
||||||
|
padding-right: .5em;
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,7 +26,11 @@
|
|||||||
.inventory .item {
|
.inventory .item {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: solid 1px #777;
|
border: solid 1px #777;
|
||||||
padding: .1em .5em;
|
padding-left: .5rem;
|
||||||
|
padding-right: 1.0rem;
|
||||||
|
padding-top: .1rem;
|
||||||
|
padding-bottom: .1rem;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inventory .item.locked {
|
.inventory .item.locked {
|
||||||
@@ -72,3 +76,21 @@
|
|||||||
color: black;
|
color: black;
|
||||||
border-bottom: 0.3em solid #999;
|
border-bottom: 0.3em solid #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.inventory .item .copy-button {
|
||||||
|
min-width: 3px;
|
||||||
|
min-height: 3px;
|
||||||
|
display: inline-block;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 0.6em;
|
||||||
|
padding-right: .2rem;
|
||||||
|
vertical-align: top;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 1rem;
|
||||||
|
align-items: end;
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ a {
|
|||||||
|
|
||||||
@viewport {
|
@viewport {
|
||||||
width: device-width ;
|
width: device-width ;
|
||||||
zoom: 1.0 ;
|
initial-scale: 1.0 ;
|
||||||
}
|
}
|
||||||
|
|
||||||
.landing-page {
|
.landing-page {
|
||||||
@@ -36,6 +36,13 @@ a {
|
|||||||
padding-bottom: 80px;
|
padding-bottom: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.landing-page-nav {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu-btn {
|
||||||
|
background-color: unset;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 440px) {
|
@media screen and (max-width: 440px) {
|
||||||
.game-list {
|
.game-list {
|
||||||
@@ -48,6 +55,7 @@ a {
|
|||||||
border: 1px solid rgb(140, 140, 140);
|
border: 1px solid rgb(140, 140, 140);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
box-shadow: 5px 5px 8px rgb(140, 140, 140);
|
box-shadow: 5px 5px 8px rgb(140, 140, 140);
|
||||||
|
width: 100%;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -181,6 +189,8 @@ footer .link {
|
|||||||
|
|
||||||
.github-link {
|
.github-link {
|
||||||
height: 24px; /* TODO: why do I need that? s*/
|
height: 24px; /* TODO: why do I need that? s*/
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.landing-page > section {
|
.landing-page > section {
|
||||||
|
|||||||
@@ -232,6 +232,20 @@ td code {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
} */
|
} */
|
||||||
|
|
||||||
|
.button-row.mobile {
|
||||||
|
margin: .5rem;
|
||||||
|
padding-top: .2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-row.mobile .btn {
|
||||||
|
padding: .5em;
|
||||||
|
border-radius: .2em;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.typewriter-interface {
|
.typewriter-interface {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
@@ -317,11 +331,6 @@ td code {
|
|||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#home-btn {
|
|
||||||
margin-right: .5em;
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu.dropdown .svg-inline--fa {
|
.menu.dropdown .svg-inline--fa {
|
||||||
width: 1.8rem;
|
width: 1.8rem;
|
||||||
}
|
}
|
||||||
@@ -336,6 +345,27 @@ td code {
|
|||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.world-image-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0px; /* somehow this has a desired affect, but why? */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.world-image-container img.contain {
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.world-image-container.center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.world-image-container img.cover {
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
.typewriter-interface .proof {
|
.typewriter-interface .proof {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
@@ -344,3 +374,22 @@ td code {
|
|||||||
min-width: 40px;
|
min-width: 40px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Fixes https://github.com/leanprover-community/lean4game/issues/202 */
|
||||||
|
.katex-mathml {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DEBUG */
|
||||||
|
/* .proof .step {
|
||||||
|
border: 2px solid rgb(0, 123, 255);
|
||||||
|
} */
|
||||||
|
|
||||||
|
.nav-btns {
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btns .language-btn {
|
||||||
|
background: #DDF6FF;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|||||||
@@ -187,3 +187,15 @@ h5, h6 {
|
|||||||
margin-left: 0.3rem;
|
margin-left: 0.3rem;
|
||||||
margin-right: 0.3rem;
|
margin-right: 0.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preferences-category.tail-category{
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences-item.first{
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences-item.leave-left-gap{
|
||||||
|
margin-left: 3em;
|
||||||
|
}
|
||||||
@@ -49,7 +49,6 @@ svg .disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.world-selection-menu {
|
.world-selection-menu {
|
||||||
position: absolute;
|
|
||||||
right: 1em;
|
right: 1em;
|
||||||
top: 1em;
|
top: 1em;
|
||||||
/* margin: 1em; */
|
/* margin: 1em; */
|
||||||
@@ -60,6 +59,10 @@ svg .disabled {
|
|||||||
filter: drop-shadow(4px 4px 5px rgba(0,0,0,0.5));
|
filter: drop-shadow(4px 4px 5px rgba(0,0,0,0.5));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.world-selection-menu.desktop {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
.world-selection-menu .btn, .welcome .btn {
|
.world-selection-menu .btn, .welcome .btn {
|
||||||
min-width: 5em;
|
min-width: 5em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import i18n from "i18next";
|
||||||
|
import Backend from "i18next-http-backend"
|
||||||
|
import { initReactI18next } from "react-i18next";
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(initReactI18next)
|
||||||
|
.use(Backend)
|
||||||
|
.init({
|
||||||
|
ns: ['translation'],
|
||||||
|
backend: {
|
||||||
|
// > see https://github.com/i18next/i18next-http-backend
|
||||||
|
loadPath: function(lngs, namespaces: Array<string>) {
|
||||||
|
if (namespaces[0].startsWith("g/")) {
|
||||||
|
return '/i18n/{{ns}}/{{lng}}/Game.json';
|
||||||
|
} else {
|
||||||
|
return '/locales/{{lng}}/{{ns}}.json';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// > language to use, more information here:
|
||||||
|
// > https://www.i18next.com/overview/configuration-options#languages-namespaces-resources
|
||||||
|
lng: "en",
|
||||||
|
// we use natural language keys, so we don't need a fallback language.
|
||||||
|
fallbackLng: false,
|
||||||
|
// > you can use the i18n.changeLanguage function to change the language manually:
|
||||||
|
// > https://www.i18next.com/overview/api#changelanguage
|
||||||
|
// > if you're using a language detector, do not define the lng option
|
||||||
|
returnEmptyString: false,
|
||||||
|
interpolation: {
|
||||||
|
// > react already safes from xss
|
||||||
|
escapeValue: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
+13
-15
@@ -1,7 +1,6 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import App from './app'
|
import App from './app'
|
||||||
import { ConnectionContext, connection } from './connection'
|
|
||||||
import { store } from './state/store'
|
import { store } from './state/store'
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import type { RouteObject } from "react-router"
|
import type { RouteObject } from "react-router"
|
||||||
@@ -10,20 +9,9 @@ import ErrorPage from './components/error_page'
|
|||||||
import Welcome from './components/welcome'
|
import Welcome from './components/welcome'
|
||||||
import LandingPage from './components/landing_page'
|
import LandingPage from './components/landing_page'
|
||||||
import Level from './components/level'
|
import Level from './components/level'
|
||||||
import { monacoSetup } from 'lean4web/client/src/monacoSetup'
|
import './i18n';
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
@@ -32,8 +20,15 @@ let root_object: RouteObject = single_game ? {
|
|||||||
path: "/",
|
path: "/",
|
||||||
loader: () => redirect("/g/local/game")
|
loader: () => redirect("/g/local/game")
|
||||||
} : {
|
} : {
|
||||||
|
path: "/",
|
||||||
|
element: <App />,
|
||||||
|
errorElement: <ErrorPage />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
element: <LandingPage />,
|
element: <LandingPage />,
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const router = createHashRouter([
|
const router = createHashRouter([
|
||||||
@@ -43,6 +38,11 @@ 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 />,
|
||||||
@@ -65,9 +65,7 @@ const root = createRoot(container!);
|
|||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ConnectionContext.Provider value={connection}>
|
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</ConnectionContext.Provider>
|
|
||||||
</Provider>
|
</Provider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|||||||
+23
-26
@@ -2,16 +2,29 @@
|
|||||||
* @fileOverview Define API of the server-client communication
|
* @fileOverview Define API of the server-client communication
|
||||||
*/
|
*/
|
||||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
||||||
import { Connection } from '../connection'
|
|
||||||
|
|
||||||
|
export interface GameTile {
|
||||||
|
title: string
|
||||||
|
short: string
|
||||||
|
long: string
|
||||||
|
languages: Array<string>
|
||||||
|
prerequisites: Array<string>
|
||||||
|
worlds: number
|
||||||
|
levels: number
|
||||||
|
image: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface GameInfo {
|
export interface GameInfo {
|
||||||
title: null|string,
|
title: null|string,
|
||||||
introduction: null|string,
|
introduction: null|string,
|
||||||
info: null|string,
|
info: null|string,
|
||||||
worlds: null|{nodes: {[id:string]: {id: string, title: string, introduction: string}}, edges: string[][]},
|
worlds: null|{nodes: {[id:string]: {id: string, title: string, introduction: string, image: string}}, edges: string[][]},
|
||||||
worldSize: null|{[key: string]: number},
|
worldSize: null|{[key: string]: number},
|
||||||
authors: null|string[],
|
authors: null|string[],
|
||||||
conclusion: null|string,
|
conclusion: null|string,
|
||||||
|
tile: null|GameTile,
|
||||||
|
image: null|string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InventoryTile {
|
export interface InventoryTile {
|
||||||
@@ -22,6 +35,7 @@ export interface InventoryTile {
|
|||||||
locked: boolean,
|
locked: boolean,
|
||||||
new: boolean,
|
new: boolean,
|
||||||
hidden: boolean
|
hidden: boolean
|
||||||
|
altTitle: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LevelInfo {
|
export interface LevelInfo {
|
||||||
@@ -37,7 +51,8 @@ export interface LevelInfo {
|
|||||||
lemmaTab: null|string,
|
lemmaTab: null|string,
|
||||||
statementName: null|string,
|
statementName: null|string,
|
||||||
displayName: null|string,
|
displayName: null|string,
|
||||||
template: null|string
|
template: null|string,
|
||||||
|
image: null|string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Used to display the inventory on the welcome page */
|
/** Used to display the inventory on the welcome page */
|
||||||
@@ -57,40 +72,22 @@ interface Doc {
|
|||||||
category: string,
|
category: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const customBaseQuery = async (
|
|
||||||
args : {game: string, method: string, params?: any},
|
|
||||||
{ signal, dispatch, getState, extra },
|
|
||||||
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: customBaseQuery,
|
baseQuery: fetchBaseQuery({ baseUrl: window.location.origin + "/data" }),
|
||||||
endpoints: (builder) => ({
|
endpoints: (builder) => ({
|
||||||
getGameInfo: builder.query<GameInfo, {game: string}>({
|
getGameInfo: builder.query<GameInfo, {game: string}>({
|
||||||
query: ({game}) => {return {game, method: 'info', params: {}}},
|
query: ({game}) => `${game}/game.json`,
|
||||||
}),
|
}),
|
||||||
loadLevel: builder.query<LevelInfo, {game: string, world: string, level: number}>({
|
loadLevel: builder.query<LevelInfo, {game: string, world: string, level: number}>({
|
||||||
query: ({game, world, level}) => {return {game, method: "loadLevel", params: {world, level}}},
|
query: ({game, world, level}) => `${game}/level__${world}__${level}.json`,
|
||||||
}),
|
}),
|
||||||
loadInventoryOverview: builder.query<InventoryOverview, {game: string}>({
|
loadInventoryOverview: builder.query<InventoryOverview, {game: string}>({
|
||||||
query: ({game}) => {return {game, method: "loadInventoryOverview", params: {}}},
|
query: ({game}) => `${game}/inventory.json`,
|
||||||
}),
|
}),
|
||||||
loadDoc: builder.query<Doc, {game: string, name: string, type: "lemma"|"tactic"}>({
|
loadDoc: builder.query<Doc, {game: string, name: string, type: "lemma"|"tactic"}>({
|
||||||
query: ({game, name, type}) => {return {game, method: "loadDoc", params: {name, type}}},
|
query: ({game, type, name}) => `${game}/doc__${type}__${name}.json`,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { useAppDispatch, useAppSelector } from "../../hooks";
|
||||||
|
import {
|
||||||
|
PreferencesState,
|
||||||
|
setLayout as setPreferencesLayout,
|
||||||
|
setIsSavePreferences as setPreferencesIsSavePreferences,
|
||||||
|
setLanguage as setLanguagePreferences,
|
||||||
|
getWindowDimensions,
|
||||||
|
AUTO_SWITCH_THRESHOLD
|
||||||
|
} from "../preferences";
|
||||||
|
|
||||||
|
|
||||||
|
const UsePreferences = () => {
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
const [mobile, setMobile] = React.useState<boolean>()
|
||||||
|
|
||||||
|
const layout = useAppSelector((state) => state.preferences.layout);
|
||||||
|
const setLayout = (layout: PreferencesState["layout"]) => dispatch(setPreferencesLayout(layout))
|
||||||
|
|
||||||
|
const isSavePreferences = useAppSelector((state) => state.preferences.isSavePreferences);
|
||||||
|
const setIsSavePreferences = (isSave: boolean) => dispatch(setPreferencesIsSavePreferences(isSave))
|
||||||
|
|
||||||
|
const language = useAppSelector((state) => state.preferences.language);
|
||||||
|
const setLanguage = (lang: string) => dispatch(setLanguagePreferences(lang))
|
||||||
|
|
||||||
|
const automaticallyAdjustLayout = () => {
|
||||||
|
const {width} = getWindowDimensions()
|
||||||
|
setMobile(width < AUTO_SWITCH_THRESHOLD)
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(()=>{
|
||||||
|
if (layout === "auto"){
|
||||||
|
void automaticallyAdjustLayout()
|
||||||
|
window.addEventListener('resize', automaticallyAdjustLayout)
|
||||||
|
|
||||||
|
return () => window.removeEventListener('resize', automaticallyAdjustLayout)
|
||||||
|
} else {
|
||||||
|
setMobile(layout === "mobile")
|
||||||
|
}
|
||||||
|
}, [layout])
|
||||||
|
|
||||||
|
return {mobile, layout, isSavePreferences, language, setLayout, setIsSavePreferences, setLanguage}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UsePreferences;
|
||||||
@@ -36,3 +36,33 @@ export async function saveState(state: any) {
|
|||||||
// Ignore
|
// Ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PREFERENCES_KEY = "preferences"
|
||||||
|
|
||||||
|
/** Load from browser storage */
|
||||||
|
export function loadPreferences() {
|
||||||
|
try {
|
||||||
|
const serializedState = localStorage.getItem(PREFERENCES_KEY);
|
||||||
|
return JSON.parse(serializedState)
|
||||||
|
} catch (e) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function savePreferences(state: any) {
|
||||||
|
try {
|
||||||
|
const serializedState = JSON.stringify(state)
|
||||||
|
localStorage.setItem(PREFERENCES_KEY, serializedState);
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removePreferences() {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(PREFERENCES_KEY);
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
import { loadPreferences, removePreferences, savePreferences } from "./local_storage";
|
||||||
|
|
||||||
|
export interface PreferencesState {
|
||||||
|
layout: "mobile" | "auto" | "desktop";
|
||||||
|
isSavePreferences: boolean;
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWindowDimensions() {
|
||||||
|
const {innerWidth: width, innerHeight: height } = window
|
||||||
|
return {width, height}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AUTO_SWITCH_THRESHOLD = 800
|
||||||
|
|
||||||
|
const initialState: PreferencesState = loadPreferences() ??{
|
||||||
|
layout: "auto",
|
||||||
|
isSavePreferences: false,
|
||||||
|
language: "en",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const preferencesSlice = createSlice({
|
||||||
|
name: "preferences",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setLayout: (state, action) => {
|
||||||
|
state.layout = action.payload;
|
||||||
|
},
|
||||||
|
setIsSavePreferences: (state, action) => {
|
||||||
|
state.isSavePreferences = action.payload;
|
||||||
|
},
|
||||||
|
setLanguage: (state, action) => {
|
||||||
|
state.language = action.payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setLayout, setIsSavePreferences, setLanguage } = preferencesSlice.actions;
|
||||||
@@ -26,7 +26,8 @@ export interface GameProgressState {
|
|||||||
inventory: string[],
|
inventory: string[],
|
||||||
difficulty: number,
|
difficulty: number,
|
||||||
openedIntro: boolean,
|
openedIntro: boolean,
|
||||||
data: WorldProgressState
|
data: WorldProgressState,
|
||||||
|
typewriterMode?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,22 +53,22 @@ const initalLevelProgressState: LevelProgressState = {code: "", completed: false
|
|||||||
|
|
||||||
/** Add an empty skeleton with progress for the current game */
|
/** Add an empty skeleton with progress for the current game */
|
||||||
function addGameProgress (state: ProgressState, action: PayloadAction<{game: string}>) {
|
function addGameProgress (state: ProgressState, action: PayloadAction<{game: string}>) {
|
||||||
if (!state.games[action.payload.game]) {
|
if (!state.games[action.payload.game.toLowerCase()]) {
|
||||||
state.games[action.payload.game] = {inventory: [], openedIntro: true, data: {}, difficulty: DEFAULT_DIFFICULTY}
|
state.games[action.payload.game.toLowerCase()] = {inventory: [], openedIntro: true, data: {}, difficulty: DEFAULT_DIFFICULTY}
|
||||||
}
|
}
|
||||||
if (!state.games[action.payload.game].data) {
|
if (!state.games[action.payload.game.toLowerCase()].data) {
|
||||||
state.games[action.payload.game].data = {}
|
state.games[action.payload.game.toLowerCase()].data = {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Add an empty skeleton with progress for the current level */
|
/** Add an empty skeleton with progress for the current level */
|
||||||
function addLevelProgress(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number}>) {
|
function addLevelProgress(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number}>) {
|
||||||
addGameProgress(state, action)
|
addGameProgress(state, action)
|
||||||
if (!state.games[action.payload.game].data[action.payload.world]) {
|
if (!state.games[action.payload.game.toLowerCase()].data[action.payload.world]) {
|
||||||
state.games[action.payload.game].data[action.payload.world] = {}
|
state.games[action.payload.game.toLowerCase()].data[action.payload.world] = {}
|
||||||
}
|
}
|
||||||
if (!state.games[action.payload.game].data[action.payload.world][action.payload.level]) {
|
if (!state.games[action.payload.game.toLowerCase()].data[action.payload.world][action.payload.level]) {
|
||||||
state.games[action.payload.game].data[action.payload.world][action.payload.level] = {...initalLevelProgressState}
|
state.games[action.payload.game.toLowerCase()].data[action.payload.world][action.payload.level] = {...initalLevelProgressState}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,125 +79,137 @@ export const progressSlice = createSlice({
|
|||||||
/** put edited code in the state and set completed to false */
|
/** put edited code in the state and set completed to false */
|
||||||
codeEdited(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number, code: string}>) {
|
codeEdited(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number, code: string}>) {
|
||||||
addLevelProgress(state, action)
|
addLevelProgress(state, action)
|
||||||
state.games[action.payload.game].data[action.payload.world][action.payload.level].code = action.payload.code
|
state.games[action.payload.game.toLowerCase()].data[action.payload.world][action.payload.level].code = action.payload.code
|
||||||
state.games[action.payload.game].data[action.payload.world][action.payload.level].completed = false
|
state.games[action.payload.game.toLowerCase()].data[action.payload.world][action.payload.level].completed = false
|
||||||
},
|
},
|
||||||
/** TODO: docstring */
|
/** TODO: docstring */
|
||||||
changedSelection(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number, selections: Selection[]}>) {
|
changedSelection(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number, selections: Selection[]}>) {
|
||||||
addLevelProgress(state, action)
|
addLevelProgress(state, action)
|
||||||
state.games[action.payload.game].data[action.payload.world][action.payload.level].selections = action.payload.selections
|
state.games[action.payload.game.toLowerCase()].data[action.payload.world][action.payload.level].selections = action.payload.selections
|
||||||
},
|
},
|
||||||
/** mark level as completed */
|
/** mark level as completed */
|
||||||
levelCompleted(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number}>) {
|
levelCompleted(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number}>) {
|
||||||
addLevelProgress(state, action)
|
addLevelProgress(state, action)
|
||||||
state.games[action.payload.game].data[action.payload.world][action.payload.level].completed = true
|
state.games[action.payload.game.toLowerCase()].data[action.payload.world][action.payload.level].completed = true
|
||||||
},
|
},
|
||||||
/** Set the list of rows where help is displayed */
|
/** Set the list of rows where help is displayed */
|
||||||
helpEdited(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number, help: number[]}>) {
|
helpEdited(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number, help: number[]}>) {
|
||||||
addLevelProgress(state, action)
|
addLevelProgress(state, action)
|
||||||
console.debug(`!setting help to: ${action.payload.help}`)
|
console.debug(`!setting help to: ${action.payload.help}`)
|
||||||
state.games[action.payload.game].data[action.payload.world][action.payload.level].help = action.payload.help
|
state.games[action.payload.game.toLowerCase()].data[action.payload.world][action.payload.level].help = action.payload.help
|
||||||
},
|
},
|
||||||
/** delete all progress for this game */
|
/** delete all progress for this game */
|
||||||
deleteProgress(state: ProgressState, action: PayloadAction<{game: string}>) {
|
deleteProgress(state: ProgressState, action: PayloadAction<{game: string}>) {
|
||||||
state.games[action.payload.game] = {inventory: [], data: {}, openedIntro: true, difficulty: DEFAULT_DIFFICULTY}
|
state.games[action.payload.game.toLowerCase()] = {inventory: [], data: {}, openedIntro: true, difficulty: DEFAULT_DIFFICULTY}
|
||||||
},
|
},
|
||||||
/** delete progress for this level */
|
/** delete progress for this level */
|
||||||
deleteLevelProgress(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number}>) {
|
deleteLevelProgress(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number}>) {
|
||||||
addLevelProgress(state, action)
|
addLevelProgress(state, action)
|
||||||
state.games[action.payload.game].data[action.payload.world][action.payload.level] = initalLevelProgressState
|
state.games[action.payload.game.toLowerCase()].data[action.payload.world][action.payload.level] = initalLevelProgressState
|
||||||
},
|
},
|
||||||
/** load progress, e.g. from external import */
|
/** load progress, e.g. from external import */
|
||||||
loadProgress(state: ProgressState, action: PayloadAction<{game: string, data:GameProgressState}>) {
|
loadProgress(state: ProgressState, action: PayloadAction<{game: string, data:GameProgressState}>) {
|
||||||
console.debug(`setting data to:\n ${action.payload.data}`)
|
console.debug(`setting data to:\n ${action.payload.data}`)
|
||||||
state.games[action.payload.game] = action.payload.data
|
state.games[action.payload.game.toLowerCase()] = action.payload.data
|
||||||
},
|
},
|
||||||
/** set the current inventory */
|
/** set the current inventory */
|
||||||
changedInventory(state: ProgressState, action: PayloadAction<{game: string, inventory: string[]}>) {
|
changedInventory(state: ProgressState, action: PayloadAction<{game: string, inventory: string[]}>) {
|
||||||
addGameProgress(state, action)
|
addGameProgress(state, action)
|
||||||
state.games[action.payload.game].inventory = action.payload.inventory
|
state.games[action.payload.game.toLowerCase()].inventory = action.payload.inventory
|
||||||
},
|
},
|
||||||
/** set the difficulty */
|
/** set the difficulty */
|
||||||
changedDifficulty(state: ProgressState, action: PayloadAction<{game: string, difficulty: number}>) {
|
changedDifficulty(state: ProgressState, action: PayloadAction<{game: string, difficulty: number}>) {
|
||||||
addGameProgress(state, action)
|
addGameProgress(state, action)
|
||||||
state.games[action.payload.game].difficulty = action.payload.difficulty
|
state.games[action.payload.game.toLowerCase()].difficulty = action.payload.difficulty
|
||||||
},
|
},
|
||||||
/** set the difficulty */
|
/** set the difficulty */
|
||||||
changedOpenedIntro(state: ProgressState, action: PayloadAction<{game: string, openedIntro: boolean}>) {
|
changedOpenedIntro(state: ProgressState, action: PayloadAction<{game: string, openedIntro: boolean}>) {
|
||||||
addGameProgress(state, action)
|
addGameProgress(state, action)
|
||||||
state.games[action.payload.game].openedIntro = action.payload.openedIntro
|
state.games[action.payload.game.toLowerCase()].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.toLowerCase()].typewriterMode = action.payload.typewriterMode
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/** if the level does not exist, return default values */
|
/** if the level does not exist, return default values */
|
||||||
export function selectLevel(game: string, world: string, level: number) {
|
export function selectLevel(game: string, world: string, level: number) {
|
||||||
return (state) =>{
|
return (state) =>{
|
||||||
if (!state.progress.games[game]) { return initalLevelProgressState }
|
if (!state.progress.games[game.toLowerCase()]) { return initalLevelProgressState }
|
||||||
if (!state.progress.games[game].data[world]) { return initalLevelProgressState }
|
if (!state.progress.games[game.toLowerCase()].data[world]) { return initalLevelProgressState }
|
||||||
if (!state.progress.games[game].data[world][level]) { return initalLevelProgressState }
|
if (!state.progress.games[game.toLowerCase()].data[world][level]) { return initalLevelProgressState }
|
||||||
return state.progress.games[game].data[world][level]
|
return state.progress.games[game.toLowerCase()].data[world][level]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** return the code of the current level */
|
/** return the code of the current level */
|
||||||
export function selectCode(game: string, world: string, level: number) {
|
export function selectCode(game: string, world: string, level: number) {
|
||||||
return (state) => {
|
return (state) => {
|
||||||
return selectLevel(game, world, level)(state).code
|
return selectLevel(game.toLowerCase(), world, level)(state).code
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** return the current inventory */
|
/** return the current inventory */
|
||||||
export function selectInventory(game: string) {
|
export function selectInventory(game: string) {
|
||||||
return (state) => {
|
return (state) => {
|
||||||
if (!state.progress.games[game]) { return [] }
|
if (!state.progress.games[game.toLowerCase()]) { return [] }
|
||||||
return state.progress.games[game].inventory
|
return state.progress.games[game.toLowerCase()].inventory
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** return the code of the current level */
|
/** return the code of the current level */
|
||||||
export function selectHelp(game: string, world: string, level: number) {
|
export function selectHelp(game: string, world: string, level: number) {
|
||||||
return (state) => {
|
return (state) => {
|
||||||
return selectLevel(game, world, level)(state).help
|
return selectLevel(game.toLowerCase(), world, level)(state).help
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** return the selections made in the current level */
|
/** return the selections made in the current level */
|
||||||
export function selectSelections(game: string, world: string, level: number) {
|
export function selectSelections(game: string, world: string, level: number) {
|
||||||
return (state) => {
|
return (state) => {
|
||||||
return selectLevel(game, world, level)(state).selections
|
return selectLevel(game.toLowerCase(), world, level)(state).selections
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** return whether the current level is clompleted */
|
/** return whether the current level is clompleted */
|
||||||
export function selectCompleted(game: string, world: string, level: number) {
|
export function selectCompleted(game: string, world: string, level: number) {
|
||||||
return (state) => {
|
return (state) => {
|
||||||
return selectLevel(game, world, level)(state).completed
|
return selectLevel(game.toLowerCase(), world, level)(state).completed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** return progress for the current game if it exists */
|
/** return progress for the current game if it exists */
|
||||||
export function selectProgress(game: string) {
|
export function selectProgress(game: string) {
|
||||||
return (state) => {
|
return (state) => {
|
||||||
return state.progress.games[game] ?? null
|
return state.progress.games[game.toLowerCase()] ?? null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** return difficulty for the current game if it exists */
|
/** return difficulty for the current game if it exists */
|
||||||
export function selectDifficulty(game: string) {
|
export function selectDifficulty(game: string) {
|
||||||
return (state) => {
|
return (state) => {
|
||||||
return state.progress.games[game]?.difficulty ?? DEFAULT_DIFFICULTY
|
return state.progress.games[game.toLowerCase()]?.difficulty ?? DEFAULT_DIFFICULTY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** return whether the intro has been read */
|
/** return whether the intro has been read */
|
||||||
export function selectOpenedIntro(game: string) {
|
export function selectOpenedIntro(game: string) {
|
||||||
return (state) => {
|
return (state) => {
|
||||||
return state.progress.games[game]?.openedIntro
|
return state.progress.games[game.toLowerCase()]?.openedIntro
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** return typewriter mode for the current game if it exists */
|
||||||
|
export function selectTypewriterMode(game: string) {
|
||||||
|
return (state) => {
|
||||||
|
return state.progress.games[game.toLowerCase()]?.typewriterMode ?? true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Export actions to modify the progress */
|
/** Export actions to modify the progress */
|
||||||
export const { changedSelection, codeEdited, levelCompleted, deleteProgress,
|
export const { changedSelection, codeEdited, levelCompleted, deleteProgress,
|
||||||
deleteLevelProgress, loadProgress, helpEdited, changedInventory, changedOpenedIntro,
|
deleteLevelProgress, loadProgress, helpEdited, changedInventory, changedOpenedIntro,
|
||||||
changedDifficulty } = progressSlice.actions
|
changedDifficulty, changeTypewriterMode} = progressSlice.actions
|
||||||
|
|||||||
@@ -7,21 +7,19 @@ import { debounce } from "debounce";
|
|||||||
import { connection } from '../connection'
|
import { connection } from '../connection'
|
||||||
import { apiSlice } from './api'
|
import { apiSlice } from './api'
|
||||||
import { progressSlice } from './progress'
|
import { progressSlice } from './progress'
|
||||||
import { saveState } from "./local_storage";
|
import { preferencesSlice } from "./preferences"
|
||||||
|
import { saveState, savePreferences, removePreferences} from "./local_storage";
|
||||||
|
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
[apiSlice.reducerPath]: apiSlice.reducer,
|
[apiSlice.reducerPath]: apiSlice.reducer,
|
||||||
[progressSlice.name]: progressSlice.reducer,
|
[progressSlice.name]: progressSlice.reducer,
|
||||||
|
[preferencesSlice.name]: preferencesSlice.reducer,
|
||||||
},
|
},
|
||||||
// Make connection available in thunks:
|
// Make connection available in thunks:
|
||||||
middleware: getDefaultMiddleware =>
|
middleware: getDefaultMiddleware =>
|
||||||
getDefaultMiddleware({
|
getDefaultMiddleware().concat(apiSlice.middleware),
|
||||||
thunk: {
|
|
||||||
extraArgument: { connection }
|
|
||||||
}
|
|
||||||
}).concat(apiSlice.middleware),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,6 +29,9 @@ export const store = configureStore({
|
|||||||
store.subscribe(
|
store.subscribe(
|
||||||
debounce(() => {
|
debounce(() => {
|
||||||
saveState(store.getState()[progressSlice.name]);
|
saveState(store.getState()[progressSlice.name]);
|
||||||
|
|
||||||
|
const preferencesState = store.getState()[preferencesSlice.name]
|
||||||
|
preferencesState.isSavePreferences ? savePreferences(preferencesState) : removePreferences()
|
||||||
}, 800)
|
}, 800)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import {useState, useEffect} from 'react'
|
|
||||||
|
|
||||||
function getWindowDimensions() {
|
|
||||||
const {innerWidth: width, innerHeight: height } = window
|
|
||||||
return {width, height}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useWindowDimensions() {
|
|
||||||
const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions())
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function handleResize() {
|
|
||||||
setWindowDimensions(getWindowDimensions())
|
|
||||||
}
|
|
||||||
window.addEventListener('resize', handleResize)
|
|
||||||
return () => window.removeEventListener('resize', handleResize)
|
|
||||||
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return windowDimensions
|
|
||||||
}
|
|
||||||
+54
-319
@@ -1,332 +1,67 @@
|
|||||||
# Creating a game.
|
# Server
|
||||||
|
|
||||||
Ideally one takes the [NNG template](https://github.com/hhu-adam/NNG4) to create a new game.
|
The server is made out of two parts, named "relay" and "server".
|
||||||
|
|
||||||
## Game Structure
|
The former, "relay", is the server which
|
||||||
|
sets up a socket connection to the client, starts the lean servers to work on files and
|
||||||
|
relays messages between the lean server and the client. `index.mjs` is the file that needs to
|
||||||
|
be run, which is done for example using `pm2` or by calling `npm run start_server` or
|
||||||
|
`npm run production`, see more later.
|
||||||
|
|
||||||
A game consist of worlds which have multiple levels each. In the following we describe how to create a level file and how to combine these into a game.
|
The latter, "server", is the lean server which has two jobs. For one, it produces the "gameserver"
|
||||||
|
executable which is the lean server that handles the files the player plays on. The second job
|
||||||
|
is to provide the lean commands which are used when creating a game. These are located in
|
||||||
|
`Commands.lean`.
|
||||||
|
|
||||||
### Level
|
|
||||||
|
|
||||||
A level file is a lean file that imports at least `import GameServer.Commands` and starts with the following Lean commands.
|
## Integration into Games
|
||||||
|
|
||||||
|
Games need the "server" as a lake-dependency, which is done in the game's lakefile.
|
||||||
|
|
||||||
|
A game imports `GameServer.Commands` which provides to all the API required to
|
||||||
|
create a game.
|
||||||
|
|
||||||
|
In particular the lean command `MakeGame` compiles the entire game. Static information is
|
||||||
|
stored as JSON files in `.lake/gamedata` for faster loading, while other data is only
|
||||||
|
saved to lean env-extensions which the lean server has access to after loading the lean file.
|
||||||
|
|
||||||
|
For games to be run successfully, it is important that the "gameserver" executable inside
|
||||||
|
the game's `.lake` folder is actually built.
|
||||||
|
Currently, this happens through a lake-post-update-hook when calling `lake update -R` (in the game's folder), but if this fails, you can always build it manually by calling `lake build gameserver`.
|
||||||
|
(both commands are to be executed in the game's directory!)
|
||||||
|
|
||||||
|
## Modifying the server
|
||||||
|
|
||||||
|
### Starting the server
|
||||||
|
|
||||||
|
When using the [manual installation](running_locally.md#manual-installation) you can run the server
|
||||||
|
using
|
||||||
|
|
||||||
```lean
|
|
||||||
Game "NNG"
|
|
||||||
World "Addition"
|
|
||||||
Level 1
|
|
||||||
Title "The rfl tactic"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that the levels inside a world must have consecutive numbering starting with `1`. The `Game`
|
|
||||||
and `World` strings can be anything, see below.
|
|
||||||
|
|
||||||
#### Statement
|
|
||||||
|
|
||||||
The core of a level is the `Statement`, which is the exercise that should be proven.
|
|
||||||
|
|
||||||
```lean
|
|
||||||
/-- For all natural numbers $n$, we have $0 + n = n$. -/
|
|
||||||
@[simp]
|
|
||||||
Statement MyNat.zero_add
|
|
||||||
(n : ℕ) : 0 + n = n := by
|
|
||||||
Hint "You can start a proof by `induction n`."
|
|
||||||
induction n with n hn
|
|
||||||
· Hint "This is the base case."
|
|
||||||
rw [add_zero]
|
|
||||||
rfl
|
|
||||||
· Hint "This is the induction hypothesis"
|
|
||||||
rw [add_succ]
|
|
||||||
Branch
|
|
||||||
simp
|
|
||||||
Hint "A branch is an alternative tactic sequence. Does not need to finish the proof."
|
|
||||||
rw [hn]
|
|
||||||
rfl
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Proof
|
|
||||||
|
|
||||||
The proof must always be a tactic proof, i.e. `:= by` is a mandatory part of the syntax.
|
|
||||||
|
|
||||||
There are a few extra tactics that help you structuring the proof:
|
|
||||||
|
|
||||||
- `Hint`: You can use `Hint "text"` to display text if the goal state in-game matches
|
|
||||||
the one where `Hint` is placed. For more options about hints, see below.
|
|
||||||
- `Branch`: In the proof you can add a `Branch` that runs an alternative tactic sequence, which
|
|
||||||
helps setting `Hints` in different places. The `Branch` does not affect the main
|
|
||||||
proof and does not need to finish any goals.
|
|
||||||
- `Template`/`Hole`: Used to provide a sample proof template. Anything inside `Template`
|
|
||||||
will be copied into the editor with all `Hole`s replaced with `sorry`. Note that
|
|
||||||
having a `Template` will force the user to use Editor-mode for this level.
|
|
||||||
|
|
||||||
##### Statement Name (optional)
|
|
||||||
|
|
||||||
If you specify a name (`MyNat.zero_add`), this lemma will be available in future levels.
|
|
||||||
(Note that a future level must also import this level,
|
|
||||||
so that Lean knows about the added statement).
|
|
||||||
|
|
||||||
The name must be *fully qualified*. (TODO: is that still true? Did we implement namespaces?)
|
|
||||||
|
|
||||||
##### Doc Comment (optional)
|
|
||||||
|
|
||||||
There are three places where the documentation comment appears:
|
|
||||||
|
|
||||||
1. as doc comment when hovering over the theorem
|
|
||||||
2. as exercise description at the top of the level: ``Theorem `zero_add`: yada yada.``
|
|
||||||
3. in the inventory. This can be overwritten by using
|
|
||||||
`LemmaDoc MyNat.zero_add "different yada yada"` as one might want to add a more detailed
|
|
||||||
description there including examples etc.
|
|
||||||
|
|
||||||
Both latter points support Markdown (including katex).
|
|
||||||
|
|
||||||
##### Attributes (optional)
|
|
||||||
|
|
||||||
the `@[ attributes ]` prefix should work just like you know it from the `theorem` keyword.
|
|
||||||
|
|
||||||
#### Introduction/Conclusion
|
|
||||||
|
|
||||||
Optionally, you can add an `Introduction "some text"` and `Conclusion "some text"` to your level.
|
|
||||||
The introduction will be shown at the beginning, the conclusion is displayed once the level
|
|
||||||
is solved.
|
|
||||||
|
|
||||||
#### Theorems/Tactics/Definitions
|
|
||||||
|
|
||||||
Only enabled theorems/tactics/definitions (called "items" here) are available in a level.
|
|
||||||
|
|
||||||
To add a new item in a level, you can add
|
|
||||||
|
|
||||||
```lean
|
|
||||||
NewTactic rfl simp
|
|
||||||
NewLemma MyNat.add_zero MyNat.add_succ
|
|
||||||
NewDefinition Nat Pow Mul
|
|
||||||
```
|
|
||||||
|
|
||||||
Once added, items will be available in all future levels/worlds,
|
|
||||||
unless you disable them for a particular level with
|
|
||||||
|
|
||||||
```lean
|
|
||||||
DisabledTactic tauto
|
|
||||||
DisabledLemma MyNat.add_zero
|
|
||||||
```
|
|
||||||
|
|
||||||
or specify explicitly which items should be available with
|
|
||||||
|
|
||||||
```lean
|
|
||||||
OnlyTactic rw rfl apply
|
|
||||||
OnlyLemma MyNat.add_zero
|
|
||||||
```
|
|
||||||
|
|
||||||
Lastly, all items need documentation entries (which are imported in the level),
|
|
||||||
see more about that below. There is also explains the `LemmaTab` keyword.
|
|
||||||
|
|
||||||
### World
|
|
||||||
|
|
||||||
Multiple levels are combined into a world and the worlds are then added to the game. It is recommended that all levels of a world are inside one folder (e.g. `NNG/Levels/Addition/`) and
|
|
||||||
then there is one world file (`NNG/Levels/Addition.lean`) which contains the following
|
|
||||||
|
|
||||||
```lean
|
|
||||||
import NNG.Levels.Addition.Level_1
|
|
||||||
import NNG.Levels.Addition.Level_2
|
|
||||||
|
|
||||||
Game "NNG"
|
|
||||||
World "Addition"
|
|
||||||
Title "Addition World"
|
|
||||||
|
|
||||||
Introduction "some text"
|
|
||||||
```
|
|
||||||
|
|
||||||
The `Title` is the world's display title. The `Introduction` is displayed before loading level 1.
|
|
||||||
Note that all levels of a world should be imported by the world file.
|
|
||||||
|
|
||||||
BUG: A level **must not** be imported in a different world's level. Instead, you have to import an entire world there: `import NNG.Levels.Addition`
|
|
||||||
|
|
||||||
### Game
|
|
||||||
|
|
||||||
The Game itself (i.e. the main file of you lake project, `NNG.lean`) should import all worlds and have the following layout, concluding with `MakeGame`:
|
|
||||||
|
|
||||||
```lean
|
|
||||||
import NNG.Levels.Addition
|
|
||||||
import NNG.Levels.Multiplication
|
|
||||||
import NNG.Levels.Power
|
|
||||||
|
|
||||||
Game "NNG"
|
|
||||||
Title "Natural Number Game"
|
|
||||||
Introduction "some text"
|
|
||||||
|
|
||||||
MakeGame
|
|
||||||
```
|
|
||||||
|
|
||||||
The game will automatically compute the order of the worlds depending on the sample proofs of the Levels (ignoring anything inside a `Branch`). You can add additional dependencies manually by adding `Dependency PowerWorld → ImpossibleWorld` before `MakeGame`.
|
|
||||||
The order of worlds influences which tactics and lemmas will be unlocked in a given level.
|
|
||||||
|
|
||||||
`MakeGame` will display warnings about things in the game that need to be fixed, like missing
|
|
||||||
documentation or if a tactic is never introduced.
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
Each tactic, theorem, or definition (all called items here) that is introduced in the game
|
|
||||||
needs a documentation entry. These are statements of the following form:
|
|
||||||
|
|
||||||
```lean
|
|
||||||
LemmaDoc MyNat.add_squared as "add_squared" in "Pow"
|
|
||||||
"(missing)"
|
|
||||||
|
|
||||||
TacticDoc constructor
|
|
||||||
"(missing)"
|
|
||||||
|
|
||||||
DefinitionDoc One as "1"
|
|
||||||
"(missing)"
|
|
||||||
```
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
|
|
||||||
* The lemma name must be **fully qualified**. The string display name can be arbitrary.
|
|
||||||
* Tactics must have their proper name. use `TacticDoc «have» ""` if it does not work
|
|
||||||
without french quotes.
|
|
||||||
* Definition names can be arbitrary. E.g. I used `DefinitionDoc Symbol.Fun as "fun x ↦ x" "(missing)"` once.
|
|
||||||
|
|
||||||
Moreover, the lemmas are in sorted in tabs (the `in "Pow`) part. In each level file, you
|
|
||||||
can define which tab is open when the level is loaded by adding `LemmaTab "Pow"`.
|
|
||||||
|
|
||||||
There will be features added to get automatic information from mathlib!
|
|
||||||
|
|
||||||
## Escaping
|
|
||||||
(TODO: Move)
|
|
||||||
|
|
||||||
|
|
||||||
Inside the doc comment you don't need to escape the backslashes:
|
|
||||||
|
|
||||||
```lean
|
|
||||||
/-- $\operatorname{succ}(n)$. notation for naturals is `\N`. -/
|
|
||||||
Statement ...
|
|
||||||
```
|
|
||||||
|
|
||||||
However, inside interpolated strings (e.g. in `Hint`, `Introduction` and `Conclusion`)
|
|
||||||
you do need to escape backslashes
|
|
||||||
with `\\` and `{` with `\{`:
|
|
||||||
|
|
||||||
```lean
|
|
||||||
Hint "This code has some $\\operatorname\{succ}(n)$ math. The value of `h` is {h}.
|
|
||||||
Notation for naturals is `\\N`."
|
|
||||||
```
|
|
||||||
|
|
||||||
## Game design
|
|
||||||
Here are some things you should consider designing a new game:
|
|
||||||
|
|
||||||
* A world with more than 16 levels will be displayed with the levels spiraling outwards,
|
|
||||||
it might be desirable to stay below that bound. Above 22 levels the spiral start getting out
|
|
||||||
of control.
|
|
||||||
|
|
||||||
# Running Games Locally
|
|
||||||
|
|
||||||
The installation instructions are not yet tested on Mac/Windows. Comments very welcome!
|
|
||||||
|
|
||||||
## VSCode Dev Containers
|
|
||||||
|
|
||||||
1. **Install Docker and Dev Containers** *(once)*:<br/>
|
|
||||||
See [official instructions](https://code.visualstudio.com/docs/devcontainers/containers#_getting-started).
|
|
||||||
Explicitly this means:
|
|
||||||
* Install docker engine if you have not yet: [Instructions](https://docs.docker.com/engine/install/).
|
|
||||||
I followed the "Server" instructions for linux.
|
|
||||||
* Note that on Linux you need to add your user to the `docker` group
|
|
||||||
([see instructions](https://docs.docker.com/engine/install/linux-postinstall/)) and probably reboot.
|
|
||||||
* Open the games folder in VSCode: `cd NNG4 && code .` or "Open Folder" within VSCode
|
|
||||||
* a message appears prompting you to install the "Dev Containers" extension (by Microsoft).
|
|
||||||
|
|
||||||
2. **Open Project in Dev Container** *(everytime)*:<br/>
|
|
||||||
Once you have the Dev Containers Extension installed, (re)open the project folder of your game in VSCode.
|
|
||||||
A message appears asking you to "Reopen in Container".
|
|
||||||
|
|
||||||
* The first start will take a while, ca. 2-10 minutes. After the first
|
|
||||||
start this should be very quickly.
|
|
||||||
* Once built, it should open a tab "Simple Browser" inside VSCode displaying
|
|
||||||
the game. (Alternatively, open http://localhost:3000 in your browser).
|
|
||||||
|
|
||||||
3. **Editing Files** *(everytime)*:<br/>
|
|
||||||
After editing some files in VSCode, open VSCode's terminal (View > Terminal) and run `lake build`.
|
|
||||||
Now you can reload your browser to see the changes.
|
|
||||||
|
|
||||||
### Errors
|
|
||||||
|
|
||||||
* If you don't get the pop-up, you might have disabled them and you can reenable it by
|
|
||||||
running the `remote-containers.showReopenInContainerNotificationReset` command in vscode.
|
|
||||||
* If the starting the container fails, in particular with a message `Error: network xyz not found`,
|
|
||||||
you might have deleted stuff from docker via your shell. Try deleting the container and image
|
|
||||||
explicitly in VSCode (left side, "Docker" icon). Then reopen vscode and let it rebuild the
|
|
||||||
container. (this will again take some time)
|
|
||||||
|
|
||||||
|
|
||||||
## Without Dev Containers
|
|
||||||
Install `nvm`:
|
|
||||||
```bash
|
|
||||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.2/install.sh | bash
|
|
||||||
```
|
|
||||||
then reopen bash and test with `command -v nvm` if it is available (Should print "nvm").
|
|
||||||
|
|
||||||
Now install node:
|
|
||||||
```bash
|
|
||||||
nvm install node
|
|
||||||
```
|
|
||||||
|
|
||||||
Clone the game (e.g. `NNG4` here):
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/hhu-adam/NNG4.git
|
|
||||||
# or: git clone git@github.com:hhu-adam/NNG4.git
|
|
||||||
```
|
|
||||||
|
|
||||||
Download dependencies and build the game:
|
|
||||||
```bash
|
|
||||||
cd NNG4
|
|
||||||
lake update
|
|
||||||
lake exe cache get # if your game depends on mathlib
|
|
||||||
lake build
|
|
||||||
```
|
|
||||||
|
|
||||||
Clone the game repository into a directory next to the game:
|
|
||||||
```bash
|
|
||||||
cd ..
|
|
||||||
git clone https://github.com/leanprover-community/lean4game.git
|
|
||||||
# or: git clone git@github.com:leanprover-community/lean4game.git
|
|
||||||
```
|
|
||||||
The folders `NNG4` and `lean4game` must be in the same directory!
|
|
||||||
|
|
||||||
In `lean4game`, install dependencies:
|
|
||||||
```bash
|
|
||||||
cd lean4game
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
If you are developing a game other than `Robo` or `NNG4`, adapt the
|
|
||||||
code at the beginning of `lean4game/server/index.mjs`:
|
|
||||||
```typescript
|
|
||||||
const games = {
|
|
||||||
"g/hhu-adam/robo": {
|
|
||||||
dir: "../../../../Robo",
|
|
||||||
queueLength: 5
|
|
||||||
},
|
|
||||||
"g/hhu-adam/nng4": {
|
|
||||||
dir: "../../../../NNG4",
|
|
||||||
queueLength: 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Run the game:
|
|
||||||
```bash
|
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
This takes a little time. Eventually, the server is available on http://localhost:3000/
|
This way any changes to files in `client/` or `relay/` will cause the server to restart automatically.
|
||||||
and the game is available on http://localhost:3000/#/g/hhu-adam/NNG4.
|
|
||||||
|
|
||||||
### Modifying the GameServer
|
Alternative, you can run `npm run build` followed by the commands
|
||||||
|
|
||||||
When modifying the game engine itself (in particular the content in `lean4game/server`) you can test it live with this
|
|
||||||
setup by setting `export NODE_ENV=development` inside your local game before building it:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd NNG4
|
|
||||||
export NODE_ENV=development
|
|
||||||
lake update
|
|
||||||
lake build
|
|
||||||
```
|
```
|
||||||
This causes lake to search locally for the `GameServer` lake package instead of using the version from github.
|
npm run start_client
|
||||||
|
npm run production
|
||||||
|
```
|
||||||
|
|
||||||
|
(in two separate terminals) to test the production mode of the server. This way it will only
|
||||||
|
change once you build and restart the server.
|
||||||
|
|
||||||
|
### Modifying the lean server
|
||||||
|
|
||||||
|
To test a modified lean server (i.e. content of `server/`), you can use the local dev setup and call
|
||||||
|
`lake update -R -Klean4game.local` in your game followed by `lake build`.
|
||||||
|
This will cause lake to look for the
|
||||||
|
local lean server as a dependency instead of the version it downloaded from git.
|
||||||
|
|
||||||
|
You can play a local game at https://localhost:3000/#/g/local/{FolderName} where you replace `{FolderName}` with the game folder name.
|
||||||
|
|
||||||
|
After modifications in `server/`, you will need to call `lake build gameserver` (called in `server/` or in your game's folder) to rebuild
|
||||||
|
the gameserver executable and
|
||||||
|
`lake build` (called in the game's folder) to rebuild the game.
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## v4.5.0
|
||||||
|
|
||||||
|
### Breaking changes
|
||||||
|
|
||||||
|
* Fix (#183): local store accepts case insensitive URL. The game progress has previously been saved under case sensitive URLs. You might need to recover old progress from your browser storage.
|
||||||
|
|
||||||
|
## Other
|
||||||
+51
-37
@@ -98,7 +98,7 @@ This introduction text is shown when one first enters a world.
|
|||||||
1. Use the template above and make sure you import all levels of this world.
|
1. Use the template above and make sure you import all levels of this world.
|
||||||
1. In `Game.lean` import the world with `import Game.Levels.MyWorld`
|
1. In `Game.lean` import the world with `import Game.Levels.MyWorld`
|
||||||
|
|
||||||
Now you created a world with one level and added it🎉 The command `MakeGame` in `Game.lean` shows you any problems you might need to fix. Currently it shows
|
Now you created a world with one level and added it🎉 The command `MakeGame` in `Game.lean` shows you any problems you might need to fix. Currently, it shows
|
||||||
|
|
||||||
```text
|
```text
|
||||||
No world introducing sorry, but required by MyWorld
|
No world introducing sorry, but required by MyWorld
|
||||||
@@ -125,23 +125,32 @@ The player has an inventory with tactics, theorems, and definitions that unlock
|
|||||||
|
|
||||||
```lean
|
```lean
|
||||||
NewTactic induction simp
|
NewTactic induction simp
|
||||||
NewLemma Nat.zero_mul
|
NewTheorem Nat.zero_mul
|
||||||
NewDefinition Pow
|
NewDefinition Pow
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Important:** All commands in this section 6a) expect the `Name` they take as input
|
||||||
|
to be **fully qualified**. For example `NewTheorem Nat.zero_mul` and not `NewTheorem zero_mul`.
|
||||||
|
|
||||||
#### Doc entries
|
#### Doc entries
|
||||||
|
|
||||||
You'll see a warning about a missing Lemma documentation. You can fix it by adding doc-entries like the following somewhere above it.
|
You'll see a warning about a missing Theorem documentation. You can fix it by adding doc-entries like the following somewhere above it.
|
||||||
|
|
||||||
```lean
|
```lean
|
||||||
LemmaDoc Nat.zero_mul as "zero_mul" in "Mul"
|
/--
|
||||||
"some description"
|
some description
|
||||||
|
-/
|
||||||
|
TheoremDoc Nat.zero_mul as "zero_mul" in "Mul"
|
||||||
|
|
||||||
|
/--
|
||||||
|
some description
|
||||||
|
-/
|
||||||
TacticDoc simp
|
TacticDoc simp
|
||||||
"some description"
|
|
||||||
|
|
||||||
|
/--
|
||||||
|
some description
|
||||||
|
-/
|
||||||
DefinitionDoc Pow as "^"
|
DefinitionDoc Pow as "^"
|
||||||
"some description"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
(e.g. you could also create a file `Game/Doc/MyTheorems.lean`, add there your documentation and import it)
|
(e.g. you could also create a file `Game/Doc/MyTheorems.lean`, add there your documentation and import it)
|
||||||
@@ -153,7 +162,7 @@ If you do not provide any content for the inventory, the game will try to find a
|
|||||||
You have a few options to disable inventory items that have been unlocked in previous levels:
|
You have a few options to disable inventory items that have been unlocked in previous levels:
|
||||||
|
|
||||||
```lean
|
```lean
|
||||||
DisableTactic, DisableLemma, OnlyTactic, OnlyLemma
|
DisabledTactic, DisabledTheorem, OnlyTactic, OnlyTheorem
|
||||||
```
|
```
|
||||||
|
|
||||||
have the same syntax as above. The former two disable items for this level, the latter two
|
have the same syntax as above. The former two disable items for this level, the latter two
|
||||||
@@ -161,7 +170,7 @@ disable all items except the ones specified.
|
|||||||
|
|
||||||
#### Theorem Tab
|
#### Theorem Tab
|
||||||
|
|
||||||
Theorems are sorted into tabs. with `LemmaTab "Mul"` you specify which tab should be open by default in this level.
|
Theorems are sorted into tabs. With `TheoremTab "Mul"` you specify which tab should be open by default in this level.
|
||||||
|
|
||||||
#### HiddenTactic
|
#### HiddenTactic
|
||||||
|
|
||||||
@@ -176,11 +185,12 @@ and only `rw` would show up in the inventory.
|
|||||||
|
|
||||||
### 6. b) Statement
|
### 6. b) Statement
|
||||||
|
|
||||||
The statement is the exercise of the level. the basics work the same as they would in `example` or `theorem`. Note however, that you **must** do a tactic proof, i.e. the `:= by` is a hard-coded part of the syntax
|
The statement is the exercise of the level. The basics work the same as they would in `example` or `theorem`. Note however, that you **must** do a tactic proof, i.e. the `:= by` is a hard-coded part of the syntax
|
||||||
|
|
||||||
#### Name
|
#### Name
|
||||||
|
|
||||||
You can give your exercise a name: `Statement my_first_exercise (n : Nat) ...`. If you do so, it will be added to the inventory and be available in future levels.
|
You can give your exercise a name: `Statement my_first_exercise (n : Nat) …`. If you do so, it will be added to the inventory and be available in future levels.
|
||||||
|
You can but a `Statement` inside namespaces like you would with `theorem`.
|
||||||
|
|
||||||
#### Doc String / Exercise statement
|
#### Doc String / Exercise statement
|
||||||
|
|
||||||
@@ -192,25 +202,18 @@ Statement ...
|
|||||||
sorry
|
sorry
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Attributes
|
For more details and features, read [Writing Exercises](writing_exercises.md)
|
||||||
|
|
||||||
You can add attributes as you would for a `theorem`. Most notably, you can make your named exercise a `simp` lemma:
|
|
||||||
|
|
||||||
```lean
|
|
||||||
@[simp]
|
|
||||||
Statement my_simp_lemma ...
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. c) Proof
|
### 6. c) Proof
|
||||||
|
|
||||||
The proof must always be a tactic proof, i.e. `:= by` is a mandatory part of the syntax.
|
The proof must always be a tactic proof, i.e. `:= by` is a mandatory part of the syntax.
|
||||||
|
|
||||||
There are a few extra tactics that help you structuring the proof:
|
There are a few extra tactics that help you with structuring the proof:
|
||||||
|
|
||||||
- `Hint`: You can use `Hint "text"` to display text if the goal state in-game matches
|
- `Hint`: You can use `Hint "text"` to display text if the goal state in-game matches
|
||||||
the one where `Hint` is placed. For more options about hints, see below.
|
the one where `Hint` is placed. For more options about hints, see below.
|
||||||
- `Branch`: In the proof you can add a `Branch` that runs an alternative tactic sequence, which
|
- `Branch`: In the proof you can add a `Branch` that runs an alternative tactic sequence, which
|
||||||
helps setting `Hints` in different places. The `Branch` does not affect the main
|
helps to set `Hints` in different places. The `Branch` does not affect the main
|
||||||
proof and does not need to finish any goals.
|
proof and does not need to finish any goals.
|
||||||
- `Template`/`Hole`: Used to provide a sample proof template. Anything inside `Template`
|
- `Template`/`Hole`: Used to provide a sample proof template. Anything inside `Template`
|
||||||
will be copied into the editor with all `Hole`s replaced with `sorry`. Note that
|
will be copied into the editor with all `Hole`s replaced with `sorry`. Note that
|
||||||
@@ -223,29 +226,40 @@ One thing to keep in mind is that the game will look at the main proof to figure
|
|||||||
Most important for game development are probably the `Hints`.
|
Most important for game development are probably the `Hints`.
|
||||||
|
|
||||||
The hints will be displayed whenever the player's current goal matches the goal the hint is
|
The hints will be displayed whenever the player's current goal matches the goal the hint is
|
||||||
placed at inside the sample proof. You can use `Branch` to place hints in dead ends or alternative proof strands. If you specify
|
placed at inside the sample proof. You can use `Branch` to place hints in dead ends or alternative proof strands.
|
||||||
|
|
||||||
```
|
Read [More about Hints](hints.md) for how they work and what the options are.
|
||||||
Hint (strict := true) "some hidden hint"
|
|
||||||
```
|
|
||||||
|
|
||||||
a hint only matches iff the assumptions match exactly one-to-one. (Otherwise, it does not care if there are additional assumptions in context)
|
### 6. e) Extra: Images
|
||||||
|
You can add images on any layer of the game (i.e. game/world/level). These will be displayed in your game.
|
||||||
|
|
||||||
Further, you can choose to hide hints and only have them displayed when the player presses "More Help":
|
The images need to be placed in `images/` and you need to add a command like `Image "images/path/to/myWorldImage.png"`
|
||||||
```
|
in one of the files you created in 2), 3), or 4) (i.e. game/world/level).
|
||||||
Hint (hidden := true) "some hidden hint"
|
|
||||||
```
|
|
||||||
|
|
||||||
Lastly, you should put variable names in hints inside brackets:
|
NOTE: At present, only the images for a world are displayed. They appear in the introduction of the world.
|
||||||
|
|
||||||
```
|
|
||||||
Hint "now use `rw [{h}]` to use your assumption {h}."
|
|
||||||
```
|
|
||||||
That way, the game will replace it with the actual name the assumption has in the player's proof state.
|
|
||||||
|
|
||||||
## 7. Update your game
|
## 7. Update your game
|
||||||
|
|
||||||
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).
|
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](update_game.md).
|
||||||
|
|
||||||
|
## 8. Publish your game
|
||||||
|
|
||||||
|
To publish your game on the official server, see [Publishing a game](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 catchphrase. 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
|
||||||
|
|
||||||
|
|||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
# 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"
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that hints are only **context-aware but not history-aware**. In particular, they only look at the assumptions and the current goal. Player's might encounter hints in a different order - or not at all - if they decide to go for a unique proof idea. The `Branch` tactic helps to place hints outside the sample solution's proof.
|
||||||
|
|
||||||
|
## 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 "use `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$`
|
||||||
|
|
||||||
|
**Escaping**: Generally, if you add text inside quotes `" "` (e.g. in `Hint`) you need to escape
|
||||||
|
backslashes, but if you provide text inside a doc comment
|
||||||
|
`/-- -/` (e.g. in the `Statement` description) you do not!
|
||||||
|
|
||||||
|
TODO: Write a doc about latex/markdown options available.
|
||||||
|
|
||||||
|
### Commutative diagrams
|
||||||
|
|
||||||
|
Here is an example of how to write a commutative diagram in KaTeX:
|
||||||
|
|
||||||
|
$$
|
||||||
|
\begin{CD}
|
||||||
|
A @>{f}>> B @<{g}<< C \\
|
||||||
|
@V{h}VV @V{i}VV @V{j}VV \\
|
||||||
|
D @<{k}<< E @>{l}>> F \\
|
||||||
|
@A{m}AA @A{n}AA @V{p}VV \\
|
||||||
|
G @<{q}<< H @>{r}>> I
|
||||||
|
\end{CD}
|
||||||
|
$$
|
||||||
|
|
||||||
|
```
|
||||||
|
$$
|
||||||
|
\\begin{CD}
|
||||||
|
A @>{f}>> B @<{g}<< C \\\\
|
||||||
|
@V{h}VV @V{i}VV @V{j}VV \\\\
|
||||||
|
D @<{k}<< E @>{l}>> F \\\\
|
||||||
|
@A{m}AA @A{n}AA @V{p}VV \\\\
|
||||||
|
G @<{q}<< H @>{r}>> I
|
||||||
|
\\end{CD}
|
||||||
|
$$
|
||||||
|
```
|
||||||
|
|
||||||
|
See https://www.jmilne.org/not/Mamscd.pdf
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Publishing games
|
||||||
|
|
||||||
|
You can publish your game on the official [Lean Game Server](https://adam.math.hhu.de) in a few simple
|
||||||
|
steps.
|
||||||
|
|
||||||
|
## 1. Upload Game to github
|
||||||
|
|
||||||
|
First, you need your game in a public Github repository and make sure the github action has run.
|
||||||
|
You can check this by spotting the green checkmark on the start page, or by looking at the "Actions"
|
||||||
|
tab.
|
||||||
|
|
||||||
|
## 2. Import the game
|
||||||
|
|
||||||
|
You call the URL that's listed under "What's Next?" in the latest action run. Explicitly 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).
|
||||||
+33
-19
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
The installation instructions are not yet tested on Mac/Windows. Comments very welcome!
|
The installation instructions are not yet tested on Mac/Windows. Comments very welcome!
|
||||||
|
|
||||||
|
Please also consult the [Troubleshooting Collection](troubleshoot.md), where some known pitfalls are collected.
|
||||||
|
|
||||||
There are several options to play a game locally:
|
There are several options to play a game locally:
|
||||||
|
|
||||||
1. VSCode Dev Container: needs `docker` installed on your machine
|
1. VSCode Dev Container: needs `docker` installed on your machine
|
||||||
@@ -31,24 +33,14 @@ The template game [GameSkeleton](https://github.com/hhu-adam/GameSkeleton) conta
|
|||||||
|
|
||||||
* The first start will take a while, ca. 2-15 minutes. After the first
|
* The first start will take a while, ca. 2-15 minutes. After the first
|
||||||
start this should be very quickly.
|
start this should be very quickly.
|
||||||
* Once built, you can open http://localhost:3000 in your browser. which should load the game.
|
* Once built, you can open http://localhost:3000 in your browser, which should load the game.
|
||||||
|
|
||||||
3. **Editing Files** *(everytime)*:<br/>
|
3. **Editing Files** *(everytime)*:<br/>
|
||||||
After editing some Lean files in VSCode, open VSCode's terminal (View > Terminal) and run `lake build`. Now you can reload your browser to see the changes.
|
After editing some Lean files in VSCode, open VSCode's terminal (View > Terminal) and run `lake build`. Now you can reload your browser to see the changes.
|
||||||
|
|
||||||
### Errors
|
|
||||||
|
|
||||||
* If you don't get the pop-up, you might have disabled them and you can reenable it by
|
|
||||||
running the `remote-containers.showReopenInContainerNotificationReset` command in vscode.
|
|
||||||
* If the starting the container fails, in particular with a message `Error: network xyz not found`,
|
|
||||||
you might have deleted stuff from docker via your shell. Try deleting the container and image
|
|
||||||
explicitely in VSCode (left side, "Docker" icon). Then reopen vscode and let it rebuild the
|
|
||||||
container. (this will again take some time)
|
|
||||||
* On a working dev container setup, http://localhost:3000 should directly redirect you to http://localhost:3000/#/g/local/game, try if the latter is accessible.
|
|
||||||
|
|
||||||
## Codespaces
|
## Codespaces
|
||||||
|
|
||||||
You can work on your game using Github codespaces (click "Code" and then "Codespaces" and then "create codespace on main"). It it should run the game locally in the background. You can open it for example under "Ports" and clicking on "Open in Browser".
|
You can work on your game using Github codespaces (click "Code" and then "Codespaces" and then "create codespace on main"). It should run the game locally in the background. You can open it for example under "Ports" and clicking on "Open in Browser".
|
||||||
|
|
||||||
Note: You have to wait until npm started properly, which might take a good while.
|
Note: You have to wait until npm started properly, which might take a good while.
|
||||||
|
|
||||||
@@ -85,12 +77,11 @@ 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
|
lake update -R
|
||||||
lake exe cache get # if your game depends on mathlib
|
|
||||||
lake build
|
lake build
|
||||||
```
|
```
|
||||||
|
|
||||||
Clone the game repository into a directory next to the game:
|
Clone the server repository into a directory next to the game:
|
||||||
```bash
|
```bash
|
||||||
cd ..
|
cd ..
|
||||||
git clone https://github.com/leanprover-community/lean4game.git
|
git clone https://github.com/leanprover-community/lean4game.git
|
||||||
@@ -109,16 +100,39 @@ Run the game:
|
|||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You should see a message like this:
|
||||||
|
```bash
|
||||||
|
[server] > lean4-game@0.1.0 start_server
|
||||||
|
[server] > (cd server && lake build) && (cd relay && cross-env NODE_ENV=development nodemon -e mjs --exec "node ./index.mjs")
|
||||||
|
[server]
|
||||||
|
[client]
|
||||||
|
[client] > lean4-game@0.1.0 start_client
|
||||||
|
[client] > cross-env NODE_ENV=development vite --host
|
||||||
|
[client]
|
||||||
|
[server] [nodemon] 3.0.#
|
||||||
|
[server] [nodemon] to restart at any time, enter `rs`
|
||||||
|
[server] [nodemon] watching path(s): *.*
|
||||||
|
[server] [nodemon] watching extensions: mjs
|
||||||
|
[server] [nodemon] starting `node ./index.mjs`
|
||||||
|
[client]
|
||||||
|
[client] VITE v4.5.1 ready in \#\#\# ms
|
||||||
|
[client]
|
||||||
|
[client] ➜ Local: http://localhost:3000/
|
||||||
|
[client] ➜ Network: http://###.###.###.##:3000/
|
||||||
|
[client] [vite-plugin-static-copy] Collected 7 items.
|
||||||
|
[server] (node:#####) [DEP0040] [server] Listening on 8080
|
||||||
|
```
|
||||||
|
|
||||||
This takes a little time. Eventually, the game is available on http://localhost:3000/#/g/local/GameSkeleton. Replace `GameSkeleton` with the folder name of your local game.
|
This takes a little time. Eventually, the game is available on http://localhost:3000/#/g/local/GameSkeleton. Replace `GameSkeleton` with the folder name of your local game.
|
||||||
|
|
||||||
## Modifying the GameServer
|
## 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 setting `export LEAN4GAME=local` inside your local game before building it:
|
When modifying the game engine itself (in particular the content in `lean4game/server`) you can test it live with the same setup as above (manual installation) by using `lake update -R -Klean4game.local`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd NNG4
|
cd NNG4
|
||||||
export LEAN4GAME=local
|
lake update -R -Klean4game.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, when you `lake build` your game, it will rebuild with the modified `GameServer`.
|
This causes lake to search locally for the `GameServer` lake package instead of using the version from github. Therefore, you can the local copy of the edit `GameServer` in `../lean4game` and
|
||||||
|
`lake build` will then directly use this modified copy to build your game.
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
# Notes for Server maintainer
|
||||||
|
|
||||||
|
In order to set up the server to allow imports, one needs to create a
|
||||||
|
[Github Access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens). A fine-grained access token with only reading rights for public
|
||||||
|
repos will suffice.
|
||||||
|
|
||||||
|
You need to set the environment variables `LEAN4GAME_GITHUB_USER` and `LEAN4GAME_GITHUB_TOKEN`
|
||||||
|
with your user name and access token. For example, you can set these in `ecosystem.config.cjs` if
|
||||||
|
you're using `pm2`
|
||||||
|
|
||||||
|
Then people can call:
|
||||||
|
|
||||||
|
> https://{website}/import/trigger/{owner}/{repo}
|
||||||
|
|
||||||
|
where you replace:
|
||||||
|
- website: The website your server runs on, e.g. `localhost:3000`
|
||||||
|
- owner, repo: The owner and repository name of the game you want to load from github.
|
||||||
|
|
||||||
|
will trigger to download the latest version of your game from github onto your server.
|
||||||
|
Once this import reports "Done", you should be able to play your game under:
|
||||||
|
|
||||||
|
> https://{website}/#/g/{owner}/{repo}
|
||||||
|
|
||||||
|
## Data management
|
||||||
|
Everything downloaded remains in the folder `lean4game/games`.
|
||||||
|
The subfolder `tmp` contains downloaded artifacts and can be deleted without loss.
|
||||||
|
The other folders should only contain the built lean-games, sorted by owner and repo.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
Here are some issues experienced by users.
|
||||||
|
|
||||||
|
- You can reset the lake projects involved (i.e. the `server/` folder here as well as your [game's folder](https://github.com/hhu-adam/GameSkeleton)) with the following commands:
|
||||||
|
```
|
||||||
|
cd [THE PROJECT]
|
||||||
|
rm -rf .lake/
|
||||||
|
lake update -R
|
||||||
|
lake build
|
||||||
|
```
|
||||||
|
If you experience problems related to Lean or lake, you should first try to reset it this way.
|
||||||
|
|
||||||
|
# VSCode Dev-Container
|
||||||
|
* If you don't get the pop-up, you might have disabled them, and you can reenable it by
|
||||||
|
running the `remote-containers.showReopenInContainerNotificationReset` command in vscode.
|
||||||
|
* If the starting the container fails, in particular with a message `Error: network xyz not found`,
|
||||||
|
you might have deleted stuff from docker via your shell. Try deleting the container and image
|
||||||
|
explicitly in VSCode (left side, "Docker" icon). Then reopen vscode and let it rebuild the
|
||||||
|
container. (this will again take some time)
|
||||||
|
* On a working dev container setup, http://localhost:3000 should directly redirect you to http://localhost:3000/#/g/local/game, try if the latter is accessible.
|
||||||
|
|
||||||
|
|
||||||
|
# Manual Installation
|
||||||
|
Here are known issues/pitfalls with the local setup using `npm`.
|
||||||
|
|
||||||
|
* If `CDPATH` is set on your mac/linux system, it might provide issues with `npm start` resulting in a server crash or blank screen. In particular `npm start` will display
|
||||||
|
```
|
||||||
|
[server] sh: line 0: cd: server: No such file or directory
|
||||||
|
[server] npm run start_server exited with code 1
|
||||||
|
```
|
||||||
|
As a fix you might need to delete your manually set `CDPATH` environment variable.
|
||||||
|
|
||||||
|
# Publication
|
||||||
|
Errors concerning uploads to the server.
|
||||||
|
|
||||||
|
* Your game overview loads but the server crashes on loading a level: Check your game's github action is identical to the [GameSkeleton's](https://github.com/hhu-adam/GameSkeleton/blob/main/.github/workflows/build.yml), in particular that there is a step about building the "`gameserver`-executable".
|
||||||
+26
-11
@@ -9,29 +9,44 @@ 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 (without Cache!).
|
||||||
* Local Setup: run `lake update` (followed by `lake exe cache get` if you depend on mathlib.)
|
* **Local Setup**: in your game's folder run the following:
|
||||||
* Gitpod/Codespaces: Create a fresh one
|
```
|
||||||
|
lake update -R
|
||||||
|
lake build
|
||||||
|
```
|
||||||
|
|
||||||
This will update `lean4game` and `mathlib` in your project to the new lean version.
|
* Additionally, if you have a local copy of the server `lean4game`,
|
||||||
|
you should update this one to the matching version. Run the following in the folder `lean4game/`:
|
||||||
|
```
|
||||||
|
git fetch
|
||||||
|
git checkout {VERSION_TAG}
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
where `{VERSION_TAG}` is the tag from above of the form `v4.X.0`
|
||||||
|
* **Gitpod/Codespaces**: Create a fresh one
|
||||||
|
|
||||||
|
This will update your game (and the mathlib version you might be using) to the new lean version.
|
||||||
|
|
||||||
## Newest developing setup
|
## 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're developing setup, for example because it doesn't work
|
(dev container/codespaces/gitpod). If you need to update your developing setup, for example because it doesn't work
|
||||||
anymore, you will need to copy the relevant files from the [GameSkeleton](https://github.com/hhu-adam/GameSkeleton) template into your game repo.
|
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.
|
simply copy them from the `GameSkeleton` into your game and proceed as above,
|
||||||
|
i.e. `lake update -R && lake build`.
|
||||||
|
|
||||||
(Note: You should not need to modify any of these files, with the exception of the `lakefile.lean`,
|
(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.)
|
where you need to add any dependencies of your game, or remove mathlib if you don't need it.)
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Writing exercises
|
||||||
|
|
||||||
|
This page deals in more details with the `Statement` command and all the options you have
|
||||||
|
to write better exercises/levels.
|
||||||
|
|
||||||
|
|
||||||
|
## Local `let` definitions
|
||||||
|
|
||||||
|
If you want to make a local definition/notation which only holds for this exercise (e.g.
|
||||||
|
a function `f : ℤ → ℤ := fun x ↦ 2 * x`) the recommended way is to use a `let`-statement:
|
||||||
|
|
||||||
|
```lean
|
||||||
|
Statement (a : ℤ) (h : 0 < a) :
|
||||||
|
let f : ℤ → ℤ := fun x ↦ 2 * x
|
||||||
|
0 < f a := by
|
||||||
|
sorry
|
||||||
|
```
|
||||||
|
|
||||||
|
The game automatically `intros` such `let`-statements, such that you and the player will see
|
||||||
|
the following initial proof state:
|
||||||
|
|
||||||
|
```
|
||||||
|
a: ℤ
|
||||||
|
h: 0 < a
|
||||||
|
f: ℤ → ℤ := fun x => 2 * x
|
||||||
|
⊢ 0 < f a
|
||||||
|
```
|
||||||
|
|
||||||
|
## "Preamble" & non-`Prop`-valued exercises
|
||||||
|
|
||||||
|
You can use the following syntax with `(preamble := tac)` where `tac` is a tactic sequence.
|
||||||
|
|
||||||
|
```
|
||||||
|
Statement my_statement (preamble := dsimp) (a : ℤ) (h : 0 < a) :
|
||||||
|
0 < f a := by
|
||||||
|
sorry
|
||||||
|
```
|
||||||
|
|
||||||
|
This tactic sequence will be executed before the exercise is handed to the player.
|
||||||
|
|
||||||
|
For example, if your exercise is to construct a structure, you could use `preamble` to fill
|
||||||
|
all data fields correctly, leaving all `Prop`-valued fields of the structure as separate goals
|
||||||
|
for the player to proof.
|
||||||
|
|
||||||
|
Note: `(preamble := tac)` always has to be written between the optional name and the first
|
||||||
|
hypothesis. Nevertheless, you can use all hypotheses in the tactic sequence, because it is
|
||||||
|
only evaluated at the beginning of the proof.
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
You can add attributes as you would for a `theorem`. Most notably, you can make your named exercise a `simp` lemma:
|
||||||
|
|
||||||
|
```lean
|
||||||
|
@[simp]
|
||||||
|
Statement my_simp_lemma ...
|
||||||
|
```
|
||||||
@@ -2,8 +2,10 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
apps : [{
|
apps : [{
|
||||||
name : "lean4game",
|
name : "lean4game",
|
||||||
script : "server/index.mjs",
|
script : "relay/index.mjs",
|
||||||
env: {
|
env: {
|
||||||
|
LEAN4GAME_GITHUB_USER: "",
|
||||||
|
LEAN4GAME_GITHUB_TOKEN: "",
|
||||||
NODE_ENV: "production",
|
NODE_ENV: "production",
|
||||||
PORT: 8002
|
PORT: 8002
|
||||||
},
|
},
|
||||||
Generated
+2601
-2593
File diff suppressed because it is too large
Load Diff
+19
-5
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "lean4-game",
|
"name": "lean4-game",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": ".",
|
"homepage": ".",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -9,6 +10,10 @@
|
|||||||
"@emotion/styled": "^11.10.5",
|
"@emotion/styled": "^11.10.5",
|
||||||
"@fontsource/roboto": "^4.5.8",
|
"@fontsource/roboto": "^4.5.8",
|
||||||
"@fontsource/roboto-mono": "^4.5.8",
|
"@fontsource/roboto-mono": "^4.5.8",
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "^6.5.1",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@leanprover/infoview": "^0.4.3",
|
"@leanprover/infoview": "^0.4.3",
|
||||||
"@mui/icons-material": "^5.11.0",
|
"@mui/icons-material": "^5.11.0",
|
||||||
"@mui/material": "^5.11.1",
|
"@mui/material": "^5.11.1",
|
||||||
@@ -22,12 +27,17 @@
|
|||||||
"cytoscape-klay": "^3.1.4",
|
"cytoscape-klay": "^3.1.4",
|
||||||
"debounce": "^1.2.1",
|
"debounce": "^1.2.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"i18next": "^23.10.1",
|
||||||
|
"i18next-http-backend": "^2.5.0",
|
||||||
|
"i18next-scanner-typescript": "^1.2.0",
|
||||||
"lean4-infoview": "https://gitpkg.now.sh/leanprover/vscode-lean4/lean4-infoview?de0062c",
|
"lean4-infoview": "https://gitpkg.now.sh/leanprover/vscode-lean4/lean4-infoview?de0062c",
|
||||||
"lean4web": "github:hhu-adam/lean4web",
|
"lean4web": "github:hhu-adam/lean4web#414d9e62638a392fca278761b4c61a1d2e138bc7",
|
||||||
"octokit": "^2.0.14",
|
"octokit": "^3.1.2",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
"react-country-flag": "^3.1.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-i18next": "^14.1.0",
|
||||||
"react-markdown": "^8.0.4",
|
"react-markdown": "^8.0.4",
|
||||||
"react-native": "^0.72.3",
|
"react-native": "^0.72.3",
|
||||||
"react-redux": "^8.0.5",
|
"react-redux": "^8.0.5",
|
||||||
@@ -36,6 +46,7 @@
|
|||||||
"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",
|
||||||
@@ -52,7 +63,8 @@
|
|||||||
"concurrently": "^7.6.0",
|
"concurrently": "^7.6.0",
|
||||||
"css-loader": "^6.7.3",
|
"css-loader": "^6.7.3",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"nodemon": "^2.0.20",
|
"i18next-scanner": "^4.4.0",
|
||||||
|
"nodemon": "^3.0.1",
|
||||||
"react-refresh": "^0.14.0",
|
"react-refresh": "^0.14.0",
|
||||||
"style-loader": "^3.3.1",
|
"style-loader": "^3.3.1",
|
||||||
"ts-loader": "^9.4.2",
|
"ts-loader": "^9.4.2",
|
||||||
@@ -61,12 +73,14 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "concurrently -n server,client -c blue,green \"npm run start_server\" \"npm run start_client\"",
|
"start": "concurrently -n server,client -c blue,green \"npm run start_server\" \"npm run start_client\"",
|
||||||
"start_server": "cd server && lake build && cross-env NODE_ENV=development nodemon -e mjs --exec \"node ./index.mjs\"",
|
"start_server": "(cd server && lake build) && (cd relay && cross-env NODE_ENV=development nodemon -e mjs --exec \"node ./index.mjs\")",
|
||||||
"start_client": "cross-env NODE_ENV=development vite --host",
|
"start_client": "cross-env NODE_ENV=development vite --host",
|
||||||
"build": "npm run build_server && npm run build_client",
|
"build": "npm run build_server && npm run build_client",
|
||||||
|
"preview": "vite preview",
|
||||||
"build_server": "cd server && lake build",
|
"build_server": "cd server && lake build",
|
||||||
"build_client": "cross-env NODE_ENV=production vite build",
|
"build_client": "cross-env NODE_ENV=production vite build",
|
||||||
"production": "cross-env NODE_ENV=production node server/index.mjs"
|
"production": "cross-env NODE_ENV=production node relay/index.mjs",
|
||||||
|
"translate": "npx i18next-scanner --config client/i18next-scanner.config.cjs"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
#/bin/bash
|
#/bin/bash
|
||||||
|
|
||||||
|
# Note: This fails if there is no default toolchain installed
|
||||||
ELAN_HOME=$(lake env printenv ELAN_HOME)
|
ELAN_HOME=$(lake env printenv ELAN_HOME)
|
||||||
|
|
||||||
|
# $1 : the game directory
|
||||||
|
# $2 : the lean4game folder
|
||||||
|
# $3 : the gameserver executable
|
||||||
|
|
||||||
(exec bwrap\
|
(exec bwrap\
|
||||||
--ro-bind ../../lean4game /lean4game \
|
--bind $2 /lean4game \
|
||||||
--ro-bind ../../$1 /game \
|
--bind $1 /game \
|
||||||
--ro-bind $ELAN_HOME /elan \
|
--bind $ELAN_HOME /elan \
|
||||||
--ro-bind /usr /usr \
|
--bind /usr /usr \
|
||||||
--dev /dev \
|
--dev /dev \
|
||||||
--proc /proc \
|
--proc /proc \
|
||||||
--symlink usr/lib /lib\
|
--symlink usr/lib /lib\
|
||||||
@@ -22,6 +27,6 @@ ELAN_HOME=$(lake env printenv ELAN_HOME)
|
|||||||
--unshare-uts \
|
--unshare-uts \
|
||||||
--unshare-cgroup \
|
--unshare-cgroup \
|
||||||
--die-with-parent \
|
--die-with-parent \
|
||||||
--chdir "/lean4game/server/build/bin/" \
|
--chdir "/game/.lake/packages/GameServer/server/.lake/build/bin/" \
|
||||||
./gameserver --server /game
|
./gameserver --server /game
|
||||||
)
|
)
|
||||||
@@ -11,6 +11,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
const TOKEN = process.env.LEAN4GAME_GITHUB_TOKEN
|
const TOKEN = process.env.LEAN4GAME_GITHUB_TOKEN
|
||||||
|
const USERNAME = process.env.LEAN4GAME_GITHUB_USER
|
||||||
const octokit = new Octokit({
|
const octokit = new Octokit({
|
||||||
auth: TOKEN
|
auth: TOKEN
|
||||||
})
|
})
|
||||||
@@ -41,9 +42,10 @@ async function download(id, url, dest) {
|
|||||||
requestProgress(request({
|
requestProgress(request({
|
||||||
url,
|
url,
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'abentkamp',
|
'accept': 'application/vnd.github+json',
|
||||||
|
'User-Agent': USERNAME,
|
||||||
'X-GitHub-Api-Version': '2022-11-28',
|
'X-GitHub-Api-Version': '2022-11-28',
|
||||||
'Authorization': 'Bearer '+TOKEN
|
'Authorization': 'Bearer ' + TOKEN
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.on('progress', function (state) {
|
.on('progress', function (state) {
|
||||||
@@ -76,35 +78,44 @@ async function doImport (owner, repo, id) {
|
|||||||
.reduce((acc, cur) => acc.created_at < cur.created_at ? cur : acc)
|
.reduce((acc, cur) => acc.created_at < cur.created_at ? cur : acc)
|
||||||
artifactId = artifact.id
|
artifactId = artifact.id
|
||||||
const url = artifact.archive_download_url
|
const url = artifact.archive_download_url
|
||||||
if (!fs.existsSync("tmp")){
|
// Make sure the download folder exists
|
||||||
fs.mkdirSync("tmp");
|
if (!fs.existsSync(path.join(__dirname, "..", "games"))){
|
||||||
|
fs.mkdirSync(path.join(__dirname, "..", "games"));
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(path.join(__dirname, "..", "games", "tmp"))){
|
||||||
|
fs.mkdirSync(path.join(__dirname, "..", "games", "tmp"));
|
||||||
}
|
}
|
||||||
progress[id].output += `Download from ${url}\n`
|
progress[id].output += `Download from ${url}\n`
|
||||||
await download(id, url, `tmp/artifact_${artifactId}.zip`)
|
await download(id, url, path.join(__dirname, "..", "games", "tmp", `${owner.toLowerCase()}_${repo.toLowerCase()}_${artifactId}.zip`))
|
||||||
progress[id].output += `Download finished.\n`
|
progress[id].output += `Download finished.\n`
|
||||||
await runProcess(id, "/bin/bash", [`${__dirname}/unpack.sh`, artifactId],".")
|
|
||||||
let manifest = fs.readFileSync(`tmp/artifact_${artifactId}_inner/manifest.json`);
|
await runProcess(id, "/bin/bash", [path.join(__dirname, "unpack.sh"), artifactId, owner.toLowerCase(), repo.toLowerCase()], path.join(__dirname, ".."))
|
||||||
manifest = JSON.parse(manifest);
|
|
||||||
if (manifest.length !== 1) {
|
|
||||||
throw `Unexpected manifest: ${JSON.stringify(manifest)}`
|
// let manifest = fs.readFileSync(`tmp/artifact_${artifactId}_inner/manifest.json`);
|
||||||
}
|
// manifest = JSON.parse(manifest);
|
||||||
manifest[0].RepoTags = [`g/${owner.toLowerCase()}/${repo.toLowerCase()}:latest`]
|
// if (manifest.length !== 1) {
|
||||||
fs.writeFileSync(`tmp/artifact_${artifactId}_inner/manifest.json`, JSON.stringify(manifest));
|
// throw `Unexpected manifest: ${JSON.stringify(manifest)}`
|
||||||
await runProcess(id, "tar", ["-cvf", `../archive_${artifactId}.tar`, "."], `tmp/artifact_${artifactId}_inner/`)
|
// }
|
||||||
await runProcess(id, "docker", ["load", "-i", `tmp/archive_${artifactId}.tar`])
|
// manifest[0].RepoTags = [`g/${owner.toLowerCase()}/${repo.toLowerCase()}:latest`]
|
||||||
|
// fs.writeFileSync(`tmp/artifact_${artifactId}_inner/manifest.json`, JSON.stringify(manifest));
|
||||||
|
// await runProcess(id, "tar", ["-cvf", `../archive_${artifactId}.tar`, "."], `tmp/artifact_${artifactId}_inner/`)
|
||||||
|
// // await runProcess(id, "docker", ["load", "-i", `tmp/archive_${artifactId}.tar`])
|
||||||
|
|
||||||
progress[id].done = true
|
progress[id].done = true
|
||||||
progress[id].output += `Done.\n`
|
progress[id].output += `Done!\n`
|
||||||
|
progress[id].output += `Play the game at: {your website}/#/g/${owner}/${repo}\n`
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
progress[id].output += `Error: ${e.toString()}\n${e.stack}`
|
progress[id].output += `Error: ${e.toString()}\n${e.stack}`
|
||||||
} finally {
|
} finally {
|
||||||
|
// clean-up temp. files
|
||||||
if (artifactId) {
|
if (artifactId) {
|
||||||
fs.rmSync(`tmp/artifact_${artifactId}.zip`, {force: true, recursive: true});
|
fs.rmSync(path.join(__dirname, "..", "games", "tmp", `${owner}_${repo}_${artifactId}.zip`), {force: true, recursive: false});
|
||||||
fs.rmSync(`tmp/artifact_${artifactId}`, {force: true, recursive: true});
|
fs.rmSync(path.join(__dirname, "..", "games", "tmp", `${owner}_${repo}_${artifactId}`), {force: true, recursive: true});
|
||||||
fs.rmSync(`tmp/artifact_${artifactId}_inner`, {force: true, recursive: true});
|
|
||||||
fs.rmSync(`tmp/archive_${artifactId}.tar`, {force: true, recursive: true});
|
|
||||||
}
|
}
|
||||||
progress[id].done = true
|
progress[id].done = true
|
||||||
}
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10000))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const importTrigger = (req, res) => {
|
export const importTrigger = (req, res) => {
|
||||||
+212
@@ -0,0 +1,212 @@
|
|||||||
|
import { WebSocketServer } from 'ws';
|
||||||
|
import express from 'express'
|
||||||
|
import path from 'path'
|
||||||
|
import * as cp from 'child_process';
|
||||||
|
import * as url from 'url';
|
||||||
|
import * as rpc from 'vscode-ws-jsonrpc';
|
||||||
|
import * as jsonrpcserver from 'vscode-ws-jsonrpc/server';
|
||||||
|
import os from 'os';
|
||||||
|
import fs from 'fs';
|
||||||
|
import anonymize from 'ip-anonymize';
|
||||||
|
import { importTrigger, importStatus } from './import.mjs'
|
||||||
|
// import fs from 'fs'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a game here if the server should keep a queue of pre-loaded games ready at all times.
|
||||||
|
*
|
||||||
|
* IMPORTANT! Tags here need to be lower case!
|
||||||
|
*/
|
||||||
|
const queueLength = {
|
||||||
|
"g/hhu-adam/robo": 2,
|
||||||
|
"g/hhu-adam/nng4": 5,
|
||||||
|
"g/djvelleman/stg4": 0,
|
||||||
|
"g/trequetrum/lean4game-logic": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const __filename = url.fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 8080;
|
||||||
|
|
||||||
|
var router = express.Router();
|
||||||
|
|
||||||
|
router.get('/import/status/:owner/:repo', importStatus)
|
||||||
|
router.get('/import/trigger/:owner/:repo', importTrigger)
|
||||||
|
|
||||||
|
const server = app
|
||||||
|
.use(express.static(path.join(__dirname, '..', 'client', 'dist'))) // TODO: add a dist folder from inside the game
|
||||||
|
.use('/i18n/g/:owner/:repo/:lang/*', (req, res, next) => {
|
||||||
|
const owner = req.params.owner;
|
||||||
|
const repo = req.params.repo
|
||||||
|
const lang = req.params.lang
|
||||||
|
const filename = req.params[0];
|
||||||
|
req.url = filename;
|
||||||
|
express.static(path.join(getGameDir(owner,repo),".i18n",lang))(req, res, next);
|
||||||
|
})
|
||||||
|
.use('/data/g/:owner/:repo/*', (req, res, next) => {
|
||||||
|
const owner = req.params.owner;
|
||||||
|
const repo = req.params.repo
|
||||||
|
const filename = req.params[0];
|
||||||
|
req.url = filename;
|
||||||
|
express.static(path.join(getGameDir(owner,repo),".lake","gamedata"))(req, res, next);
|
||||||
|
})
|
||||||
|
.use('/', router)
|
||||||
|
.listen(PORT, () => console.log(`Listening on ${PORT}`));
|
||||||
|
|
||||||
|
const wss = new WebSocketServer({ server })
|
||||||
|
|
||||||
|
var socketCounter = 0
|
||||||
|
|
||||||
|
const environment = process.env.NODE_ENV
|
||||||
|
const isDevelopment = environment === 'development'
|
||||||
|
|
||||||
|
/** We keep queues of started Lean Server processes to be ready when a user arrives */
|
||||||
|
const queue = {}
|
||||||
|
|
||||||
|
function getTag(owner, repo) {
|
||||||
|
return `g/${owner.toLowerCase()}/${repo.toLowerCase()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGameDir(owner, repo) {
|
||||||
|
owner = owner.toLowerCase()
|
||||||
|
if (owner == 'local') {
|
||||||
|
if(!isDevelopment) {
|
||||||
|
console.error(`No local games in production mode.`)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(!fs.existsSync(path.join(__dirname, '..', 'games'))) {
|
||||||
|
console.error(`Did not find the following folder: ${path.join(__dirname, '..', 'games')}`)
|
||||||
|
console.error('Did you already import any games?')
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let game_dir = (owner == 'local') ?
|
||||||
|
path.join(__dirname, '..', '..', repo) : // note: here we need `repo` to be case sensitive
|
||||||
|
path.join(__dirname, '..', 'games', `${owner}`, `${repo.toLowerCase()}`)
|
||||||
|
|
||||||
|
if(!fs.existsSync(game_dir)) {
|
||||||
|
console.error(`Game '${game_dir}' does not exist!`)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return game_dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startServerProcess(owner, repo) {
|
||||||
|
|
||||||
|
let game_dir = getGameDir(owner, repo)
|
||||||
|
if (!game_dir) return;
|
||||||
|
|
||||||
|
let serverProcess
|
||||||
|
if (isDevelopment) {
|
||||||
|
let args = ["--server", game_dir]
|
||||||
|
let binDir = path.join(game_dir, ".lake", "packages", "GameServer", "server", ".lake", "build", "bin")
|
||||||
|
// Note: `cwd` is important to be the `bin` directory as `Watchdog` calls `./gameserver` again
|
||||||
|
if (fs.existsSync(binDir)) {
|
||||||
|
// Try to use the game's own copy of `gameserver`.
|
||||||
|
serverProcess = cp.spawn("./gameserver", args, { cwd: binDir })
|
||||||
|
} else {
|
||||||
|
// If the game is built with `-Klean4game.local` there is no copy in the lake packages.
|
||||||
|
serverProcess = cp.spawn("./gameserver", args,
|
||||||
|
{ cwd: path.join(__dirname, "..", "server", ".lake", "build", "bin") })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
serverProcess = cp.spawn("./bubblewrap.sh",
|
||||||
|
[ game_dir, path.join(__dirname, '..')],
|
||||||
|
{ cwd: __dirname })
|
||||||
|
}
|
||||||
|
|
||||||
|
serverProcess.on('error', error =>
|
||||||
|
console.error(`Launching Lean Server failed: ${error}`)
|
||||||
|
)
|
||||||
|
if (serverProcess.stderr !== null) {
|
||||||
|
serverProcess.stderr.on('data', data =>
|
||||||
|
console.error(`Lean Server: ${data}`)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return serverProcess
|
||||||
|
}
|
||||||
|
|
||||||
|
/** start Lean Server processes to refill the queue */
|
||||||
|
function fillQueue(tag) {
|
||||||
|
while (queue[tag].length < queueLength[tag]) {
|
||||||
|
let serverProcess
|
||||||
|
serverProcess = startServerProcess(tag)
|
||||||
|
if (serverProcess == null) {
|
||||||
|
console.error('serverProcess was undefined/null')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
queue[tag].push(serverProcess)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// // TODO: We disabled queue for now
|
||||||
|
// if (!isDevelopment) { // Don't use queue in development
|
||||||
|
// for (let tag in queueLength) {
|
||||||
|
// queue[tag] = []
|
||||||
|
// fillQueue(tag)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
const urlRegEx = /^\/websocket\/g\/([\w.-]+)\/([\w.-]+)$/
|
||||||
|
|
||||||
|
wss.addListener("connection", function(ws, req) {
|
||||||
|
const reRes = urlRegEx.exec(req.url)
|
||||||
|
if (!reRes) { console.error(`Connection refused because of invalid URL: ${req.url}`); return; }
|
||||||
|
const owner = reRes[1]
|
||||||
|
const repo = reRes[2]
|
||||||
|
|
||||||
|
const tag = getTag(owner, repo)
|
||||||
|
|
||||||
|
let ps
|
||||||
|
if (!queue[tag] || queue[tag].length == 0) {
|
||||||
|
ps = startServerProcess(owner, repo)
|
||||||
|
} else {
|
||||||
|
console.info('Got process from the queue')
|
||||||
|
ps = queue[tag].shift() // Pick the first Lean process; it's likely to be ready immediately
|
||||||
|
fillQueue(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps == null) {
|
||||||
|
console.error('server process is undefined/null')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
socketCounter += 1;
|
||||||
|
const ip = anonymize(req.headers['x-forwarded-for'] || req.socket.remoteAddress)
|
||||||
|
console.log(`[${new Date()}] Socket opened - ${ip}`)
|
||||||
|
|
||||||
|
const socket = {
|
||||||
|
onMessage: (cb) => { ws.on("message", cb) },
|
||||||
|
onError: (cb) => { ws.on("error", cb) },
|
||||||
|
onClose: (cb) => { ws.on("close", cb) },
|
||||||
|
send: (data, cb) => { ws.send(data,cb) }
|
||||||
|
}
|
||||||
|
const reader = new rpc.WebSocketMessageReader(socket)
|
||||||
|
const writer = new rpc.WebSocketMessageWriter(socket)
|
||||||
|
const socketConnection = jsonrpcserver.createConnection(reader, writer, () => ws.close())
|
||||||
|
const serverConnection = jsonrpcserver.createProcessStreamConnection(ps)
|
||||||
|
socketConnection.forward(serverConnection, message => {
|
||||||
|
if (isDevelopment) {console.log(`CLIENT: ${JSON.stringify(message)}`)}
|
||||||
|
return message;
|
||||||
|
})
|
||||||
|
serverConnection.forward(socketConnection, message => {
|
||||||
|
if (isDevelopment) {console.log(`SERVER: ${JSON.stringify(message)}`)}
|
||||||
|
return message;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[${new Date()}] Number of open sockets - ${socketCounter}`)
|
||||||
|
console.log(`[${new Date()}] Free RAM - ${Math.round(os.freemem() / 1024 / 1024)} / ${Math.round(os.totalmem() / 1024 / 1024)} MB`)
|
||||||
|
|
||||||
|
ws.on('close', () => {
|
||||||
|
console.log(`[${new Date()}] Socket closed - ${ip}`)
|
||||||
|
socketCounter -= 1
|
||||||
|
})
|
||||||
|
|
||||||
|
socketConnection.onClose(() => serverConnection.dispose())
|
||||||
|
serverConnection.onClose(() => socketConnection.dispose())
|
||||||
|
})
|
||||||
Executable
+34
@@ -0,0 +1,34 @@
|
|||||||
|
#/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
|
||||||
|
|
||||||
|
# Delete temporary files
|
||||||
|
rm -f tmp/${OWNER}_${REPO}_${ARTIFACT_ID}.zip
|
||||||
|
rm -fr tmp/${OWNER}_${REPO}_${ARTIFACT_ID}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
build
|
|
||||||
adam
|
|
||||||
nng
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import GameServer.FileWorker
|
import GameServer.FileWorker
|
||||||
import GameServer.Watchdog
|
|
||||||
import GameServer.Commands
|
import GameServer.Commands
|
||||||
|
|
||||||
-- TODO: The only reason we import `Commands` is so that it gets built to on `lake build`
|
-- TODO: The only reason we import `Commands` is so that it gets built to on `lake build`
|
||||||
@@ -10,13 +9,9 @@ unsafe def main : List String → IO UInt32 := fun args => do
|
|||||||
|
|
||||||
Lean.enableInitializersExecution
|
Lean.enableInitializersExecution
|
||||||
|
|
||||||
|
-- TODO: remove this argument
|
||||||
if args[0]? == some "--server" then
|
if args[0]? == some "--server" then
|
||||||
MyServer.Watchdog.watchdogMain args
|
GameServer.FileWorker.workerMain {} args
|
||||||
else if args[0]? == some "--worker" then
|
|
||||||
MyServer.FileWorker.workerMain {}
|
|
||||||
else
|
else
|
||||||
e.putStrLn s!"Expected `--server` or `--worker`"
|
e.putStrLn s!"Expected `--server`"
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
-- TODO: Potentially it could be useful to pass in the `gameName` via the websocket connection
|
|
||||||
+295
-355
@@ -1,23 +1,18 @@
|
|||||||
import GameServer.EnvExtensions
|
import GameServer.Helpers
|
||||||
|
import GameServer.Inventory
|
||||||
|
import GameServer.Options
|
||||||
|
import GameServer.SaveData
|
||||||
|
import GameServer.Hints
|
||||||
|
import GameServer.Tactic.LetIntros
|
||||||
|
import GameServer.RpcHandlers -- only needed to collect the translations of "level completed" msgs
|
||||||
|
import I18n
|
||||||
|
|
||||||
open Lean Meta Elab Command
|
open Lean Meta Elab Command
|
||||||
|
|
||||||
|
open GameServer
|
||||||
|
|
||||||
set_option autoImplicit false
|
set_option autoImplicit false
|
||||||
|
|
||||||
/-- Let `MakeGame` print the reasons why the worlds depend on each other. -/
|
|
||||||
register_option lean4game.showDependencyReasons : Bool := {
|
|
||||||
defValue := false
|
|
||||||
descr := "show reasons for calculated world dependencies."
|
|
||||||
}
|
|
||||||
|
|
||||||
/-- Let `MakeGame` print the reasons why the worlds depend on each other.
|
|
||||||
|
|
||||||
Note: currently unused in favour of setting `set_option trace.debug true`. -/
|
|
||||||
register_option lean4game.verbose : Bool := {
|
|
||||||
defValue := false
|
|
||||||
descr := "display more info messages to help developing the game."
|
|
||||||
}
|
|
||||||
|
|
||||||
/-! # Game metadata -/
|
/-! # Game metadata -/
|
||||||
|
|
||||||
/-- Switch to the specified `Game` (and create it if non-existent). Example: `Game "NNG"` -/
|
/-- Switch to the specified `Game` (and create it if non-existent). Example: `Game "NNG"` -/
|
||||||
@@ -43,150 +38,106 @@ elab "Level" level:num : command => do
|
|||||||
|
|
||||||
/-- Define the title of the current game/world/level. -/
|
/-- Define the title of the current game/world/level. -/
|
||||||
elab "Title" t:str : command => do
|
elab "Title" t:str : command => do
|
||||||
|
let title ← t.getString.translate
|
||||||
match ← getCurLayer with
|
match ← getCurLayer with
|
||||||
| .Level => modifyCurLevel fun level => pure {level with title := t.getString}
|
| .Level => modifyCurLevel fun level => pure {level with title := title}
|
||||||
| .World => modifyCurWorld fun world => pure {world with title := t.getString}
|
| .World => modifyCurWorld fun world => pure {world with title := title}
|
||||||
| .Game => modifyCurGame fun game => pure {game with title := t.getString}
|
| .Game => modifyCurGame fun game => pure {game with
|
||||||
|
title := title
|
||||||
|
tile := {game.tile with title := title}}
|
||||||
|
|
||||||
/-- Define the introduction of the current game/world/level. -/
|
/-- Define the introduction of the current game/world/level. -/
|
||||||
elab "Introduction" t:str : command => do
|
elab "Introduction" t:str : command => do
|
||||||
|
let intro ← t.getString.translate
|
||||||
match ← getCurLayer with
|
match ← getCurLayer with
|
||||||
| .Level => modifyCurLevel fun level => pure {level with introduction := t.getString}
|
| .Level => modifyCurLevel fun level => pure {level with introduction := intro}
|
||||||
| .World => modifyCurWorld fun world => pure {world with introduction := t.getString}
|
| .World => modifyCurWorld fun world => pure {world with introduction := intro}
|
||||||
| .Game => modifyCurGame fun game => pure {game with introduction := t.getString}
|
| .Game => modifyCurGame fun game => pure {game with introduction := intro}
|
||||||
|
|
||||||
/-- Define the info of the current game. Used for e.g. credits -/
|
/-- Define the info of the current game. Used for e.g. credits -/
|
||||||
elab "Info" t:str : command => do
|
elab "Info" t:str : command => do
|
||||||
|
let info ← t.getString.translate
|
||||||
match ← getCurLayer with
|
match ← getCurLayer with
|
||||||
| .Level => pure ()
|
| .Level =>
|
||||||
| .World => pure ()
|
logError "Can't use `Info` in a level!"
|
||||||
| .Game => modifyCurGame fun game => pure {game with info := t.getString}
|
pure ()
|
||||||
|
| .World =>
|
||||||
|
logError "Can't use `Info` in a world"
|
||||||
|
pure ()
|
||||||
|
| .Game => modifyCurGame fun game => pure {game with info := info}
|
||||||
|
|
||||||
|
/-- Provide the location of the image for the current game/world/level.
|
||||||
|
Paths are relative to the lean project's root. -/
|
||||||
|
elab "Image" t:str : command => do
|
||||||
|
let file := t.getString
|
||||||
|
if not <| ← System.FilePath.pathExists file then
|
||||||
|
logWarningAt t s!"Make sure the cover image '{file}' exists."
|
||||||
|
if not <| file.startsWith "images/" then
|
||||||
|
logWarningAt t s!"The file name should start with `images/`. Make sure all images are in that folder."
|
||||||
|
|
||||||
|
match ← getCurLayer with
|
||||||
|
| .Level =>
|
||||||
|
logWarning "Level-images not implemented yet" -- TODO
|
||||||
|
modifyCurLevel fun level => pure {level with image := file}
|
||||||
|
| .World =>
|
||||||
|
modifyCurWorld fun world => pure {world with image := file}
|
||||||
|
| .Game =>
|
||||||
|
logWarning "Main image of the game not implemented yet" -- TODO
|
||||||
|
modifyCurGame fun game => pure {game with image := file}
|
||||||
|
|
||||||
/-- Define the conclusion of the current game or current level if some
|
/-- Define the conclusion of the current game or current level if some
|
||||||
building a level. -/
|
building a level. -/
|
||||||
elab "Conclusion" t:str : command => do
|
elab "Conclusion" t:str : command => do
|
||||||
|
let conclusion ← t.getString.translate
|
||||||
match ← getCurLayer with
|
match ← getCurLayer with
|
||||||
| .Level => modifyCurLevel fun level => pure {level with conclusion := t.getString}
|
| .Level => modifyCurLevel fun level => pure {level with conclusion := conclusion}
|
||||||
| .World => modifyCurWorld fun world => pure {world with conclusion := t.getString}
|
| .World => modifyCurWorld fun world => pure {world with conclusion := conclusion}
|
||||||
| .Game => modifyCurGame fun game => pure {game with conclusion := t.getString}
|
| .Game => modifyCurGame fun game => pure {game with conclusion := conclusion}
|
||||||
|
|
||||||
|
/-- A list of games that should be played before this one. Example `Prerequisites "NNG" "STG"`. -/
|
||||||
|
elab "Prerequisites" t:str* : command => do
|
||||||
|
modifyCurGame fun game => pure {game with
|
||||||
|
tile := {game.tile with prerequisites := t.map (·.getString) |>.toList}}
|
||||||
|
|
||||||
|
/-- Short caption for the game (1 sentence) -/
|
||||||
|
elab "CaptionShort" t:str : command => do
|
||||||
|
let caption ← t.getString.translate
|
||||||
|
modifyCurGame fun game => pure {game with
|
||||||
|
tile := {game.tile with short := caption}}
|
||||||
|
|
||||||
|
/-- More detailed description what the game is about (2-4 sentences). -/
|
||||||
|
elab "CaptionLong" t:str : command => do
|
||||||
|
let caption ← t.getString.translate
|
||||||
|
modifyCurGame fun game => pure {game with
|
||||||
|
tile := {game.tile with long := caption}}
|
||||||
|
|
||||||
|
/-- A list of Languages the game is translated to. For example `Languages "de" "en"`.
|
||||||
|
|
||||||
|
The keys are ISO language codes.
|
||||||
|
-/
|
||||||
|
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
|
||||||
|
|
||||||
The inventory contains docs for tactics, lemmas, and definitions. These are all locked
|
The inventory contains docs for tactics, theorems, and definitions. These are all locked
|
||||||
in the first level and get enabled during the game.
|
in the first level and get enabled during the game.
|
||||||
-/
|
-/
|
||||||
|
|
||||||
/-! ## Doc entries -/
|
/-! ## Doc entries -/
|
||||||
|
|
||||||
/-- Copied from `Mathlib.Tactic.HelpCmd`.
|
|
||||||
|
|
||||||
Gets the initial string token in a parser description. For example, for a declaration like
|
|
||||||
`syntax "bla" "baz" term : tactic`, it returns `some "bla"`. Returns `none` for syntax declarations
|
|
||||||
that don't start with a string constant. -/
|
|
||||||
partial def getHeadTk (e : Expr) : Option String :=
|
|
||||||
match (Expr.withApp e λ e a => (e.constName?.getD Name.anonymous, a)) with
|
|
||||||
| (``ParserDescr.node, #[_, _, p]) => getHeadTk p
|
|
||||||
| (``ParserDescr.unary, #[.app _ (.lit (.strVal "withPosition")), p]) => getHeadTk p
|
|
||||||
| (``ParserDescr.unary, #[.app _ (.lit (.strVal "atomic")), p]) => getHeadTk p
|
|
||||||
| (``ParserDescr.binary, #[.app _ (.lit (.strVal "andthen")), p, _]) => getHeadTk p
|
|
||||||
| (``ParserDescr.nonReservedSymbol, #[.lit (.strVal tk), _]) => some tk
|
|
||||||
| (``ParserDescr.symbol, #[.lit (.strVal tk)]) => some tk
|
|
||||||
| (``Parser.withAntiquot, #[_, p]) => getHeadTk p
|
|
||||||
| (``Parser.leadingNode, #[_, _, p]) => getHeadTk p
|
|
||||||
| (``HAndThen.hAndThen, #[_, _, _, _, p, _]) => getHeadTk p
|
|
||||||
| (``Parser.nonReservedSymbol, #[.lit (.strVal tk), _]) => some tk
|
|
||||||
| (``Parser.symbol, #[.lit (.strVal tk)]) => some tk
|
|
||||||
| _ => none
|
|
||||||
|
|
||||||
/-- Modified from `#help` in `Mathlib.Tactic.HelpCmd` -/
|
|
||||||
def getTacticDocstring (env : Environment) (name: Name) : CommandElabM (Option String) := do
|
|
||||||
let name := name.toString (escape := false)
|
|
||||||
let mut decls : Lean.RBMap String (Array SyntaxNodeKind) compare := {}
|
|
||||||
|
|
||||||
let catName : Name := `tactic
|
|
||||||
let catStx : Ident := mkIdent catName -- TODO
|
|
||||||
let some cat := (Parser.parserExtension.getState env).categories.find? catName
|
|
||||||
| throwErrorAt catStx "{catStx} is not a syntax category"
|
|
||||||
liftTermElabM <| Term.addCategoryInfo catStx catName
|
|
||||||
for (k, _) in cat.kinds do
|
|
||||||
let mut used := false
|
|
||||||
if let some tk := do getHeadTk (← (← env.find? k).value?) then
|
|
||||||
let tk := tk.trim
|
|
||||||
if name ≠ tk then -- was `!name.isPrefixOf tk`
|
|
||||||
continue
|
|
||||||
used := true
|
|
||||||
decls := decls.insert tk ((decls.findD tk #[]).push k)
|
|
||||||
for (_name, ks) in decls do
|
|
||||||
for k in ks do
|
|
||||||
if let some doc ← findDocString? env k then
|
|
||||||
return doc
|
|
||||||
|
|
||||||
logWarning <| m!"Could not find a docstring for tactic {name}, consider adding one " ++
|
|
||||||
m!"using `TacticDoc {name} \"some doc\"`"
|
|
||||||
return none
|
|
||||||
|
|
||||||
/-- Retrieve the docstring associated to an inventory item. For Tactics, this
|
|
||||||
is not guaranteed to work. -/
|
|
||||||
def getDocstring (env : Environment) (name : Name) (type : InventoryType) :
|
|
||||||
CommandElabM (Option String) :=
|
|
||||||
match type with
|
|
||||||
-- for tactics it's a lookup following mathlib's `#help`. not guaranteed to be the correct one.
|
|
||||||
| .Tactic => getTacticDocstring env name
|
|
||||||
| .Lemma => findDocString? env name
|
|
||||||
-- TODO: for definitions not implemented yet, does it work?
|
|
||||||
| .Definition => findDocString? env name
|
|
||||||
|
|
||||||
/-- Checks if `inventoryTemplateExt` contains an entry with `(type, name)` and yields
|
|
||||||
a warning otherwise. If `template` is provided, it will add such an entry instead of yielding a
|
|
||||||
warning.
|
|
||||||
|
|
||||||
`ref` is the syntax piece. If `name` is not provided, it will use `ident.getId`.
|
|
||||||
I used this workaround, because I needed a new name (with correct namespace etc)
|
|
||||||
to be used, and I don't know how to create a new ident with same position but different name.
|
|
||||||
-/
|
|
||||||
def checkInventoryDoc (type : InventoryType) (ref : Ident) (name : Name := ref.getId)
|
|
||||||
(template : Option String := none) : CommandElabM Unit := do
|
|
||||||
-- note: `name` is an `Ident` (instead of `Name`) for the log messages.
|
|
||||||
let env ← getEnv
|
|
||||||
let n := name
|
|
||||||
-- Find a key with matching `(type, name)`.
|
|
||||||
match (inventoryTemplateExt.getState env).findIdx?
|
|
||||||
(fun x => x.name == n && x.type == type) with
|
|
||||||
-- Nothing to do if the entry exists
|
|
||||||
| some _ => pure ()
|
|
||||||
| none =>
|
|
||||||
match template with
|
|
||||||
-- Warn about missing documentation
|
|
||||||
| none =>
|
|
||||||
let docstring ← match (← getDocstring env name type) with
|
|
||||||
| some ds =>
|
|
||||||
logInfoAt ref (m!"Missing {type} Documentation. Using existing docstring. " ++
|
|
||||||
m!"Add {name}\nAdd `{type}Doc {name}` somewhere above this statement.")
|
|
||||||
pure s!"*(lean docstring)*\\\n{ds}"
|
|
||||||
| none =>
|
|
||||||
logWarningAt ref (m!"Missing {type} Documentation: {name}\nAdd `{type}Doc {name}` " ++
|
|
||||||
m!"somewhere above this statement.")
|
|
||||||
pure "(missing)"
|
|
||||||
|
|
||||||
-- We just add a dummy entry
|
|
||||||
modifyEnv (inventoryTemplateExt.addEntry · {
|
|
||||||
type := type
|
|
||||||
name := name
|
|
||||||
category := if type == .Lemma then s!"{n.getPrefix}" else ""
|
|
||||||
content := docstring})
|
|
||||||
-- Add the default documentation
|
|
||||||
| some s =>
|
|
||||||
modifyEnv (inventoryTemplateExt.addEntry · {
|
|
||||||
type := type
|
|
||||||
name := name
|
|
||||||
category := if type == .Lemma then s!"{n.getPrefix}" else ""
|
|
||||||
content := s })
|
|
||||||
logInfoAt ref (m!"Missing {type} Documentation: {name}, used default (e.g. provided " ++
|
|
||||||
m!"docstring) instead. If you want to write a different description, add " ++
|
|
||||||
m!"`{type}Doc {name}` somewhere above this statement.")
|
|
||||||
|
|
||||||
/-- Documentation entry of a tactic. Example:
|
/-- Documentation entry of a tactic. Example:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -196,37 +147,42 @@ TacticDoc rw "`rw` stands for rewrite, etc. "
|
|||||||
* The identifier is the tactics name. Some need to be escaped like `«have»`.
|
* The identifier is the tactics name. Some need to be escaped like `«have»`.
|
||||||
* The description is a string supporting Markdown.
|
* The description is a string supporting Markdown.
|
||||||
-/
|
-/
|
||||||
elab "TacticDoc" name:ident content:str : command =>
|
elab doc:docComment ? "TacticDoc" name:ident content:str ? : command => do
|
||||||
|
let doc ← parseDocCommentLegacy doc content
|
||||||
|
let doc ← doc.translate
|
||||||
modifyEnv (inventoryTemplateExt.addEntry · {
|
modifyEnv (inventoryTemplateExt.addEntry · {
|
||||||
type := .Tactic
|
type := .Tactic
|
||||||
name := name.getId
|
name := name.getId
|
||||||
displayName := name.getId.toString
|
displayName := name.getId.toString
|
||||||
content := content.getString })
|
content := doc })
|
||||||
|
|
||||||
/-- Documentation entry of a lemma. Example:
|
/-- Documentation entry of a theorem. Example:
|
||||||
|
|
||||||
```
|
```
|
||||||
LemmaDoc Nat.succ_pos as "succ_pos" in "Nat" "says `0 < n.succ`, etc."
|
TheoremDoc Nat.succ_pos as "succ_pos" in "Nat" "says `0 < n.succ`, etc."
|
||||||
```
|
```
|
||||||
|
|
||||||
* The first identifier is used in the commands `[New/Only/Disabled]Lemma`.
|
* The first identifier is used in the commands `[New/Only/Disabled]Theorem`.
|
||||||
It is preferably the true name of the lemma. However, this is not required.
|
It is preferably the true name of the theorem. However, this is not required.
|
||||||
* The string following `as` is the displayed name (in the Inventory).
|
* The string following `as` is the displayed name (in the Inventory).
|
||||||
* The identifier after `in` is the category to group lemmas by (in the Inventory).
|
* The identifier after `in` is the category to group theorems by (in the Inventory).
|
||||||
* The description is a string supporting Markdown.
|
* The description is a string supporting Markdown.
|
||||||
|
|
||||||
Use `[[mathlib_doc]]` in the string to insert a link to the mathlib doc page. This requires
|
Use `[[mathlib_doc]]` in the string to insert a link to the mathlib doc page. This requires
|
||||||
The lemma/definition to have the same fully qualified name as in mathlib.
|
The theorem/definition to have the same fully qualified name as in mathlib.
|
||||||
-/
|
-/
|
||||||
elab "LemmaDoc" name:ident "as" displayName:str "in" category:str content:str : command =>
|
elab doc:docComment ? "TheoremDoc" name:ident "as" displayName:str "in" category:str content:str ? :
|
||||||
|
command => do
|
||||||
|
let doc ← parseDocCommentLegacy doc content
|
||||||
|
let doc ← doc.translate
|
||||||
modifyEnv (inventoryTemplateExt.addEntry · {
|
modifyEnv (inventoryTemplateExt.addEntry · {
|
||||||
type := .Lemma
|
type := .Lemma
|
||||||
name := name.getId
|
name := name.getId
|
||||||
category := category.getString
|
category := category.getString
|
||||||
displayName := displayName.getString
|
displayName := displayName.getString
|
||||||
content := content.getString })
|
content := doc })
|
||||||
-- TODO: Catch the following behaviour.
|
-- TODO: Catch the following behaviour.
|
||||||
-- 1. if `LemmaDoc` appears in the same file as `Statement`, it will silently use
|
-- 1. if `TheoremDoc` appears in the same file as `Statement`, it will silently use
|
||||||
-- it but display the info that it wasn't found in `Statement`
|
-- it but display the info that it wasn't found in `Statement`
|
||||||
-- 2. if it appears in a later file, however, it will silently not do anything and keep
|
-- 2. if it appears in a later file, however, it will silently not do anything and keep
|
||||||
-- the first one.
|
-- the first one.
|
||||||
@@ -244,37 +200,26 @@ DefinitionDoc Function.Bijective as "Bijective" "defined as `Injective f ∧ Sur
|
|||||||
* The description is a string supporting Markdown.
|
* The description is a string supporting Markdown.
|
||||||
|
|
||||||
Use `[[mathlib_doc]]` in the string to insert a link to the mathlib doc page. This requires
|
Use `[[mathlib_doc]]` in the string to insert a link to the mathlib doc page. This requires
|
||||||
The lemma/definition to have the same fully qualified name as in mathlib.
|
The theorem/definition to have the same fully qualified name as in mathlib.
|
||||||
-/
|
-/
|
||||||
elab "DefinitionDoc" name:ident "as" displayName:str template:str : command =>
|
elab doc:docComment ? "DefinitionDoc" name:ident "as" displayName:str template:str ? : command => do
|
||||||
|
let doc ← parseDocCommentLegacy doc template
|
||||||
|
let doc ← doc.translate
|
||||||
modifyEnv (inventoryTemplateExt.addEntry · {
|
modifyEnv (inventoryTemplateExt.addEntry · {
|
||||||
type := .Definition
|
type := .Definition
|
||||||
name := name.getId,
|
name := name.getId,
|
||||||
displayName := displayName.getString,
|
displayName := displayName.getString,
|
||||||
content := template.getString })
|
content := doc })
|
||||||
|
|
||||||
/-! ## Add inventory items -/
|
/-! ## Add inventory items -/
|
||||||
|
|
||||||
def getStatement (name : Name) : CommandElabM MessageData := do
|
def checkCommandNotDuplicated (items : Array Name) (cmd := "Command") : CommandElabM Unit := do
|
||||||
return ← addMessageContextPartial (.ofPPFormat { pp := fun
|
if ¬ items.isEmpty then
|
||||||
| some ctx => ctx.runMetaM <| PrettyPrinter.ppSignature name
|
logWarning s!"You should only use one `{cmd}` per level, but it takes multiple arguments: `{cmd} obj₁ obj₂ obj₃`!"
|
||||||
| none => return "that's a bug." })
|
|
||||||
|
|
||||||
-- Note: We use `String` because we can't send `MessageData` as json, but
|
|
||||||
-- `MessageData` might be better for interactive highlighting.
|
|
||||||
/-- Get a string of the form `my_lemma (n : ℕ) : n + n = 2 * n`.
|
|
||||||
|
|
||||||
Note: A statement like `theorem abc : ∀ x : Nat, x ≥ 0` would be turned into
|
|
||||||
`theorem abc (x : Nat) : x ≥ 0` by `PrettyPrinter.ppSignature`. -/
|
|
||||||
def getStatementString (name : Name) : CommandElabM String := do
|
|
||||||
try
|
|
||||||
return ← (← getStatement name).toString
|
|
||||||
catch
|
|
||||||
| _ => throwError m!"Could not find {name} in context."
|
|
||||||
-- TODO: I think it would be nicer to unresolve Namespaces as much as possible.
|
|
||||||
|
|
||||||
/-- Declare tactics that are introduced by this level. -/
|
/-- Declare tactics that are introduced by this level. -/
|
||||||
elab "NewTactic" args:ident* : command => do
|
elab "NewTactic" args:ident* : command => do
|
||||||
|
checkCommandNotDuplicated ((←getCurLevel).tactics.new) "NewTactic"
|
||||||
for name in ↑args do
|
for name in ↑args do
|
||||||
checkInventoryDoc .Tactic name -- TODO: Add (template := "[docstring]")
|
checkInventoryDoc .Tactic name -- TODO: Add (template := "[docstring]")
|
||||||
modifyCurLevel fun level => pure {level with
|
modifyCurLevel fun level => pure {level with
|
||||||
@@ -282,14 +227,16 @@ elab "NewTactic" args:ident* : command => do
|
|||||||
|
|
||||||
/-- Declare tactics that are introduced by this level but do not show up in inventory. -/
|
/-- Declare tactics that are introduced by this level but do not show up in inventory. -/
|
||||||
elab "NewHiddenTactic" args:ident* : command => do
|
elab "NewHiddenTactic" args:ident* : command => do
|
||||||
|
checkCommandNotDuplicated ((←getCurLevel).tactics.hidden) "NewHiddenTactic"
|
||||||
for name in ↑args do
|
for name in ↑args do
|
||||||
checkInventoryDoc .Tactic name (template := "")
|
checkInventoryDoc .Tactic name (template := "")
|
||||||
modifyCurLevel fun level => pure {level with
|
modifyCurLevel fun level => pure {level with
|
||||||
tactics := {level.tactics with new := level.tactics.new ++ args.map (·.getId),
|
tactics := {level.tactics with new := level.tactics.new ++ args.map (·.getId),
|
||||||
hidden := level.tactics.hidden ++ args.map (·.getId)}}
|
hidden := level.tactics.hidden ++ args.map (·.getId)}}
|
||||||
|
|
||||||
/-- Declare lemmas that are introduced by this level. -/
|
/-- Declare theorems that are introduced by this level. -/
|
||||||
elab "NewLemma" args:ident* : command => do
|
elab "NewTheorem" args:ident* : command => do
|
||||||
|
checkCommandNotDuplicated ((←getCurLevel).lemmas.new) "NewTheorem"
|
||||||
for name in ↑args do
|
for name in ↑args do
|
||||||
try let _decl ← getConstInfo name.getId catch
|
try let _decl ← getConstInfo name.getId catch
|
||||||
| _ => logErrorAt name m!"unknown identifier '{name}'."
|
| _ => logErrorAt name m!"unknown identifier '{name}'."
|
||||||
@@ -299,6 +246,7 @@ elab "NewLemma" args:ident* : command => do
|
|||||||
|
|
||||||
/-- Declare definitions that are introduced by this level. -/
|
/-- Declare definitions that are introduced by this level. -/
|
||||||
elab "NewDefinition" args:ident* : command => do
|
elab "NewDefinition" args:ident* : command => do
|
||||||
|
checkCommandNotDuplicated ((←getCurLevel).definitions.new) "NewDefinition"
|
||||||
for name in ↑args do checkInventoryDoc .Definition name -- TODO: Add (template := "[mathlib]")
|
for name in ↑args do checkInventoryDoc .Definition name -- TODO: Add (template := "[mathlib]")
|
||||||
modifyCurLevel fun level => pure {level with
|
modifyCurLevel fun level => pure {level with
|
||||||
definitions := {level.definitions with new := args.map (·.getId)}}
|
definitions := {level.definitions with new := args.map (·.getId)}}
|
||||||
@@ -306,31 +254,36 @@ elab "NewDefinition" args:ident* : command => do
|
|||||||
/-- Declare tactics that are temporarily disabled in this level.
|
/-- Declare tactics that are temporarily disabled in this level.
|
||||||
This is ignored if `OnlyTactic` is set. -/
|
This is ignored if `OnlyTactic` is set. -/
|
||||||
elab "DisabledTactic" args:ident* : command => do
|
elab "DisabledTactic" args:ident* : command => do
|
||||||
|
checkCommandNotDuplicated ((←getCurLevel).tactics.disabled) "DisabledTactic"
|
||||||
for name in ↑args do checkInventoryDoc .Tactic name
|
for name in ↑args do checkInventoryDoc .Tactic name
|
||||||
modifyCurLevel fun level => pure {level with
|
modifyCurLevel fun level => pure {level with
|
||||||
tactics := {level.tactics with disabled := args.map (·.getId)}}
|
tactics := {level.tactics with disabled := args.map (·.getId)}}
|
||||||
|
|
||||||
/-- Declare lemmas that are temporarily disabled in this level.
|
/-- Declare theorems that are temporarily disabled in this level.
|
||||||
This is ignored if `OnlyLemma` is set. -/
|
This is ignored if `OnlyTheorem` is set. -/
|
||||||
elab "DisabledLemma" args:ident* : command => do
|
elab "DisabledTheorem" args:ident* : command => do
|
||||||
|
checkCommandNotDuplicated ((←getCurLevel).lemmas.disabled) "DisabledTheorem"
|
||||||
for name in ↑args do checkInventoryDoc .Lemma name
|
for name in ↑args do checkInventoryDoc .Lemma name
|
||||||
modifyCurLevel fun level => pure {level with
|
modifyCurLevel fun level => pure {level with
|
||||||
lemmas := {level.lemmas with disabled := args.map (·.getId)}}
|
lemmas := {level.lemmas with disabled := args.map (·.getId)}}
|
||||||
|
|
||||||
/-- Declare definitions that are temporarily disabled in this level -/
|
/-- Declare definitions that are temporarily disabled in this level -/
|
||||||
elab "DisabledDefinition" args:ident* : command => do
|
elab "DisabledDefinition" args:ident* : command => do
|
||||||
|
checkCommandNotDuplicated ((←getCurLevel).definitions.disabled) "DisabledDefinition"
|
||||||
for name in ↑args do checkInventoryDoc .Definition name
|
for name in ↑args do checkInventoryDoc .Definition name
|
||||||
modifyCurLevel fun level => pure {level with
|
modifyCurLevel fun level => pure {level with
|
||||||
definitions := {level.definitions with disabled := args.map (·.getId)}}
|
definitions := {level.definitions with disabled := args.map (·.getId)}}
|
||||||
|
|
||||||
/-- Temporarily disable all tactics except the ones declared here -/
|
/-- Temporarily disable all tactics except the ones declared here -/
|
||||||
elab "OnlyTactic" args:ident* : command => do
|
elab "OnlyTactic" args:ident* : command => do
|
||||||
|
checkCommandNotDuplicated ((←getCurLevel).tactics.only) "OnlyTactic"
|
||||||
for name in ↑args do checkInventoryDoc .Tactic name
|
for name in ↑args do checkInventoryDoc .Tactic name
|
||||||
modifyCurLevel fun level => pure {level with
|
modifyCurLevel fun level => pure {level with
|
||||||
tactics := {level.tactics with only := args.map (·.getId)}}
|
tactics := {level.tactics with only := args.map (·.getId)}}
|
||||||
|
|
||||||
/-- Temporarily disable all lemmas except the ones declared here -/
|
/-- Temporarily disable all theorems except the ones declared here -/
|
||||||
elab "OnlyLemma" args:ident* : command => do
|
elab "OnlyTheorem" args:ident* : command => do
|
||||||
|
checkCommandNotDuplicated ((←getCurLevel).lemmas.only) "OnlyTheorem"
|
||||||
for name in ↑args do checkInventoryDoc .Lemma name
|
for name in ↑args do checkInventoryDoc .Lemma name
|
||||||
modifyCurLevel fun level => pure {level with
|
modifyCurLevel fun level => pure {level with
|
||||||
lemmas := {level.lemmas with only := args.map (·.getId)}}
|
lemmas := {level.lemmas with only := args.map (·.getId)}}
|
||||||
@@ -338,65 +291,94 @@ elab "OnlyLemma" args:ident* : command => do
|
|||||||
/-- Temporarily disable all definitions except the ones declared here.
|
/-- Temporarily disable all definitions except the ones declared here.
|
||||||
This is ignored if `OnlyDefinition` is set. -/
|
This is ignored if `OnlyDefinition` is set. -/
|
||||||
elab "OnlyDefinition" args:ident* : command => do
|
elab "OnlyDefinition" args:ident* : command => do
|
||||||
|
checkCommandNotDuplicated ((←getCurLevel).definitions.only) "OnlyDefinition"
|
||||||
for name in ↑args do checkInventoryDoc .Definition name
|
for name in ↑args do checkInventoryDoc .Definition name
|
||||||
modifyCurLevel fun level => pure {level with
|
modifyCurLevel fun level => pure {level with
|
||||||
definitions := {level.definitions with only := args.map (·.getId)}}
|
definitions := {level.definitions with only := args.map (·.getId)}}
|
||||||
|
|
||||||
/-- Define which tab of Lemmas is opened by default. Usage: `LemmaTab "Nat"`.
|
/-- Define which tab of Lemmas is opened by default. Usage: `TheoremTab "Nat"`.
|
||||||
If omitted, the current tab will remain open. -/
|
If omitted, the current tab will remain open. -/
|
||||||
elab "LemmaTab" category:str : command =>
|
elab "TheoremTab" category:str : command =>
|
||||||
|
modifyCurLevel fun level => pure {level with lemmaTab := category.getString}
|
||||||
|
|
||||||
|
|
||||||
|
/-! DEPRECATED -/
|
||||||
|
|
||||||
|
elab doc:docComment ? "LemmaDoc" name:ident "as" displayName:str "in" category:str content:str ? :
|
||||||
|
command => do
|
||||||
|
logWarning "Deprecated. Has been renamed to `TheoremDoc`"
|
||||||
|
let doc ← parseDocCommentLegacy doc content
|
||||||
|
modifyEnv (inventoryTemplateExt.addEntry · {
|
||||||
|
type := .Lemma
|
||||||
|
name := name.getId
|
||||||
|
category := category.getString
|
||||||
|
displayName := displayName.getString
|
||||||
|
content := doc })
|
||||||
|
|
||||||
|
elab "NewLemma" args:ident* : command => do
|
||||||
|
logWarning "Deprecated. Has been renamed to `NewTheorem`"
|
||||||
|
checkCommandNotDuplicated ((←getCurLevel).lemmas.new) "NewLemma"
|
||||||
|
for name in ↑args do
|
||||||
|
try let _decl ← getConstInfo name.getId catch
|
||||||
|
| _ => logErrorAt name m!"unknown identifier '{name}'."
|
||||||
|
checkInventoryDoc .Lemma name -- TODO: Add (template := "[mathlib]")
|
||||||
|
modifyCurLevel fun level => pure {level with
|
||||||
|
lemmas := {level.lemmas with new := args.map (·.getId)}}
|
||||||
|
|
||||||
|
elab "DisabledLemma" args:ident* : command => do
|
||||||
|
logWarning "Deprecated. Has been renamed to `DisabledTheorem`"
|
||||||
|
checkCommandNotDuplicated ((←getCurLevel).lemmas.disabled) "DisabledLemma"
|
||||||
|
for name in ↑args do checkInventoryDoc .Lemma name
|
||||||
|
modifyCurLevel fun level => pure {level with
|
||||||
|
lemmas := {level.lemmas with disabled := args.map (·.getId)}}
|
||||||
|
|
||||||
|
elab "OnlyLemma" args:ident* : command => do
|
||||||
|
logWarning "Deprecated. Has been renamed to `OnlyTheorem`"
|
||||||
|
checkCommandNotDuplicated ((←getCurLevel).lemmas.only) "OnlyLemma"
|
||||||
|
for name in ↑args do checkInventoryDoc .Lemma name
|
||||||
|
modifyCurLevel fun level => pure {level with
|
||||||
|
lemmas := {level.lemmas with only := args.map (·.getId)}}
|
||||||
|
|
||||||
|
elab "LemmaTab" category:str : command => do
|
||||||
|
logWarning "Deprecated. Has been renamed to `TheoremTab`"
|
||||||
modifyCurLevel fun level => pure {level with lemmaTab := category.getString}
|
modifyCurLevel fun level => pure {level with lemmaTab := category.getString}
|
||||||
|
|
||||||
/-! # Exercise Statement -/
|
/-! # Exercise Statement -/
|
||||||
|
|
||||||
/-- A `attr := ...` option for `Statement`. Add attributes to the defined theorem. -/
|
/-- You can write `Statement add_comm (preamble := simp) .... := by` which
|
||||||
syntax statementAttr := "(" &"attr" ":=" Parser.Term.attrInstance,* ")"
|
will automatically execute the given tactic sequence before the exercise
|
||||||
-- TODO
|
is handed to the player.
|
||||||
|
|
||||||
-- TODO: Reuse the following code for checking available tactics in user code:
|
A common example is to use
|
||||||
structure UsedInventory where
|
|
||||||
(tactics : HashSet Name := {})
|
|
||||||
(definitions : HashSet Name := {})
|
|
||||||
(lemmas : HashSet Name := {})
|
|
||||||
|
|
||||||
partial def collectUsedInventory (stx : Syntax) (acc : UsedInventory := {}) : CommandElabM UsedInventory := do
|
```
|
||||||
match stx with
|
refine { carrier := M, ?.. }
|
||||||
| .missing => return acc
|
```
|
||||||
| .node _info kind args =>
|
|
||||||
if kind == `GameServer.Tactic.Hint || kind == `GameServer.Tactic.Branch then return acc
|
|
||||||
return ← args.foldlM (fun acc arg => collectUsedInventory arg acc) acc
|
|
||||||
| .atom _info val =>
|
|
||||||
-- ignore syntax elements that do not start with a letter
|
|
||||||
-- and ignore some standard keywords
|
|
||||||
let allowed := ["with", "fun", "at", "only", "by"]
|
|
||||||
if 0 < val.length ∧ val.data[0]!.isAlpha ∧ not (allowed.contains val) then
|
|
||||||
let val := val.dropRightWhile (fun c => c == '!' || c == '?') -- treat `simp?` and `simp!` like `simp`
|
|
||||||
return {acc with tactics := acc.tactics.insert val}
|
|
||||||
else
|
|
||||||
return acc
|
|
||||||
| .ident _info _rawVal val _preresolved =>
|
|
||||||
let ns ←
|
|
||||||
try resolveGlobalConst (mkIdent val)
|
|
||||||
catch | _ => pure [] -- catch "unknown constant" error
|
|
||||||
return ← ns.foldlM (fun acc n => do
|
|
||||||
if let some (.thmInfo ..) := (← getEnv).find? n then
|
|
||||||
return {acc with lemmas := acc.lemmas.insertMany ns}
|
|
||||||
else
|
|
||||||
return {acc with definitions := acc.definitions.insertMany ns}
|
|
||||||
) acc
|
|
||||||
|
|
||||||
-- #check expandOptDocComment?
|
in exercises, where the statement is a structure, to fill in all the data fields.
|
||||||
|
|
||||||
|
For example in "Show that all matrices with first column zero form a submodule",
|
||||||
|
you could provide the set of all these matrices as `carrier` and the player will receive
|
||||||
|
all the `Prop`-valued fields as goals.
|
||||||
|
-/
|
||||||
|
syntax preambleArg := atomic(" (preamble := " withoutPosition(tacticSeq) ")")
|
||||||
|
|
||||||
/-- Define the statement of the current level. -/
|
/-- Define the statement of the current level. -/
|
||||||
elab doc:docComment ? attrs:Parser.Term.attributes ?
|
elab doc:docComment ? attrs:Parser.Term.attributes ?
|
||||||
"Statement" statementName:ident ? sig:declSig val:declVal : command => do
|
"Statement" statementName:ident ? preamble:preambleArg ? sig:declSig val:declVal : command => do
|
||||||
let lvlIdx ← getCurLevelIdx
|
let lvlIdx ← getCurLevelIdx
|
||||||
|
|
||||||
let docContent : Option String := match doc with
|
-- add an optional tactic sequence that the engine executes before the game starts
|
||||||
| none => none
|
let preambleSeq : TSyntax `Lean.Parser.Tactic.tacticSeq ← match preamble with
|
||||||
| some s => match s.raw[1] with
|
| none => `(Parser.Tactic.tacticSeq|skip)
|
||||||
| .atom _ val => val.dropRight 2 |>.trim -- some (val.extract 0 (val.endPos - ⟨2⟩))
|
| some x => match x with
|
||||||
| _ => none --panic "not implemented error message" --throwErrorAt s "unexpected doc string{indentD s.raw[1]}"
|
| `(preambleArg| (preamble := $tac)) => pure tac
|
||||||
|
| _ => `(Parser.Tactic.tacticSeq|skip)
|
||||||
|
|
||||||
|
let docContent ← parseDocComment doc
|
||||||
|
let docContent ← match docContent with
|
||||||
|
| none => pure none
|
||||||
|
| some d => d.translate
|
||||||
|
|
||||||
-- Save the messages before evaluation of the proof.
|
-- Save the messages before evaluation of the proof.
|
||||||
let initMsgs ← modifyGet fun st => (st.messages, { st with messages := {} })
|
let initMsgs ← modifyGet fun st => (st.messages, { st with messages := {} })
|
||||||
@@ -412,36 +394,40 @@ elab doc:docComment ? attrs:Parser.Term.attributes ?
|
|||||||
collectUsedInventory proof
|
collectUsedInventory proof
|
||||||
| _ => throwError "expected `:=`"
|
| _ => throwError "expected `:=`"
|
||||||
|
|
||||||
|
-- extract the `tacticSeq` from `val` in order to add `let_intros` in front.
|
||||||
|
-- TODO: don't understand meta-programming enough to avoid having `let_intros`
|
||||||
|
-- duplicated three times below…
|
||||||
|
let tacticStx : TSyntax `Lean.Parser.Tactic.tacticSeq := match val with
|
||||||
|
| `(Parser.Command.declVal| := by $proof) => proof
|
||||||
|
| _ => panic "expected `:= by`"
|
||||||
|
|
||||||
-- Add theorem to context.
|
-- Add theorem to context.
|
||||||
match statementName with
|
match statementName with
|
||||||
| some name =>
|
| some name =>
|
||||||
let env ← getEnv
|
let env ← getEnv
|
||||||
|
|
||||||
let fullName := (← getCurrNamespace) ++ name.getId
|
let fullName := (← getCurrNamespace) ++ name.getId
|
||||||
|
|
||||||
if env.contains fullName then
|
if env.contains fullName then
|
||||||
let origType := (env.constants.map₁.find! fullName).type
|
let some orig := env.constants.map₁.find? fullName
|
||||||
|
| throwError s!"error in \"Statement\": `{fullName}` not found."
|
||||||
|
let origType := orig.type
|
||||||
-- TODO: Check if `origType` agrees with `sig` and output `logInfo` instead of `logWarning`
|
-- TODO: Check if `origType` agrees with `sig` and output `logInfo` instead of `logWarning`
|
||||||
-- in that case.
|
-- in that case.
|
||||||
logWarningAt name (m!"Environment already contains {fullName}! Only the existing " ++
|
logWarningAt name (m!"Environment already contains {fullName}! Only the existing " ++
|
||||||
m!"statement will be available in later levels:\n\n{origType}")
|
m!"statement will be available in later levels:\n\n{origType}")
|
||||||
let thmStatement ← `(command| $[$doc]? $[$attrs:attributes]? theorem $defaultDeclName $sig $val)
|
let thmStatement ← `(command| $[$doc]? $[$attrs:attributes]? theorem $defaultDeclName $sig := by {let_intros; $(⟨preambleSeq⟩); $(⟨tacticStx⟩)})
|
||||||
elabCommand thmStatement
|
elabCommand thmStatement
|
||||||
-- Check that statement has a docs entry.
|
-- Check that statement has a docs entry.
|
||||||
checkInventoryDoc .Lemma name (name := fullName) (template := docContent)
|
checkInventoryDoc .Lemma name (name := fullName) (template := docContent)
|
||||||
|
|
||||||
else
|
else
|
||||||
let thmStatement ← `(command| $[$doc]? $[$attrs:attributes]? theorem $name $sig $val)
|
let thmStatement ← `(command| $[$doc]? $[$attrs:attributes]? theorem $name $sig := by {let_intros; $(⟨preambleSeq⟩); $(⟨tacticStx⟩)})
|
||||||
elabCommand thmStatement
|
elabCommand thmStatement
|
||||||
-- Check that statement has a docs entry.
|
-- Check that statement has a docs entry.
|
||||||
checkInventoryDoc .Lemma name (name := fullName) (template := docContent)
|
checkInventoryDoc .Lemma name (name := fullName) (template := docContent)
|
||||||
|
|
||||||
| none =>
|
| none =>
|
||||||
let thmStatement ← `(command| $[$doc]? $[$attrs:attributes]? theorem $defaultDeclName $sig $val)
|
let thmStatement ← `(command| $[$doc]? $[$attrs:attributes]? theorem $defaultDeclName $sig := by {let_intros; $(⟨preambleSeq⟩); $(⟨tacticStx⟩)})
|
||||||
elabCommand thmStatement
|
elabCommand thmStatement
|
||||||
|
|
||||||
let msgs := (← get).messages
|
let msgs := (← get).messages
|
||||||
|
|
||||||
let mut hints := #[]
|
let mut hints := #[]
|
||||||
let mut nonHintMsgs := #[]
|
let mut nonHintMsgs := #[]
|
||||||
for msg in msgs.msgs do
|
for msg in msgs.msgs do
|
||||||
@@ -453,12 +439,41 @@ elab doc:docComment ? attrs:Parser.Term.attributes ?
|
|||||||
.nest hidden $
|
.nest hidden $
|
||||||
.compose (.ofGoal text) (.ofGoal goal) := msg.data then
|
.compose (.ofGoal text) (.ofGoal goal) := msg.data then
|
||||||
let hint ← liftTermElabM $ withMCtx ctx.mctx $ withLCtx ctx.lctx #[] $ withEnv ctx.env do
|
let hint ← liftTermElabM $ withMCtx ctx.mctx $ withLCtx ctx.lctx #[] $ withEnv ctx.env do
|
||||||
|
|
||||||
|
let goalDecl ← goal.getDecl
|
||||||
|
let fvars := goalDecl.lctx.decls.toArray.filterMap id |> Array.map (·.fvarId)
|
||||||
|
|
||||||
|
-- NOTE: This code about `hintFVarsNames` is duplicated from `RpcHandlers`
|
||||||
|
-- where the variable bijection is constructed, and they
|
||||||
|
-- need to be matching.
|
||||||
|
-- NOTE: This is a bit a hack of somebody who does not know how meta-programming works.
|
||||||
|
-- All we want here is a list of `userNames` for the `FVarId`s in `hintFVars`...
|
||||||
|
-- and we wrap them in `«{}»` here since I don't know how to do it later.
|
||||||
|
let mut hintFVarsNames : Array Expr := #[]
|
||||||
|
for fvar in fvars do
|
||||||
|
let name₁ ← fvar.getUserName
|
||||||
|
hintFVarsNames := hintFVarsNames.push <| Expr.fvar ⟨s!"«\{{name₁}}»"⟩
|
||||||
|
|
||||||
|
let text ← instantiateMVars (mkMVar text)
|
||||||
|
|
||||||
|
-- Evaluate the text in the `Hint`'s context to get the old variable names.
|
||||||
|
let rawText := (← GameServer.evalHintMessage text) hintFVarsNames
|
||||||
|
let ctx₂ := {env := ← getEnv, mctx := ← getMCtx, lctx := ← getLCtx, opts := {}}
|
||||||
|
let rawText : String ← (MessageData.withContext ctx₂ rawText).toString
|
||||||
|
|
||||||
return {
|
return {
|
||||||
goal := ← abstractCtx goal
|
goal := ← abstractCtx goal
|
||||||
text := ← instantiateMVars (mkMVar text)
|
text := text
|
||||||
|
rawText := rawText
|
||||||
strict := strict == 1
|
strict := strict == 1
|
||||||
hidden := hidden == 1
|
hidden := hidden == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- Note: The current setup for hints is a bit convoluted, but for now we need to
|
||||||
|
-- send the text once through i18n to register it in the env extension.
|
||||||
|
-- This could probably be rewritten once i18n works fully.
|
||||||
|
let _ ← hint.rawText.translate
|
||||||
|
|
||||||
hints := hints.push hint
|
hints := hints.push hint
|
||||||
else
|
else
|
||||||
nonHintMsgs := nonHintMsgs.push msg
|
nonHintMsgs := nonHintMsgs.push msg
|
||||||
@@ -484,6 +499,7 @@ elab doc:docComment ? attrs:Parser.Term.attributes ?
|
|||||||
modifyCurLevel fun level => pure { level with
|
modifyCurLevel fun level => pure { level with
|
||||||
module := env.header.mainModule
|
module := env.header.mainModule
|
||||||
goal := sig,
|
goal := sig,
|
||||||
|
preamble := preambleSeq
|
||||||
scope := scope,
|
scope := scope,
|
||||||
descrText := docContent
|
descrText := docContent
|
||||||
statementName := match statementName with
|
statementName := match statementName with
|
||||||
@@ -493,26 +509,12 @@ elab doc:docComment ? attrs:Parser.Term.attributes ?
|
|||||||
hints := hints
|
hints := hints
|
||||||
tactics := {level.tactics with used := usedInventory.tactics.toArray}
|
tactics := {level.tactics with used := usedInventory.tactics.toArray}
|
||||||
definitions := {level.definitions with used := usedInventory.definitions.toArray}
|
definitions := {level.definitions with used := usedInventory.definitions.toArray}
|
||||||
lemmas := {level.lemmas with used := usedInventory.lemmas.toArray} }
|
lemmas := {level.lemmas with used := usedInventory.lemmas.toArray}
|
||||||
|
}
|
||||||
|
|
||||||
/-! # Hints -/
|
/-! # Hints -/
|
||||||
|
|
||||||
syntax hintArg := atomic(" (" (&"strict" <|> &"hidden") " := " withoutPosition(term) ")")
|
open GameServer in
|
||||||
|
|
||||||
/-- Remove any spaces at the beginning of a new line -/
|
|
||||||
partial def removeIndentation (s : String) : String :=
|
|
||||||
let rec loop (i : String.Pos) (acc : String) (removeSpaces := false) : String :=
|
|
||||||
let c := s.get i
|
|
||||||
let i := s.next i
|
|
||||||
if s.atEnd i then
|
|
||||||
acc.push c
|
|
||||||
else if removeSpaces && c == ' ' then
|
|
||||||
loop i acc (removeSpaces := true)
|
|
||||||
else if c == '\n' then
|
|
||||||
loop i (acc.push c) (removeSpaces := true)
|
|
||||||
else
|
|
||||||
loop i (acc.push c)
|
|
||||||
loop ⟨0⟩ ""
|
|
||||||
|
|
||||||
/-- A tactic that can be used inside `Statement`s to indicate in which proof states players should
|
/-- A tactic that can be used inside `Statement`s to indicate in which proof states players should
|
||||||
see hints. The tactic does not affect the goal state.
|
see hints. The tactic does not affect the goal state.
|
||||||
@@ -575,7 +577,8 @@ elab (name := GameServer.Tactic.Branch) "Branch" t:tacticSeq : tactic => do
|
|||||||
-- Show an info whether the branch proofs all remaining goals.
|
-- Show an info whether the branch proofs all remaining goals.
|
||||||
let gs ← Tactic.getUnsolvedGoals
|
let gs ← Tactic.getUnsolvedGoals
|
||||||
if gs.isEmpty then
|
if gs.isEmpty then
|
||||||
trace[debug] "This branch finishes the proof."
|
-- trace[debug] "This branch finishes the proof."
|
||||||
|
pure ()
|
||||||
else
|
else
|
||||||
trace[debug] "This branch leaves open goals."
|
trace[debug] "This branch leaves open goals."
|
||||||
|
|
||||||
@@ -626,6 +629,7 @@ elab "Template" tacs:tacticSeq : tactic => do
|
|||||||
modifyLevel (←getCurLevelId) fun level => do
|
modifyLevel (←getCurLevelId) fun level => do
|
||||||
return {level with template := s!"{template}"}
|
return {level with template := s!"{template}"}
|
||||||
|
|
||||||
|
|
||||||
-- TODO: Notes for testing if a declaration has the simp attribute
|
-- TODO: Notes for testing if a declaration has the simp attribute
|
||||||
|
|
||||||
-- -- Test: From zulip
|
-- -- Test: From zulip
|
||||||
@@ -644,98 +648,6 @@ elab "Template" tacs:tacticSeq : tactic => do
|
|||||||
|
|
||||||
/-! # Make Game -/
|
/-! # Make Game -/
|
||||||
|
|
||||||
def GameLevel.getInventory (level : GameLevel) : InventoryType → InventoryInfo
|
|
||||||
| .Tactic => level.tactics
|
|
||||||
| .Definition => level.definitions
|
|
||||||
| .Lemma => level.lemmas
|
|
||||||
|
|
||||||
def GameLevel.setComputedInventory (level : GameLevel) :
|
|
||||||
InventoryType → Array InventoryTile → GameLevel
|
|
||||||
| .Tactic, v => {level with tactics := {level.tactics with tiles := v}}
|
|
||||||
| .Definition, v => {level with definitions := {level.definitions with tiles := v}}
|
|
||||||
| .Lemma, v => {level with lemmas := {level.lemmas with tiles := v}}
|
|
||||||
|
|
||||||
partial def removeTransitiveAux (id : Name) (arrows : HashMap Name (HashSet Name))
|
|
||||||
(newArrows : HashMap Name (HashSet Name)) (decendants : HashMap Name (HashSet Name)) :
|
|
||||||
HashMap Name (HashSet Name) × HashMap Name (HashSet Name) := Id.run do
|
|
||||||
match (newArrows.find? id, decendants.find? id) with
|
|
||||||
| (some _, some _) => return (newArrows, decendants)
|
|
||||||
| _ =>
|
|
||||||
let mut newArr := newArrows
|
|
||||||
let mut desc := decendants
|
|
||||||
desc := desc.insert id {} -- mark as worked in case of loops
|
|
||||||
newArr := newArr.insert id {} -- mark as worked in case of loops
|
|
||||||
let children := arrows.findD id {}
|
|
||||||
let mut trimmedChildren := children
|
|
||||||
let mut theseDescs := children
|
|
||||||
for child in children do
|
|
||||||
(newArr, desc) := removeTransitiveAux child arrows newArr desc
|
|
||||||
let childDescs := desc.findD child {}
|
|
||||||
theseDescs := theseDescs.insertMany childDescs
|
|
||||||
for d in childDescs do
|
|
||||||
trimmedChildren := trimmedChildren.erase d
|
|
||||||
desc := desc.insert id theseDescs
|
|
||||||
newArr := newArr.insert id trimmedChildren
|
|
||||||
return (newArr, desc)
|
|
||||||
|
|
||||||
def removeTransitive (arrows : HashMap Name (HashSet Name)) : CommandElabM (HashMap Name (HashSet Name)) := do
|
|
||||||
let mut newArr := {}
|
|
||||||
let mut desc := {}
|
|
||||||
for id in arrows.toArray.map Prod.fst do
|
|
||||||
(newArr, desc) := removeTransitiveAux id arrows newArr desc
|
|
||||||
if (desc.findD id {}).contains id then
|
|
||||||
logError <| m!"Loop at {id}. " ++
|
|
||||||
m!"This should not happen and probably means that `findLoops` has a bug."
|
|
||||||
-- DEBUG:
|
|
||||||
-- for ⟨x, hx⟩ in desc.toList do
|
|
||||||
-- m := m ++ m!"{x}: {hx.toList}\n"
|
|
||||||
-- logError m
|
|
||||||
|
|
||||||
return newArr
|
|
||||||
|
|
||||||
/-- The recursive part of `findLoops`. Finds loops that appear as successors of `node`.
|
|
||||||
|
|
||||||
For performance reason it returns a HashSet of visited
|
|
||||||
nodes as well. This is filled with all nodes ever looked at as they cannot be
|
|
||||||
part of a loop anymore. -/
|
|
||||||
partial def findLoopsAux (arrows : HashMap Name (HashSet Name)) (node : Name)
|
|
||||||
(path : Array Name := #[]) (visited : HashSet Name := {}) :
|
|
||||||
Array Name × HashSet Name := Id.run do
|
|
||||||
let mut visited := visited
|
|
||||||
match path.getIdx? node with
|
|
||||||
| some i =>
|
|
||||||
-- Found a loop: `node` is already the iᵗʰ element of the path
|
|
||||||
return (path.extract i path.size, visited.insert node)
|
|
||||||
| none =>
|
|
||||||
for successor in arrows.findD node {} do
|
|
||||||
-- If we already visited the successor, it cannot be part of a loop anymore
|
|
||||||
if visited.contains successor then
|
|
||||||
continue
|
|
||||||
-- Find any loop involving `successor`
|
|
||||||
let (loop, _) := findLoopsAux arrows successor (path.push node) visited
|
|
||||||
visited := visited.insert successor
|
|
||||||
-- No loop found in the dependants of `successor`
|
|
||||||
if loop.isEmpty then
|
|
||||||
continue
|
|
||||||
-- Found a loop, return it
|
|
||||||
return (loop, visited)
|
|
||||||
return (#[], visited.insert node)
|
|
||||||
|
|
||||||
/-- Find a loop in the graph and return it. Returns `[]` if there are no loops. -/
|
|
||||||
partial def findLoops (arrows : HashMap Name (HashSet Name)) : List Name := Id.run do
|
|
||||||
let mut visited : HashSet Name := {}
|
|
||||||
for node in arrows.toArray.map (·.1) do
|
|
||||||
-- Skip a node if it was already visited
|
|
||||||
if visited.contains node then
|
|
||||||
continue
|
|
||||||
-- `findLoopsAux` returns a loop or `[]` together with a set of nodes it visited on its
|
|
||||||
-- search starting from `node`
|
|
||||||
let (loop, moreVisited) := (findLoopsAux arrows node (visited := visited))
|
|
||||||
visited := moreVisited
|
|
||||||
if !loop.isEmpty then
|
|
||||||
return loop.toList
|
|
||||||
return []
|
|
||||||
|
|
||||||
/-- The worlds of a game are joint by dependencies. These are
|
/-- The worlds of a game are joint by dependencies. These are
|
||||||
automatically computed but can also be defined with the syntax
|
automatically computed but can also be defined with the syntax
|
||||||
`Dependency World₁ → World₂ → World₃`. -/
|
`Dependency World₁ → World₂ → World₃`. -/
|
||||||
@@ -810,8 +722,12 @@ elab "MakeGame" : command => do
|
|||||||
-- Items that should not be displayed in inventory
|
-- Items that should not be displayed in inventory
|
||||||
let mut hiddenItems : HashSet Name := {}
|
let mut hiddenItems : HashSet Name := {}
|
||||||
|
|
||||||
|
let allWorlds := game.worlds.nodes.toArray
|
||||||
|
let nrWorlds := allWorlds.size
|
||||||
|
let mut nrLevels := 0
|
||||||
|
|
||||||
-- Calculate which "items" are used/new in which world
|
-- Calculate which "items" are used/new in which world
|
||||||
for (worldId, world) in game.worlds.nodes.toArray do
|
for (worldId, world) in allWorlds do
|
||||||
let mut usedItems : HashSet Name := {}
|
let mut usedItems : HashSet Name := {}
|
||||||
let mut newItems : HashSet Name := {}
|
let mut newItems : HashSet Name := {}
|
||||||
for inventoryType in #[.Tactic, .Definition, .Lemma] do
|
for inventoryType in #[.Tactic, .Definition, .Lemma] do
|
||||||
@@ -826,7 +742,8 @@ elab "MakeGame" : command => do
|
|||||||
| 0 => pure ()
|
| 0 => pure ()
|
||||||
| 1 => pure () -- level ids start with 1, so we need to skip 1, too
|
| 1 => pure () -- level ids start with 1, so we need to skip 1, too
|
||||||
| i₀ + 1 =>
|
| i₀ + 1 =>
|
||||||
match (world.levels.find! (i₀)).statementName with
|
let some idx := world.levels.find? (i₀) | throwError s!"Level {i₀ + 1} not found for world {worldId}!"
|
||||||
|
match (idx).statementName with
|
||||||
| .anonymous => pure ()
|
| .anonymous => pure ()
|
||||||
| .num _ _ => panic "Did not expect to get a numerical statement name!"
|
| .num _ _ => panic "Did not expect to get a numerical statement name!"
|
||||||
| .str pre s =>
|
| .str pre s =>
|
||||||
@@ -837,7 +754,9 @@ elab "MakeGame" : command => do
|
|||||||
|
|
||||||
-- if the last level was named, we need to add it as a new lemma
|
-- if the last level was named, we need to add it as a new lemma
|
||||||
let i₀ := world.levels.size
|
let i₀ := world.levels.size
|
||||||
match (world.levels.find! (i₀)).statementName with
|
|
||||||
|
let some idx := world.levels.find? (i₀) | throwError s!"Level {i₀} not found for world {worldId}!"
|
||||||
|
match (idx).statementName with
|
||||||
| .anonymous => pure ()
|
| .anonymous => pure ()
|
||||||
| .num _ _ => panic "Did not expect to get a numerical statement name!"
|
| .num _ _ => panic "Did not expect to get a numerical statement name!"
|
||||||
| .str pre s =>
|
| .str pre s =>
|
||||||
@@ -850,9 +769,12 @@ elab "MakeGame" : command => do
|
|||||||
-- logInfo m!"{worldId} uses: {usedItems.toList}"
|
-- logInfo m!"{worldId} uses: {usedItems.toList}"
|
||||||
-- logInfo m!"{worldId} introduces: {newItems.toList}"
|
-- logInfo m!"{worldId} introduces: {newItems.toList}"
|
||||||
|
|
||||||
|
-- Moreover, count the number of levels in the game
|
||||||
|
nrLevels := nrLevels + world.levels.toArray.size
|
||||||
|
|
||||||
/- for each "item" this is a HashSet of `worldId`s that introduce this item -/
|
/- for each "item" this is a HashSet of `worldId`s that introduce this item -/
|
||||||
let mut worldsWithNewItem : HashMap Name (HashSet Name) := {}
|
let mut worldsWithNewItem : HashMap Name (HashSet Name) := {}
|
||||||
for (worldId, _world) in game.worlds.nodes.toArray do
|
for (worldId, _world) in allWorlds do
|
||||||
for newItem in newItemsInWorld.findD worldId {} do
|
for newItem in newItemsInWorld.findD worldId {} do
|
||||||
worldsWithNewItem := worldsWithNewItem.insert newItem $
|
worldsWithNewItem := worldsWithNewItem.insert newItem $
|
||||||
(worldsWithNewItem.findD newItem {}).insert worldId
|
(worldsWithNewItem.findD newItem {}).insert worldId
|
||||||
@@ -864,7 +786,7 @@ elab "MakeGame" : command => do
|
|||||||
let mut dependencyReasons : HashMap (Name × Name) (HashSet Name) := {}
|
let mut dependencyReasons : HashMap (Name × Name) (HashSet Name) := {}
|
||||||
|
|
||||||
-- Calculate world dependency graph `game.worlds`
|
-- Calculate world dependency graph `game.worlds`
|
||||||
for (dependentWorldId, _dependentWorld) in game.worlds.nodes.toArray do
|
for (dependentWorldId, _dependentWorld) in allWorlds do
|
||||||
let mut dependsOnWorlds : HashSet Name := {}
|
let mut dependsOnWorlds : HashSet Name := {}
|
||||||
-- Adding manual dependencies that were specified via the `Dependency` command.
|
-- Adding manual dependencies that were specified via the `Dependency` command.
|
||||||
for (sourceId, targetId) in game.worlds.edges do
|
for (sourceId, targetId) in game.worlds.edges do
|
||||||
@@ -915,14 +837,25 @@ elab "MakeGame" : command => do
|
|||||||
logError m!"{w1} depends on {w2} because of {items.toList}."
|
logError m!"{w1} depends on {w2} because of {items.toList}."
|
||||||
else
|
else
|
||||||
worldDependsOnWorlds ← removeTransitive worldDependsOnWorlds
|
worldDependsOnWorlds ← removeTransitive worldDependsOnWorlds
|
||||||
|
|
||||||
|
-- need to delete all existing edges as they are already present in `worldDependsOnWorlds`.
|
||||||
|
modifyCurGame fun game =>
|
||||||
|
pure {game with worlds := {game.worlds with edges := Array.empty}}
|
||||||
|
|
||||||
for (dependentWorldId, worldIds) in worldDependsOnWorlds.toArray do
|
for (dependentWorldId, worldIds) in worldDependsOnWorlds.toArray do
|
||||||
modifyCurGame fun game =>
|
modifyCurGame fun game =>
|
||||||
pure {game with worlds := {game.worlds with
|
pure {game with worlds := {game.worlds with
|
||||||
edges := game.worlds.edges.append (worldIds.toArray.map fun wid => (wid, dependentWorldId))}}
|
edges := game.worlds.edges.append (worldIds.toArray.map fun wid => (wid, dependentWorldId))}}
|
||||||
|
|
||||||
|
-- Add the number of levels and worlds to the tile for the landing page
|
||||||
|
modifyCurGame fun game => pure {game with tile := {game.tile with
|
||||||
|
levels := nrLevels
|
||||||
|
worlds := nrWorlds }}
|
||||||
|
|
||||||
-- Apparently we need to reload `game` to get the changes to `game.worlds` we just made
|
-- Apparently we need to reload `game` to get the changes to `game.worlds` we just made
|
||||||
let game ← getCurGame
|
let game ← getCurGame
|
||||||
|
|
||||||
|
let mut allItemsByType : HashMap InventoryType (HashSet Name) := {}
|
||||||
-- Compute which inventory items are available in which level:
|
-- Compute which inventory items are available in which level:
|
||||||
for inventoryType in #[.Tactic, .Definition, .Lemma] do
|
for inventoryType in #[.Tactic, .Definition, .Lemma] do
|
||||||
|
|
||||||
@@ -945,7 +878,8 @@ elab "MakeGame" : command => do
|
|||||||
| 1 => pure () -- level ids start with 1, so we need to skip 1, too.
|
| 1 => pure () -- level ids start with 1, so we need to skip 1, too.
|
||||||
| i₀ + 1 =>
|
| i₀ + 1 =>
|
||||||
-- add named statement from previous level to the available lemmas.
|
-- add named statement from previous level to the available lemmas.
|
||||||
match (world.levels.find! (i₀)).statementName with
|
let some idx := world.levels.find? (i₀) | throwError s!"Level {i₀ + 1} not found for world {worldId}!"
|
||||||
|
match (idx).statementName with
|
||||||
| .anonymous => pure ()
|
| .anonymous => pure ()
|
||||||
| .num _ _ => panic "Did not expect to get a numerical statement name!"
|
| .num _ _ => panic "Did not expect to get a numerical statement name!"
|
||||||
| .str pre s =>
|
| .str pre s =>
|
||||||
@@ -959,7 +893,8 @@ elab "MakeGame" : command => do
|
|||||||
match i₀ with
|
match i₀ with
|
||||||
| 0 => logWarning m!"World `{worldId}` contains no levels."
|
| 0 => logWarning m!"World `{worldId}` contains no levels."
|
||||||
| i₀ =>
|
| i₀ =>
|
||||||
match (world.levels.find! (i₀)).statementName with
|
let some idx := world.levels.find? (i₀) | throwError s!"Level {i₀} not found for world {worldId}!"
|
||||||
|
match (idx).statementName with
|
||||||
| .anonymous => pure ()
|
| .anonymous => pure ()
|
||||||
| .num _ _ => panic "Did not expect to get a numerical statement name!"
|
| .num _ _ => panic "Did not expect to get a numerical statement name!"
|
||||||
| .str pre s =>
|
| .str pre s =>
|
||||||
@@ -984,6 +919,7 @@ elab "MakeGame" : command => do
|
|||||||
name := item
|
name := item
|
||||||
displayName := data.displayName
|
displayName := data.displayName
|
||||||
category := data.category
|
category := data.category
|
||||||
|
altTitle := data.statement
|
||||||
hidden := hiddenItems.contains item })
|
hidden := hiddenItems.contains item })
|
||||||
|
|
||||||
|
|
||||||
@@ -1003,6 +939,7 @@ elab "MakeGame" : command => do
|
|||||||
displayName := data.displayName
|
displayName := data.displayName
|
||||||
category := data.category
|
category := data.category
|
||||||
locked := false
|
locked := false
|
||||||
|
altTitle := data.statement
|
||||||
hidden := hiddenItems.contains item }
|
hidden := hiddenItems.contains item }
|
||||||
itemsInWorld := itemsInWorld.insert worldId items
|
itemsInWorld := itemsInWorld.insert worldId items
|
||||||
|
|
||||||
@@ -1022,7 +959,8 @@ elab "MakeGame" : command => do
|
|||||||
displayName := data.displayName
|
displayName := data.displayName
|
||||||
category := data.category
|
category := data.category
|
||||||
locked := false
|
locked := false
|
||||||
hidden := levelInfo.hidden.contains item }
|
altTitle := data.statement
|
||||||
|
hidden := hiddenItems.contains item }
|
||||||
|
|
||||||
-- add the exercise statement from the previous level
|
-- add the exercise statement from the previous level
|
||||||
-- if it was named
|
-- if it was named
|
||||||
@@ -1035,6 +973,7 @@ elab "MakeGame" : command => do
|
|||||||
name := name
|
name := name
|
||||||
displayName := data.displayName
|
displayName := data.displayName
|
||||||
category := data.category
|
category := data.category
|
||||||
|
altTitle := data.statement
|
||||||
locked := false }
|
locked := false }
|
||||||
|
|
||||||
-- add marks for `disabled` and `new` lemmas here, so that they only apply to
|
-- add marks for `disabled` and `new` lemmas here, so that they only apply to
|
||||||
@@ -1052,17 +991,18 @@ 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
|
||||||
|
|
||||||
/-! # Debugging tools -/
|
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).map (fun tile => {tile with hidden := hiddenItems.contains tile.name})
|
||||||
|
tactics := (← getTiles .Tactic).map (fun tile => {tile with hidden := hiddenItems.contains tile.name})
|
||||||
|
definitions := (← getTiles .Definition).map (fun tile => {tile with hidden := hiddenItems.contains tile.name})
|
||||||
|
lemmaTab := none
|
||||||
|
}
|
||||||
|
|
||||||
-- /-- Print current game for debugging purposes. -/
|
saveGameData allItemsByType inventory
|
||||||
-- elab "PrintCurGame" : command => do
|
|
||||||
-- logInfo (toJson (← getCurGame))
|
|
||||||
|
|
||||||
/-- Print current level for debugging purposes. -/
|
|
||||||
elab "PrintCurLevel" : command => do
|
|
||||||
logInfo (repr (← getCurLevel))
|
|
||||||
|
|
||||||
/-- Print levels for debugging purposes. -/
|
|
||||||
elab "PrintLevels" : command => do
|
|
||||||
logInfo $ repr $ (← getCurWorld).levels.toArray
|
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import GameServer.AbstractCtx
|
import GameServer.AbstractCtx
|
||||||
import GameServer.Graph
|
import GameServer.Graph
|
||||||
|
import GameServer.Hints
|
||||||
|
|
||||||
|
open GameServer
|
||||||
|
|
||||||
|
-- TODO: Is there a better place?
|
||||||
|
/-- Keywords that the server should not consider as tactics. -/
|
||||||
|
def GameServer.ALLOWED_KEYWORDS : List String :=
|
||||||
|
["with", "fun", "at", "only", "by", "generalizing"]
|
||||||
|
|
||||||
/-- The default game name if `Game "MyGame"` is not used. -/
|
/-- The default game name if `Game "MyGame"` is not used. -/
|
||||||
def defaultGameName: String := "MyGame"
|
def defaultGameName: String := "MyGame"
|
||||||
@@ -18,29 +24,13 @@ defined in this file.
|
|||||||
|
|
||||||
open Lean
|
open Lean
|
||||||
|
|
||||||
/-! ## Hints -/
|
|
||||||
|
|
||||||
/-- A hint to help the user with a specific goal state -/
|
|
||||||
structure GoalHintEntry where
|
|
||||||
goal : AbstractCtxResult
|
|
||||||
/-- Text of the hint as an expression of type `Array Expr → MessageData` -/
|
|
||||||
text : Expr
|
|
||||||
/-- If true, then hint should be hidden and only be shown on player's request -/
|
|
||||||
hidden : Bool := false
|
|
||||||
/-- If true, then the goal must contain only the assumptions specified in `goal` and no others -/
|
|
||||||
strict : Bool := false
|
|
||||||
|
|
||||||
instance : Repr GoalHintEntry := {
|
|
||||||
reprPrec := fun a n => reprPrec a.text n
|
|
||||||
}
|
|
||||||
|
|
||||||
/-! ## Inventory (documentation)
|
/-! ## Inventory (documentation)
|
||||||
|
|
||||||
The inventory contains documentation that the user can access.
|
The inventory contains documentation that the user can access.
|
||||||
There are three inventory types: Lemma, Tactic, Definition. They vary about in the information
|
There are three inventory types: Lemma, Tactic, Definition. They vary about in the information
|
||||||
they carry.
|
they carry.
|
||||||
|
|
||||||
The commands `LemmaDoc`, `TacticDoc`, and `DefinitionDoc` add keys and templates to an
|
The commands `TheoremDoc`, `TacticDoc`, and `DefinitionDoc` add keys and templates to an
|
||||||
env. extension called `InventoryTemplateExt`. Commands like `NewLemma`, etc. as well as
|
env. extension called `InventoryTemplateExt`. Commands like `NewLemma`, etc. as well as
|
||||||
`Statement` check if there is a key registered in this extension and might add a default or
|
`Statement` check if there is a key registered in this extension and might add a default or
|
||||||
print a warning if not.
|
print a warning if not.
|
||||||
@@ -106,8 +96,16 @@ structure InventoryTile where
|
|||||||
new := false
|
new := false
|
||||||
/-- hide the item in the inventory display -/
|
/-- hide the item in the inventory display -/
|
||||||
hidden := false
|
hidden := false
|
||||||
|
/-- hover text -/
|
||||||
|
altTitle : String := default
|
||||||
deriving ToJson, FromJson, Repr, Inhabited
|
deriving ToJson, FromJson, Repr, Inhabited
|
||||||
|
|
||||||
|
def InventoryItem.toTile (item : InventoryItem) : InventoryTile := {
|
||||||
|
name := item.name,
|
||||||
|
displayName := item.displayName
|
||||||
|
category := item.category
|
||||||
|
}
|
||||||
|
|
||||||
/-- The extension that stores the doc templates. Note that you can only add, but never modify
|
/-- The extension that stores the doc templates. Note that you can only add, but never modify
|
||||||
entries! -/
|
entries! -/
|
||||||
initialize inventoryTemplateExt :
|
initialize inventoryTemplateExt :
|
||||||
@@ -135,7 +133,18 @@ 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
|
||||||
|
|
||||||
|
-- TODO: Reuse the following code for checking available tactics in user code:
|
||||||
|
structure UsedInventory where
|
||||||
|
(tactics : HashSet Name := {})
|
||||||
|
(definitions : HashSet Name := {})
|
||||||
|
(lemmas : HashSet Name := {})
|
||||||
|
|
||||||
/-! ## Environment extensions for game specification -/
|
/-! ## Environment extensions for game specification -/
|
||||||
|
|
||||||
@@ -254,8 +263,65 @@ structure GameLevel where
|
|||||||
lemmas: InventoryInfo := default
|
lemmas: InventoryInfo := default
|
||||||
/-- A proof template that is printed in an empty editor. -/
|
/-- A proof template that is printed in an empty editor. -/
|
||||||
template: Option String := none
|
template: Option String := none
|
||||||
deriving Inhabited, Repr
|
/-- The image for this level. -/
|
||||||
|
image : String := default
|
||||||
|
/-- A sequence of tactics the game automatically executes before the first step. -/
|
||||||
|
preamble : TSyntax `Lean.Parser.Tactic.tacticSeq := default
|
||||||
|
deriving 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
|
||||||
|
module : Name
|
||||||
|
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
|
||||||
|
module := lvl.module
|
||||||
|
displayName := match lvl.statementName with
|
||||||
|
| .anonymous => none
|
||||||
|
| name => match (inventoryExt.getState env).find?
|
||||||
|
(fun x => x.name == name && x.type == .Lemma) with
|
||||||
|
| some n => n.displayName
|
||||||
|
| none => name.toString
|
||||||
|
-- Note: we could call `.find!` because we check in `Statement` that the
|
||||||
|
-- lemma doc must exist.
|
||||||
|
template := lvl.template
|
||||||
|
image := lvl.image
|
||||||
|
}
|
||||||
|
|
||||||
/-! ## World -/
|
/-! ## World -/
|
||||||
|
|
||||||
@@ -271,17 +337,46 @@ structure World where
|
|||||||
conclusion : String := default
|
conclusion : String := default
|
||||||
/-- The levels of the world. -/
|
/-- The levels of the world. -/
|
||||||
levels: HashMap Nat GameLevel := default
|
levels: HashMap Nat GameLevel := default
|
||||||
|
/-- The introduction image of the world. -/
|
||||||
|
image: String := default
|
||||||
deriving Inhabited
|
deriving Inhabited
|
||||||
|
|
||||||
instance : ToJson World := ⟨
|
instance : ToJson World := ⟨
|
||||||
fun world => Json.mkObj [
|
fun world => Json.mkObj [
|
||||||
("name", toJson world.name),
|
("name", toJson world.name),
|
||||||
("title", world.title),
|
("title", world.title),
|
||||||
("introduction", world.introduction)]
|
("introduction", world.introduction),
|
||||||
|
("image", world.image)]
|
||||||
⟩
|
⟩
|
||||||
|
|
||||||
/-! ## Game -/
|
/-! ## Game -/
|
||||||
|
|
||||||
|
/-- A tile as they are displayed on the servers landing page. -/
|
||||||
|
structure GameTile where
|
||||||
|
/-- The title of the game -/
|
||||||
|
title: String
|
||||||
|
/-- One catch phrase about the game -/
|
||||||
|
short: String := default
|
||||||
|
/-- One paragraph description what the game is about -/
|
||||||
|
long: String := default
|
||||||
|
/-- List of languages the game supports
|
||||||
|
|
||||||
|
TODO: What's the expectected format
|
||||||
|
TODO: Must be a list with a single language currently
|
||||||
|
-/
|
||||||
|
languages: List String := default
|
||||||
|
/-- A list of games which this one builds upon -/
|
||||||
|
prerequisites: List String := default
|
||||||
|
/-- Number of worlds in the game -/
|
||||||
|
worlds: Nat := default
|
||||||
|
/-- Number of levels in the game -/
|
||||||
|
levels: Nat := default
|
||||||
|
/-- A cover image of the game
|
||||||
|
|
||||||
|
TODO: What's the format? -/
|
||||||
|
image: String := default
|
||||||
|
deriving Inhabited, ToJson, FromJson
|
||||||
|
|
||||||
structure Game where
|
structure Game where
|
||||||
/-- Internal name of the game. -/
|
/-- Internal name of the game. -/
|
||||||
name : Name
|
name : Name
|
||||||
@@ -296,7 +391,18 @@ 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
|
||||||
deriving Inhabited, ToJson
|
/-- 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, FromJson
|
||||||
|
|
||||||
|
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 -/
|
||||||
|
|
||||||
|
|||||||
+347
-154
@@ -1,8 +1,13 @@
|
|||||||
/- This file is mostly copied from `Lean/Server/FileWorker.lean`. -/
|
/- This file is adapted from `Lean/Server/FileWorker.lean`. -/
|
||||||
import Lean.Server.FileWorker
|
import Lean.Server.FileWorker
|
||||||
import GameServer.Game
|
import GameServer.Game
|
||||||
|
import GameServer.ImportModules
|
||||||
|
import GameServer.SaveData
|
||||||
|
import GameServer.EnvExtensions
|
||||||
|
import GameServer.Tactic.LetIntros
|
||||||
|
|
||||||
namespace MyModule
|
namespace MyModule
|
||||||
|
|
||||||
open Lean
|
open Lean
|
||||||
open Elab
|
open Elab
|
||||||
open Parser
|
open Parser
|
||||||
@@ -16,7 +21,7 @@ private def mkEOI (pos : String.Pos) : Syntax :=
|
|||||||
mkNode ``Command.eoi #[atom]
|
mkNode ``Command.eoi #[atom]
|
||||||
|
|
||||||
partial def parseTactic (inputCtx : InputContext) (pmctx : ParserModuleContext)
|
partial def parseTactic (inputCtx : InputContext) (pmctx : ParserModuleContext)
|
||||||
(mps : ModuleParserState) (messages : MessageLog) (couldBeEndSnap : Bool) :
|
(mps : ModuleParserState) (messages : MessageLog) :
|
||||||
Syntax × ModuleParserState × MessageLog × String.Pos := Id.run do
|
Syntax × ModuleParserState × MessageLog × String.Pos := Id.run do
|
||||||
let mut pos := mps.pos
|
let mut pos := mps.pos
|
||||||
let mut recovering := mps.recovering
|
let mut recovering := mps.recovering
|
||||||
@@ -46,7 +51,8 @@ partial def parseTactic (inputCtx : InputContext) (pmctx : ParserModuleContext)
|
|||||||
|
|
||||||
end MyModule
|
end MyModule
|
||||||
|
|
||||||
namespace MyServer.FileWorker
|
namespace GameServer.FileWorker
|
||||||
|
|
||||||
open Lean
|
open Lean
|
||||||
open Lean.Server
|
open Lean.Server
|
||||||
open Lean.Server.FileWorker
|
open Lean.Server.FileWorker
|
||||||
@@ -55,88 +61,142 @@ open IO
|
|||||||
open Snapshots
|
open Snapshots
|
||||||
open JsonRpc
|
open JsonRpc
|
||||||
|
|
||||||
|
/--
|
||||||
|
Game-specific state to be packed on top of the `Server.FileWorker.WorkerState`
|
||||||
|
used by the Lean server.
|
||||||
|
-/
|
||||||
|
structure WorkerState :=
|
||||||
|
/--
|
||||||
|
Collection of items which are considered unlocked.
|
||||||
|
Tactics and theorems are mixed together.
|
||||||
|
-/
|
||||||
|
inventory : Array String
|
||||||
|
/--
|
||||||
|
Difficulty determines whether tactics/theorems can be locked.
|
||||||
|
* 0: do not check
|
||||||
|
* 1: give warnings when locked items are used
|
||||||
|
* 2: give errors when locked items are used
|
||||||
|
-/
|
||||||
|
difficulty : Nat
|
||||||
|
/--
|
||||||
|
`levelInfo` contains all the (static) information about the level which is not influenced
|
||||||
|
by the user's progress.
|
||||||
|
-/
|
||||||
|
levelInfo : LevelInfo
|
||||||
|
deriving ToJson, FromJson
|
||||||
|
|
||||||
|
/--
|
||||||
|
Pack the our custom `WorkerState` on top of the normal worker monad
|
||||||
|
`Server.FileWorker.WorkerM`.
|
||||||
|
-/
|
||||||
|
abbrev WorkerM := StateT WorkerState Server.FileWorker.WorkerM
|
||||||
|
|
||||||
section Elab
|
section Elab
|
||||||
|
|
||||||
def addErrorMessage (info : SourceInfo) (inputCtx : Parser.InputContext) (s : MessageData) :
|
/-- Add a message. use `(severity := .warning)` to specify the severity-/
|
||||||
|
def addMessage (info : SourceInfo) (inputCtx : Parser.InputContext)
|
||||||
|
(severity := MessageSeverity.warning) (s : MessageData) :
|
||||||
Elab.Command.CommandElabM Unit := do
|
Elab.Command.CommandElabM Unit := do
|
||||||
modify fun st => { st with
|
modify fun st => { st with
|
||||||
messages := st.messages.add {
|
messages := st.messages.add {
|
||||||
fileName := inputCtx.fileName
|
fileName := inputCtx.fileName
|
||||||
severity := MessageSeverity.error
|
severity := severity
|
||||||
pos := inputCtx.fileMap.toPosition (info.getPos?.getD 0)
|
pos := inputCtx.fileMap.toPosition (info.getPos?.getD 0)
|
||||||
data := s
|
data := s }}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
-- TODO: use HashSet for allowed tactics?
|
-- TODO: use HashSet for allowed tactics?
|
||||||
/-- Find all tactics in syntax object that are forbidden according to a
|
/--
|
||||||
set `allowed` of allowed tactics. -/
|
Find all tactics in syntax object that are forbidden according to a
|
||||||
partial def findForbiddenTactics (inputCtx : Parser.InputContext)
|
set `allowed` of allowed tactics.
|
||||||
(levelParams : Game.DidOpenLevelParams) (stx : Syntax) :
|
-/
|
||||||
Elab.Command.CommandElabM Unit := do
|
partial def findForbiddenTactics (inputCtx : Parser.InputContext) (workerState : WorkerState)
|
||||||
|
(stx : Syntax) : Elab.Command.CommandElabM Unit := do
|
||||||
|
let levelInfo := workerState.levelInfo
|
||||||
|
-- Parse the syntax object and look for tactics and declarations.
|
||||||
match stx with
|
match stx with
|
||||||
| .missing => return ()
|
| .missing => return ()
|
||||||
| .node _info _kind args =>
|
| .node _info _kind args =>
|
||||||
|
-- Go inside a node.
|
||||||
for arg in args do
|
for arg in args do
|
||||||
findForbiddenTactics inputCtx levelParams arg
|
findForbiddenTactics inputCtx workerState arg
|
||||||
| .atom info val =>
|
| .atom info val =>
|
||||||
-- ignore syntax elements that do not start with a letter
|
-- Atoms might be tactic names or other keywords.
|
||||||
-- and ignore "with" keyword
|
-- Note: We whitelisted known keywords because we cannot
|
||||||
let allowed := ["with", "fun", "at", "only", "by", "to"]
|
-- distinguish keywords from tactic names.
|
||||||
|
let allowed := GameServer.ALLOWED_KEYWORDS
|
||||||
|
-- Ignore syntax elements that do not start with a letter or are listed above.
|
||||||
if 0 < val.length ∧ val.data[0]!.isAlpha ∧ not (allowed.contains val) then
|
if 0 < val.length ∧ val.data[0]!.isAlpha ∧ not (allowed.contains val) then
|
||||||
let val := val.dropRightWhile (fun c => c == '!' || c == '?') -- treat `simp?` and `simp!` like `simp`
|
-- Treat `simp?` and `simp!` like `simp`
|
||||||
match levelParams.tactics.find? (·.name.toString == val) with
|
let val := val.dropRightWhile (fun c => c == '!' || c == '?')
|
||||||
|
match levelInfo.tactics.find? (·.name.toString == val) with
|
||||||
| none =>
|
| none =>
|
||||||
-- Note: This case means that the tactic will never be introduced in the game.
|
-- Tactic will never be introduced in the game.
|
||||||
match levelParams.inventory.find? (· == val) with
|
match workerState.inventory.find? (· == val) with
|
||||||
|
| some _ =>
|
||||||
|
-- Tactic is in the inventory, allow it.
|
||||||
|
-- Note: This case shouldn't be possible...
|
||||||
|
pure ()
|
||||||
| none =>
|
| none =>
|
||||||
addWarningMessage info s!"You have not unlocked the tactic '{val}' yet!"
|
-- Tactic is not in the inventory.
|
||||||
| some _ => pure () -- tactic is in the inventory, allow it.
|
addMessageByDifficulty info s!"The tactic '{val}' is not available in this game!"
|
||||||
| some tac =>
|
| some tac =>
|
||||||
if tac.locked then
|
-- Tactic is introduced at some point in the game.
|
||||||
match levelParams.inventory.find? (· == val) with
|
if tac.disabled then
|
||||||
|
-- Tactic is disabled in this level.
|
||||||
|
addMessageByDifficulty info s!"The tactic '{val}' is disabled in this level!"
|
||||||
|
else if tac.locked then
|
||||||
|
match workerState.inventory.find? (· == val) with
|
||||||
| none =>
|
| none =>
|
||||||
addWarningMessage info s!"You have not unlocked the tactic '{val}' yet!"
|
-- Tactic is marked as locked and not in the inventory.
|
||||||
| some _ => pure () -- tactic is in the inventory, allow it.
|
addMessageByDifficulty info s!"You have not unlocked the tactic '{val}' yet!"
|
||||||
else if tac.disabled then
|
| some _ =>
|
||||||
addWarningMessage info s!"The tactic '{val}' is disabled in this level!"
|
-- Tactic is in the inventory, allow it.
|
||||||
|
pure ()
|
||||||
| .ident info _rawVal val _preresolved =>
|
| .ident info _rawVal val _preresolved =>
|
||||||
|
-- Try to resolve the name
|
||||||
let ns ←
|
let ns ←
|
||||||
try resolveGlobalConst (mkIdent val)
|
try resolveGlobalConst (mkIdent val)
|
||||||
catch | _ => pure [] -- catch "unknown constant" error
|
-- Catch "unknown constant" error
|
||||||
|
catch | _ => pure []
|
||||||
for n in ns do
|
for n in ns do
|
||||||
let some (.thmInfo ..) := (← getEnv).find? n
|
let some (.thmInfo ..) := (← getEnv).find? n
|
||||||
| return () -- not a theorem -> ignore
|
-- Not a theorem, no checks needed.
|
||||||
|
| return ()
|
||||||
|
if some n = levelInfo.statementName then
|
||||||
-- Forbid the theorem we are proving currently
|
-- Forbid the theorem we are proving currently
|
||||||
if n = levelParams.statementName then
|
addMessage info inputCtx (severity := .error)
|
||||||
addErrorMessage info inputCtx s!"Structural recursion: you can't use '{n}' to proof itself!"
|
s!"Structural recursion: you can't use '{n}' to proof itself!"
|
||||||
|
let theoremsAndDefs := levelInfo.lemmas ++ levelInfo.definitions
|
||||||
|
match theoremsAndDefs.find? (·.name == n) with
|
||||||
|
| none =>
|
||||||
|
-- Theorem will never be introduced in this game
|
||||||
|
addMessageByDifficulty info s!"The theorem/definition '{n}' is not available in this game!"
|
||||||
|
| some thm =>
|
||||||
|
-- Theorem is introduced at some point in the game.
|
||||||
|
if thm.disabled then
|
||||||
|
-- Theorem is disabled in this level.
|
||||||
|
addMessageByDifficulty info s!"The theorem/definition '{n}' is disabled in this level!"
|
||||||
|
else if thm.locked then
|
||||||
|
match workerState.inventory.find? (· == n.toString) with
|
||||||
|
| none =>
|
||||||
|
-- Theorem is still locked.
|
||||||
|
addMessageByDifficulty info s!"You have not unlocked the theorem/definition '{n}' yet!"
|
||||||
|
| some _ =>
|
||||||
|
-- Theorem is in the inventory, allow it.
|
||||||
|
pure ()
|
||||||
|
|
||||||
let lemmasAndDefs := levelParams.lemmas ++ levelParams.definitions
|
where addMessageByDifficulty (info : SourceInfo) (s : MessageData) :=
|
||||||
match lemmasAndDefs.find? (fun l => l.name == n) with
|
-- See `GameServer.FileWorker.WorkerState.difficulty`. Send nothing/warnings/errors
|
||||||
| none => addWarningMessage info s!"You have not unlocked the lemma/definition '{n}' yet!"
|
-- deppending on difficulty.
|
||||||
| some lem =>
|
let difficulty := workerState.difficulty
|
||||||
if lem.locked then
|
|
||||||
addWarningMessage info s!"You have not unlocked the lemma/definition '{n}' yet!"
|
|
||||||
else if lem.disabled then
|
|
||||||
addWarningMessage info s!"The lemma/definition '{n}' is disabled in this level!"
|
|
||||||
where addWarningMessage (info : SourceInfo) (s : MessageData) :=
|
|
||||||
let difficulty := levelParams.difficulty
|
|
||||||
if difficulty > 0 then
|
if difficulty > 0 then
|
||||||
modify fun st => { st with
|
addMessage info inputCtx (if difficulty > 1 then .error else .warning) s
|
||||||
messages := st.messages.add {
|
|
||||||
fileName := inputCtx.fileName
|
|
||||||
severity := if difficulty > 1 then MessageSeverity.error else MessageSeverity.warning
|
|
||||||
pos := inputCtx.fileMap.toPosition (info.getPos?.getD 0)
|
|
||||||
data := s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else pure ()
|
else pure ()
|
||||||
-- where addErrorMessage (info : SourceInfo) (s : MessageData) :=
|
|
||||||
-- pure ()
|
|
||||||
|
|
||||||
open Elab Meta Expr in
|
open Elab Meta Expr in
|
||||||
|
|
||||||
def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets : Bool)
|
def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets : Bool)
|
||||||
(couldBeEndSnap : Bool) (levelParams : Game.DidOpenLevelParams)
|
(couldBeEndSnap : Bool) (gameWorkerState : WorkerState)
|
||||||
(initParams : Lsp.InitializeParams) : IO Snapshot := do
|
(initParams : Lsp.InitializeParams) : IO Snapshot := do
|
||||||
-- Recognize end snap
|
-- Recognize end snap
|
||||||
if inputCtx.input.atEnd snap.mpState.pos ∧ couldBeEndSnap then
|
if inputCtx.input.atEnd snap.mpState.pos ∧ couldBeEndSnap then
|
||||||
@@ -167,7 +227,7 @@ def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets
|
|||||||
Elab.Command.catchExceptions
|
Elab.Command.catchExceptions
|
||||||
(getResetInfoTrees *> do
|
(getResetInfoTrees *> do
|
||||||
let some level ← GameServer.getLevelByFileName? initParams inputCtx.fileName
|
let some level ← GameServer.getLevelByFileName? initParams inputCtx.fileName
|
||||||
| throwError "Level not found: {inputCtx.fileName}"
|
| panic! s!"Level not found: {inputCtx.fileName} / {GameServer.levelIdFromFileName? initParams inputCtx.fileName}"
|
||||||
let scope := level.scope
|
let scope := level.scope
|
||||||
|
|
||||||
-- use open namespaces and options as in the level file
|
-- use open namespaces and options as in the level file
|
||||||
@@ -185,12 +245,12 @@ def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets
|
|||||||
currNamespace := scope.currNamespace,
|
currNamespace := scope.currNamespace,
|
||||||
openDecls := scope.openDecls }
|
openDecls := scope.openDecls }
|
||||||
let (tacticStx, cmdParserState, msgLog, endOfWhitespace) :=
|
let (tacticStx, cmdParserState, msgLog, endOfWhitespace) :=
|
||||||
MyModule.parseTactic inputCtx pmctx snap.mpState snap.msgLog couldBeEndSnap
|
MyModule.parseTactic inputCtx pmctx snap.mpState snap.msgLog
|
||||||
modify (fun s => { s with messages := msgLog })
|
modify (fun s => { s with messages := msgLog })
|
||||||
parseResultRef.set (tacticStx, cmdParserState)
|
parseResultRef.set (tacticStx, cmdParserState)
|
||||||
|
|
||||||
-- Check for forbidden tactics
|
-- Check for forbidden tactics
|
||||||
findForbiddenTactics inputCtx levelParams tacticStx
|
findForbiddenTactics inputCtx gameWorkerState tacticStx
|
||||||
|
|
||||||
-- Insert invisible `skip` command to make sure we always display the initial goal
|
-- Insert invisible `skip` command to make sure we always display the initial goal
|
||||||
let skip := Syntax.node (.original default 0 default endOfWhitespace) ``Lean.Parser.Tactic.skip #[]
|
let skip := Syntax.node (.original default 0 default endOfWhitespace) ``Lean.Parser.Tactic.skip #[]
|
||||||
@@ -199,8 +259,11 @@ def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets
|
|||||||
let tacticStx := (#[skip] ++ tacticStx.getArgs ++ #[done]).map (⟨.⟩)
|
let tacticStx := (#[skip] ++ tacticStx.getArgs ++ #[done]).map (⟨.⟩)
|
||||||
let tacticStx := ← `(Lean.Parser.Tactic.tacticSeq| $[$(tacticStx)]*)
|
let tacticStx := ← `(Lean.Parser.Tactic.tacticSeq| $[$(tacticStx)]*)
|
||||||
|
|
||||||
|
-- Always call `let_intros` to get rid `let` statements in the goal.
|
||||||
|
-- This makes the experience for the user much nicer and allows for local
|
||||||
|
-- definitions in the exercise.
|
||||||
let cmdStx ← `(command|
|
let cmdStx ← `(command|
|
||||||
theorem the_theorem $(level.goal) := by {$(⟨tacticStx⟩)} )
|
theorem the_theorem $(level.goal) := by {let_intros; $(⟨level.preamble⟩); $(⟨tacticStx⟩)} )
|
||||||
Elab.Command.elabCommandTopLevel cmdStx)
|
Elab.Command.elabCommandTopLevel cmdStx)
|
||||||
cmdCtx cmdStateRef
|
cmdCtx cmdStateRef
|
||||||
let postNew := (← tacticCacheNew.get).post
|
let postNew := (← tacticCacheNew.get).post
|
||||||
@@ -218,6 +281,7 @@ def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets
|
|||||||
}
|
}
|
||||||
|
|
||||||
let (tacticStx, cmdParserState) ← parseResultRef.get
|
let (tacticStx, cmdParserState) ← parseResultRef.get
|
||||||
|
if tacticStx.isMissing then throwServerError "Tactic execution went wrong. No stx found."
|
||||||
|
|
||||||
let postCmdSnap : Snapshot := {
|
let postCmdSnap : Snapshot := {
|
||||||
beginPos := tacticStx.getPos?.getD 0
|
beginPos := tacticStx.getPos?.getD 0
|
||||||
@@ -244,7 +308,7 @@ where
|
|||||||
private def publishIleanInfo (method : String) (m : DocumentMeta) (hOut : FS.Stream)
|
private def publishIleanInfo (method : String) (m : DocumentMeta) (hOut : FS.Stream)
|
||||||
(snaps : Array Snapshot) : IO Unit := do
|
(snaps : Array Snapshot) : IO Unit := do
|
||||||
let trees := snaps.map fun snap => snap.infoTree
|
let trees := snaps.map fun snap => snap.infoTree
|
||||||
let references := findModuleRefs m.text trees (localVars := true)
|
let references ← findModuleRefs m.text trees (localVars := true) |>.toLspModuleRefs
|
||||||
let param := { version := m.version, references : LeanIleanInfoParams }
|
let param := { version := m.version, references : LeanIleanInfoParams }
|
||||||
hOut.writeLspNotification { method, param }
|
hOut.writeLspNotification { method, param }
|
||||||
|
|
||||||
@@ -258,8 +322,84 @@ where
|
|||||||
uri : String
|
uri : String
|
||||||
deriving ToJson, FromJson
|
deriving ToJson, FromJson
|
||||||
|
|
||||||
/-- Checks whether game level has been completed and sends a notification to the client -/
|
structure GameDiagnostics where
|
||||||
def publishGameCompleted (m : DocumentMeta) (hOut : FS.Stream) (snaps : Array Snapshot) : IO Unit := do
|
diagnostics : List Diagnostic
|
||||||
|
deriving ToJson, FromJson
|
||||||
|
|
||||||
|
structure GameParams where
|
||||||
|
uri : String
|
||||||
|
diagnostics : GameDiagnostics
|
||||||
|
deriving ToJson, FromJson
|
||||||
|
|
||||||
|
-- `snap` and `initParams` are unused
|
||||||
|
set_option linter.unusedVariables false in
|
||||||
|
|
||||||
|
/-- WIP: publish diagnostics, all intermediate goals and if the game is completed. -/
|
||||||
|
def publishProofState (m : DocumentMeta) (snap : Snapshot) (initParams : Lsp.InitializeParams) (hOut : FS.Stream) :
|
||||||
|
IO Unit := do
|
||||||
|
-- let text := m.text
|
||||||
|
|
||||||
|
-- -- `snap` is the one snapshot containing the entire proof.
|
||||||
|
-- let mut goals : Array <| InteractiveGoalsWithHints := #[]
|
||||||
|
-- for pos in text.positions do
|
||||||
|
-- let source := text.getLineBefore pos
|
||||||
|
-- -- iterate over all newlines in the proof and get the goals and hints at each position
|
||||||
|
-- if let goalsAtResult@(_ :: _) := snap.infoTree.goalsAt? text pos then
|
||||||
|
-- pure ()
|
||||||
|
-- let goalAtPos : List <| List InteractiveGoalWithHints ← goalsAtResult.mapM
|
||||||
|
-- fun { ctxInfo := ci, tacticInfo := tacticInfo, useAfter := useAfter, .. } => do
|
||||||
|
-- -- TODO: What does this function body do?
|
||||||
|
-- -- let ciAfter := { ci with mctx := ti.mctxAfter }
|
||||||
|
-- let ci := if useAfter then
|
||||||
|
-- { ci with mctx := tacticInfo.mctxAfter }
|
||||||
|
-- else
|
||||||
|
-- { ci with mctx := tacticInfo.mctxBefore }
|
||||||
|
-- -- compute the interactive goals
|
||||||
|
-- let goalMvars : List MVarId ← ci.runMetaM {} do
|
||||||
|
-- return if useAfter then tacticInfo.goalsAfter else tacticInfo.goalsBefore
|
||||||
|
|
||||||
|
-- let interactiveGoals : List InteractiveGoalWithHints ← ci.runMetaM {} do
|
||||||
|
-- goalMvars.mapM fun goal => do
|
||||||
|
-- let hints ← findHints goal m initParams
|
||||||
|
-- let interactiveGoal ← goalToInteractive goal
|
||||||
|
-- return ⟨interactiveGoal, hints⟩
|
||||||
|
-- -- TODO: This code is way old, can it be deleted?
|
||||||
|
-- -- compute the goal diff
|
||||||
|
-- -- let goals ← ciAfter.runMetaM {} (do
|
||||||
|
-- -- try
|
||||||
|
-- -- Widget.diffInteractiveGoals useAfter ti goals
|
||||||
|
-- -- catch _ =>
|
||||||
|
-- -- -- fail silently, since this is just a bonus feature
|
||||||
|
-- -- return goals
|
||||||
|
-- -- )
|
||||||
|
-- return interactiveGoals
|
||||||
|
-- let goalAtPos : Array InteractiveGoalWithHints := ⟨goalAtPos.foldl (· ++ ·) []⟩
|
||||||
|
-- goals := goals.push ⟨goalAtPos, source⟩
|
||||||
|
-- else
|
||||||
|
-- -- No goals present
|
||||||
|
-- goals := goals.push default
|
||||||
|
|
||||||
|
-- -- Question: Is there a difference between the diags of this snap and the last snap?
|
||||||
|
-- -- Should we get the diags from there?
|
||||||
|
-- let diag : Array Widget.InteractiveDiagnostic := snap.interactiveDiags.toArray
|
||||||
|
|
||||||
|
-- -- Level is completed if there are no errrors or warnings
|
||||||
|
-- let completed : Bool := ¬ diag.any (fun d =>
|
||||||
|
-- d.severity? == some .error ∨ d.severity? == some .warning)
|
||||||
|
|
||||||
|
-- let param : ProofState := {
|
||||||
|
-- steps := goals,
|
||||||
|
-- diagnostics := diag,
|
||||||
|
-- completed := completed }
|
||||||
|
|
||||||
|
-- TODO
|
||||||
|
let param := { uri := m.uri : GameCompletedParams}
|
||||||
|
|
||||||
|
|
||||||
|
hOut.writeLspNotification { method := "$/game/publishProofState", param }
|
||||||
|
|
||||||
|
/-- Checks whether game level has been completed and sends a notification to the client -/
|
||||||
|
def publishGameCompleted (m : DocumentMeta) (hOut : FS.Stream) (snaps : Array Snapshot) : IO Unit := do
|
||||||
-- check if there is any error or warning
|
-- check if there is any error or warning
|
||||||
for snap in snaps do
|
for snap in snaps do
|
||||||
if snap.diagnostics.any fun d => d.severity? == some .error ∨ d.severity? == some .warning
|
if snap.diagnostics.any fun d => d.severity? == some .error ∨ d.severity? == some .warning
|
||||||
@@ -267,79 +407,59 @@ where
|
|||||||
let param := { uri := m.uri : GameCompletedParams}
|
let param := { uri := m.uri : GameCompletedParams}
|
||||||
hOut.writeLspNotification { method := "$/game/completed", param }
|
hOut.writeLspNotification { method := "$/game/completed", param }
|
||||||
|
|
||||||
/-- Elaborates the next command after `parentSnap` and emits diagnostics into `hOut`. -/
|
/-- copied from `Lean.Server.FileWorker.nextCmdSnap`. -/
|
||||||
private def nextSnap (ctx : WorkerContext) (m : DocumentMeta) (cancelTk : CancelToken)
|
-- @[inherit_doc Lean.Server.FileWorker.nextCmdSnap] -- cannot inherit from private
|
||||||
(levelParams : Game.DidOpenLevelParams) (initParams : Lsp.InitializeParams)
|
private def nextCmdSnap (ctx : WorkerContext) (m : DocumentMeta) (cancelTk : CancelToken)
|
||||||
: AsyncElabM (Option Snapshot) := do
|
(gameWorkerState : WorkerState) (initParams : Lsp.InitializeParams) :
|
||||||
|
AsyncElabM (Option Snapshot) := do
|
||||||
cancelTk.check
|
cancelTk.check
|
||||||
let s ← get
|
let s ← get
|
||||||
let .some lastSnap := s.snaps.back? | panic! "empty snapshots"
|
let .some lastSnap := s.snaps.back? | panic! "empty snapshots"
|
||||||
if lastSnap.isAtEnd then
|
if lastSnap.isAtEnd then
|
||||||
publishGameCompleted m ctx.hOut s.snaps
|
|
||||||
publishDiagnostics m lastSnap.diagnostics.toArray ctx.hOut
|
publishDiagnostics m lastSnap.diagnostics.toArray ctx.hOut
|
||||||
publishProgressDone m ctx.hOut
|
publishProgressDone m ctx.hOut
|
||||||
-- This will overwrite existing ilean info for the file, in case something
|
|
||||||
-- went wrong during the incremental updates.
|
|
||||||
publishIleanInfoFinal m ctx.hOut s.snaps
|
publishIleanInfoFinal m ctx.hOut s.snaps
|
||||||
return none
|
return none
|
||||||
publishProgressAtPos m lastSnap.endPos ctx.hOut
|
publishProgressAtPos m lastSnap.endPos ctx.hOut
|
||||||
|
|
||||||
|
-- (modified part)
|
||||||
-- Make sure that there is at least one snap after the head snap, so that
|
-- Make sure that there is at least one snap after the head snap, so that
|
||||||
-- we can see the current goal even on an empty document
|
-- we can see the current goal even on an empty document
|
||||||
let couldBeEndSnap := s.snaps.size > 1
|
let couldBeEndSnap := s.snaps.size > 1
|
||||||
let snap ← compileProof m.mkInputContext lastSnap ctx.clientHasWidgets couldBeEndSnap
|
let snap ← compileProof m.mkInputContext lastSnap ctx.clientHasWidgets couldBeEndSnap
|
||||||
levelParams initParams
|
gameWorkerState initParams
|
||||||
|
|
||||||
set { s with snaps := s.snaps.push snap }
|
set { s with snaps := s.snaps.push snap }
|
||||||
-- TODO(MH): check for interrupt with increased precision
|
|
||||||
cancelTk.check
|
cancelTk.check
|
||||||
/- NOTE(MH): This relies on the client discarding old diagnostics upon receiving new ones
|
-- publishProofState m snap initParams ctx.hOut
|
||||||
while preferring newer versions over old ones. The former is necessary because we do
|
|
||||||
not explicitly clear older diagnostics, while the latter is necessary because we do
|
|
||||||
not guarantee that diagnostics are emitted in order. Specifically, it may happen that
|
|
||||||
we interrupted this elaboration task right at this point and a newer elaboration task
|
|
||||||
emits diagnostics, after which we emit old diagnostics because we did not yet detect
|
|
||||||
the interrupt. Explicitly clearing diagnostics is difficult for a similar reason,
|
|
||||||
because we cannot guarantee that no further diagnostics are emitted after clearing
|
|
||||||
them. -/
|
|
||||||
-- NOTE(WN): this is *not* redundant even if there are no new diagnostics in this snapshot
|
|
||||||
-- because empty diagnostics clear existing error/information squiggles. Therefore we always
|
|
||||||
-- want to publish in case there was previously a message at this position.
|
|
||||||
publishDiagnostics m snap.diagnostics.toArray ctx.hOut
|
publishDiagnostics m snap.diagnostics.toArray ctx.hOut
|
||||||
publishIleanInfoUpdate m ctx.hOut #[snap]
|
publishIleanInfoUpdate m ctx.hOut #[snap]
|
||||||
return some snap
|
return some snap
|
||||||
|
|
||||||
/-- Elaborates all commands after the last snap (at least the header snap is assumed to exist), emitting the diagnostics into `hOut`. -/
|
-- Copied from `Lean.Server.FileWorker.unfoldCmdSnaps` using our own `nextCmdSnap`.
|
||||||
def unfoldSnaps (m : DocumentMeta) (snaps : Array Snapshot) (cancelTk : CancelToken)
|
@[inherit_doc Lean.Server.FileWorker.unfoldCmdSnaps]
|
||||||
(startAfterMs : UInt32) (levelParams : Game.DidOpenLevelParams)
|
def unfoldCmdSnaps (m : DocumentMeta) (snaps : Array Snapshot) (cancelTk : CancelToken)
|
||||||
|
(startAfterMs : UInt32) (gameWorkerState : WorkerState)
|
||||||
: ReaderT WorkerContext IO (AsyncList ElabTaskError Snapshot) := do
|
: ReaderT WorkerContext IO (AsyncList ElabTaskError Snapshot) := do
|
||||||
let ctx ← read
|
let ctx ← read
|
||||||
let some headerSnap := snaps[0]? | panic! "empty snapshots"
|
let some headerSnap := snaps[0]? | panic! "empty snapshots"
|
||||||
if headerSnap.msgLog.hasErrors then
|
if headerSnap.msgLog.hasErrors then
|
||||||
-- Treat header processing errors as fatal so users aren't swamped with
|
|
||||||
-- followup errors
|
|
||||||
publishProgressAtPos m headerSnap.beginPos ctx.hOut (kind := LeanFileProgressKind.fatalError)
|
publishProgressAtPos m headerSnap.beginPos ctx.hOut (kind := LeanFileProgressKind.fatalError)
|
||||||
publishIleanInfoFinal m ctx.hOut #[headerSnap]
|
publishIleanInfoFinal m ctx.hOut #[headerSnap]
|
||||||
return AsyncList.ofList [headerSnap]
|
return AsyncList.ofList [headerSnap]
|
||||||
else
|
else
|
||||||
-- This will overwrite existing ilean info for the file since this has a
|
|
||||||
-- higher version number.
|
|
||||||
publishIleanInfoUpdate m ctx.hOut snaps
|
publishIleanInfoUpdate m ctx.hOut snaps
|
||||||
return AsyncList.ofList snaps.toList ++ AsyncList.delayed (← EIO.asTask (ε := ElabTaskError) (prio := .dedicated) do
|
return AsyncList.ofList snaps.toList ++ AsyncList.delayed (← EIO.asTask (ε := ElabTaskError) (prio := .dedicated) do
|
||||||
IO.sleep startAfterMs
|
IO.sleep startAfterMs
|
||||||
AsyncList.unfoldAsync (nextSnap ctx m cancelTk levelParams ctx.initParams) { snaps })
|
AsyncList.unfoldAsync (nextCmdSnap ctx m cancelTk gameWorkerState ctx.initParams) { snaps })
|
||||||
|
|
||||||
end Elab
|
end Elab
|
||||||
|
|
||||||
structure GameWorkerState :=
|
|
||||||
(levelParams : Game.DidOpenLevelParams)
|
|
||||||
|
|
||||||
abbrev GameWorkerM := StateT GameWorkerState Server.FileWorker.WorkerM
|
|
||||||
|
|
||||||
section Updates
|
section Updates
|
||||||
|
|
||||||
/-- Given the new document, updates editable doc state. -/
|
/-- Given the new document, updates editable doc state. -/
|
||||||
def updateDocument (newMeta : DocumentMeta) : GameWorkerM Unit := do
|
def updateDocument (newMeta : DocumentMeta) : WorkerM Unit := do
|
||||||
let s ← get
|
let s ← get
|
||||||
let levelParams := s.levelParams
|
|
||||||
let ctx ← read
|
let ctx ← read
|
||||||
let oldDoc := (← StateT.lift get).doc
|
let oldDoc := (← StateT.lift get).doc
|
||||||
oldDoc.cancelTk.set
|
oldDoc.cancelTk.set
|
||||||
@@ -381,7 +501,7 @@ section Updates
|
|||||||
validSnaps := validSnaps.dropLast
|
validSnaps := validSnaps.dropLast
|
||||||
-- wait for a bit, giving the initial `cancelTk.check` in `nextCmdSnap` time to trigger
|
-- wait for a bit, giving the initial `cancelTk.check` in `nextCmdSnap` time to trigger
|
||||||
-- before kicking off any expensive elaboration (TODO: make expensive elaboration cancelable)
|
-- before kicking off any expensive elaboration (TODO: make expensive elaboration cancelable)
|
||||||
unfoldSnaps newMeta validSnaps.toArray cancelTk levelParams ctx
|
unfoldCmdSnaps newMeta validSnaps.toArray cancelTk s ctx
|
||||||
(startAfterMs := ctx.initParams.editDelay.toUInt32)
|
(startAfterMs := ctx.initParams.editDelay.toUInt32)
|
||||||
StateT.lift <| modify fun st => { st with
|
StateT.lift <| modify fun st => { st with
|
||||||
doc := { meta := newMeta, cmdSnaps := AsyncList.delayed newSnaps, cancelTk }}
|
doc := { meta := newMeta, cmdSnaps := AsyncList.delayed newSnaps, cancelTk }}
|
||||||
@@ -390,30 +510,35 @@ end Updates
|
|||||||
|
|
||||||
section Initialization
|
section Initialization
|
||||||
|
|
||||||
def DocumentMeta.mkInputContext (doc : DocumentMeta) : Parser.InputContext where
|
def DocumentMeta.mkInputContext (doc : DocumentMeta) : Parser.InputContext where
|
||||||
input := "" -- No header!
|
input := "" -- No header!
|
||||||
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)
|
/-- `gameDir` and `module` were added.
|
||||||
(levelParams : Game.DidOpenLevelParams) (initParams : InitializeParams) :
|
|
||||||
|
TODO: In general this resembles little similarity with the
|
||||||
|
original code, and I don't know why...
|
||||||
|
-/
|
||||||
|
-- @[inherit_doc Lean.Server.FileWorker.compileHeader]
|
||||||
|
def compileHeader (m : DocumentMeta) (hOut : FS.Stream) (opts : Options) (hasWidgets : Bool)
|
||||||
|
(gameDir : String) (module : Name):
|
||||||
IO (Syntax × Task (Except Error (Snapshot × SearchPath))) := do
|
IO (Syntax × Task (Except Error (Snapshot × SearchPath))) := do
|
||||||
-- Determine search paths of the game project by running `lake env printenv LEAN_PATH`.
|
-- Determine search paths of the game project by running `lake env printenv LEAN_PATH`.
|
||||||
let out ← IO.Process.output
|
let out ← IO.Process.output
|
||||||
{ cwd := levelParams.gameDir, cmd := "lake", args := #["env","printenv","LEAN_PATH"] }
|
{ cwd := gameDir, cmd := "lake", args := #["env","printenv","LEAN_PATH"] }
|
||||||
if out.exitCode != 0 then
|
if out.exitCode != 0 then
|
||||||
throwServerError s!"Error while running Lake: {out.stderr}"
|
throwServerError s!"Error while running Lake: {out.stderr}"
|
||||||
|
|
||||||
-- Make the paths relative to the current directory
|
-- Make the paths relative to the current directory
|
||||||
let paths : List System.FilePath := System.SearchPath.parse out.stdout.trim
|
let paths : List System.FilePath := System.SearchPath.parse out.stdout.trim
|
||||||
let currentDir ← IO.currentDir
|
let currentDir ← IO.currentDir
|
||||||
let paths := paths.map fun p => currentDir / (levelParams.gameDir : System.FilePath) / p
|
let paths := paths.map fun p => currentDir / (gameDir : System.FilePath) / p
|
||||||
|
|
||||||
-- Set the search path
|
-- Set the search path
|
||||||
Lean.searchPathRef.set paths
|
Lean.searchPathRef.set paths
|
||||||
|
|
||||||
let env ← importModules #[{ module := `Init : Import }, { module := levelParams.levelModule : Import }] {} 0
|
let env ← importModules' #[{ module := `Init : Import }, { module := module : Import }]
|
||||||
-- return (env, paths)
|
|
||||||
|
|
||||||
-- use empty header
|
-- use empty header
|
||||||
let (headerStx, headerParserState, msgLog) ← Parser.parseHeader
|
let (headerStx, headerParserState, msgLog) ← Parser.parseHeader
|
||||||
@@ -431,7 +556,7 @@ section Initialization
|
|||||||
let cmdState := Elab.Command.mkState headerEnv {} opts
|
let cmdState := Elab.Command.mkState headerEnv {} opts
|
||||||
let cmdState := { cmdState with infoState := {
|
let cmdState := { cmdState with infoState := {
|
||||||
enabled := true
|
enabled := true
|
||||||
trees := #[Elab.InfoTree.context ({
|
trees := #[Elab.InfoTree.context (.commandCtx {
|
||||||
env := headerEnv
|
env := headerEnv
|
||||||
fileMap := m.text
|
fileMap := m.text
|
||||||
ngen := { namePrefix := `_worker }
|
ngen := { namePrefix := `_worker }
|
||||||
@@ -448,7 +573,7 @@ section Initialization
|
|||||||
let headerSnap := {
|
let headerSnap := {
|
||||||
beginPos := 0
|
beginPos := 0
|
||||||
stx := headerStx
|
stx := headerStx
|
||||||
mpState := {}
|
mpState := {} -- was `headerParserState`
|
||||||
cmdState := cmdState
|
cmdState := cmdState
|
||||||
interactiveDiags := ← cmdState.messages.msgs.mapM (Widget.msgToInteractiveDiagnostic m.text · hasWidgets)
|
interactiveDiags := ← cmdState.messages.msgs.mapM (Widget.msgToInteractiveDiagnostic m.text · hasWidgets)
|
||||||
tacticCache := (← IO.mkRef {})
|
tacticCache := (← IO.mkRef {})
|
||||||
@@ -456,14 +581,17 @@ section Initialization
|
|||||||
publishDiagnostics m headerSnap.diagnostics.toArray hOut
|
publishDiagnostics m headerSnap.diagnostics.toArray hOut
|
||||||
return (headerSnap, srcSearchPath)
|
return (headerSnap, srcSearchPath)
|
||||||
|
|
||||||
def initializeWorker (meta : DocumentMeta) (i o e : FS.Stream) (initParams : InitializeParams) (opts : Options)
|
/-- Copied from `Lean.Server.FileWorker.initializeWorker`. Added `gameDir` and
|
||||||
(levelParams : Game.DidOpenLevelParams) : IO (WorkerContext × WorkerState) := do
|
`gameWorkerState` arguments and use custom `unfoldCmdSnaps`. -/
|
||||||
|
-- @[inherit_doc Lean.Server.FileWorker.initializeWorker]
|
||||||
|
def initializeWorker (meta : DocumentMeta) (i o e : FS.Stream) (initParams : InitializeParams) (opts : Options)
|
||||||
|
(gameDir : String) (gameWorkerState : WorkerState) : IO (WorkerContext × Server.FileWorker.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
|
(gameDir := gameDir) (module := gameWorkerState.levelInfo.module)
|
||||||
let cancelTk ← CancelToken.new
|
let cancelTk ← CancelToken.new
|
||||||
let ctx :=
|
let ctx := {
|
||||||
{ hIn := i
|
hIn := i
|
||||||
hOut := o
|
hOut := o
|
||||||
hLog := e
|
hLog := e
|
||||||
headerTask
|
headerTask
|
||||||
@@ -471,12 +599,14 @@ section Initialization
|
|||||||
clientHasWidgets
|
clientHasWidgets
|
||||||
}
|
}
|
||||||
let cmdSnaps ← EIO.mapTask (t := headerTask) (match · with
|
let cmdSnaps ← EIO.mapTask (t := headerTask) (match · with
|
||||||
| Except.ok (s, _) => unfoldSnaps meta #[s] cancelTk levelParams ctx (startAfterMs := 0)
|
| Except.ok (s, _) => unfoldCmdSnaps meta #[s] cancelTk gameWorkerState ctx (startAfterMs := 0)
|
||||||
| Except.error e => throw (e : ElabTaskError))
|
| Except.error e => throw (e : ElabTaskError))
|
||||||
let doc : EditableDocument := { meta, cmdSnaps := AsyncList.delayed cmdSnaps, cancelTk }
|
let doc : EditableDocument := { meta, cmdSnaps := AsyncList.delayed cmdSnaps, cancelTk }
|
||||||
return (ctx,
|
return (ctx, {
|
||||||
{ doc := doc
|
doc := doc
|
||||||
initHeaderStx := headerStx
|
initHeaderStx := headerStx
|
||||||
|
currHeaderStx := headerStx
|
||||||
|
importCachingTask? := none
|
||||||
pendingRequests := RBMap.empty
|
pendingRequests := RBMap.empty
|
||||||
rpcSessions := RBMap.empty
|
rpcSessions := RBMap.empty
|
||||||
})
|
})
|
||||||
@@ -485,101 +615,156 @@ end Initialization
|
|||||||
|
|
||||||
section NotificationHandling
|
section NotificationHandling
|
||||||
|
|
||||||
def handleDidChange (p : DidChangeTextDocumentParams) : GameWorkerM Unit := do
|
/-- Copied from `Lean.Server.FileWorker.handleDidChange` but with our custom `WorkerM` and
|
||||||
|
`updateDocument` -/
|
||||||
|
-- @[inherit_doc Lean.Server.FileWorker.handleDidChange]
|
||||||
|
def handleDidChange (p : DidChangeTextDocumentParams) : WorkerM Unit := do
|
||||||
let docId := p.textDocument
|
let docId := p.textDocument
|
||||||
let changes := p.contentChanges
|
let changes := p.contentChanges
|
||||||
let oldDoc := (← StateT.lift get).doc
|
let oldDoc := (← StateT.lift get).doc -- needed a lift to our custom `WorkerM`
|
||||||
let some newVersion ← pure docId.version?
|
let newVersion := docId.version?.getD 0
|
||||||
| throwServerError "Expected version number"
|
if ¬ changes.isEmpty then
|
||||||
if newVersion ≤ oldDoc.meta.version then
|
|
||||||
-- TODO(WN): This happens on restart sometimes.
|
|
||||||
IO.eprintln s!"Got outdated version number: {newVersion} ≤ {oldDoc.meta.version}"
|
|
||||||
else if ¬ changes.isEmpty then
|
|
||||||
let newDocText := foldDocumentChanges changes oldDoc.meta.text
|
let newDocText := foldDocumentChanges changes oldDoc.meta.text
|
||||||
|
-- modification: set the `DependencyBuildMode` from
|
||||||
|
-- `oldDoc.meta.dependencyBuildMode` to `.always`
|
||||||
updateDocument ⟨docId.uri, newVersion, newDocText, .always⟩
|
updateDocument ⟨docId.uri, newVersion, newDocText, .always⟩
|
||||||
|
|
||||||
end NotificationHandling
|
end NotificationHandling
|
||||||
|
|
||||||
section MessageHandling
|
section MessageHandling
|
||||||
|
|
||||||
def handleNotification (method : String) (params : Json) : GameWorkerM Unit := do
|
|
||||||
let handle := fun paramType [FromJson paramType] (handler : paramType → GameWorkerM Unit) =>
|
/--
|
||||||
|
Modified notification handler.
|
||||||
|
|
||||||
|
Compare to `Lean.Server.FileWorker.handleNotification`.
|
||||||
|
We use the modified `WorkerM` and use our custom `handleDidChange`.
|
||||||
|
|
||||||
|
-/
|
||||||
|
def handleNotification (method : String) (params : Json) : WorkerM Unit := do
|
||||||
|
let handle := fun paramType [FromJson paramType] (handler : paramType → WorkerM Unit) =>
|
||||||
(StateT.lift <| parseParams paramType params) >>= handler
|
(StateT.lift <| parseParams paramType params) >>= handler
|
||||||
match method with
|
match method with
|
||||||
|
-- Modified `textDocument/didChange`, using a custom `handleDidChange`
|
||||||
| "textDocument/didChange" => handle DidChangeTextDocumentParams (handleDidChange)
|
| "textDocument/didChange" => handle DidChangeTextDocumentParams (handleDidChange)
|
||||||
|
-- unmodified
|
||||||
| "$/cancelRequest" => handle CancelParams (handleCancelRequest ·)
|
| "$/cancelRequest" => handle CancelParams (handleCancelRequest ·)
|
||||||
|
-- unmodified
|
||||||
| "$/lean/rpc/release" => handle RpcReleaseParams (handleRpcRelease ·)
|
| "$/lean/rpc/release" => handle RpcReleaseParams (handleRpcRelease ·)
|
||||||
|
-- unmodified
|
||||||
| "$/lean/rpc/keepAlive" => handle RpcKeepAliveParams (handleRpcKeepAlive ·)
|
| "$/lean/rpc/keepAlive" => handle RpcKeepAliveParams (handleRpcKeepAlive ·)
|
||||||
|
-- New. TODO: What is this for?
|
||||||
|
| "$/setTrace" => pure ()
|
||||||
| _ => throwServerError s!"Got unsupported notification method: {method}"
|
| _ => throwServerError s!"Got unsupported notification method: {method}"
|
||||||
|
|
||||||
end MessageHandling
|
end MessageHandling
|
||||||
|
|
||||||
section MainLoop
|
section MainLoop
|
||||||
partial def mainLoop : GameWorkerM Unit := do
|
|
||||||
|
/--
|
||||||
|
The main-loop. Copied from `Lean.Server.FileWorker.mainLoop`. Use custom `WorkerM` as well
|
||||||
|
as custom `handleNotification`.
|
||||||
|
-/
|
||||||
|
--@[inherit_doc Lean.Server.FileWorker.mainLoop]
|
||||||
|
partial def mainLoop : WorkerM Unit := do
|
||||||
let ctx ← read
|
let ctx ← read
|
||||||
let mut st ← StateT.lift get
|
let mut st ← StateT.lift get
|
||||||
let msg ← ctx.hIn.readLspMessage
|
let msg ← ctx.hIn.readLspMessage
|
||||||
|
-- Erase finished tasks if there are no errors.
|
||||||
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
|
||||||
/- Handler tasks are constructed so that the only possible errors here
|
|
||||||
are failures of writing a response into the stream. -/
|
|
||||||
if let Except.error e := task.get then
|
if let Except.error e := task.get then
|
||||||
throwServerError s!"Failed responding to request {id}: {e}"
|
throwServerError s!"Failed responding to request {id}: {e}"
|
||||||
pure <| acc.erase id
|
pure <| acc.erase id
|
||||||
else pure acc
|
else pure acc
|
||||||
let pendingRequests ← st.pendingRequests.foldM (fun acc id task => filterFinishedTasks acc id task) st.pendingRequests
|
let pendingRequests ← st.pendingRequests.foldM (fun acc id task => filterFinishedTasks acc id task) st.pendingRequests
|
||||||
st := { st with pendingRequests }
|
st := { st with pendingRequests }
|
||||||
|
|
||||||
-- Opportunistically (i.e. when we wake up on messages) check if any RPC session has expired.
|
|
||||||
for (id, seshRef) in st.rpcSessions do
|
for (id, seshRef) in st.rpcSessions do
|
||||||
let sesh ← seshRef.get
|
let sesh ← seshRef.get
|
||||||
if (← sesh.hasExpired) then
|
if (← sesh.hasExpired) then
|
||||||
st := { st with rpcSessions := st.rpcSessions.erase id }
|
st := { st with rpcSessions := st.rpcSessions.erase id }
|
||||||
|
|
||||||
set st
|
set st
|
||||||
|
-- Process the RPC-message and restart main-loop.
|
||||||
match msg with
|
match msg with
|
||||||
|
| Message.request id "shutdown" none =>
|
||||||
|
--added. TODO: why do we need that? Or has it just removed in Lean since when we started?
|
||||||
|
ctx.hOut.writeLspResponse ⟨id, Json.null⟩
|
||||||
|
mainLoop
|
||||||
| Message.request id method (some params) =>
|
| Message.request id method (some params) =>
|
||||||
|
-- Requests are handled by the unmodified lean server.
|
||||||
handleRequest id method (toJson params)
|
handleRequest id method (toJson params)
|
||||||
mainLoop
|
mainLoop
|
||||||
| Message.notification "exit" none =>
|
| Message.notification "exit" none =>
|
||||||
let doc := st.doc
|
let doc := st.doc
|
||||||
doc.cancelTk.set
|
doc.cancelTk.set
|
||||||
|
doc.cmdSnaps.cancel
|
||||||
return ()
|
return ()
|
||||||
| Message.notification "$/game/setInventory" params =>
|
|
||||||
let p := (← parseParams Game.SetInventoryParams (toJson params))
|
|
||||||
let s ← get
|
|
||||||
|
|
||||||
set {s with levelParams := {s.levelParams with
|
|
||||||
inventory := p.inventory,
|
|
||||||
difficulty := p.difficulty}}
|
|
||||||
mainLoop
|
|
||||||
| Message.notification method (some params) =>
|
| Message.notification method (some params) =>
|
||||||
|
-- Custom notification handler
|
||||||
handleNotification method (toJson params)
|
handleNotification method (toJson params)
|
||||||
mainLoop
|
mainLoop
|
||||||
| _ => throwServerError "Got invalid JSON-RPC message"
|
| _ =>
|
||||||
|
throwServerError s!"Got invalid JSON-RPC message: {toJson msg}"
|
||||||
|
|
||||||
end MainLoop
|
end MainLoop
|
||||||
|
|
||||||
def initAndRunWorker (i o e : FS.Stream) (opts : Options) : IO UInt32 := do
|
/-- Modified from `Lean.Server.FileWorker.initAndRunWorker`.
|
||||||
|
Added `gameDir` argument, -/
|
||||||
|
-- @[inherit_doc Lean.Server.FileWorker.initAndRunWorker]
|
||||||
|
def initAndRunWorker (i o e : FS.Stream) (opts : Options) (gameDir : String) : IO UInt32 := do
|
||||||
let i ← maybeTee "fwIn.txt" false i
|
let i ← maybeTee "fwIn.txt" false i
|
||||||
let o ← maybeTee "fwOut.txt" true o
|
let o ← maybeTee "fwOut.txt" true o
|
||||||
let initParams ← i.readLspRequestAs "initialize" InitializeParams
|
|
||||||
|
|
||||||
|
-- BIG MODIFICATION
|
||||||
|
let initRequest ← i.readLspRequestAs "initialize" Game.InitializeParams
|
||||||
|
o.writeLspResponse {
|
||||||
|
id := initRequest.id
|
||||||
|
result := {
|
||||||
|
capabilities := Watchdog.mkLeanServerCapabilities
|
||||||
|
serverInfo? := some {
|
||||||
|
name := "Lean 4 Game Server"
|
||||||
|
version? := "0.1.1"
|
||||||
|
}
|
||||||
|
: InitializeResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
discard $ i.readLspNotificationAs "initialized" InitializedParams
|
||||||
let ⟨_, param⟩ ← i.readLspNotificationAs "textDocument/didOpen" DidOpenTextDocumentParams
|
let ⟨_, param⟩ ← i.readLspNotificationAs "textDocument/didOpen" DidOpenTextDocumentParams
|
||||||
let ⟨_, levelParams⟩ ← i.readLspNotificationAs "$/game/didOpenLevel" Game.DidOpenLevelParams
|
|
||||||
|
|
||||||
let doc := param.textDocument
|
let doc := param.textDocument
|
||||||
/- NOTE(WN): `toFileMap` marks line beginnings as immediately following
|
-- modification: using `.always`
|
||||||
"\n", which should be enough to handle both LF and CRLF correctly.
|
|
||||||
This is because LSP always refers to characters by (line, column),
|
|
||||||
so if we get the line number correct it shouldn't matter that there
|
|
||||||
is a CR there. -/
|
|
||||||
let meta : DocumentMeta := ⟨doc.uri, doc.version, doc.text.toFileMap, .always⟩
|
let meta : DocumentMeta := ⟨doc.uri, doc.version, doc.text.toFileMap, .always⟩
|
||||||
let e := e.withPrefix s!"[{param.textDocument.uri}] "
|
let e := e.withPrefix s!"[{param.textDocument.uri}] "
|
||||||
let _ ← IO.setStderr e
|
let _ ← IO.setStderr e
|
||||||
try
|
try
|
||||||
let (ctx, st) ← initializeWorker meta i o e initParams.param opts levelParams
|
|
||||||
|
|
||||||
|
-- BIG MODIFICATION
|
||||||
|
let game ← loadGameData gameDir
|
||||||
|
-- TODO: We misuse the `rootUri` field to the gameName
|
||||||
|
let rootUri? : Option String := some (toString game.name)
|
||||||
|
let initParams := {initRequest.param.toLeanInternal with rootUri?}
|
||||||
|
let some (levelId : LevelId) := GameServer.levelIdFromFileName?
|
||||||
|
initParams meta.mkInputContext.fileName
|
||||||
|
| throwServerError s!"Could not determine level ID: {meta.mkInputContext.fileName}"
|
||||||
|
let levelInfo ← loadLevelData gameDir levelId.world levelId.level
|
||||||
|
let some initializationOptions := initRequest.param.initializationOptions?
|
||||||
|
| throwServerError "no initialization options found"
|
||||||
|
let gameWorkerState : WorkerState := {
|
||||||
|
inventory := initializationOptions.inventory
|
||||||
|
difficulty := initializationOptions.difficulty
|
||||||
|
levelInfo
|
||||||
|
}
|
||||||
|
let (ctx, st) ← initializeWorker meta i o e initParams opts gameDir gameWorkerState
|
||||||
|
-- Run the main loop
|
||||||
let _ ← StateRefT'.run (s := st) <| ReaderT.run (r := ctx) <|
|
let _ ← StateRefT'.run (s := st) <| ReaderT.run (r := ctx) <|
|
||||||
StateT.run (s := {levelParams := levelParams}) <| (mainLoop)
|
StateT.run (s := gameWorkerState) <| (mainLoop)
|
||||||
|
|
||||||
|
|
||||||
return (0 : UInt32)
|
return (0 : UInt32)
|
||||||
catch e =>
|
catch e =>
|
||||||
IO.eprintln e
|
IO.eprintln e
|
||||||
@@ -589,14 +774,22 @@ def initAndRunWorker (i o e : FS.Stream) (opts : Options) : IO UInt32 := do
|
|||||||
message := e.toString }] o
|
message := e.toString }] o
|
||||||
return (1 : UInt32)
|
return (1 : UInt32)
|
||||||
|
|
||||||
def workerMain (opts : Options) : IO UInt32 := do
|
/--
|
||||||
|
The main function. Simply wrapping `initAndRunWorker`.
|
||||||
|
|
||||||
|
Copied from `Lean.Server.FileWorker.workerMain`. We add `args` as an argument to pass on
|
||||||
|
the `gameDir`.
|
||||||
|
|
||||||
|
TODO: The first arg `args[0]` is always expected to be `--server`. We could drop this completely.
|
||||||
|
-/
|
||||||
|
-- @[inherit_doc Lean.Server.FileWorker.workerMain]
|
||||||
|
def workerMain (opts : Options) (args : List String): IO UInt32 := do
|
||||||
let i ← IO.getStdin
|
let i ← IO.getStdin
|
||||||
let o ← IO.getStdout
|
let o ← IO.getStdout
|
||||||
let e ← IO.getStderr
|
let e ← IO.getStderr
|
||||||
try
|
try
|
||||||
let exitCode ← initAndRunWorker i o e opts
|
let some gameDir := args[1]? | throwServerError "Expected second argument: gameDir"
|
||||||
-- HACK: all `Task`s are currently "foreground", i.e. we join on them on main thread exit, but we definitely don't
|
let exitCode ← initAndRunWorker i o e opts gameDir
|
||||||
-- want to do that in the case of the worker processes, which can produce non-terminating tasks evaluating user code
|
|
||||||
o.flush
|
o.flush
|
||||||
e.flush
|
e.flush
|
||||||
IO.Process.exit exitCode.toUInt8
|
IO.Process.exit exitCode.toUInt8
|
||||||
@@ -604,4 +797,4 @@ def workerMain (opts : Options) : IO UInt32 := do
|
|||||||
e.putStrLn s!"worker initialization error: {err}"
|
e.putStrLn s!"worker initialization error: {err}"
|
||||||
return (1 : UInt32)
|
return (1 : UInt32)
|
||||||
|
|
||||||
end MyServer.FileWorker
|
end GameServer.FileWorker
|
||||||
|
|||||||
+45
-216
@@ -25,227 +25,56 @@ open Lsp
|
|||||||
open JsonRpc
|
open JsonRpc
|
||||||
open IO
|
open IO
|
||||||
|
|
||||||
def getGame (game : Name): GameServerM Json := do
|
/- Game-specific version of `InitializeParams` that allows for extra options: -/
|
||||||
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
|
|
||||||
|
|
||||||
/--
|
structure InitializationOptions extends Lean.Lsp.InitializationOptions :=
|
||||||
Fields:
|
difficulty : Nat
|
||||||
- description: Lemma in mathematical language.
|
inventory : Array String
|
||||||
- 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
|
deriving ToJson, FromJson
|
||||||
|
|
||||||
-- structure LoadTemplateParams where
|
structure InitializeParams where
|
||||||
-- world : Name
|
processId? : Option Int := none
|
||||||
-- level : Nat
|
clientInfo? : Option ClientInfo := none
|
||||||
-- deriving ToJson, FromJson
|
/- We don't support the deprecated rootPath
|
||||||
|
(rootPath? : Option String) -/
|
||||||
|
rootUri? : Option String := none
|
||||||
|
initializationOptions? : Option InitializationOptions := none
|
||||||
|
capabilities : ClientCapabilities
|
||||||
|
/-- If omitted, we default to off. -/
|
||||||
|
trace : Trace := Trace.off
|
||||||
|
workspaceFolders? : Option (Array WorkspaceFolder) := none
|
||||||
|
deriving ToJson
|
||||||
|
|
||||||
structure DidOpenLevelParams where
|
instance : FromJson InitializeParams where
|
||||||
uri : String
|
fromJson? j := do
|
||||||
gameDir : String
|
let processId? := j.getObjValAs? Int "processId"
|
||||||
levelModule : Name
|
let clientInfo? := j.getObjValAs? ClientInfo "clientInfo"
|
||||||
tactics : Array InventoryTile
|
let rootUri? := j.getObjValAs? String "rootUri"
|
||||||
lemmas : Array InventoryTile
|
let initializationOptions? := j.getObjValAs? InitializationOptions "initializationOptions"
|
||||||
definitions : Array InventoryTile
|
let capabilities ← j.getObjValAs? ClientCapabilities "capabilities"
|
||||||
inventory : Array String
|
let trace := (j.getObjValAs? Trace "trace").toOption.getD Trace.off
|
||||||
/--
|
let workspaceFolders? := j.getObjValAs? (Array WorkspaceFolder) "workspaceFolders"
|
||||||
Check for tactics/theorems that are not unlocked.
|
return ⟨
|
||||||
0: no check
|
processId?.toOption,
|
||||||
1: give warnings
|
clientInfo?.toOption,
|
||||||
2: give errors
|
rootUri?.toOption,
|
||||||
-/
|
initializationOptions?.toOption,
|
||||||
difficulty : Nat
|
capabilities,
|
||||||
/-- The name of the theorem to be proven in this level. -/
|
trace,
|
||||||
statementName : Name
|
workspaceFolders?.toOption⟩
|
||||||
deriving ToJson, FromJson
|
|
||||||
|
|
||||||
structure LoadDocParams where
|
def InitializeParams.toLeanInternal (p : InitializeParams) : Lean.Lsp.InitializeParams :=
|
||||||
name : Name
|
{
|
||||||
type : InventoryType
|
processId? := p.processId?
|
||||||
deriving ToJson, FromJson
|
clientInfo? := p.clientInfo?
|
||||||
|
rootUri? := p.rootUri?
|
||||||
structure SetInventoryParams where
|
initializationOptions? := p.initializationOptions?.map fun o => {
|
||||||
inventory : Array String
|
editDelay? := o.editDelay?
|
||||||
difficulty : Nat
|
hasWidgets? := o.hasWidgets?
|
||||||
deriving ToJson, FromJson
|
|
||||||
|
|
||||||
def handleDidOpenLevel (params : Json) : GameServerM Unit := do
|
|
||||||
let p ← parseParams _ params
|
|
||||||
let m := p.textDocument
|
|
||||||
-- Execute the regular handling of the `didOpen` event
|
|
||||||
handleDidOpen p
|
|
||||||
let fw ← findFileWorker! m.uri
|
|
||||||
-- let s ← get
|
|
||||||
let c ← read
|
|
||||||
let some lvl ← GameServer.getLevelByFileName? c.initParams ((System.Uri.fileUriToPath? m.uri).getD m.uri |>.toString)
|
|
||||||
| do
|
|
||||||
c.hLog.putStr s!"Level not found: {m.uri} {c.initParams.rootUri?}"
|
|
||||||
c.hLog.flush
|
|
||||||
-- Send an extra notification to the file worker to inform it about the level data
|
|
||||||
let s ← get
|
|
||||||
fw.stdin.writeLspNotification {
|
|
||||||
method := "$/game/didOpenLevel"
|
|
||||||
param := {
|
|
||||||
uri := m.uri
|
|
||||||
gameDir := s.gameDir
|
|
||||||
levelModule := lvl.module
|
|
||||||
tactics := lvl.tactics.tiles
|
|
||||||
lemmas := lvl.lemmas.tiles
|
|
||||||
definitions := lvl.definitions.tiles
|
|
||||||
inventory := s.inventory
|
|
||||||
difficulty := s.difficulty
|
|
||||||
statementName := lvl.statementName
|
|
||||||
: DidOpenLevelParams
|
|
||||||
}
|
}
|
||||||
}
|
capabilities := p.capabilities
|
||||||
|
trace := p.trace
|
||||||
|
workspaceFolders? := p.workspaceFolders?
|
||||||
partial def handleServerEvent (ev : ServerEvent) : GameServerM Bool := do
|
}
|
||||||
match ev with
|
|
||||||
| ServerEvent.clientMsg msg =>
|
|
||||||
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 =>
|
|
||||||
let p := (← parseParams SetInventoryParams (toJson params))
|
|
||||||
let s ← get
|
|
||||||
set {s with inventory := p.inventory, difficulty := p.difficulty}
|
|
||||||
let st ← read
|
|
||||||
let workers ← st.fileWorkersRef.get
|
|
||||||
for (_, fw) in workers do
|
|
||||||
fw.stdin.writeLspMessage msg
|
|
||||||
|
|
||||||
return true
|
|
||||||
| Message.request id "loadInventoryOverview" _ =>
|
|
||||||
let s ← get
|
|
||||||
let some game ← getGame? s.game
|
|
||||||
| return false
|
|
||||||
-- All Levels have the same tiles, so we just load them from level 1 of an arbitrary world
|
|
||||||
-- and reset `new`, `disabled` and `unlocked`.
|
|
||||||
-- Note: as we allow worlds without any levels (for developing), we might need
|
|
||||||
-- to try until we find the first world with levels.
|
|
||||||
for ⟨worldId, _⟩ in game.worlds.nodes.toList do
|
|
||||||
let some lvl ← getLevel? {game := s.game, world := worldId, level := 1}
|
|
||||||
| do continue
|
|
||||||
let inventory : InventoryOverview := {
|
|
||||||
tactics := lvl.tactics.tiles.map
|
|
||||||
({ · with locked := true, disabled := false, new := false }),
|
|
||||||
lemmas := lvl.lemmas.tiles.map
|
|
||||||
({ · with locked := true, disabled := false, new := false }),
|
|
||||||
definitions := lvl.definitions.tiles.map
|
|
||||||
({ · with locked := true, disabled := false, new := false }),
|
|
||||||
lemmaTab := none
|
|
||||||
}
|
|
||||||
let c ← read
|
|
||||||
c.hOut.writeLspResponse ⟨id, ToJson.toJson inventory⟩
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
| _ => return false
|
|
||||||
| _ => return false
|
|
||||||
|
|
||||||
end Game
|
end Game
|
||||||
|
|||||||
@@ -18,6 +18,14 @@ instance [ToJson β] : ToJson (Graph Name β) := {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- Just a dummy implementation for now:
|
||||||
|
instance : FromJson (Graph Name β) := {
|
||||||
|
fromJson? := fun _ => .ok {
|
||||||
|
nodes := {}
|
||||||
|
edges := {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
instance : EmptyCollection (Graph α β) := ⟨default⟩
|
instance : EmptyCollection (Graph α β) := ⟨default⟩
|
||||||
|
|
||||||
def Graph.insertNode (g : Graph α β) (a : α) (b : β) :=
|
def Graph.insertNode (g : Graph α β) (a : α) (b : β) :=
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
import Lean
|
||||||
|
import GameServer.Helpers.PrettyPrinter
|
||||||
|
|
||||||
|
/-! This document contains various things which cluttered `Commands.lean`. -/
|
||||||
|
|
||||||
|
open Lean Meta Elab Command
|
||||||
|
|
||||||
|
/-! ## Doc Comment Parsing -/
|
||||||
|
|
||||||
|
namespace GameServer
|
||||||
|
|
||||||
|
/-- Read a doc comment and get its content. Return `""` if no doc comment available. -/
|
||||||
|
def parseDocComment! (doc: Option (TSyntax `Lean.Parser.Command.docComment)) :
|
||||||
|
CommandElabM String := do
|
||||||
|
match doc with
|
||||||
|
| none =>
|
||||||
|
logWarning "Add a text to this command with `/-- yada yada -/ MyCommand`!"
|
||||||
|
pure ""
|
||||||
|
| some s => match s.raw[1] with
|
||||||
|
| .atom _ val => pure <| val.dropRight 2 |>.trim -- some (val.extract 0 (val.endPos - ⟨2⟩))
|
||||||
|
| _ => pure "" --panic "not implemented error message" --throwErrorAt s "unexpected doc string{indentD s.raw[1]}"
|
||||||
|
|
||||||
|
/-- Read a doc comment and get its content. Return `none` if no doc comment available. -/
|
||||||
|
def parseDocComment (doc: Option (TSyntax `Lean.Parser.Command.docComment)) :
|
||||||
|
CommandElabM <| Option String := do
|
||||||
|
match doc with
|
||||||
|
| none => pure none
|
||||||
|
| some _ => parseDocComment! doc
|
||||||
|
|
||||||
|
|
||||||
|
/-- TODO: This is only used to provide some backwards compatibility and you can
|
||||||
|
replace `parseDocCommentLegacy` with `parseDocComment` in the future. -/
|
||||||
|
def parseDocCommentLegacy (doc: Option (TSyntax `Lean.Parser.Command.docComment))
|
||||||
|
(t : Option (TSyntax `str)) : CommandElabM <| String := do
|
||||||
|
match doc with
|
||||||
|
| none =>
|
||||||
|
match t with
|
||||||
|
| none =>
|
||||||
|
pure <| ← parseDocComment! doc
|
||||||
|
| some t =>
|
||||||
|
logWarningAt t "You should use the new Syntax:
|
||||||
|
|
||||||
|
/-- yada yada -/
|
||||||
|
YourCommand
|
||||||
|
|
||||||
|
instead of
|
||||||
|
|
||||||
|
YourCommand \"yada yada\"
|
||||||
|
"
|
||||||
|
pure t.getString
|
||||||
|
| some _ =>
|
||||||
|
match t with
|
||||||
|
| none =>
|
||||||
|
pure <| ← parseDocComment! doc
|
||||||
|
| some t =>
|
||||||
|
logErrorAt t "You must not provide both, a docstring and a string following the command!
|
||||||
|
Only use
|
||||||
|
|
||||||
|
/-- yada yada -/
|
||||||
|
YourCommand
|
||||||
|
|
||||||
|
and remove the string following it!"
|
||||||
|
pure <| ← parseDocComment! doc
|
||||||
|
|
||||||
|
/-- A `attr := ...` option for `Statement`. Add attributes to the defined theorem. -/
|
||||||
|
syntax statementAttr := "(" &"attr" ":=" Parser.Term.attrInstance,* ")"
|
||||||
|
-- TODO
|
||||||
|
|
||||||
|
/-! ## Loops in Graph-like construct
|
||||||
|
|
||||||
|
TODO: Why are we not using graphs here but our own construct `HashMap Name (HashSet Name)`?
|
||||||
|
-/
|
||||||
|
|
||||||
|
partial def removeTransitiveAux (id : Name) (arrows : HashMap Name (HashSet Name))
|
||||||
|
(newArrows : HashMap Name (HashSet Name)) (decendants : HashMap Name (HashSet Name)) :
|
||||||
|
HashMap Name (HashSet Name) × HashMap Name (HashSet Name) := Id.run do
|
||||||
|
match (newArrows.find? id, decendants.find? id) with
|
||||||
|
| (some _, some _) => return (newArrows, decendants)
|
||||||
|
| _ =>
|
||||||
|
let mut newArr := newArrows
|
||||||
|
let mut desc := decendants
|
||||||
|
desc := desc.insert id {} -- mark as worked in case of loops
|
||||||
|
newArr := newArr.insert id {} -- mark as worked in case of loops
|
||||||
|
let children := arrows.findD id {}
|
||||||
|
let mut trimmedChildren := children
|
||||||
|
let mut theseDescs := children
|
||||||
|
for child in children do
|
||||||
|
(newArr, desc) := removeTransitiveAux child arrows newArr desc
|
||||||
|
let childDescs := desc.findD child {}
|
||||||
|
theseDescs := theseDescs.insertMany childDescs
|
||||||
|
for d in childDescs do
|
||||||
|
trimmedChildren := trimmedChildren.erase d
|
||||||
|
desc := desc.insert id theseDescs
|
||||||
|
newArr := newArr.insert id trimmedChildren
|
||||||
|
return (newArr, desc)
|
||||||
|
|
||||||
|
|
||||||
|
def removeTransitive (arrows : HashMap Name (HashSet Name)) : CommandElabM (HashMap Name (HashSet Name)) := do
|
||||||
|
let mut newArr := {}
|
||||||
|
let mut desc := {}
|
||||||
|
for id in arrows.toArray.map Prod.fst do
|
||||||
|
(newArr, desc) := removeTransitiveAux id arrows newArr desc
|
||||||
|
if (desc.findD id {}).contains id then
|
||||||
|
logError <| m!"Loop at {id}. " ++
|
||||||
|
m!"This should not happen and probably means that `findLoops` has a bug."
|
||||||
|
-- DEBUG:
|
||||||
|
-- for ⟨x, hx⟩ in desc.toList do
|
||||||
|
-- m := m ++ m!"{x}: {hx.toList}\n"
|
||||||
|
-- logError m
|
||||||
|
|
||||||
|
return newArr
|
||||||
|
|
||||||
|
/-- The recursive part of `findLoops`. Finds loops that appear as successors of `node`.
|
||||||
|
|
||||||
|
For performance reason it returns a HashSet of visited
|
||||||
|
nodes as well. This is filled with all nodes ever looked at as they cannot be
|
||||||
|
part of a loop anymore. -/
|
||||||
|
partial def findLoopsAux (arrows : HashMap Name (HashSet Name)) (node : Name)
|
||||||
|
(path : Array Name := #[]) (visited : HashSet Name := {}) :
|
||||||
|
Array Name × HashSet Name := Id.run do
|
||||||
|
let mut visited := visited
|
||||||
|
match path.getIdx? node with
|
||||||
|
| some i =>
|
||||||
|
-- Found a loop: `node` is already the iᵗʰ element of the path
|
||||||
|
return (path.extract i path.size, visited.insert node)
|
||||||
|
| none =>
|
||||||
|
for successor in arrows.findD node {} do
|
||||||
|
-- If we already visited the successor, it cannot be part of a loop anymore
|
||||||
|
if visited.contains successor then
|
||||||
|
continue
|
||||||
|
-- Find any loop involving `successor`
|
||||||
|
let (loop, _) := findLoopsAux arrows successor (path.push node) visited
|
||||||
|
visited := visited.insert successor
|
||||||
|
-- No loop found in the dependants of `successor`
|
||||||
|
if loop.isEmpty then
|
||||||
|
continue
|
||||||
|
-- Found a loop, return it
|
||||||
|
return (loop, visited)
|
||||||
|
return (#[], visited.insert node)
|
||||||
|
|
||||||
|
/-- Find a loop in the graph and return it. Returns `[]` if there are no loops. -/
|
||||||
|
partial def findLoops (arrows : HashMap Name (HashSet Name)) : List Name := Id.run do
|
||||||
|
let mut visited : HashSet Name := {}
|
||||||
|
for node in arrows.toArray.map (·.1) do
|
||||||
|
-- Skip a node if it was already visited
|
||||||
|
if visited.contains node then
|
||||||
|
continue
|
||||||
|
-- `findLoopsAux` returns a loop or `[]` together with a set of nodes it visited on its
|
||||||
|
-- search starting from `node`
|
||||||
|
let (loop, moreVisited) := (findLoopsAux arrows node (visited := visited))
|
||||||
|
visited := moreVisited
|
||||||
|
if !loop.isEmpty then
|
||||||
|
return loop.toList
|
||||||
|
return []
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
--import Lean
|
||||||
|
import Lean.PrettyPrinter.Delaborator.Builtins
|
||||||
|
import Lean.PrettyPrinter
|
||||||
|
import Lean
|
||||||
|
|
||||||
|
import Std.Tactic.OpenPrivate
|
||||||
|
|
||||||
|
namespace GameServer
|
||||||
|
|
||||||
|
namespace PrettyPrinter
|
||||||
|
|
||||||
|
open Lean Meta
|
||||||
|
open Lean.Parser Term
|
||||||
|
open PrettyPrinter Delaborator SubExpr
|
||||||
|
open TSyntax.Compat
|
||||||
|
|
||||||
|
open private shouldGroupWithNext evalSyntaxConstant from Lean.PrettyPrinter.Delaborator.Builtins
|
||||||
|
|
||||||
|
-- def typeSpec := leading_parser " :\\n: " >> termParser
|
||||||
|
|
||||||
|
-- def declSig := leading_parser
|
||||||
|
-- many (ppSpace >> (Term.binderIdent <|> Term.bracketedBinder)) >> typeSpec
|
||||||
|
|
||||||
|
|
||||||
|
@[inherit_doc Lean.PrettyPrinter.Delaborator.delabConstWithSignature]
|
||||||
|
partial def delabConstWithSignature : Delab := do
|
||||||
|
let e ← getExpr
|
||||||
|
-- use virtual expression node of arity 2 to separate name and type info
|
||||||
|
let idStx ← descend e 0 <|
|
||||||
|
withOptions (pp.universes.set · true |> (pp.fullNames.set · true)) <|
|
||||||
|
delabConst
|
||||||
|
descend (← inferType e) 1 <|
|
||||||
|
delabParams idStx #[] #[]
|
||||||
|
where
|
||||||
|
-- follows `delabBinders`, but does not uniquify binder names and accumulates all binder groups
|
||||||
|
delabParams (idStx : Ident) (groups : TSyntaxArray ``bracketedBinder) (curIds : Array Ident) := do
|
||||||
|
if let .forallE n d _ i ← getExpr then
|
||||||
|
let stxN ← annotateCurPos (mkIdent n)
|
||||||
|
let curIds := curIds.push ⟨stxN⟩
|
||||||
|
if ← shouldGroupWithNext then
|
||||||
|
withBindingBody n <| delabParams idStx groups curIds
|
||||||
|
else
|
||||||
|
let delabTy := withOptions (pp.piBinderTypes.set · false) delab
|
||||||
|
let group ← withBindingDomain do
|
||||||
|
match i with
|
||||||
|
| .implicit => `(bracketedBinderF|{$curIds*})
|
||||||
|
| .strictImplicit => `(bracketedBinderF|⦃$curIds*⦄)
|
||||||
|
| .instImplicit => `(bracketedBinderF|[$(← delabTy)])
|
||||||
|
| _ =>
|
||||||
|
if d.isOptParam then
|
||||||
|
`(bracketedBinderF|($curIds* : $(← withAppFn <| withAppArg delabTy) := $(← withAppArg delabTy)))
|
||||||
|
else if let some (.const tacticDecl _) := d.getAutoParamTactic? then
|
||||||
|
let tacticSyntax ← ofExcept <| evalSyntaxConstant (← getEnv) (← getOptions) tacticDecl
|
||||||
|
`(bracketedBinderF|($curIds* : $(← withAppFn <| withAppArg delabTy) := by $tacticSyntax))
|
||||||
|
else
|
||||||
|
`(bracketedBinderF|($curIds* : $(← delabTy)))
|
||||||
|
withBindingBody n <| delabParams idStx (groups.push group) #[]
|
||||||
|
else
|
||||||
|
let type ← delab
|
||||||
|
|
||||||
|
-- pure type
|
||||||
|
`(Command.declSig| $groups* : $type)
|
||||||
|
|
||||||
|
@[inherit_doc Lean.PrettyPrinter.ppSignature]
|
||||||
|
def ppSignature (c : Name) : MetaM FormatWithInfos := do
|
||||||
|
let decl ← getConstInfo c
|
||||||
|
let e := .const c (decl.levelParams.map mkLevelParam)
|
||||||
|
let (stx, infos) ← delabCore e (delab := delabConstWithSignature)
|
||||||
|
return ⟨← ppTerm ⟨stx⟩, infos⟩ -- HACK: not a term
|
||||||
|
|
||||||
|
end PrettyPrinter
|
||||||
|
|
||||||
|
open Lean Meta Elab Command
|
||||||
|
|
||||||
|
/-! ## Statement string -/
|
||||||
|
|
||||||
|
def getStatement (name : Name) : CommandElabM MessageData := do
|
||||||
|
return ← addMessageContextPartial (.ofPPFormat { pp := fun
|
||||||
|
| some ctx => ctx.runMetaM <| GameServer.PrettyPrinter.ppSignature name
|
||||||
|
| none => return "that's a bug." })
|
||||||
|
|
||||||
|
-- Note: We use `String` because we can't send `MessageData` as json, but
|
||||||
|
-- `MessageData` might be better for interactive highlighting.
|
||||||
|
/-- Get a string of the form `my_lemma (n : ℕ) : n + n = 2 * n`.
|
||||||
|
|
||||||
|
Note: A statement like `theorem abc : ∀ x : Nat, x ≥ 0` would be turned into
|
||||||
|
`theorem abc (x : Nat) : x ≥ 0` by `PrettyPrinter.ppSignature`. -/
|
||||||
|
def getStatementString (name : Name) : CommandElabM String := do
|
||||||
|
--try
|
||||||
|
return ← (← getStatement name).toString
|
||||||
|
--catch
|
||||||
|
--| _ => throwError m!"Could not find {name} in context."
|
||||||
|
-- TODO: I think it would be nicer to unresolve Namespaces as much as possible.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import GameServer.AbstractCtx
|
||||||
|
|
||||||
|
/-!
|
||||||
|
This file contains anything related to the `Hint` tactic used to add hints to a game level.
|
||||||
|
-/
|
||||||
|
|
||||||
|
open Lean Meta Elab
|
||||||
|
|
||||||
|
namespace GameServer
|
||||||
|
|
||||||
|
syntax hintArg := atomic(" (" (&"strict" <|> &"hidden") " := " withoutPosition(term) ")")
|
||||||
|
|
||||||
|
/-- A hint to help the user with a specific goal state -/
|
||||||
|
structure GoalHintEntry where
|
||||||
|
goal : AbstractCtxResult
|
||||||
|
/-- Text of the hint as an expression of type `Array Expr → MessageData` -/
|
||||||
|
text : Expr
|
||||||
|
rawText : String
|
||||||
|
/-- If true, then hint should be hidden and only be shown on player's request -/
|
||||||
|
hidden : Bool := false
|
||||||
|
/-- If true, then the goal must contain only the assumptions specified in `goal` and no others -/
|
||||||
|
strict : Bool := false
|
||||||
|
|
||||||
|
instance : Repr GoalHintEntry := {
|
||||||
|
reprPrec := fun a n => reprPrec a.text n
|
||||||
|
}
|
||||||
|
|
||||||
|
/-- For a hint `(hint : GoalHintEntry)` one uses `(← evalHintMessage hint.text) x`
|
||||||
|
where `(x : Array Expr)` contains the names of all the variables that should be inserted
|
||||||
|
in the text.
|
||||||
|
|
||||||
|
TODO: explain better. -/
|
||||||
|
unsafe def evalHintMessageUnsafe : Expr → MetaM (Array Expr → MessageData) :=
|
||||||
|
evalExpr (Array Expr → MessageData)
|
||||||
|
(.forallE default (mkApp (mkConst ``Array [levelZero]) (mkConst ``Expr))
|
||||||
|
(mkConst ``MessageData) .default)
|
||||||
|
|
||||||
|
@[implemented_by evalHintMessageUnsafe]
|
||||||
|
def evalHintMessage : Expr → MetaM (Array Expr → MessageData) := fun _ => pure (fun _ => "")
|
||||||
|
|
||||||
|
/-- Remove any spaces at the beginning of a new line -/
|
||||||
|
partial def removeIndentation (s : String) : String :=
|
||||||
|
let rec loop (i : String.Pos) (acc : String) (removeSpaces := false) : String :=
|
||||||
|
let c := s.get i
|
||||||
|
let i := s.next i
|
||||||
|
if s.atEnd i then
|
||||||
|
acc.push c
|
||||||
|
else if removeSpaces && c == ' ' then
|
||||||
|
loop i acc (removeSpaces := true)
|
||||||
|
else if c == '\n' then
|
||||||
|
loop i (acc.push c) (removeSpaces := true)
|
||||||
|
else
|
||||||
|
loop i (acc.push c)
|
||||||
|
loop ⟨0⟩ ""
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import Lean.Environment
|
||||||
|
import Std.Tactic.OpenPrivate
|
||||||
|
import Lean.Data.Lsp.Communication
|
||||||
|
|
||||||
|
open Lean
|
||||||
|
|
||||||
|
inductive LoadingKind := | finalizeExtensions | loadConstants
|
||||||
|
deriving ToJson
|
||||||
|
|
||||||
|
structure LoadingParams : Type where
|
||||||
|
counter : Nat
|
||||||
|
kind : LoadingKind
|
||||||
|
deriving ToJson
|
||||||
|
|
||||||
|
-- Code adapted from `Lean/Environment.lean`
|
||||||
|
|
||||||
|
partial def importModulesCore' (imports : Array Import) : ImportStateM Unit := do
|
||||||
|
for i in imports do
|
||||||
|
if i.runtimeOnly || (← get).moduleNameSet.contains i.module then
|
||||||
|
continue
|
||||||
|
modify fun s => { s with moduleNameSet := s.moduleNameSet.insert i.module }
|
||||||
|
let mFile ← findOLean i.module
|
||||||
|
unless (← mFile.pathExists) do
|
||||||
|
throw <| IO.userError s!"object file '{mFile}' of module {i.module} does not exist"
|
||||||
|
let (mod, region) ← readModuleData mFile
|
||||||
|
importModulesCore' mod.imports
|
||||||
|
modify fun s => { s with
|
||||||
|
moduleData := s.moduleData.push mod
|
||||||
|
regions := s.regions.push region
|
||||||
|
moduleNames := s.moduleNames.push i.module
|
||||||
|
}
|
||||||
|
|
||||||
|
open private mkInitialExtensionStates Environment.mk setImportedEntries finalizePersistentExtensions
|
||||||
|
ensureExtensionsArraySize from Lean.Environment
|
||||||
|
|
||||||
|
private partial def finalizePersistentExtensions' (env : Environment) (mods : Array ModuleData) (opts : Options) : IO Environment := do
|
||||||
|
loop 0 env
|
||||||
|
where
|
||||||
|
loop (i : Nat) (env : Environment) : IO Environment := do
|
||||||
|
(← IO.getStdout).writeLspNotification {
|
||||||
|
method := "$/game/loading",
|
||||||
|
param := {counter := i, kind := .finalizeExtensions : LoadingParams} }
|
||||||
|
-- Recall that the size of the array stored `persistentEnvExtensionRef` may increase when we import user-defined environment extensions.
|
||||||
|
let pExtDescrs ← persistentEnvExtensionsRef.get
|
||||||
|
if i < pExtDescrs.size then
|
||||||
|
let extDescr := pExtDescrs[i]!
|
||||||
|
let s := extDescr.toEnvExtension.getState env
|
||||||
|
let prevSize := (← persistentEnvExtensionsRef.get).size
|
||||||
|
let prevAttrSize ← getNumBuiltinAttributes
|
||||||
|
let newState ← extDescr.addImportedFn s.importedEntries { env := env, opts := opts }
|
||||||
|
let mut env := extDescr.toEnvExtension.setState env { s with state := newState }
|
||||||
|
env ← ensureExtensionsArraySize env
|
||||||
|
if (← persistentEnvExtensionsRef.get).size > prevSize || (← getNumBuiltinAttributes) > prevAttrSize then
|
||||||
|
-- This branch is executed when `pExtDescrs[i]` is the extension associated with the `init` attribute, and
|
||||||
|
-- a user-defined persistent extension is imported.
|
||||||
|
-- Thus, we invoke `setImportedEntries` to update the array `importedEntries` with the entries for the new extensions.
|
||||||
|
env ← setImportedEntries env mods prevSize
|
||||||
|
-- See comment at `updateEnvAttributesRef`
|
||||||
|
env ← updateEnvAttributes env
|
||||||
|
loop (i + 1) env
|
||||||
|
else
|
||||||
|
return env
|
||||||
|
|
||||||
|
def finalizeImport' (s : ImportState) (imports : Array Import) (opts : Options) (trustLevel : UInt32 := 0) : IO Environment := do
|
||||||
|
let numConsts := s.moduleData.foldl (init := 0) fun numConsts mod =>
|
||||||
|
numConsts + mod.constants.size + mod.extraConstNames.size
|
||||||
|
let mut const2ModIdx : HashMap Name ModuleIdx := mkHashMap (capacity := numConsts)
|
||||||
|
let mut constantMap : HashMap Name ConstantInfo := mkHashMap (capacity := numConsts)
|
||||||
|
for h:modIdx in [0:s.moduleData.size] do
|
||||||
|
if modIdx % 100 = 0 then
|
||||||
|
let percentage := modIdx * 100 / s.moduleData.size
|
||||||
|
(← IO.getStdout).writeLspNotification {
|
||||||
|
method := "$/game/loading",
|
||||||
|
param := {counter := percentage, kind := .loadConstants : LoadingParams} }
|
||||||
|
let mod := s.moduleData[modIdx]'h.upper
|
||||||
|
for cname in mod.constNames, cinfo in mod.constants do
|
||||||
|
match constantMap.insert' cname cinfo with
|
||||||
|
| (constantMap', replaced) =>
|
||||||
|
constantMap := constantMap'
|
||||||
|
if replaced then
|
||||||
|
throwAlreadyImported s const2ModIdx modIdx cname
|
||||||
|
const2ModIdx := const2ModIdx.insert cname modIdx
|
||||||
|
for cname in mod.extraConstNames do
|
||||||
|
const2ModIdx := const2ModIdx.insert cname modIdx
|
||||||
|
let constants : ConstMap := SMap.fromHashMap constantMap false
|
||||||
|
let exts ← mkInitialExtensionStates
|
||||||
|
let env : Environment := Environment.mk
|
||||||
|
(const2ModIdx := const2ModIdx)
|
||||||
|
(constants := constants)
|
||||||
|
(extraConstNames := {})
|
||||||
|
(extensions := exts)
|
||||||
|
(header := {
|
||||||
|
quotInit := !imports.isEmpty -- We assume `core.lean` initializes quotient module
|
||||||
|
trustLevel := trustLevel
|
||||||
|
imports := imports
|
||||||
|
regions := s.regions
|
||||||
|
moduleNames := s.moduleNames
|
||||||
|
moduleData := s.moduleData
|
||||||
|
})
|
||||||
|
|
||||||
|
let env ← setImportedEntries env s.moduleData
|
||||||
|
finalizePersistentExtensions' env s.moduleData opts
|
||||||
|
|
||||||
|
def importModules' (imports : Array Import) : IO Environment := do
|
||||||
|
withImporting do
|
||||||
|
let (_, s) ← importModulesCore' imports |>.run
|
||||||
|
let env ← finalizeImport' s imports {} 0
|
||||||
|
return env
|
||||||
@@ -1,74 +1,19 @@
|
|||||||
/- This file is mostly copied from `Lean/Widget/InteractiveGoal.lean`. -/
|
import GameServer.Structures
|
||||||
|
|
||||||
import Lean.Widget.InteractiveGoal
|
/-!
|
||||||
|
This file is a modified copy of `Lean.Widget.InteractiveGoal`.
|
||||||
|
|
||||||
/-! Functionality related to tactic-mode and term-mode goals with embedded `CodeWithInfos`. -/
|
Note that the structures have been moved to `Structures.lean`, but most of the
|
||||||
|
functions here must be duplicated from `Lean.Widget.InteractiveGoal` in order
|
||||||
|
to use the duplicated structures.
|
||||||
|
-/
|
||||||
|
|
||||||
namespace GameServer
|
namespace GameServer
|
||||||
|
|
||||||
open Lean Lean.Widget Lean.Server
|
open Lean Lean.Widget Lean.Server
|
||||||
|
|
||||||
structure GameHint where
|
-- duplicated with custom `InteractiveGoalCore`
|
||||||
text : String
|
-- @[inherit_doc Lean.Widget.InteractiveGoalCore.pretty]
|
||||||
hidden : Bool
|
|
||||||
deriving FromJson, ToJson
|
|
||||||
|
|
||||||
/-- In the infoview, if multiple hypotheses `h₁`, `h₂` have the same type `α`, they are rendered
|
|
||||||
as `h₁ h₂ : α`. We call this a 'hypothesis bundle'. We use `none` instead of `some false` for
|
|
||||||
booleans to save space in the json encoding. -/
|
|
||||||
structure InteractiveHypothesisBundle where
|
|
||||||
/-- The user-friendly name for each hypothesis. -/
|
|
||||||
names : Array Name
|
|
||||||
/-- The ids for each variable. Should have the same length as `names`. -/
|
|
||||||
fvarIds : Array FVarId
|
|
||||||
type : CodeWithInfos
|
|
||||||
/-- The value, in the case the hypothesis is a `let`-binder. -/
|
|
||||||
val? : Option CodeWithInfos := none
|
|
||||||
/-- The hypothesis is a typeclass instance. -/
|
|
||||||
isInstance? : Option Bool := none
|
|
||||||
/-- The hypothesis is a type. -/
|
|
||||||
isType? : Option Bool := none
|
|
||||||
/-- The hypothesis's type is of type `Prop` -/
|
|
||||||
isAssumption? : Option Bool := none
|
|
||||||
/-- If true, the hypothesis was not present on the previous tactic state.
|
|
||||||
Only present in tactic-mode goals. -/
|
|
||||||
isInserted? : Option Bool := none
|
|
||||||
/-- If true, the hypothesis will be removed in the next tactic state.
|
|
||||||
Only present in tactic-mode goals. -/
|
|
||||||
isRemoved? : Option Bool := none
|
|
||||||
deriving Inhabited, RpcEncodable
|
|
||||||
|
|
||||||
/-- The shared parts of interactive term-mode and tactic-mode goals. -/
|
|
||||||
structure InteractiveGoalCore where
|
|
||||||
hyps : Array InteractiveHypothesisBundle
|
|
||||||
/-- The target type. -/
|
|
||||||
type : CodeWithInfos
|
|
||||||
/-- Metavariable context that the goal is well-typed in. -/
|
|
||||||
ctx : WithRpcRef Elab.ContextInfo
|
|
||||||
|
|
||||||
/-- An interactive tactic-mode goal. -/
|
|
||||||
structure InteractiveGoal extends InteractiveGoalCore where
|
|
||||||
/-- The name `foo` in `case foo`, if any. -/
|
|
||||||
userName? : Option String
|
|
||||||
/-- The symbol to display before the target type. Usually `⊢ ` but `conv` goals use `∣ `
|
|
||||||
and it could be extended. -/
|
|
||||||
goalPrefix : String
|
|
||||||
/-- Identifies the goal (ie with the unique name of the MVar that it is a goal for.) -/
|
|
||||||
mvarId : MVarId
|
|
||||||
/-- If true, the goal was not present on the previous tactic state. -/
|
|
||||||
isInserted? : Option Bool := none
|
|
||||||
/-- If true, the goal will be removed on the next tactic state. -/
|
|
||||||
isRemoved? : Option Bool := none
|
|
||||||
hints : Array GameHint := #[]
|
|
||||||
deriving RpcEncodable
|
|
||||||
|
|
||||||
/-- An interactive term-mode goal. -/
|
|
||||||
structure InteractiveTermGoal extends InteractiveGoalCore where
|
|
||||||
/-- Syntactic range of the term. -/
|
|
||||||
range : Lsp.Range
|
|
||||||
/-- Information about the term whose type is the term-mode goal. -/
|
|
||||||
term : WithRpcRef Elab.TermInfo
|
|
||||||
deriving RpcEncodable
|
|
||||||
|
|
||||||
def InteractiveGoalCore.pretty (g : InteractiveGoalCore) (userName? : Option String)
|
def InteractiveGoalCore.pretty (g : InteractiveGoalCore) (userName? : Option String)
|
||||||
(goalPrefix : String) : Format := Id.run do
|
(goalPrefix : String) : Format := Id.run do
|
||||||
let indent := 2 -- Use option
|
let indent := 2 -- Use option
|
||||||
@@ -79,8 +24,7 @@ def InteractiveGoalCore.pretty (g : InteractiveGoalCore) (userName? : Option Str
|
|||||||
ret := addLine ret
|
ret := addLine ret
|
||||||
let names := hyp.names
|
let names := hyp.names
|
||||||
|>.toList
|
|>.toList
|
||||||
|>.filter (not ∘ Name.isAnonymous)
|
|>.filter (· != toString Name.anonymous)
|
||||||
|>.map toString
|
|
||||||
|> " ".intercalate
|
|> " ".intercalate
|
||||||
match names with
|
match names with
|
||||||
| "" =>
|
| "" =>
|
||||||
@@ -97,16 +41,24 @@ where
|
|||||||
addLine (fmt : Format) : Format :=
|
addLine (fmt : Format) : Format :=
|
||||||
if fmt.isNil then fmt else fmt ++ Format.line
|
if fmt.isNil then fmt else fmt ++ Format.line
|
||||||
|
|
||||||
|
-- duplicated with custom `InteractiveGoal`
|
||||||
|
-- @[inherit_doc Lean.Widget.InteractiveGoal.pretty]
|
||||||
def InteractiveGoal.pretty (g : InteractiveGoal) : Format :=
|
def InteractiveGoal.pretty (g : InteractiveGoal) : Format :=
|
||||||
g.toInteractiveGoalCore.pretty g.userName? g.goalPrefix
|
g.toInteractiveGoalCore.pretty g.userName? g.goalPrefix
|
||||||
|
|
||||||
|
-- duplicated with custom `InteractiveTermGoal`
|
||||||
|
-- @[inherit_doc Lean.Widget.InteractiveTermGoal.pretty]
|
||||||
def InteractiveTermGoal.pretty (g : InteractiveTermGoal) : Format :=
|
def InteractiveTermGoal.pretty (g : InteractiveTermGoal) : Format :=
|
||||||
g.toInteractiveGoalCore.pretty none "⊢ "
|
g.toInteractiveGoalCore.pretty none "⊢ "
|
||||||
|
|
||||||
|
-- duplicated with custom `InteractiveGoal`
|
||||||
|
-- @[inherit_doc Lean.Widget.InteractiveGoals]
|
||||||
structure InteractiveGoals where
|
structure InteractiveGoals where
|
||||||
goals : Array InteractiveGoal
|
goals : Array InteractiveGoal
|
||||||
deriving RpcEncodable
|
deriving RpcEncodable
|
||||||
|
|
||||||
|
-- duplicated with custom `InteractiveGoals`
|
||||||
|
-- @[inherit_doc Lean.Widget.InteractiveGoals.append]
|
||||||
def InteractiveGoals.append (l r : InteractiveGoals) : InteractiveGoals where
|
def InteractiveGoals.append (l r : InteractiveGoals) : InteractiveGoals where
|
||||||
goals := l.goals ++ r.goals
|
goals := l.goals ++ r.goals
|
||||||
|
|
||||||
@@ -114,9 +66,10 @@ instance : Append InteractiveGoals := ⟨InteractiveGoals.append⟩
|
|||||||
instance : EmptyCollection InteractiveGoals := ⟨{goals := #[]}⟩
|
instance : EmptyCollection InteractiveGoals := ⟨{goals := #[]}⟩
|
||||||
|
|
||||||
open Meta in
|
open Meta in
|
||||||
/-- Extend an array of hypothesis bundles with another bundle. -/
|
-- duplicated with custom `InteractiveHypothesisBundle` and therefore added `isAssumption?`
|
||||||
|
@[inherit_doc Lean.Widget.addInteractiveHypothesisBundle]
|
||||||
def addInteractiveHypothesisBundle (hyps : Array InteractiveHypothesisBundle)
|
def addInteractiveHypothesisBundle (hyps : Array InteractiveHypothesisBundle)
|
||||||
(ids : Array (Name × FVarId)) (type : Expr) (value? : Option Expr := none) :
|
(ids : Array (String × FVarId)) (type : Expr) (value? : Option Expr := none) :
|
||||||
MetaM (Array InteractiveHypothesisBundle) := do
|
MetaM (Array InteractiveHypothesisBundle) := do
|
||||||
if ids.size == 0 then
|
if ids.size == 0 then
|
||||||
throwError "Can only add a nonzero number of ids as an InteractiveHypothesisBundle."
|
throwError "Can only add a nonzero number of ids as an InteractiveHypothesisBundle."
|
||||||
@@ -129,6 +82,7 @@ def addInteractiveHypothesisBundle (hyps : Array InteractiveHypothesisBundle)
|
|||||||
val? := (← value?.mapM ppExprTagged)
|
val? := (← value?.mapM ppExprTagged)
|
||||||
isInstance? := if (← isClass? type).isSome then true else none
|
isInstance? := if (← isClass? type).isSome then true else none
|
||||||
isType? := if (← instantiateMVars type).isSort then true else none
|
isType? := if (← instantiateMVars type).isSort then true else none
|
||||||
|
-- Added:
|
||||||
isAssumption? := if (← inferType type).isProp then true else none
|
isAssumption? := if (← inferType type).isProp then true else none
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,13 +96,15 @@ def withGoalCtx (goal : MVarId) (action : LocalContext → MetavarDecl → n α)
|
|||||||
withLCtx lctx mvarDecl.localInstances (action lctx mvarDecl)
|
withLCtx lctx mvarDecl.localInstances (action lctx mvarDecl)
|
||||||
|
|
||||||
open Meta in
|
open Meta in
|
||||||
/-- A variant of `Meta.ppGoal` which preserves subexpression information for interactivity. -/
|
|
||||||
def goalToInteractive (mvarId : MVarId) (hints : Array GameHint): MetaM InteractiveGoal := do
|
-- Duplicated from `Lean.Widget.goalToInteractive` with custom structures
|
||||||
|
@[inherit_doc Lean.Widget.goalToInteractive]
|
||||||
|
def goalToInteractive (mvarId : MVarId) : MetaM InteractiveGoal := do
|
||||||
let ppAuxDecls := pp.auxDecls.get (← getOptions)
|
let ppAuxDecls := pp.auxDecls.get (← getOptions)
|
||||||
let ppImplDetailHyps := pp.implementationDetailHyps.get (← getOptions)
|
let ppImplDetailHyps := pp.implementationDetailHyps.get (← getOptions)
|
||||||
let showLetValues := pp.showLetValues.get (← getOptions)
|
let showLetValues := pp.showLetValues.get (← getOptions)
|
||||||
withGoalCtx mvarId fun lctx mvarDecl => do
|
withGoalCtx mvarId fun lctx mvarDecl => do
|
||||||
let pushPending (ids : Array (Name × FVarId)) (type? : Option Expr) (hyps : Array InteractiveHypothesisBundle)
|
let pushPending (ids : Array (String × FVarId)) (type? : Option Expr) (hyps : Array InteractiveHypothesisBundle)
|
||||||
: MetaM (Array InteractiveHypothesisBundle) :=
|
: MetaM (Array InteractiveHypothesisBundle) :=
|
||||||
if ids.isEmpty then
|
if ids.isEmpty then
|
||||||
pure hyps
|
pure hyps
|
||||||
@@ -156,7 +112,7 @@ def goalToInteractive (mvarId : MVarId) (hints : Array GameHint): MetaM Interact
|
|||||||
match type? with
|
match type? with
|
||||||
| none => pure hyps
|
| none => pure hyps
|
||||||
| some type => addInteractiveHypothesisBundle hyps ids type
|
| some type => addInteractiveHypothesisBundle hyps ids type
|
||||||
let mut varNames : Array (Name × FVarId) := #[]
|
let mut varNames : Array (String × FVarId) := #[]
|
||||||
let mut prevType? : Option Expr := none
|
let mut prevType? : Option Expr := none
|
||||||
let mut hyps : Array InteractiveHypothesisBundle := #[]
|
let mut hyps : Array InteractiveHypothesisBundle := #[]
|
||||||
for localDecl in lctx do
|
for localDecl in lctx do
|
||||||
@@ -165,7 +121,7 @@ def goalToInteractive (mvarId : MVarId) (hints : Array GameHint): MetaM Interact
|
|||||||
else
|
else
|
||||||
match localDecl with
|
match localDecl with
|
||||||
| LocalDecl.cdecl _index fvarId varName type _ _ =>
|
| LocalDecl.cdecl _index fvarId varName type _ _ =>
|
||||||
let varName := varName.simpMacroScopes
|
let varName := toString varName
|
||||||
let type ← instantiateMVars type
|
let type ← instantiateMVars type
|
||||||
if prevType? == none || prevType? == some type then
|
if prevType? == none || prevType? == some type then
|
||||||
varNames := varNames.push (varName, fvarId)
|
varNames := varNames.push (varName, fvarId)
|
||||||
@@ -174,7 +130,7 @@ def goalToInteractive (mvarId : MVarId) (hints : Array GameHint): MetaM Interact
|
|||||||
varNames := #[(varName, fvarId)]
|
varNames := #[(varName, fvarId)]
|
||||||
prevType? := some type
|
prevType? := some type
|
||||||
| LocalDecl.ldecl _index fvarId varName type val _ _ => do
|
| LocalDecl.ldecl _index fvarId varName type val _ _ => do
|
||||||
let varName := varName.simpMacroScopes
|
let varName := toString varName
|
||||||
hyps ← pushPending varNames prevType? hyps
|
hyps ← pushPending varNames prevType? hyps
|
||||||
let type ← instantiateMVars type
|
let type ← instantiateMVars type
|
||||||
let val? ← if showLetValues then pure (some (← instantiateMVars val)) else pure none
|
let val? ← if showLetValues then pure (some (← instantiateMVars val)) else pure none
|
||||||
@@ -190,11 +146,10 @@ def goalToInteractive (mvarId : MVarId) (hints : Array GameHint): MetaM Interact
|
|||||||
return {
|
return {
|
||||||
hyps
|
hyps
|
||||||
type := goalFmt
|
type := goalFmt
|
||||||
ctx := ⟨← Elab.ContextInfo.save⟩
|
ctx := ⟨{← Elab.CommandContextInfo.save with }⟩
|
||||||
userName?
|
userName?
|
||||||
goalPrefix := getGoalPrefix mvarDecl
|
goalPrefix := getGoalPrefix mvarDecl
|
||||||
mvarId
|
mvarId
|
||||||
hints
|
|
||||||
}
|
}
|
||||||
|
|
||||||
end GameServer
|
end GameServer
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import Lean
|
||||||
|
import GameServer.EnvExtensions
|
||||||
|
|
||||||
|
open Lean Elab Command
|
||||||
|
|
||||||
|
/-- Copied from `Mathlib.Tactic.HelpCmd`.
|
||||||
|
|
||||||
|
Gets the initial string token in a parser description. For example, for a declaration like
|
||||||
|
`syntax "bla" "baz" term : tactic`, it returns `some "bla"`. Returns `none` for syntax declarations
|
||||||
|
that don't start with a string constant. -/
|
||||||
|
partial def getHeadTk (e : Expr) : Option String :=
|
||||||
|
match (Expr.withApp e λ e a => (e.constName?.getD Name.anonymous, a)) with
|
||||||
|
| (``ParserDescr.node, #[_, _, p]) => getHeadTk p
|
||||||
|
| (``ParserDescr.unary, #[.app _ (.lit (.strVal "withPosition")), p]) => getHeadTk p
|
||||||
|
| (``ParserDescr.unary, #[.app _ (.lit (.strVal "atomic")), p]) => getHeadTk p
|
||||||
|
| (``ParserDescr.binary, #[.app _ (.lit (.strVal "andthen")), p, _]) => getHeadTk p
|
||||||
|
| (``ParserDescr.nonReservedSymbol, #[.lit (.strVal tk), _]) => some tk
|
||||||
|
| (``ParserDescr.symbol, #[.lit (.strVal tk)]) => some tk
|
||||||
|
| (``Parser.withAntiquot, #[_, p]) => getHeadTk p
|
||||||
|
| (``Parser.leadingNode, #[_, _, p]) => getHeadTk p
|
||||||
|
| (``HAndThen.hAndThen, #[_, _, _, _, p, _]) => getHeadTk p
|
||||||
|
| (``Parser.nonReservedSymbol, #[.lit (.strVal tk), _]) => some tk
|
||||||
|
| (``Parser.symbol, #[.lit (.strVal tk)]) => some tk
|
||||||
|
| _ => none
|
||||||
|
|
||||||
|
/-! ## Doc entries -/
|
||||||
|
|
||||||
|
/-- Modified from `#help` in `Mathlib.Tactic.HelpCmd` -/
|
||||||
|
def getTacticDocstring (env : Environment) (name: Name) : CommandElabM (Option String) := do
|
||||||
|
let name := name.toString (escape := false)
|
||||||
|
let mut decls : Lean.RBMap String (Array SyntaxNodeKind) compare := {}
|
||||||
|
|
||||||
|
let catName : Name := `tactic
|
||||||
|
let catStx : Ident := mkIdent catName -- TODO
|
||||||
|
let some cat := (Parser.parserExtension.getState env).categories.find? catName
|
||||||
|
| throwErrorAt catStx "{catStx} is not a syntax category"
|
||||||
|
liftTermElabM <| Term.addCategoryInfo catStx catName
|
||||||
|
for (k, _) in cat.kinds do
|
||||||
|
let mut used := false
|
||||||
|
if let some tk := do getHeadTk (← (← env.find? k).value?) then
|
||||||
|
let tk := tk.trim
|
||||||
|
if name ≠ tk then -- was `!name.isPrefixOf tk`
|
||||||
|
continue
|
||||||
|
used := true
|
||||||
|
decls := decls.insert tk ((decls.findD tk #[]).push k)
|
||||||
|
for (_name, ks) in decls do
|
||||||
|
for k in ks do
|
||||||
|
if let some doc ← findDocString? env k then
|
||||||
|
return doc
|
||||||
|
|
||||||
|
logWarning <| m!"Could not find a docstring for tactic {name}, consider adding one " ++
|
||||||
|
m!"using `TacticDoc {name} \"some doc\"`"
|
||||||
|
return none
|
||||||
|
|
||||||
|
/-- Retrieve the docstring associated to an inventory item. For Tactics, this
|
||||||
|
is not guaranteed to work. -/
|
||||||
|
def getDocstring (env : Environment) (name : Name) (type : InventoryType) :
|
||||||
|
CommandElabM (Option String) :=
|
||||||
|
match type with
|
||||||
|
-- for tactics it's a lookup following mathlib's `#help`. not guaranteed to be the correct one.
|
||||||
|
| .Tactic => getTacticDocstring env name
|
||||||
|
| .Lemma => findDocString? env name
|
||||||
|
-- TODO: for definitions not implemented yet, does it work?
|
||||||
|
| .Definition => findDocString? env name
|
||||||
|
|
||||||
|
/-- Checks if `inventoryTemplateExt` contains an entry with `(type, name)` and yields
|
||||||
|
a warning otherwise. If `template` is provided, it will add such an entry instead of yielding a
|
||||||
|
warning.
|
||||||
|
|
||||||
|
`ref` is the syntax piece. If `name` is not provided, it will use `ident.getId`.
|
||||||
|
I used this workaround, because I needed a new name (with correct namespace etc)
|
||||||
|
to be used, and I don't know how to create a new ident with same position but different name.
|
||||||
|
-/
|
||||||
|
def checkInventoryDoc (type : InventoryType) (ref : Ident) (name : Name := ref.getId)
|
||||||
|
(template : Option String := none) : CommandElabM Unit := do
|
||||||
|
-- note: `name` is an `Ident` (instead of `Name`) for the log messages.
|
||||||
|
let env ← getEnv
|
||||||
|
let n := name
|
||||||
|
-- Find a key with matching `(type, name)`.
|
||||||
|
match (inventoryTemplateExt.getState env).findIdx?
|
||||||
|
(fun x => x.name == n && x.type == type) with
|
||||||
|
-- Nothing to do if the entry exists
|
||||||
|
| some _ => pure ()
|
||||||
|
| none =>
|
||||||
|
match template with
|
||||||
|
-- Warn about missing documentation
|
||||||
|
| none =>
|
||||||
|
let docstring ← match (← getDocstring env name type) with
|
||||||
|
| some ds =>
|
||||||
|
logInfoAt ref (m!"Missing {type} Documentation. Using existing docstring. " ++
|
||||||
|
m!"Add {name}\nAdd `{type}Doc {name}` somewhere above this statement.")
|
||||||
|
pure s!"*(lean docstring)*\\\n{ds}"
|
||||||
|
| none =>
|
||||||
|
logWarningAt ref (m!"Missing {type} Documentation: {name}\nAdd `{type}Doc {name}` " ++
|
||||||
|
m!"somewhere above this statement.")
|
||||||
|
pure "(missing)"
|
||||||
|
|
||||||
|
-- We just add a dummy entry
|
||||||
|
modifyEnv (inventoryTemplateExt.addEntry · {
|
||||||
|
type := type
|
||||||
|
name := name
|
||||||
|
category := if type == .Lemma then s!"{n.getPrefix}" else ""
|
||||||
|
content := docstring})
|
||||||
|
-- Add the default documentation
|
||||||
|
| some s =>
|
||||||
|
modifyEnv (inventoryTemplateExt.addEntry · {
|
||||||
|
type := type
|
||||||
|
name := name
|
||||||
|
category := if type == .Lemma then s!"{n.getPrefix}" else ""
|
||||||
|
content := s })
|
||||||
|
logInfoAt ref (m!"Missing {type} Documentation: {name}, used default (e.g. provided " ++
|
||||||
|
m!"docstring) instead. If you want to write a different description, add " ++
|
||||||
|
m!"`{type}Doc {name}` somewhere above this statement.")
|
||||||
|
|
||||||
|
partial def collectUsedInventory (stx : Syntax) (acc : UsedInventory := {}) : CommandElabM UsedInventory := do
|
||||||
|
match stx with
|
||||||
|
| .missing => return acc
|
||||||
|
| .node _info kind args =>
|
||||||
|
if kind == `GameServer.Tactic.Hint || kind == `GameServer.Tactic.Branch then return acc
|
||||||
|
return ← args.foldlM (fun acc arg => collectUsedInventory arg acc) acc
|
||||||
|
| .atom _info val =>
|
||||||
|
-- ignore syntax elements that do not start with a letter
|
||||||
|
-- and ignore some standard keywords
|
||||||
|
let allowed := GameServer.ALLOWED_KEYWORDS
|
||||||
|
if 0 < val.length ∧ val.data[0]!.isAlpha ∧ not (allowed.contains val) then
|
||||||
|
let val := val.dropRightWhile (fun c => c == '!' || c == '?') -- treat `simp?` and `simp!` like `simp`
|
||||||
|
return {acc with tactics := acc.tactics.insert val}
|
||||||
|
else
|
||||||
|
return acc
|
||||||
|
| .ident _info _rawVal val _preresolved =>
|
||||||
|
let ns ←
|
||||||
|
try resolveGlobalConst (mkIdent val)
|
||||||
|
catch | _ => pure [] -- catch "unknown constant" error
|
||||||
|
return ← ns.foldlM (fun acc n => do
|
||||||
|
if let some (.thmInfo ..) := (← getEnv).find? n then
|
||||||
|
return {acc with lemmas := acc.lemmas.insertMany ns}
|
||||||
|
else
|
||||||
|
return {acc with definitions := acc.definitions.insertMany ns}
|
||||||
|
) acc
|
||||||
|
|
||||||
|
-- #check expandOptDocComment?
|
||||||
|
|
||||||
|
def GameLevel.getInventory (level : GameLevel) : InventoryType → InventoryInfo
|
||||||
|
| .Tactic => level.tactics
|
||||||
|
| .Definition => level.definitions
|
||||||
|
| .Lemma => level.lemmas
|
||||||
|
|
||||||
|
def GameLevel.setComputedInventory (level : GameLevel) :
|
||||||
|
InventoryType → Array InventoryTile → GameLevel
|
||||||
|
| .Tactic, v => {level with tactics := {level.tactics with tiles := v}}
|
||||||
|
| .Definition, v => {level with definitions := {level.definitions with tiles := v}}
|
||||||
|
| .Lemma, v => {level with lemmas := {level.lemmas with tiles := v}}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import Lean
|
||||||
|
|
||||||
|
/-! This document contains custom options available in the game. -/
|
||||||
|
|
||||||
|
/-- Let `MakeGame` print the reasons why the worlds depend on each other. -/
|
||||||
|
register_option lean4game.showDependencyReasons : Bool := {
|
||||||
|
defValue := false
|
||||||
|
descr := "show reasons for calculated world dependencies."
|
||||||
|
}
|
||||||
|
|
||||||
|
/-- Let `MakeGame` print the reasons why the worlds depend on each other.
|
||||||
|
|
||||||
|
Note: currently unused in favour of setting `set_option trace.debug true`. -/
|
||||||
|
register_option lean4game.verbose : Bool := {
|
||||||
|
defValue := false
|
||||||
|
descr := "display more info messages to help developing the game."
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import GameServer.EnvExtensions
|
import GameServer.EnvExtensions
|
||||||
import GameServer.InteractiveGoal
|
import GameServer.InteractiveGoal
|
||||||
|
import GameServer.Hints
|
||||||
|
import I18n
|
||||||
|
|
||||||
open Lean
|
open Lean
|
||||||
open Server
|
open Server
|
||||||
@@ -7,7 +9,6 @@ open Widget
|
|||||||
open RequestM
|
open RequestM
|
||||||
open Meta
|
open Meta
|
||||||
|
|
||||||
|
|
||||||
/-! ## GameGoal -/
|
/-! ## GameGoal -/
|
||||||
|
|
||||||
namespace GameServer
|
namespace GameServer
|
||||||
@@ -46,8 +47,8 @@ partial def matchExpr (pattern : Expr) (e : Expr) (bij : FVarBijection := {}) :
|
|||||||
| .bvar i1, .bvar i2 => if i1 == i2 then bij else none
|
| .bvar i1, .bvar i2 => if i1 == i2 then bij else none
|
||||||
| .fvar i1, .fvar i2 => bij.insert? i1 i2
|
| .fvar i1, .fvar i2 => bij.insert? i1 i2
|
||||||
| .mvar _, .mvar _ => bij
|
| .mvar _, .mvar _ => bij
|
||||||
| .sort u1, .sort u2 => bij -- TODO?
|
| .sort _u1, .sort _u2 => bij -- TODO?
|
||||||
| .const n1 ls1, .const n2 ls2 =>
|
| .const n1 _ls1, .const n2 _ls2 =>
|
||||||
if n1 == n2 then bij else none -- && (← (ls1.zip ls2).allM fun (l1, l2) => Meta.isLevelDefEq l1 l2)
|
if n1 == n2 then bij else none -- && (← (ls1.zip ls2).allM fun (l1, l2) => Meta.isLevelDefEq l1 l2)
|
||||||
| .app f1 a1, .app f2 a2 =>
|
| .app f1 a1, .app f2 a2 =>
|
||||||
some bij
|
some bij
|
||||||
@@ -78,7 +79,7 @@ partial def matchExpr (pattern : Expr) (e : Expr) (bij : FVarBijection := {}) :
|
|||||||
| _, _ => none
|
| _, _ => none
|
||||||
|
|
||||||
/-- Check if each fvar in `patterns` has a matching fvar in `fvars` -/
|
/-- Check if each fvar in `patterns` has a matching fvar in `fvars` -/
|
||||||
def matchDecls (patterns : Array Expr) (fvars : Array Expr) (strict := true) (initBij : FVarBijection := {}) : MetaM Bool := do
|
def matchDecls (patterns : Array Expr) (fvars : Array Expr) (strict := true) (initBij : FVarBijection := {}) : MetaM (Option FVarBijection) := do
|
||||||
-- We iterate through the array backwards hoping that this will find us faster results
|
-- We iterate through the array backwards hoping that this will find us faster results
|
||||||
-- TODO: implement backtracking
|
-- TODO: implement backtracking
|
||||||
let mut bij := initBij
|
let mut bij := initBij
|
||||||
@@ -97,63 +98,196 @@ def matchDecls (patterns : Array Expr) (fvars : Array Expr) (strict := true) (in
|
|||||||
-- usedFvars := usedFvars.set! (fvars.size - j - 1) true
|
-- usedFvars := usedFvars.set! (fvars.size - j - 1) true
|
||||||
bij := bij'.insert pattern.fvarId! fvar.fvarId!
|
bij := bij'.insert pattern.fvarId! fvar.fvarId!
|
||||||
break
|
break
|
||||||
if ! bij.forward.contains pattern.fvarId! then return false
|
if ! bij.forward.contains pattern.fvarId! then return none
|
||||||
|
|
||||||
if strict then
|
if !strict || fvars.all (fun fvar => bij.backward.contains fvar.fvarId!)
|
||||||
return fvars.all (fun fvar => bij.backward.contains fvar.fvarId!)
|
then return some bij
|
||||||
return true
|
else return none
|
||||||
|
|
||||||
unsafe def evalHintMessageUnsafe : Expr → MetaM (Array Expr → MessageData) :=
|
|
||||||
evalExpr (Array Expr → MessageData)
|
|
||||||
(.forallE default (mkApp (mkConst ``Array [levelZero]) (mkConst ``Expr))
|
|
||||||
(mkConst ``MessageData) .default)
|
|
||||||
|
|
||||||
@[implemented_by evalHintMessageUnsafe]
|
|
||||||
def evalHintMessage : Expr → MetaM (Array Expr → MessageData) := fun _ => pure (fun _ => "")
|
|
||||||
|
|
||||||
open Meta in
|
open Meta in
|
||||||
/-- Find all hints whose trigger matches the current goal -/
|
/-- Find all hints whose trigger matches the current goal -/
|
||||||
def findHints (goal : MVarId) (doc : FileWorker.EditableDocument) (initParams : Lsp.InitializeParams) : MetaM (Array GameHint) := do
|
def findHints (goal : MVarId) (m : DocumentMeta) (initParams : Lsp.InitializeParams) : MetaM (Array GameHint) := do
|
||||||
goal.withContext do
|
goal.withContext do
|
||||||
let some level ← getLevelByFileName? initParams doc.meta.mkInputContext.fileName
|
let some level ← getLevelByFileName? initParams m.mkInputContext.fileName
|
||||||
| throwError "Level not found: {doc.meta.mkInputContext.fileName}"
|
| throwError "Level not found: {m.mkInputContext.fileName}"
|
||||||
let hints ← level.hints.filterMapM fun hint => do
|
let hints ← level.hints.filterMapM fun hint => do
|
||||||
openAbstractCtxResult hint.goal fun hintFVars hintGoal => do
|
openAbstractCtxResult hint.goal fun hintFVars hintGoal => do
|
||||||
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
|
|
||||||
if ← matchDecls hintFVars lctx.getFVars (strict := hint.strict) (initBij := fvarBij)
|
-- NOTE: This code for `hintFVarsNames` is also duplicated in the
|
||||||
|
-- "Statement" command, where `hint.rawText` is created. They need to be matching.
|
||||||
|
-- NOTE: This is a bit a hack of somebody who does not know how meta-programming works.
|
||||||
|
-- All we want here is a list of `userNames` for the `FVarId`s in `hintFVars`...
|
||||||
|
-- and we wrap them in `«{}»` here since I don't know how to do it later.
|
||||||
|
let mut hintFVarsNames : Array Expr := #[]
|
||||||
|
for fvar in hintFVars do
|
||||||
|
let name₁ ← fvar.fvarId!.getUserName
|
||||||
|
hintFVarsNames := hintFVarsNames.push <| Expr.fvar ⟨s!"«\{{name₁}}»"⟩
|
||||||
|
|
||||||
|
let lctx := (← goal.getDecl).lctx -- the player's local context
|
||||||
|
if let some bij ← matchDecls hintFVars lctx.getFVars
|
||||||
|
(strict := hint.strict) (initBij := fvarBij)
|
||||||
then
|
then
|
||||||
let text := (← evalHintMessage hint.text) hintFVars
|
let userFVars := hintFVars.map fun v => bij.forward.findD v.fvarId! v.fvarId!
|
||||||
let ctx := {env := ← getEnv, mctx := ← getMCtx, lctx := ← getLCtx, opts := {}}
|
-- Evaluate the text in the player's context to get the new variable names.
|
||||||
|
let text := (← evalHintMessage hint.text) (userFVars.map Expr.fvar)
|
||||||
|
let ctx := {env := ← getEnv, mctx := ← getMCtx, lctx := lctx, opts := {}}
|
||||||
let text ← (MessageData.withContext ctx text).toString
|
let text ← (MessageData.withContext ctx text).toString
|
||||||
return some { text := text, hidden := hint.hidden }
|
|
||||||
|
-- Here we map the goal's variable names to the player's variable names.
|
||||||
|
let mut varNames : Array <| Name × Name := #[]
|
||||||
|
for (fvar₁, fvar₂) in bij.forward.toArray do
|
||||||
|
-- get the `userName` of the fvar in the opened local context of the hint.
|
||||||
|
let name₁ ← fvar₁.getUserName
|
||||||
|
-- get the `userName` in the player's local context.
|
||||||
|
let name₂ := (lctx.get! fvar₂).userName
|
||||||
|
varNames := varNames.push (name₁, name₂)
|
||||||
|
|
||||||
|
return some {
|
||||||
|
text := text,
|
||||||
|
hidden := hint.hidden,
|
||||||
|
rawText := hint.rawText,
|
||||||
|
varNames := varNames }
|
||||||
|
|
||||||
else return none
|
else return none
|
||||||
else
|
else
|
||||||
return none
|
return none
|
||||||
return hints
|
return hints
|
||||||
|
|
||||||
open RequestM in
|
def filterUnsolvedGoal (a : Array InteractiveDiagnostic) :
|
||||||
def getInteractiveGoals (p : Lsp.PlainGoalParams) : RequestM (RequestTask (Option InteractiveGoals)) := do
|
Array InteractiveDiagnostic :=
|
||||||
|
a.filter (fun d => match d.message with
|
||||||
|
| .append ⟨(.text x) :: _⟩ => x != "unsolved goals"
|
||||||
|
| _ => true)
|
||||||
|
|
||||||
|
-- TODO: no need to have `RequestM`, just anything where `mut` works
|
||||||
|
/-- Add custom diagnostics about whether the level is completed. -/
|
||||||
|
def completionDiagnostics (goalCount : Nat) (prevGoalCount : Nat) (completed : Bool)
|
||||||
|
(completedWithWarnings : Bool) (pos : Lsp.Position)
|
||||||
|
(startDiags : Array InteractiveDiagnostic := #[]) :
|
||||||
|
RequestM <| Array InteractiveDiagnostic := do
|
||||||
|
let mut out : Array InteractiveDiagnostic := startDiags
|
||||||
|
if goalCount == 0 then
|
||||||
|
if completed then
|
||||||
|
out := out.push {
|
||||||
|
-- TODO: marking these with `t!` has the implication that every game
|
||||||
|
-- needs to translate these messages again,
|
||||||
|
-- but cannot think of another option
|
||||||
|
-- that would not involve manually adding them somewhere in the translation files.
|
||||||
|
message := .text t!"level completed! 🎉"
|
||||||
|
range := {
|
||||||
|
start := pos
|
||||||
|
«end» := pos
|
||||||
|
}
|
||||||
|
severity? := Lsp.DiagnosticSeverity.information }
|
||||||
|
else if completedWithWarnings then
|
||||||
|
out := out.push {
|
||||||
|
message := .text t!"level completed with warnings… 🎭"
|
||||||
|
range := {
|
||||||
|
start := pos
|
||||||
|
«end» := pos
|
||||||
|
}
|
||||||
|
severity? := Lsp.DiagnosticSeverity.information }
|
||||||
|
else
|
||||||
|
pure ()
|
||||||
|
else if goalCount < prevGoalCount then
|
||||||
|
-- If there is any errors, goals might vanish without being 'solved'
|
||||||
|
-- so showing the message "intermediate goal solved" would be confusing.
|
||||||
|
if (¬ (filterUnsolvedGoal startDiags).any (·.severity? == some .error)) then
|
||||||
|
out := out.push {
|
||||||
|
message := .text t!"intermediate goal solved! 🎉"
|
||||||
|
range := {
|
||||||
|
start := pos
|
||||||
|
«end» := pos
|
||||||
|
}
|
||||||
|
severity? := Lsp.DiagnosticSeverity.information
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
/-- Request that returns the goals at the end of each line of the tactic proof
|
||||||
|
plus the diagnostics (i.e. warnings/errors) for the proof.
|
||||||
|
-/
|
||||||
|
def getProofState (_ : Lsp.PlainGoalParams) : RequestM (RequestTask (Option ProofState)) := do
|
||||||
let doc ← readDoc
|
let doc ← readDoc
|
||||||
let rc ← readThe RequestContext
|
let rc ← readThe RequestContext
|
||||||
let text := doc.meta.text
|
let text := doc.meta.text
|
||||||
let hoverPos := text.lspPosToUtf8Pos p.position
|
|
||||||
-- TODO: I couldn't find a good condition to find the correct snap. So we are looking
|
withWaitFindSnap
|
||||||
-- for the first snap with goals here:
|
doc
|
||||||
withWaitFindSnap doc (fun s => ¬ (s.infoTree.goalsAt? doc.meta.text hoverPos).isEmpty)
|
-- TODO (Alex): I couldn't find a good condition to find the correct snap. So we are looking
|
||||||
(notFoundX := return none) fun snap => do
|
-- for the first snap with goals here.
|
||||||
if let rs@(_ :: _) := snap.infoTree.goalsAt? doc.meta.text hoverPos then
|
-- NOTE (Jon): The entire proof is in one snap, so hoped that Position `0` is good enough.
|
||||||
let goals : List InteractiveGoals ← rs.mapM fun { ctxInfo := ci, tacticInfo := ti, useAfter := useAfter, .. } => do
|
(fun snap => ¬ (snap.infoTree.goalsAt? doc.meta.text 0).isEmpty)
|
||||||
let ciAfter := { ci with mctx := ti.mctxAfter }
|
(notFoundX := return none)
|
||||||
let ci := if useAfter then ciAfter else { ci with mctx := ti.mctxBefore }
|
fun snap => do
|
||||||
|
-- `snap` is the one snapshot containing the entire proof.
|
||||||
|
let mut steps : Array <| InteractiveGoalsWithHints := #[]
|
||||||
|
|
||||||
|
-- Question: Is there a difference between the diags of this snap and the last snap?
|
||||||
|
-- Should we get the diags from there?
|
||||||
|
-- Answer: The last snap only copied the diags from the end of this snap
|
||||||
|
let mut diag : Array InteractiveDiagnostic := snap.interactiveDiags.toArray
|
||||||
|
|
||||||
|
-- Level is completed if there are no errrors or warnings
|
||||||
|
let completedWithWarnings : Bool := ¬ diag.any (·.severity? == some .error)
|
||||||
|
let completed : Bool := completedWithWarnings ∧ ¬ diag.any (·.severity? == some .warning)
|
||||||
|
|
||||||
|
let mut intermediateGoalCount := 0
|
||||||
|
|
||||||
|
-- only the positions that have non-whitespace characters since the last position
|
||||||
|
-- should add a new proof step.
|
||||||
|
let positionsWithSource : Array (String.Pos × String) :=
|
||||||
|
text.positions.zipWithIndex.filterMap (
|
||||||
|
fun (pos, i) => match i with
|
||||||
|
| 0 => some (pos, "")
|
||||||
|
| i' + 1 =>
|
||||||
|
let source : String := Substring.toString ⟨text.source, text.positions.get! i', pos⟩
|
||||||
|
if source.trim.length == 0 then
|
||||||
|
none
|
||||||
|
else
|
||||||
|
some (pos, source))
|
||||||
|
|
||||||
|
-- Drop the last position as we ensured that there is always a newline at the end
|
||||||
|
for ((pos, source), i) in positionsWithSource.zipWithIndex do
|
||||||
|
-- iterate over all steps in the proof and get the goals and hints at each position
|
||||||
|
|
||||||
|
-- diags are labeled in Lsp-positions, which differ from the lean-internal
|
||||||
|
-- positions by `1`.
|
||||||
|
let lspPosAt := text.utf8PosToLspPos pos
|
||||||
|
|
||||||
|
let mut diagsAtPos : Array InteractiveDiagnostic := filterUnsolvedGoal <|
|
||||||
|
-- `+1` for getting the errors after the line.
|
||||||
|
match i with
|
||||||
|
| 0 =>
|
||||||
|
-- `lspPosAt` is `(0, 0)`
|
||||||
|
diag.filter (fun d => d.range.start == lspPosAt )
|
||||||
|
| i' + 1 =>
|
||||||
|
diag.filter (fun d =>
|
||||||
|
((text.utf8PosToLspPos <| (positionsWithSource.get! i').1) ≤ d.range.start) ∧
|
||||||
|
d.range.start < lspPosAt )
|
||||||
|
|
||||||
|
if let goalsAtResult@(_ :: _) := snap.infoTree.goalsAt? doc.meta.text pos then
|
||||||
|
let goalsAtPos' : List <| List InteractiveGoalWithHints ← goalsAtResult.mapM
|
||||||
|
fun { ctxInfo := ci, tacticInfo := tacticInfo, useAfter := useAfter, .. } => do
|
||||||
|
-- TODO: What does this function body do?
|
||||||
|
-- let ciAfter := { ci with mctx := ti.mctxAfter }
|
||||||
|
let ci := if useAfter then
|
||||||
|
{ ci with mctx := tacticInfo.mctxAfter }
|
||||||
|
else
|
||||||
|
{ ci with mctx := tacticInfo.mctxBefore }
|
||||||
-- compute the interactive goals
|
-- compute the interactive goals
|
||||||
let goals ← ci.runMetaM {} do
|
let goalMvars : List MVarId ← ci.runMetaM {} do
|
||||||
return List.toArray <| if useAfter then ti.goalsAfter else ti.goalsBefore
|
return if useAfter then tacticInfo.goalsAfter else tacticInfo.goalsBefore
|
||||||
let goals ← ci.runMetaM {} do
|
|
||||||
goals.mapM fun goal => do
|
let interactiveGoals : List InteractiveGoalWithHints ← ci.runMetaM {} do
|
||||||
let hints ← findHints goal doc rc.initParams
|
goalMvars.mapM fun goal => do
|
||||||
return ← goalToInteractive goal hints
|
let hints ← findHints goal doc.meta rc.initParams
|
||||||
|
let interactiveGoal ← goalToInteractive goal
|
||||||
|
return ⟨interactiveGoal, hints⟩
|
||||||
|
-- TODO: This code is way old, can it be deleted?
|
||||||
-- compute the goal diff
|
-- compute the goal diff
|
||||||
-- let goals ← ciAfter.runMetaM {} (do
|
-- let goals ← ciAfter.runMetaM {} (do
|
||||||
-- try
|
-- try
|
||||||
@@ -162,8 +296,67 @@ def getInteractiveGoals (p : Lsp.PlainGoalParams) : RequestM (RequestTask (Optio
|
|||||||
-- -- fail silently, since this is just a bonus feature
|
-- -- fail silently, since this is just a bonus feature
|
||||||
-- return goals
|
-- return goals
|
||||||
-- )
|
-- )
|
||||||
return {goals}
|
return interactiveGoals
|
||||||
return some <| goals.foldl (· ++ ·) ⟨#[]⟩
|
let goalsAtPos : Array InteractiveGoalWithHints := ⟨goalsAtPos'.foldl (· ++ ·) []⟩
|
||||||
|
|
||||||
|
diagsAtPos ← completionDiagnostics goalsAtPos.size intermediateGoalCount
|
||||||
|
completed completedWithWarnings lspPosAt diagsAtPos
|
||||||
|
|
||||||
|
intermediateGoalCount := goalsAtPos.size
|
||||||
|
|
||||||
|
steps := steps.push ⟨goalsAtPos, source, diagsAtPos, lspPosAt.line, lspPosAt.character⟩
|
||||||
|
else
|
||||||
|
-- No goals present
|
||||||
|
steps := steps.push ⟨#[], source, diagsAtPos, lspPosAt.line, none⟩
|
||||||
|
|
||||||
|
-- Filter out the "unsolved goals" message
|
||||||
|
diag := filterUnsolvedGoal diag
|
||||||
|
|
||||||
|
let lastPos := text.utf8PosToLspPos positionsWithSource.back.1
|
||||||
|
let remainingDiags : Array InteractiveDiagnostic :=
|
||||||
|
diag.filter (fun d => lastPos ≤ d.range.start)
|
||||||
|
|
||||||
|
return some {
|
||||||
|
steps := steps,
|
||||||
|
diagnostics := remainingDiags,
|
||||||
|
completed := completed,
|
||||||
|
completedWithWarnings := completedWithWarnings,
|
||||||
|
lastPos := lastPos.line
|
||||||
|
}
|
||||||
|
|
||||||
|
open RequestM in
|
||||||
|
|
||||||
|
-- The editor apparently uses this
|
||||||
|
def getInteractiveGoals (p : Lsp.PlainGoalParams) : RequestM (RequestTask (Option <| InteractiveGoals)) := do
|
||||||
|
let doc ← readDoc
|
||||||
|
-- let rc ← readThe RequestContext
|
||||||
|
let text := doc.meta.text
|
||||||
|
let hoverPos := text.lspPosToUtf8Pos p.position
|
||||||
|
-- TODO: I couldn't find a good condition to find the correct snap. So we are looking
|
||||||
|
-- for the first snap with goals here:
|
||||||
|
withWaitFindSnap doc (fun s => ¬ (s.infoTree.goalsAt? doc.meta.text hoverPos).isEmpty)
|
||||||
|
(notFoundX := return none) fun snap => do
|
||||||
|
if let rs@(_ :: _) := snap.infoTree.goalsAt? doc.meta.text hoverPos then
|
||||||
|
let goals : List <| Array InteractiveGoal ← rs.mapM fun { ctxInfo := ci, tacticInfo := ti, useAfter := useAfter, .. } => do
|
||||||
|
let ciAfter := { ci with mctx := ti.mctxAfter }
|
||||||
|
let ci := if useAfter then ciAfter else { ci with mctx := ti.mctxBefore }
|
||||||
|
-- compute the interactive goals
|
||||||
|
let goals ← ci.runMetaM {} do
|
||||||
|
return List.toArray <| if useAfter then ti.goalsAfter else ti.goalsBefore
|
||||||
|
let goals ← ci.runMetaM {} do
|
||||||
|
goals.mapM fun goal => do
|
||||||
|
-- let hints ← findHints goal doc.meta rc.initParams
|
||||||
|
return ← goalToInteractive goal
|
||||||
|
-- compute the goal diff
|
||||||
|
-- let goals ← ciAfter.runMetaM {} (do
|
||||||
|
-- try
|
||||||
|
-- Widget.diffInteractiveGoals useAfter ti goals
|
||||||
|
-- catch _ =>
|
||||||
|
-- -- fail silently, since this is just a bonus feature
|
||||||
|
-- return goals
|
||||||
|
-- )
|
||||||
|
return goals
|
||||||
|
return some <| ⟨goals.foldl (· ++ ·) #[]⟩
|
||||||
else
|
else
|
||||||
return none
|
return none
|
||||||
|
|
||||||
@@ -171,7 +364,16 @@ builtin_initialize
|
|||||||
registerBuiltinRpcProcedure
|
registerBuiltinRpcProcedure
|
||||||
`Game.getInteractiveGoals
|
`Game.getInteractiveGoals
|
||||||
Lsp.PlainGoalParams
|
Lsp.PlainGoalParams
|
||||||
(Option InteractiveGoals)
|
(Option <| InteractiveGoals
|
||||||
|
)
|
||||||
getInteractiveGoals
|
getInteractiveGoals
|
||||||
|
|
||||||
|
builtin_initialize
|
||||||
|
registerBuiltinRpcProcedure
|
||||||
|
`Game.getProofState
|
||||||
|
Lsp.PlainGoalParams
|
||||||
|
(Option ProofState)
|
||||||
|
getProofState
|
||||||
|
|
||||||
|
|
||||||
end GameServer
|
end GameServer
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import GameServer.EnvExtensions
|
||||||
|
import I18n
|
||||||
|
|
||||||
|
open Lean Meta Elab Command
|
||||||
|
|
||||||
|
/-! ## Copy images -/
|
||||||
|
|
||||||
|
open IO.FS System FilePath in
|
||||||
|
/-- Copies the folder `images/` to `.lake/gamedata/images/` -/
|
||||||
|
def copyImages : IO Unit := do
|
||||||
|
let target : FilePath := ".lake" / "gamedata"
|
||||||
|
if ← FilePath.pathExists "images" then
|
||||||
|
for file in ← walkDir "images" do
|
||||||
|
let outFile := target.join file
|
||||||
|
-- create the directories
|
||||||
|
if ← file.isDir then
|
||||||
|
createDirAll outFile
|
||||||
|
else
|
||||||
|
if let some parent := outFile.parent then
|
||||||
|
createDirAll parent
|
||||||
|
-- copy file
|
||||||
|
let content ← readBinFile file
|
||||||
|
writeBinFile outFile content
|
||||||
|
|
||||||
|
namespace GameData
|
||||||
|
def gameDataPath : System.FilePath := ".lake" / "gamedata"
|
||||||
|
def gameFileName := s!"game.json"
|
||||||
|
def docFileName := fun (inventoryType : InventoryType) (name : Name) => s!"doc__{inventoryType}__{name}.json"
|
||||||
|
def levelFileName := fun (worldId : Name) (levelId : Nat) => s!"level__{worldId}__{levelId}.json"
|
||||||
|
def inventoryFileName := s!"inventory.json"
|
||||||
|
end GameData
|
||||||
|
|
||||||
|
open GameData in
|
||||||
|
-- TODO: register all of this as ToJson instance?
|
||||||
|
def saveGameData (allItemsByType : HashMap InventoryType (HashSet Name))
|
||||||
|
(inventory : InventoryOverview): CommandElabM Unit := do
|
||||||
|
let game ← getCurGame
|
||||||
|
let env ← getEnv
|
||||||
|
let path := (← IO.currentDir) / gameDataPath
|
||||||
|
|
||||||
|
if ← path.isDir then
|
||||||
|
IO.FS.removeDirAll path
|
||||||
|
IO.FS.createDirAll path
|
||||||
|
|
||||||
|
-- copy the images folder
|
||||||
|
copyImages
|
||||||
|
|
||||||
|
for (worldId, world) in game.worlds.nodes.toArray do
|
||||||
|
for (levelId, level) in world.levels.toArray do
|
||||||
|
IO.FS.writeFile (path / levelFileName worldId levelId) (toString (toJson (level.toInfo env)))
|
||||||
|
|
||||||
|
IO.FS.writeFile (path / gameFileName) (toString (getGameJson game))
|
||||||
|
|
||||||
|
for inventoryType in [InventoryType.Lemma, .Tactic, .Definition] do
|
||||||
|
for name in allItemsByType.findD inventoryType {} do
|
||||||
|
let some item ← getInventoryItem? name inventoryType
|
||||||
|
| throwError "Expected item to exist: {name}"
|
||||||
|
IO.FS.writeFile (path / docFileName inventoryType name) (toString (toJson item))
|
||||||
|
|
||||||
|
IO.FS.writeFile (path / inventoryFileName) (toString (toJson inventory))
|
||||||
|
|
||||||
|
-- write file for translation
|
||||||
|
I18n.createTemplate
|
||||||
|
|
||||||
|
open GameData
|
||||||
|
|
||||||
|
def loadData (f : System.FilePath) (α : Type) [FromJson α] : IO α := do
|
||||||
|
let str ← IO.FS.readFile f
|
||||||
|
let json ← match Json.parse str with
|
||||||
|
| .ok v => pure v
|
||||||
|
| .error e => throw (IO.userError e)
|
||||||
|
let data ← match fromJson? json with
|
||||||
|
| .ok v => pure v
|
||||||
|
| .error e => throw (IO.userError e)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def loadGameData (gameDir : System.FilePath) : IO Game :=
|
||||||
|
loadData (gameDir / gameDataPath / gameFileName) Game
|
||||||
|
|
||||||
|
def loadLevelData (gameDir : System.FilePath) (worldId : Name) (levelId : Nat) : IO LevelInfo :=
|
||||||
|
loadData (gameDir / gameDataPath / levelFileName worldId levelId) LevelInfo
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import Lean.Widget.InteractiveGoal
|
||||||
|
import Lean.Widget.InteractiveDiagnostic
|
||||||
|
import Lean.Data.Lsp.Diagnostics
|
||||||
|
|
||||||
|
/-!
|
||||||
|
This file contains the custom data structures use by the server.
|
||||||
|
|
||||||
|
Some of them overwrite built-in structures from Lean.
|
||||||
|
|
||||||
|
In particular, the structures from `Lean.Widget.InteractiveGoal` are duplicated with
|
||||||
|
the following extension:
|
||||||
|
|
||||||
|
* `isAssumption?` in `InteractiveHypothesisBundle`: stores if a hypothesis is of type `Prop`.
|
||||||
|
|
||||||
|
NOTE: Changes here need to be reflected in the corresponding `interface` in `rcp_api.ts`
|
||||||
|
on the client-side.
|
||||||
|
-/
|
||||||
|
|
||||||
|
open Lean Server Widget
|
||||||
|
|
||||||
|
namespace GameServer
|
||||||
|
|
||||||
|
/-- Extend the interactive hypothesis bundle with an option to distinguish
|
||||||
|
"assumptions" from "objects". "Assumptions" are hypotheses of type `Prop`. -/
|
||||||
|
-- @[inherit_doc Lean.Widget.InteractiveHypothesisBundle]
|
||||||
|
structure InteractiveHypothesisBundle extends Lean.Widget.InteractiveHypothesisBundle where
|
||||||
|
/-- The hypothesis's type is of type `Prop` -/
|
||||||
|
isAssumption? : Option Bool := none
|
||||||
|
deriving RpcEncodable
|
||||||
|
|
||||||
|
-- duplicated but with custom `InteractiveHypothesisBundle`
|
||||||
|
@[inherit_doc Lean.Widget.InteractiveGoalCore]
|
||||||
|
structure InteractiveGoalCore where
|
||||||
|
hyps : Array InteractiveHypothesisBundle
|
||||||
|
type : CodeWithInfos
|
||||||
|
ctx : WithRpcRef Elab.ContextInfo
|
||||||
|
|
||||||
|
-- duplicated but with custom `InteractiveGoalCore`
|
||||||
|
@[inherit_doc Lean.Widget.InteractiveGoal]
|
||||||
|
structure InteractiveGoal extends InteractiveGoalCore where
|
||||||
|
userName? : Option String
|
||||||
|
goalPrefix : String
|
||||||
|
mvarId : MVarId
|
||||||
|
isInserted? : Option Bool := none
|
||||||
|
isRemoved? : Option Bool := none
|
||||||
|
deriving RpcEncodable
|
||||||
|
|
||||||
|
-- duplicated with custom `InteractiveGoalCore`
|
||||||
|
@[inherit_doc Lean.Widget.InteractiveTermGoal]
|
||||||
|
structure InteractiveTermGoal extends InteractiveGoalCore where
|
||||||
|
range : Lsp.Range
|
||||||
|
term : WithRpcRef Elab.TermInfo
|
||||||
|
deriving RpcEncodable
|
||||||
|
|
||||||
|
/-- A hint in the game at the corresponding goal. -/
|
||||||
|
structure GameHint where
|
||||||
|
/-- The text with the variable names already inserted.
|
||||||
|
|
||||||
|
Note: This is in theory superfluous and will be completely replaced by `rawText`. We just left
|
||||||
|
it in for debugging for now. -/
|
||||||
|
text : String
|
||||||
|
/-- Flag whether the hint should be hidden initially. -/
|
||||||
|
hidden : Bool
|
||||||
|
/-- The text with the variables not inserted yet. -/
|
||||||
|
rawText : String
|
||||||
|
/-- The assignment of variable names in the `rawText` to the ones the player used. -/
|
||||||
|
varNames : Array <| Name × Name
|
||||||
|
deriving FromJson, ToJson
|
||||||
|
|
||||||
|
/-- Bundled `InteractiveGoal` together with an array of hints that apply at this stage. -/
|
||||||
|
structure InteractiveGoalWithHints where
|
||||||
|
goal : InteractiveGoal
|
||||||
|
/-- Extended the `InteractiveGoal` by an array of hints at that goal. -/
|
||||||
|
hints : Array GameHint
|
||||||
|
deriving RpcEncodable
|
||||||
|
|
||||||
|
structure InteractiveGoalsWithHints where
|
||||||
|
goals : Array InteractiveGoalWithHints
|
||||||
|
/-- The content of the line evaluated. -/
|
||||||
|
command : String
|
||||||
|
diags : Array InteractiveDiagnostic := default
|
||||||
|
line : Option Nat -- only for debugging
|
||||||
|
column : Option Nat -- only for debugging
|
||||||
|
|
||||||
|
deriving RpcEncodable
|
||||||
|
|
||||||
|
instance : Inhabited InteractiveGoalsWithHints := ⟨default, default, default, none, none⟩
|
||||||
|
|
||||||
|
/-- Collected goals throughout the proof. Used for communication with the game client. -/
|
||||||
|
structure ProofState where
|
||||||
|
/-- goals after each line. includes the hints. -/
|
||||||
|
steps : Array <| InteractiveGoalsWithHints
|
||||||
|
/-- diagnostics contains all errors and warnings.
|
||||||
|
|
||||||
|
TODO: I think they contain information about which line they belong to. Verify this.
|
||||||
|
-/
|
||||||
|
diagnostics : Array InteractiveDiagnostic := default
|
||||||
|
/-- Whether the level is considered solved. -/
|
||||||
|
completed : Bool
|
||||||
|
completedWithWarnings : Bool
|
||||||
|
lastPos : Nat -- only for debugging
|
||||||
|
deriving RpcEncodable
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import Lean.Elab.Binders
|
||||||
|
import Lean.Elab.Tactic.Basic
|
||||||
|
import Lean.Meta.Tactic.Intro
|
||||||
|
|
||||||
|
/-!
|
||||||
|
# `let_intros` Tactic
|
||||||
|
|
||||||
|
`let_intros` is a weaker form of `intros` aimed to only introduce `let` statements,
|
||||||
|
but not for example `∀`-binders.
|
||||||
|
|
||||||
|
Note: Mathlib has a tactic `extract_lets` which does essentially exactly this.
|
||||||
|
The only difference is that `let_intros` is unhygenic, in the sense that it will name
|
||||||
|
the introduced variables `f` instead of leaving them inaccessible `f✝`.
|
||||||
|
-/
|
||||||
|
|
||||||
|
namespace GameServer
|
||||||
|
|
||||||
|
open Lean Meta Elab Parser Tactic
|
||||||
|
|
||||||
|
/--
|
||||||
|
Copied from `Lean.Meta.getIntrosSize`.
|
||||||
|
-/
|
||||||
|
private partial def getLetIntrosSize : Expr → Nat
|
||||||
|
-- | .forallE _ _ b _ => getLetIntrosSize b + 1
|
||||||
|
| .letE _ _ _ b _ => getLetIntrosSize b + 1
|
||||||
|
| .mdata _ b => getLetIntrosSize b
|
||||||
|
| e =>
|
||||||
|
if let some (_, _, _, b) := e.letFun? then
|
||||||
|
getLetIntrosSize b + 1
|
||||||
|
else
|
||||||
|
0
|
||||||
|
|
||||||
|
/--
|
||||||
|
Copied and from `Lean.MVarId.intros`.
|
||||||
|
-/
|
||||||
|
def _root_.Lean.MVarId.letIntros (mvarId : MVarId) : MetaM (Array FVarId × MVarId) := do
|
||||||
|
let type ← mvarId.getType
|
||||||
|
let type ← instantiateMVars type
|
||||||
|
let n := getLetIntrosSize type
|
||||||
|
if n == 0 then
|
||||||
|
return (#[], mvarId)
|
||||||
|
else
|
||||||
|
-- `introNP` preserves the binder names
|
||||||
|
mvarId.introNP n
|
||||||
|
|
||||||
|
/--
|
||||||
|
`let_intros` introduces all `let` statements that are preceeding the proof. Concretely
|
||||||
|
it does a subset of what `intros` does.
|
||||||
|
|
||||||
|
If names are provided, it will introduce as many `let` statements as there are names.
|
||||||
|
-/
|
||||||
|
syntax (name := letIntros) "let_intros" : tactic
|
||||||
|
-- (ppSpace colGt (ident <|> hole))*
|
||||||
|
|
||||||
|
@[tactic letIntros] def evalLetIntros : Tactic := fun stx => do
|
||||||
|
match stx with
|
||||||
|
| `(tactic| let_intros) => liftMetaTactic fun mvarId => do
|
||||||
|
let (_, mvarId) ← mvarId.letIntros
|
||||||
|
return [mvarId]
|
||||||
|
-- | `(tactic| let_intros $ids*) => do
|
||||||
|
-- let fvars ← liftMetaTacticAux fun mvarId => do
|
||||||
|
-- let (fvars, mvarId) ← mvarId.introN ids.size (ids.map getNameOfIdent').toList
|
||||||
|
-- return (fvars, [mvarId])
|
||||||
|
-- withMainContext do
|
||||||
|
-- for stx in ids, fvar in fvars do
|
||||||
|
-- Term.addLocalVarInfo stx (mkFVar fvar)
|
||||||
|
| _ => throwUnsupportedSyntax
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
/- This file is mostly copied from `Lean/Server/Watchdog.lean`. -/
|
|
||||||
import Lean.Server.Watchdog
|
|
||||||
import GameServer.Game
|
|
||||||
|
|
||||||
namespace MyServer.Watchdog
|
|
||||||
open Lean
|
|
||||||
open Server
|
|
||||||
open Watchdog
|
|
||||||
open IO
|
|
||||||
open Lsp
|
|
||||||
open JsonRpc
|
|
||||||
open System.Uri
|
|
||||||
|
|
||||||
partial def mainLoop (clientTask : Task ServerEvent) : GameServerM Unit := do
|
|
||||||
let st ← read
|
|
||||||
let workers ← st.fileWorkersRef.get
|
|
||||||
let mut workerTasks := #[]
|
|
||||||
for (_, fw) in workers do
|
|
||||||
if let WorkerState.running := fw.state then
|
|
||||||
workerTasks := workerTasks.push <| fw.commTask.map (ServerEvent.workerEvent fw)
|
|
||||||
|
|
||||||
let ev ← IO.waitAny (clientTask :: workerTasks.toList)
|
|
||||||
|
|
||||||
if ← Game.handleServerEvent ev then -- handle Game requests
|
|
||||||
mainLoop (←runClientTask)
|
|
||||||
else
|
|
||||||
match ev with
|
|
||||||
| ServerEvent.clientMsg msg =>
|
|
||||||
match msg with
|
|
||||||
| Message.request id "shutdown" _ =>
|
|
||||||
shutdown
|
|
||||||
st.hOut.writeLspResponse ⟨id, Json.null⟩
|
|
||||||
| Message.request id method (some params) =>
|
|
||||||
handleRequest id method (toJson params)
|
|
||||||
mainLoop (←runClientTask)
|
|
||||||
| Message.response .. =>
|
|
||||||
-- TODO: handle client responses
|
|
||||||
mainLoop (←runClientTask)
|
|
||||||
| Message.responseError _ _ e .. =>
|
|
||||||
throwServerError s!"Unhandled response error: {e}"
|
|
||||||
| Message.notification method (some params) =>
|
|
||||||
if method == "textDocument/didOpen" then
|
|
||||||
-- for lean4game, we need to pass in extra information when a level is opened:
|
|
||||||
Game.handleDidOpenLevel (← parseParams _ (toJson params))
|
|
||||||
else
|
|
||||||
handleNotification method (toJson params)
|
|
||||||
mainLoop (←runClientTask)
|
|
||||||
| _ => throwServerError "Got invalid JSON-RPC message"
|
|
||||||
| ServerEvent.clientError e => throw e
|
|
||||||
| ServerEvent.workerEvent fw ev =>
|
|
||||||
match ev with
|
|
||||||
| WorkerEvent.ioError e =>
|
|
||||||
throwServerError s!"IO error while processing events for {fw.doc.uri}: {e}"
|
|
||||||
| WorkerEvent.crashed _ =>
|
|
||||||
handleCrash fw.doc.uri #[]
|
|
||||||
mainLoop clientTask
|
|
||||||
| WorkerEvent.terminated =>
|
|
||||||
throwServerError "Internal server error: got termination event for worker that should have been removed"
|
|
||||||
| .importsChanged =>
|
|
||||||
startFileWorker fw.doc
|
|
||||||
mainLoop clientTask
|
|
||||||
|
|
||||||
def initAndRunWatchdogAux : GameServerM Unit := do
|
|
||||||
let st ← read
|
|
||||||
try
|
|
||||||
discard $ st.hIn.readLspNotificationAs "initialized" InitializedParams
|
|
||||||
let clientTask ← runClientTask
|
|
||||||
mainLoop clientTask
|
|
||||||
catch err =>
|
|
||||||
shutdown
|
|
||||||
throw err
|
|
||||||
/- NOTE(WN): It looks like instead of sending the `exit` notification,
|
|
||||||
VSCode just closes the stream. In that case, pretend we got an `exit`. -/
|
|
||||||
let Message.notification "exit" none ←
|
|
||||||
try st.hIn.readLspMessage
|
|
||||||
catch _ => pure (Message.notification "exit" none)
|
|
||||||
| throwServerError "Got `shutdown` request, expected an `exit` notification"
|
|
||||||
|
|
||||||
def createEnv (gameDir : String) (module : String) : IO Environment := do
|
|
||||||
-- Determine search paths of the game project by running `lake env printenv LEAN_PATH`.
|
|
||||||
let out ← IO.Process.output
|
|
||||||
{ cwd := gameDir, cmd := "lake", args := #["env","printenv","LEAN_PATH"] }
|
|
||||||
if out.exitCode != 0 then
|
|
||||||
throwServerError s!"Error while running Lake: {out.stderr}"
|
|
||||||
|
|
||||||
-- Make the paths relative to the current directory
|
|
||||||
let paths : List System.FilePath := System.SearchPath.parse out.stdout.trim
|
|
||||||
let currentDir ← IO.currentDir
|
|
||||||
let paths := paths.map fun p => currentDir / (gameDir : System.FilePath) / p
|
|
||||||
|
|
||||||
-- Set the search path
|
|
||||||
Lean.searchPathRef.set paths
|
|
||||||
|
|
||||||
let env ← importModules #[{ module := `Init : Import }, { module := module : Import }] {} 0
|
|
||||||
return env
|
|
||||||
|
|
||||||
def initAndRunWatchdog (args : List String) (i o e : FS.Stream) : IO Unit := do
|
|
||||||
if args.length < 2 then
|
|
||||||
throwServerError s!"Expected 1-3 command line arguments in addition to `--server`:
|
|
||||||
game directory, the name of the main module (optional), and the name of the game (optional)."
|
|
||||||
let gameDir := args[1]!
|
|
||||||
let module := if args.length < 3 then defaultGameModule else args[2]!
|
|
||||||
let gameName := if args.length < 4 then defaultGameName else args[3]!
|
|
||||||
let workerPath := "./gameserver"
|
|
||||||
-- TODO: Do the following commands slow us down?
|
|
||||||
let srcSearchPath ← initSrcSearchPath (← getBuildDir)
|
|
||||||
let references ← IO.mkRef (← loadReferences)
|
|
||||||
let fileWorkersRef ← IO.mkRef (RBMap.empty : FileWorkerMap)
|
|
||||||
let i ← maybeTee "wdIn.txt" false i
|
|
||||||
let o ← maybeTee "wdOut.txt" true o
|
|
||||||
let e ← maybeTee "wdErr.txt" true e
|
|
||||||
let state := {
|
|
||||||
env := ← createEnv gameDir module,
|
|
||||||
game := gameName,
|
|
||||||
gameDir := gameDir,
|
|
||||||
inventory := #[]
|
|
||||||
difficulty := 0
|
|
||||||
}
|
|
||||||
let initRequest ← i.readLspRequestAs "initialize" InitializeParams
|
|
||||||
-- We misuse the `rootUri` field to the gameName
|
|
||||||
let rootUri? := gameName
|
|
||||||
let initRequest := {initRequest with param := {initRequest.param with rootUri?}}
|
|
||||||
o.writeLspResponse {
|
|
||||||
id := initRequest.id
|
|
||||||
result := {
|
|
||||||
capabilities := mkLeanServerCapabilities
|
|
||||||
serverInfo? := some {
|
|
||||||
name := "Lean 4 Game Server"
|
|
||||||
version? := "0.1.1"
|
|
||||||
}
|
|
||||||
: InitializeResult
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let context : ServerContext := {
|
|
||||||
hIn := i
|
|
||||||
hOut := o
|
|
||||||
hLog := e
|
|
||||||
args := args
|
|
||||||
fileWorkersRef := fileWorkersRef
|
|
||||||
initParams := initRequest.param
|
|
||||||
workerPath
|
|
||||||
srcSearchPath
|
|
||||||
references
|
|
||||||
}
|
|
||||||
discard $ ReaderT.run (StateT.run initAndRunWatchdogAux state) context
|
|
||||||
|
|
||||||
def watchdogMain (args : List String) : IO UInt32 := do
|
|
||||||
let i ← IO.getStdin
|
|
||||||
let o ← IO.getStdout
|
|
||||||
let e ← IO.getStderr
|
|
||||||
try
|
|
||||||
initAndRunWatchdog args i o e
|
|
||||||
return 0
|
|
||||||
catch err =>
|
|
||||||
e.putStrLn s!"Watchdog error: {err}"
|
|
||||||
return 1
|
|
||||||
|
|
||||||
end MyServer.Watchdog
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
import { WebSocketServer } from 'ws';
|
|
||||||
import express from 'express'
|
|
||||||
import path from 'path'
|
|
||||||
import * as cp from 'child_process';
|
|
||||||
import * as url from 'url';
|
|
||||||
import * as rpc from 'vscode-ws-jsonrpc';
|
|
||||||
import * as jsonrpcserver from 'vscode-ws-jsonrpc/server';
|
|
||||||
import os from 'os';
|
|
||||||
import anonymize from 'ip-anonymize';
|
|
||||||
// import { importTrigger, importStatus } from './import.mjs'
|
|
||||||
// import fs from 'fs'
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
const games = {
|
|
||||||
"g/hhu-adam/robo": {
|
|
||||||
dir: "Robo",
|
|
||||||
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 __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
|
||||||
|
|
||||||
const app = express()
|
|
||||||
|
|
||||||
const PORT = process.env.PORT || 8080;
|
|
||||||
|
|
||||||
var router = express.Router();
|
|
||||||
|
|
||||||
// router.get('/import/status/:owner/:repo', importStatus)
|
|
||||||
// router.get('/import/trigger/:owner/:repo', importTrigger)
|
|
||||||
|
|
||||||
const server = app
|
|
||||||
.use(express.static(path.join(__dirname, '../client/dist/')))
|
|
||||||
.use('/', router)
|
|
||||||
.listen(PORT, () => console.log(`Listening on ${PORT}`));
|
|
||||||
|
|
||||||
const wss = new WebSocketServer({ server })
|
|
||||||
|
|
||||||
var socketCounter = 0
|
|
||||||
|
|
||||||
const environment = process.env.NODE_ENV
|
|
||||||
const isDevelopment = environment === 'development'
|
|
||||||
|
|
||||||
/** We keep queues of started Lean Server processes to be ready when a user arrives */
|
|
||||||
const queue = {}
|
|
||||||
|
|
||||||
function tag(owner, repo) {
|
|
||||||
return `g/${owner.toLowerCase()}/${repo.toLowerCase()}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function startServerProcess(owner, repo) {
|
|
||||||
let game_dir = (owner == 'local') ?
|
|
||||||
repo : games[tag(owner, repo)]?.dir
|
|
||||||
|
|
||||||
if (owner == 'local') {
|
|
||||||
if(!isDevelopment) {
|
|
||||||
console.error(`No local games in production mode.`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// TODO: This test does not work
|
|
||||||
// if (!fs.existsSync(path.join("../", game_dir))) {
|
|
||||||
// console.error(`Game folder does not exists: ${game_dir}`)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!game_dir) {
|
|
||||||
console.error(`Unknown game: ${tag(owner, repo)}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let serverProcess
|
|
||||||
if (isDevelopment) {
|
|
||||||
let args = ["--server", path.join("../../../../", game_dir)]
|
|
||||||
serverProcess = cp.spawn("./gameserver", args,
|
|
||||||
{ cwd: path.join(__dirname, "./build/bin/") })
|
|
||||||
} else {
|
|
||||||
serverProcess = cp.spawn("./bubblewrap.sh",
|
|
||||||
[game_dir],
|
|
||||||
{ cwd: __dirname })
|
|
||||||
}
|
|
||||||
serverProcess.on('error', error =>
|
|
||||||
console.error(`Launching Lean Server failed: ${error}`)
|
|
||||||
)
|
|
||||||
if (serverProcess.stderr !== null) {
|
|
||||||
serverProcess.stderr.on('data', data =>
|
|
||||||
console.error(`Lean Server: ${data}`)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return serverProcess
|
|
||||||
}
|
|
||||||
|
|
||||||
/** start Lean Server processes to refill the queue */
|
|
||||||
function fillQueue(owner, repo) {
|
|
||||||
while (queue[tag(owner, repo)].length < games[tag(owner, repo)].queueLength) {
|
|
||||||
const serverProcess = startServerProcess(tag(owner, repo))
|
|
||||||
queue[tag(owner, repo)].push(serverProcess)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (!isDevelopment) { // Don't use queue in development
|
|
||||||
// for (let tag in games) {
|
|
||||||
// queue[tag] = []
|
|
||||||
// fillQueue(tag)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
const urlRegEx = /^\/websocket\/g\/([\w.-]+)\/([\w.-]+)$/
|
|
||||||
|
|
||||||
wss.addListener("connection", function(ws, req) {
|
|
||||||
const reRes = urlRegEx.exec(req.url)
|
|
||||||
if (!reRes) { console.error(`Connection refused because of invalid URL: ${req.url}`); return; }
|
|
||||||
const owner = reRes[1]
|
|
||||||
const repo = reRes[2]
|
|
||||||
// const tag = `g/${owner.toLowerCase()}/${repo.toLowerCase()}`
|
|
||||||
|
|
||||||
// // TODO
|
|
||||||
// if (isDevelopment && process.env.DEV_CONTAINER) {
|
|
||||||
// tag = `g/local/game`
|
|
||||||
// }
|
|
||||||
|
|
||||||
let ps;
|
|
||||||
if (!queue[tag(owner, repo)] || queue[tag(owner, repo)].length == 0) {
|
|
||||||
ps = startServerProcess(owner, repo)
|
|
||||||
} else {
|
|
||||||
ps = queue[tag(owner, repo)].shift() // Pick the first Lean process; it's likely to be ready immediately
|
|
||||||
fillQueue(owner, repo)
|
|
||||||
}
|
|
||||||
|
|
||||||
socketCounter += 1;
|
|
||||||
const ip = anonymize(req.headers['x-forwarded-for'] || req.socket.remoteAddress)
|
|
||||||
console.log(`[${new Date()}] Socket opened - ${ip}`)
|
|
||||||
|
|
||||||
const socket = {
|
|
||||||
onMessage: (cb) => { ws.on("message", cb) },
|
|
||||||
onError: (cb) => { ws.on("error", cb) },
|
|
||||||
onClose: (cb) => { ws.on("close", cb) },
|
|
||||||
send: (data, cb) => { ws.send(data,cb) }
|
|
||||||
}
|
|
||||||
const reader = new rpc.WebSocketMessageReader(socket);
|
|
||||||
const writer = new rpc.WebSocketMessageWriter(socket);
|
|
||||||
const socketConnection = jsonrpcserver.createConnection(reader, writer, () => ws.close())
|
|
||||||
const serverConnection = jsonrpcserver.createProcessStreamConnection(ps);
|
|
||||||
socketConnection.forward(serverConnection, message => {
|
|
||||||
if (isDevelopment) {console.log(`CLIENT: ${JSON.stringify(message)}`)}
|
|
||||||
return message;
|
|
||||||
});
|
|
||||||
serverConnection.forward(socketConnection, message => {
|
|
||||||
if (isDevelopment) {console.log(`SERVER: ${JSON.stringify(message)}`)}
|
|
||||||
return message;
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`[${new Date()}] Number of open sockets - ${socketCounter}`)
|
|
||||||
console.log(`[${new Date()}] Free RAM - ${Math.round(os.freemem() / 1024 / 1024)} / ${Math.round(os.totalmem() / 1024 / 1024)} MB`)
|
|
||||||
|
|
||||||
ws.on('close', () => {
|
|
||||||
console.log(`[${new Date()}] Socket closed - ${ip}`)
|
|
||||||
socketCounter -= 1;
|
|
||||||
})
|
|
||||||
|
|
||||||
socketConnection.onClose(() => serverConnection.dispose());
|
|
||||||
serverConnection.onClose(() => socketConnection.dispose());
|
|
||||||
})
|
|
||||||
@@ -1,4 +1,41 @@
|
|||||||
{"version": 6,
|
{"version": 7,
|
||||||
"packagesDir": "lake-packages",
|
"packagesDir": ".lake/packages",
|
||||||
"packages": [],
|
"packages":
|
||||||
"name": "GameServer"}
|
[{"url": "https://github.com/leanprover/std4.git",
|
||||||
|
"type": "git",
|
||||||
|
"subDir": null,
|
||||||
|
"rev": "32983874c1b897d78f20d620fe92fc8fd3f06c3a",
|
||||||
|
"name": "std",
|
||||||
|
"manifestFile": "lake-manifest.json",
|
||||||
|
"inputRev": "v4.7.0",
|
||||||
|
"inherited": false,
|
||||||
|
"configFile": "lakefile.lean"},
|
||||||
|
{"url": "https://github.com/mhuisi/lean4-cli",
|
||||||
|
"type": "git",
|
||||||
|
"subDir": null,
|
||||||
|
"rev": "39229f3630d734af7d9cfb5937ddc6b41d3aa6aa",
|
||||||
|
"name": "Cli",
|
||||||
|
"manifestFile": "lake-manifest.json",
|
||||||
|
"inputRev": "nightly",
|
||||||
|
"inherited": true,
|
||||||
|
"configFile": "lakefile.lean"},
|
||||||
|
{"url": "https://github.com/hhu-adam/lean-i18n.git",
|
||||||
|
"type": "git",
|
||||||
|
"subDir": null,
|
||||||
|
"rev": "7550f08140c59c9a604bbcc23ab7830c103a3e39",
|
||||||
|
"name": "i18n",
|
||||||
|
"manifestFile": "lake-manifest.json",
|
||||||
|
"inputRev": "v4.7.0",
|
||||||
|
"inherited": false,
|
||||||
|
"configFile": "lakefile.lean"},
|
||||||
|
{"url": "https://github.com/leanprover-community/import-graph",
|
||||||
|
"type": "git",
|
||||||
|
"subDir": null,
|
||||||
|
"rev": "ac07367cbdd57440e6fe78e5be13b41f9cb0f896",
|
||||||
|
"name": "importGraph",
|
||||||
|
"manifestFile": "lake-manifest.json",
|
||||||
|
"inputRev": "v4.7.0",
|
||||||
|
"inherited": false,
|
||||||
|
"configFile": "lakefile.lean"}],
|
||||||
|
"name": "GameServer",
|
||||||
|
"lakeDir": ".lake"}
|
||||||
|
|||||||
+20
-1
@@ -3,10 +3,29 @@ open Lake DSL
|
|||||||
|
|
||||||
package GameServer
|
package GameServer
|
||||||
|
|
||||||
|
-- Using this assumes that each dependency has a tag of the form `v4.X.0`.
|
||||||
|
def leanVersion : String := s!"v{Lean.versionString}"
|
||||||
|
|
||||||
|
require std from git "https://github.com/leanprover/std4.git" @ leanVersion
|
||||||
|
require i18n from git "https://github.com/hhu-adam/lean-i18n.git" @ leanVersion
|
||||||
|
|
||||||
|
require importGraph from git "https://github.com/leanprover-community/import-graph" @ leanVersion
|
||||||
|
|
||||||
|
|
||||||
lean_lib GameServer
|
lean_lib GameServer
|
||||||
|
|
||||||
@[default_target]
|
@[default_target]
|
||||||
lean_exe gameserver {
|
lean_exe gameserver {
|
||||||
root := `Main
|
root := `GameServer
|
||||||
supportInterpreter := true
|
supportInterpreter := true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/--
|
||||||
|
When a package depending on GameServer updates its dependencies,
|
||||||
|
build the `gameserver` executable.
|
||||||
|
-/
|
||||||
|
post_update pkg do
|
||||||
|
let rootPkg ← getRootPackage
|
||||||
|
if rootPkg.name = pkg.name then
|
||||||
|
return -- do not run in GameServer itself
|
||||||
|
discard <| runBuild gameserver.build >>= (·.await)
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
leanprover/lean4:v4.2.0
|
leanprover/lean4:v4.7.0
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import GameServer.Tactic.LetIntros
|
||||||
|
|
||||||
|
set_option linter.unusedVariables false in
|
||||||
|
|
||||||
|
example (f : Nat) :
|
||||||
|
let f := fun x ↦ x + 1
|
||||||
|
let g : Nat → Nat := fun y ↦ y
|
||||||
|
∀ x : Nat, x ≤ f x := by
|
||||||
|
let_intros
|
||||||
|
/-
|
||||||
|
f✝ : Nat
|
||||||
|
f : Nat → Nat := fun x => x + 1
|
||||||
|
g : Nat → Nat := fun y => y
|
||||||
|
⊢ ∀ (x : Nat), x ≤ f x
|
||||||
|
-/
|
||||||
|
intro x
|
||||||
|
exact Nat.le_succ x
|
||||||
@@ -73,10 +73,13 @@ theorem xy (n : Nat) : n + 0 = n := by
|
|||||||
|
|
||||||
/-- Doc comment -/
|
/-- Doc comment -/
|
||||||
@[simp]
|
@[simp]
|
||||||
Statement My.add_comm (n m : Nat) : n + m = m + n := by
|
Statement My.add_assoc (n m x : Nat) : (m + n) + x = m + (n + x) := by
|
||||||
rw [Nat.add_comm]
|
rw [Nat.add_assoc]
|
||||||
|
|
||||||
example (n m : Nat) : n + m = m + n := by
|
example (n m : Nat) : (m + n) + x = m + (n + x) := by
|
||||||
simp
|
simp
|
||||||
|
|
||||||
#check My.add_comm
|
#check My.add_assoc
|
||||||
|
|
||||||
|
Statement My.add_comm (preamble := simp [add_comm m n]) (n m : Nat) : n + (m + 0) = m + n := by
|
||||||
|
rw [Nat.add_comm]
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
#/bin/bash
|
|
||||||
|
|
||||||
ARTIFACT_ID=$1
|
|
||||||
|
|
||||||
echo "Unpacking ZIP."
|
|
||||||
unzip -o tmp/artifact_${ARTIFACT_ID}.zip -d tmp/artifact_${ARTIFACT_ID}
|
|
||||||
echo "Unpacking TAR."
|
|
||||||
for f in tmp/artifact_${ARTIFACT_ID}/* #Should only be one file
|
|
||||||
do
|
|
||||||
echo "Unpacking $f"
|
|
||||||
mkdir tmp/artifact_${ARTIFACT_ID}_inner
|
|
||||||
tar -xvf $f -C tmp/artifact_${ARTIFACT_ID}_inner
|
|
||||||
done
|
|
||||||
+5
-1
@@ -11,6 +11,10 @@
|
|||||||
"downlevelIteration": true,
|
"downlevelIteration": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"lib": [
|
||||||
|
"ES2021.String",
|
||||||
|
"DOM"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"exclude": ["server"]
|
"exclude": ["server", "relay"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ 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({
|
||||||
@@ -22,6 +28,9 @@ export default defineConfig({
|
|||||||
})
|
})
|
||||||
],
|
],
|
||||||
publicDir: "client/public",
|
publicDir: "client/public",
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: ['games']
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
proxy: {
|
proxy: {
|
||||||
@@ -29,6 +38,15 @@ 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',
|
||||||
|
},
|
||||||
|
'/i18n': {
|
||||||
|
target: 'http://localhost:8080',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
const path = require("path");
|
|
||||||
const webpack = require('webpack');
|
|
||||||
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
|
|
||||||
const WebpackShellPluginNext = require('webpack-shell-plugin-next');
|
|
||||||
|
|
||||||
module.exports = env => {
|
|
||||||
|
|
||||||
const single_game = process.env.LEAN4GAME_SINGLE_GAME
|
|
||||||
|
|
||||||
|
|
||||||
const environment = process.env.NODE_ENV
|
|
||||||
const isDevelopment = environment === 'development'
|
|
||||||
|
|
||||||
const babelOptions = {
|
|
||||||
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'],
|
|
||||||
plugins: [
|
|
||||||
isDevelopment && require.resolve('react-refresh/babel'),
|
|
||||||
].filter(Boolean),
|
|
||||||
};
|
|
||||||
|
|
||||||
global.$RefreshReg$ = () => {};
|
|
||||||
global.$RefreshSig$ = () => () => {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
entry: [single_game ? "./client/src/index_local.tsx" : "./client/src/index.tsx"],
|
|
||||||
mode: isDevelopment ? 'development' : 'production',
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.(js|jsx)$/,
|
|
||||||
exclude: [/server/, /node_modules/],
|
|
||||||
use: [{
|
|
||||||
loader: require.resolve('babel-loader'),
|
|
||||||
options: babelOptions,
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.tsx?$/,
|
|
||||||
use: [{
|
|
||||||
loader: 'ts-loader',
|
|
||||||
options: { allowTsInNodeModules: true }
|
|
||||||
}],
|
|
||||||
// exclude: /node_modules(?!\/(lean4web|lean4|lean4-infoview))/,
|
|
||||||
// Allow .ts imports from node_modules/lean4web and node_modules/lean4
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.css$/,
|
|
||||||
use: ["style-loader", "css-loader"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(jpg|png)$/,
|
|
||||||
use: {
|
|
||||||
loader: 'file-loader',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: ["*", ".js", ".jsx", ".tsx", ".ts"],
|
|
||||||
fallback: {
|
|
||||||
"http": require.resolve("stream-http") ,
|
|
||||||
"path": require.resolve("path-browserify")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
path: path.resolve(__dirname, "client/dist/"),
|
|
||||||
filename: "bundle.js",
|
|
||||||
},
|
|
||||||
devServer: {
|
|
||||||
proxy: {
|
|
||||||
'/websocket': {
|
|
||||||
target: 'ws://localhost:8080',
|
|
||||||
ws: true
|
|
||||||
},
|
|
||||||
'/import': {
|
|
||||||
target: 'http://localhost:3000',
|
|
||||||
router: () => 'http://localhost:8080',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
static: path.join(__dirname, 'client/public/'),
|
|
||||||
port: 3000,
|
|
||||||
hot: true,
|
|
||||||
},
|
|
||||||
devtool: "source-map",
|
|
||||||
plugins: [
|
|
||||||
!isDevelopment && new WebpackShellPluginNext({
|
|
||||||
onBuildEnd:{
|
|
||||||
scripts: [
|
|
||||||
// It's hard to set up webpack to copy the index.html correctly,
|
|
||||||
// so we copy it explicitly after every build:
|
|
||||||
'cp client/public/index.html client/dist/',
|
|
||||||
// Similarly, I haven't been able to load `onigasm.wasm` properly:
|
|
||||||
'cp client/public/onigasm.wasm client/dist/',],
|
|
||||||
blocking: false,
|
|
||||||
parallel: true
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
isDevelopment && new ReactRefreshWebpackPlugin(),
|
|
||||||
].filter(Boolean),
|
|
||||||
|
|
||||||
// Webpack is not happy about the dynamically loaded widget code in the function
|
|
||||||
// `dynamicallyLoadComponent` in `infoview/userWidget.tsx`. If we want to support
|
|
||||||
// dynamically loaded widget code, we need to make sure that the files are available.
|
|
||||||
ignoreWarnings: [/Critical dependency: the request of a dependency is an expression/]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user