Compare commits
210
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92665b7521 | ||
|
|
1845644e86 | ||
|
|
341a9ae14b | ||
|
|
0895821baa | ||
|
|
5b0715629a | ||
|
|
39e08194f6 | ||
|
|
451dc262b2 | ||
|
|
52898d93e7 | ||
|
|
5f30572741 | ||
|
|
76f2414bd5 | ||
|
|
f1f9325c54 | ||
|
|
bd3375ada7 | ||
|
|
64f34acd7a | ||
|
|
ae38ad977a | ||
|
|
a191c47b8e | ||
|
|
0e58e81875 | ||
|
|
f0aa6b58ed | ||
|
|
045b1ea3fb | ||
|
|
dc6f7b2822 | ||
|
|
db5ac7ed15 | ||
|
|
6a0e739301 | ||
|
|
ed96cf9534 | ||
|
|
64670d1579 | ||
|
|
25141b9613 | ||
|
|
ab9d0d0679 | ||
|
|
6dca770dbf | ||
|
|
2022fa9a44 | ||
|
|
b77afebe0a | ||
|
|
4ac38ef7dd | ||
|
|
6a8abf41bd | ||
|
|
1466a41169 | ||
|
|
3cdb9a026b | ||
|
|
ebd7268421 | ||
|
|
af15982804 | ||
|
|
5a404a9a58 | ||
|
|
cebea6a6aa | ||
|
|
36499c0257 | ||
|
|
8c5e47dd7b | ||
|
|
a46840d327 | ||
|
|
f518efc81c | ||
|
|
9b93e3817d | ||
|
|
54c1e0dcaf | ||
|
|
2c1e69611b | ||
|
|
2c22c445b1 | ||
|
|
b6b31a06ac | ||
|
|
255839fea7 | ||
|
|
b961030db2 | ||
|
|
c20d807d5c | ||
|
|
23c8099401 | ||
|
|
6bd9e95db9 | ||
|
|
1febc51791 | ||
|
|
d53b57a764 | ||
|
|
020b4f7803 | ||
|
|
0ae099414c | ||
|
|
b091ec579b | ||
|
|
2a14f48f45 | ||
|
|
4ed0753bb0 | ||
|
|
6b5fc80896 | ||
|
|
e56c7a0670 | ||
|
|
5b710da197 | ||
|
|
b275bbb94f | ||
|
|
29eb90e6c8 | ||
|
|
4e9ac54cde | ||
|
|
18f21fa324 | ||
|
|
d034148bec | ||
|
|
feaf6dae0c | ||
|
|
6059ff1da7 | ||
|
|
6f95f41d14 | ||
|
|
335209b094 | ||
|
|
de1ff4fadc | ||
|
|
db66cb1fcc | ||
|
|
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 | ||
|
|
c4f4dbdc6b | ||
|
|
5f52e23f29 | ||
|
|
b067dea6e7 | ||
|
|
4edf67f0d6 | ||
|
|
004f81835f |
@@ -1,6 +1,8 @@
|
||||
name: Build
|
||||
run-name: Build the project
|
||||
on: [push]
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -3,3 +3,6 @@ client/dist
|
||||
games/
|
||||
server/.lake
|
||||
**/.DS_Store
|
||||
logs/
|
||||
relay/prev_cpu_metric
|
||||
test.ecosystem.config.cjs
|
||||
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
GameSkeleton
|
||||
HiddenTactic
|
||||
subgoals
|
||||
KaTex
|
||||
gameserver
|
||||
lakefile
|
||||
Zulip
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
@@ -8,7 +8,8 @@ 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 7: [How to Update an existing Game](doc/update_game.md)
|
||||
* Step 8: [How to Publishing a Game](doc/publish_game.md)
|
||||
* Step 9: [How to Publishing a Game](doc/publish_game.md)
|
||||
* [Troubleshooting](doc/troubleshoot.md)
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -28,19 +29,33 @@ should be up-to-date:
|
||||
|
||||
### Backend
|
||||
|
||||
not written yet
|
||||
not fully written yet.
|
||||
|
||||
* [Server](doc/DOCUMENTATION.md): describes the server part (i.e. the content of `server/` und `relay/`).
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions to `lean4game` are always welcome!
|
||||
|
||||
### Translation
|
||||
|
||||
The interface can be translated to various languages. For adding a translation, one needs to do the following:
|
||||
|
||||
1. In `client/src/config.json`, add your new language. The "iso" key is the ISO language code, i.e. it should be accepted by "i18next" and "GNU gettext"; the "flag" key is once accepted by [react-country-flag](https://www.npmjs.com/package/react-country-flag).
|
||||
2. Run `npm run translate`. This should create a new file `client/public/locales/{language}/translation.json`. (alternatively you can copy-paste `client/public/locales/en/translation.json`)
|
||||
3. Add all translations.
|
||||
4. Commit the changes you made to `config.json` together with the new `translation.json`.
|
||||
|
||||
For translating games, see [Translating a game](doc/translate.md).
|
||||
|
||||
## Security
|
||||
|
||||
Providing the use access to a Lean instance running on the server is a severe security risk. That is why we start the Lean server with bubblewrap.
|
||||
|
||||
## 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 primarily 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/)
|
||||
by Kevin Buzzard and Mohammad Pedramfar.
|
||||
The project is based on Patrick Massot's prototype: [NNG4](https://github.com/PatrickMassot/NNG4).
|
||||
by Kevin Buzzard and Mohammad Pedramfar, and 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,99 @@
|
||||
{
|
||||
"Tactics": "Taktiken",
|
||||
"Lean Game Server": "Lean-Lern-Spiel-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>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": "Level",
|
||||
"tactics": "Taktiken",
|
||||
"regular": "regulär",
|
||||
"relaxed": "relaxed",
|
||||
"none": "keine",
|
||||
"Rules": "Regeln",
|
||||
"Intro": "Prolog",
|
||||
"Game Introduction": "Prolog",
|
||||
"World selection": "Übersicht",
|
||||
"Start": "Start",
|
||||
"Inventory": "Inventar",
|
||||
"next level": "nächstes Level",
|
||||
"Next": "Weiter",
|
||||
"back to world selection": "Zurück zur Übersicht",
|
||||
"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 aus JSON laden",
|
||||
"Upload": "Laden",
|
||||
"Home": "Home",
|
||||
"back to games selection": "Zurück zur Spielauswahl",
|
||||
"close inventory": "Inventar schließen",
|
||||
"show inventory": "Inventar öffnen",
|
||||
"World": "Welt",
|
||||
"Show more help!": "Mehr Hilfe",
|
||||
"Goal": "Beweisziel",
|
||||
"Current Goal": "Aktuelles Beweisziel",
|
||||
"Objects": "Objekte",
|
||||
"Assumptions": "Annahmen",
|
||||
"Further Goals": "Weitere Ziele",
|
||||
"No Goals": "Keine Beweisziele",
|
||||
"Loading goal…": "Beweisziel 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 den Lean-Server …",
|
||||
"Level completed! 🎉": "Level gelöst! 🎉",
|
||||
"Level completed with warnings 🎭": "Level mit Warnungen abgeschlossen 🎭",
|
||||
"Active Goal": "Aktuelles Ziel",
|
||||
"Crashed! Go to editor mode and fix your proof! Last server response:": "Abgestürzt! Wechsle in den Editor-Modus, um deinen Beweis zu repariaeren. Letzte Meldung vom Server:",
|
||||
"Line": "Zeile",
|
||||
"Character": "Charakter",
|
||||
"Loading messages…": "Lade Meldungen …",
|
||||
"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 aus einem lokalen Ordner zu laden.",
|
||||
"Prerequisites": "Voraussetzungen",
|
||||
"Worlds": "Welten",
|
||||
"Levels": "Level",
|
||||
"Language": "Sprache",
|
||||
"Development notes": "Entwicklungsstand",
|
||||
"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 einem Spielstand, um diesen zu laden.</p><1><0>Achtung:</0> Deraktuelle Spielstand wird dabei überschrieben! Wenn du noch einmal zum aktuellen Spielstand zurückkehren möchtest, solltest du zunächst den <2>aktuellen Spielstand herunterladen</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>": "<0>Für alle, die selbst Spiel entwickeln möchten, gibt es ein <1>GameSkeleton Github Repo</1> als Vorlage und die Anleitung <3>How to Create a Game</3>.</0><1>Die <1>Anleitung</1> erklärt auch, wie ein solches Spiel mittels einer passenden URL auf den Sever geladen und gespiel werden kann. Fragen dazu beantworten wir gern.</1><p>Als Kacheln sichtbar ist auf dieser Seite nur eine kuratierte Auswahl an existierenden Spielen. Wir erweitern diese Auswahl auf Anfrage sehr gerne.</p>",
|
||||
"Level": "Level",
|
||||
"Introduction": "Prolog",
|
||||
"Retry proof from here": "Ab hier neu ansetzen",
|
||||
"Retry": "Noch einmal",
|
||||
"Failed command": "Gescheiterter Befehl",
|
||||
"<p>As this server runs lean on our university machines, it has a limited capacity. Our current estimate is about 70 simultaneous games.</p>": "Diese Server läuft auf universitäter Infrastruktur mit begrenzten Kapazitäten. Wir schätzen, dass die Belastungsgrenze bei rund 70 gleichzeitig laufenden Spielen besteht.",
|
||||
"<0>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!</0>": "Der Spieleserver und die alle Spiele befinden sich in fortlaufender Entwicklung. Wir bitten darum, Fehler und Ungereimtheiten als <1>GitHub Issue</1> zu melden.",
|
||||
"This server has been developed as part of the project <1>ADAM: Anticipating the Digital Age of Mathematics</1> at Heinrich Heine University Düsseldorf.": "Die Lean-Spiele-Software und dieser Spiele-Server werden als Teils der Projekts <1>ADAM: Anticipating the Digital Age of Mathematics</1> an der Heinrich-Heine-Universität Düsseldorf entwickelt.",
|
||||
"Server capacity": "Server-Auslastung",
|
||||
"RAM": "RAM",
|
||||
" used": "",
|
||||
"CPU": "CPU"
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"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.",
|
||||
"Prerequisites": "Prerequisites",
|
||||
"Worlds": "Worlds",
|
||||
"Levels": "Levels",
|
||||
"Language": "Language",
|
||||
"Server capacity": "Server capacity",
|
||||
"RAM": "RAM",
|
||||
"CPU": "CPU",
|
||||
"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",
|
||||
"<p>As this server runs lean on our university machines, it has a limited capacity. Our current estimate is about 70 simultaneous games.</p>": "<p>As this server runs lean on our university machines, it has a limited capacity. Our current estimate is about 70 simultaneous games.</p>",
|
||||
"<0>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!</0>": "<0>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!</0>",
|
||||
"This server has been developed as part of the project <1>ADAM: Anticipating the Digital Age of Mathematics</1> at Heinrich Heine University Düsseldorf.": "This server has been developed as part of the project <1>ADAM: Anticipating the Digital Age of Mathematics</1> at Heinrich Heine University Düsseldorf.",
|
||||
" used": " used"
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"Intro": "Introducción",
|
||||
"Game Introduction": "Introducción del juego",
|
||||
"World selection": "Seleccionar mundo",
|
||||
"Start": "Empezar",
|
||||
"Inventory": "Inventario",
|
||||
"next level": "siguiente nivel",
|
||||
"Next": "Siguiente",
|
||||
"back to world selection": "volver a la selección de mundos",
|
||||
"Leave World": "Abandonar mundo",
|
||||
"previous level": "nivel anterior",
|
||||
"Previous": "Anterior",
|
||||
"Editor mode is enforced!": "¡El modo editor es obligatorio!",
|
||||
"Editor mode": "Modo editor",
|
||||
"Typewriter mode": "Modo línea a línea",
|
||||
"information, Impressum, privacy policy": "información, Impressum, política de privacidad",
|
||||
"Preferences": "Preferencias",
|
||||
"Game Info & Credits": "Información del juego y reconocimientos",
|
||||
"Game Info": "Información del juego",
|
||||
"Clear Progress": "Limpiar el progreso",
|
||||
"Erase": "Borrar",
|
||||
"Download Progress": "Descargar progreso",
|
||||
"Download": "Descargar",
|
||||
"Load Progress from JSON": "Cargar progreso desde JSON",
|
||||
"Upload": "Subir",
|
||||
"Home": "Inicio",
|
||||
"back to games selection": "volver a la selección de juegos",
|
||||
"close inventory": "cerrar inventario",
|
||||
"show inventory": "mostrar inventario",
|
||||
"World": "Mundo",
|
||||
"Show more help!": "¡Mostrar más ayuda!",
|
||||
"Goal": "Objetivo",
|
||||
"Objects": "Objetos",
|
||||
"Assumptions": "Hipótesis",
|
||||
"Current Goal": "Objetivo actual",
|
||||
"Further Goals": "Objetivos pendientes",
|
||||
"No Goals": "Sin objetivos",
|
||||
"Loading goal…": "Cargando objetivo…",
|
||||
"Click somewhere in the Lean file to enable the infoview.": "Pulsa en algún lugar del archivo Lean para habilitar la vista de información.",
|
||||
"Waiting for Lean server to start…": "Esperando a que el servidor Lean se inicie…",
|
||||
"Level completed! 🎉": "Nivel completado 🎉",
|
||||
"Level completed with warnings 🎭": "Nivel completado con advertencias 🎭",
|
||||
"Failed command": "Comando fallido",
|
||||
"Retry proof from here": "Reintentar la prueba desde aquí",
|
||||
"Retry": "Reintentar",
|
||||
"Active Goal": "Objetivo activo",
|
||||
"Crashed! Go to editor mode and fix your proof! Last server response:": "¡Error! Vaya al modo editor y corrija su prueba. Última respuesta del servidor:",
|
||||
"Line": "Línea",
|
||||
"Character": "Carácter",
|
||||
"Loading messages…": "Cargando mensajes…",
|
||||
"Execute": "Ejecutar",
|
||||
"Tactics": "Tácticas",
|
||||
"Definitions": "Definiciones",
|
||||
"Theorems": "Teoremas",
|
||||
"Not unlocked yet": "No desbloqueado aún",
|
||||
"Not available in this level": "No disponible en este nivel",
|
||||
"A repository of learning games for the proof assistant <1>Lean</1> <i>(Lean 4)</i> and its mathematical library <5>mathlib</5>": "Un repositorio de juegos para aprender el asistente de demostración <1>Lean</1>, <i>(Lean 4)</i> y su biblioteca matemática <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 se ha cargado ningún juego. Use <1>http://localhost:3000/#/g/local/FOLDER</1> para abrir un juego directamente desde una carpeta local",
|
||||
"<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>Si está considerando escribir su propio juego, use el <1>GameSkeleton Github Repo</1> como plantilla y lea <3>How to Create a Game</3>.</0><1>Puede cargar directamente los juegos en el servidor y jugarlo usando la URL adecuada. Las <1>instrucciones anteriores</1> también explican los detalles sobre cómo cargar su juego en el servidor. Le animamos a ponerse en contacto con nosotros si tiene preguntas.</1><p>Los juegos incluidos en esta página son añadidos manualmente. Por favor, contactenos y añadiremos el suyo encantados.</p>",
|
||||
"Prerequisites": "Requisitos previos",
|
||||
"Worlds": "Mundos",
|
||||
"Levels": "Niveles",
|
||||
"Language": "Idioma",
|
||||
"Lean Game Server": "Servidor de Juegos de Lean",
|
||||
"Development notes": "Notas de desarrollo",
|
||||
"Adding new games": "Añadir nuevos juegos",
|
||||
"Funding": "Financiación",
|
||||
"Level": "Nivel",
|
||||
"Introduction": "Introducción",
|
||||
"<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>¿Desea eliminar su progreso guardado definitivamente?</p><p>(Esto elimina sus pruebas y su inventario recopilado. Los progresos guardados de otros juegos no se eliminan.)</p>",
|
||||
"Delete Progress?": "¿Borrar Progreso?",
|
||||
"Delete": "Borrar",
|
||||
"Download & Delete": "Descargar y Borrar",
|
||||
"Cancel": "Cancelar",
|
||||
"Mobile": "Móvil",
|
||||
"Auto": "Automático",
|
||||
"Desktop": "Escritorio",
|
||||
"Layout": "Diseño",
|
||||
"Always visible": "Siempre visible",
|
||||
"Save my settings (in the browser store)": "Guardar mis ajustes (en el almacenamiento del navegador)",
|
||||
"<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>Las reglas del juego determinan si se permite saltarse niveles y si el juego realiza comprobaciones para permitir únicamente tácticas y teoremas desbloqueados en las pruebas.</p><1>Nota: las tácticas (o teoremas) \"Desbloqueadas\" está determinadas por dos cosas: el conjunto mínimo de tácticas necesarias para resolver un nivel, más cualquier táctica que hayas desbloqueado en otro nivel. Esto significa que si desbloqueas <1>simp</1> en un nivel, puedes usarlo a partir de entonces en cualquier nivel.</1><p>Las opciones son:</p>",
|
||||
"Game Rules": "Reglas del juego",
|
||||
"levels": "niveles",
|
||||
"tactics": "tácticas",
|
||||
"regular": "normal",
|
||||
"relaxed": "relajado",
|
||||
"none": "ninguno",
|
||||
"<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>Seleccione un archivo JSON con el progreso del juego guardado para cargar su progreso.</p><1><0>Advertencia:</0> Esto borrará su progreso actual en el juego. Considere <2>descargar su progreso actual</2> antes</1>",
|
||||
"Upload Saved Progress": "Subir progreso guardado",
|
||||
"Load selected file": "Cargar archivo seleccionado",
|
||||
"Rules": "Reglas",
|
||||
"<p>As this server runs lean on our university machines, it has a limited capacity. Our current estimate is about 70 simultaneous games.</p>": "<p>Como este servidor corre en máquinas de nuestra universidad, tiene una capacidad limitada. Nuestra estimación actual es de unos 70 juegos simultaneos.</p>.",
|
||||
"<0>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!</0>": "<1>Muchos aspectos de los juegos y la infrastructura están aún en desarrollo. No dude en abrir una <1>GitHub Issue</1> sobre cualquier problema que experimente.</1>",
|
||||
"This server has been developed as part of the project <1>ADAM: Anticipating the Digital Age of Mathematics</1> at Heinrich Heine University Düsseldorf.": "Este servidor se ha desarrollado como parte del proyecto <1>ADAM: Anticipating the Digital Age of Mathematics</1> en la Heinrich-Heine-Universität de Düsseldorf.",
|
||||
"Server capacity": "",
|
||||
"RAM": "",
|
||||
" used": "",
|
||||
"CPU": ""
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/ko-level1.tmx
|
||||
/ko-level2.tmx
|
||||
/ko-omegat.tmx
|
||||
/**/*.bak
|
||||
/omegat/last_entry.properties
|
||||
/omegat/files_order.txt
|
||||
/omegat/project_stats.txt
|
||||
/tm/*.tmx
|
||||
@@ -0,0 +1,15 @@
|
||||
# 린 4 게임
|
||||
|
||||
[English (영어)](./README.md) | 한국어
|
||||
|
||||
## 기여하기
|
||||
|
||||
`lean4game`의 한국어 번역에 기여하는 활동은 언제든지 환영합니다!
|
||||
|
||||
### 한국어 번역
|
||||
|
||||
저([차불휘][bc])는 [오메가T(OmegaT)][omt]를 이용해 영어 문서를 한국어로 번역합니다. 오메가T 프로젝트는 이 디렉터리, 다시 말해 `client/public/locales/ko`에 있습니다. 오메가T로 JSON 파일을 구문 분석 하려면 [오메가T를 위한 오카피(Okapi) 필터 플러그인][okapi]을 설치해야 됩니다.
|
||||
|
||||
[bc]: https://github.com/chabulhwi
|
||||
[omt]: https://omegat.org/
|
||||
[okapi]: https://okapiframework.org/wiki/index.php/Okapi_Filters_Plugin_for_OmegaT
|
||||
@@ -0,0 +1,18 @@
|
||||
# Lean 4 Game
|
||||
|
||||
English | [한국어[Korean]](./README.ko.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions to the Korean translation of `lean4game` are always welcome!
|
||||
|
||||
### Korean Translation
|
||||
|
||||
I ([Bulhwi Cha][bc]) use [OmegaT][omt] to translate English documentation into
|
||||
Korean. The OmegaT project is in this directory, that is,
|
||||
`client/public/locales/ko`. You need to install the [Okapi filters plugin for
|
||||
OmegaT][okapi] to make OmegaT parse JSON files.
|
||||
|
||||
[bc]: https://github.com/chabulhwi
|
||||
[omt]: https://omegat.org/
|
||||
[okapi]: https://okapiframework.org/wiki/index.php/Okapi_Filters_Plugin_for_OmegaT
|
||||
@@ -0,0 +1,167 @@
|
||||
# Glossary in tab-separated format -*- coding: utf-8 -*-
|
||||
St. Anselm 안셀무스
|
||||
ontological 존재론적
|
||||
argument 논증
|
||||
argument 인수
|
||||
Lean 린
|
||||
Lean community 린 커뮤니티
|
||||
God 신
|
||||
Alvin Plantinga 앨빈 플랜팅아
|
||||
exist 존재하다
|
||||
existence 존재
|
||||
the understanding 지성
|
||||
reality 현실
|
||||
assumption 가정
|
||||
reductio 귀류법
|
||||
great 큰
|
||||
premise 전제
|
||||
being 존재자
|
||||
conceive 생각하다
|
||||
definition 정의
|
||||
true 참
|
||||
true 참인
|
||||
false 거짓
|
||||
false 거짓인
|
||||
formulate 정식화하다
|
||||
formulation 정식화
|
||||
formalize 형식화하다
|
||||
formalization 형식화
|
||||
property 속성
|
||||
redundant 불필요한
|
||||
sentence 문장
|
||||
state 진술하다
|
||||
statement 진술
|
||||
proposition 명제
|
||||
negation 부정
|
||||
axiom 공리
|
||||
theorem 정리
|
||||
theory 이론
|
||||
conclude 결론하다
|
||||
prove 증명하다
|
||||
SEP 스탠퍼드 철학 백과사전
|
||||
Eric Wieser 에릭 비저
|
||||
Alistair Tucker 앨리스터 터커
|
||||
philosophy 철학
|
||||
Owl of Sogang 서강올빼미
|
||||
type theory 유형론
|
||||
universe level 유형 세계 변수
|
||||
type 유형
|
||||
type 입력하다
|
||||
type check 유형을 확인하다
|
||||
type check 유형이 확인되다
|
||||
type check 유형 확인이 잘되다
|
||||
type class 유형 클래스
|
||||
instance 사례
|
||||
category mistake 범주 실수
|
||||
class 클래스
|
||||
example 보기
|
||||
example 예
|
||||
inductive type 귀납형
|
||||
define 정의하다
|
||||
constructor 구성자
|
||||
Apache License, Version 2.0 아파치 라이선스, 버전 2.0
|
||||
under the terms of 의 조건에 따라
|
||||
OmegaT 오메가T
|
||||
symbolic link 심벌릭 링크
|
||||
directory 디렉터리
|
||||
root directory 최상위 디렉터리
|
||||
documentation 문서
|
||||
English 영어
|
||||
Korean 한국어
|
||||
chapter 장
|
||||
quiz 퀴즈
|
||||
question 문제
|
||||
command 명령어
|
||||
error 오류
|
||||
code 코드
|
||||
evaluate 계산하다
|
||||
keyword 핵심어
|
||||
declare 선언하다
|
||||
constant 상수
|
||||
reference 참고 문헌
|
||||
universe-polymorphic 세계 다형적
|
||||
parametrically polymorphic 매개 변수 다형적
|
||||
function 함수
|
||||
identifier 식별자
|
||||
definitionally equal 정의상 같은
|
||||
natural number 자연수
|
||||
input 입력값
|
||||
return 반환하다
|
||||
non-zero 영이 아닌
|
||||
zero 영
|
||||
alpha-equivalent 알파 동등한
|
||||
less-than-or-equal-to sign 작거나 같음 부호
|
||||
dependent function 의존 함수
|
||||
dependent function type 의존 함수형
|
||||
dependent product 의존곱
|
||||
dependent product type 의존곱형
|
||||
underscore 밑줄
|
||||
implicit 암시적
|
||||
explicit 명시적
|
||||
sigma type 시그마
|
||||
propositional logic 명제 논리
|
||||
term 항
|
||||
contemporary 동시대
|
||||
term 용어
|
||||
truth-value 진릿값
|
||||
bearer 담지자
|
||||
propositional attitudes 명제적 태도
|
||||
believe 믿다
|
||||
doubt 의심하다
|
||||
South Korea 한국
|
||||
school mathematics 학교 수학
|
||||
sentence 문장
|
||||
high school mathematics 고등학교 수학
|
||||
teacher 교사
|
||||
Stanford Encyclopedia of Philosophy 스탠퍼드 철학 백과사전
|
||||
ordered triple 순서세짝
|
||||
conjecture 추측
|
||||
Goldbach's conjecture 골드바흐의 추측
|
||||
interactive theorem prover 상호 작용 정리 증명기
|
||||
truth 참
|
||||
falsity 거짓
|
||||
rule of inference 추론 규칙
|
||||
introduction rule 도입 규칙
|
||||
elimination rule 제거 규칙
|
||||
ex falso quodlibet 엑스 팔소 세퀴투르 쿠오들리베트
|
||||
principle of explosion 폭발 원리
|
||||
propositional connective 명제 연결사
|
||||
symbol 기호
|
||||
refutation by contradiction 모순에 따른 부인
|
||||
contradiction 모순
|
||||
conjunction 연언
|
||||
disjunction 선언(選言)
|
||||
implication 함의
|
||||
material implication 내용적 함의
|
||||
conditional 조건문
|
||||
modus ponens 긍정 논법[모더스 포넨스]
|
||||
necessary condition 필요조건
|
||||
conversion 역
|
||||
inversion 이(裏)
|
||||
contraposition 대우(對偶)
|
||||
equivalence 동등
|
||||
biconditional 쌍조건문
|
||||
abbreviation 준말
|
||||
if and only if …일 때 그리고 그럴 때만
|
||||
editor 편집기
|
||||
shortcut 단축키
|
||||
Theorem Proving in Lean 4 린 4로 하는 정리 증명
|
||||
exercise 연습 문제
|
||||
repository 저장소
|
||||
Jeremy Avigad 제러미 아비가드
|
||||
Leonardo de Moura 레오나르두 지 모라
|
||||
Soonho Kong 공순호
|
||||
Sebastian Ullrich 제바스티안 울리히
|
||||
file 파일
|
||||
Markdown 마크다운
|
||||
quiz 퀴즈
|
||||
translation 번역
|
||||
contribute 기여하다
|
||||
progress 진도
|
||||
capacity 이용량
|
||||
GitHub 깃허브
|
||||
repo 저장소
|
||||
game rule 게임 규칙
|
||||
unlocked 잠금 해제가 된
|
||||
unlock 잠금 해제를 하다
|
||||
Markdown 마크다운
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<omegat>
|
||||
<project version="1.0">
|
||||
<source_dir>__DEFAULT__</source_dir>
|
||||
<source_dir_excludes>
|
||||
<mask>**/.svn/**</mask>
|
||||
<mask>**/CVS/**</mask>
|
||||
<mask>**/.cvs/**</mask>
|
||||
<mask>**/.git/**</mask>
|
||||
<mask>**/.hg/**</mask>
|
||||
<mask>**/.repositories/**</mask>
|
||||
<mask>**/desktop.ini</mask>
|
||||
<mask>**/Thumbs.db</mask>
|
||||
<mask>**/.DS_Store</mask>
|
||||
<mask>**/~$*</mask>
|
||||
</source_dir_excludes>
|
||||
<target_dir>__DEFAULT__</target_dir>
|
||||
<tm_dir>__DEFAULT__</tm_dir>
|
||||
<glossary_dir>__DEFAULT__</glossary_dir>
|
||||
<glossary_file>__DEFAULT__</glossary_file>
|
||||
<dictionary_dir>__DEFAULT__</dictionary_dir>
|
||||
<export_tm_dir>__DEFAULT__</export_tm_dir>
|
||||
<export_tm_levels>omegat level1 level2</export_tm_levels>
|
||||
<source_lang>en</source_lang>
|
||||
<target_lang>ko</target_lang>
|
||||
<source_tok>org.omegat.tokenizer.LuceneEnglishTokenizer</source_tok>
|
||||
<target_tok>org.omegat.tokenizer.HunspellTokenizer</target_tok>
|
||||
<sentence_seg>true</sentence_seg>
|
||||
<support_default_translations>true</support_default_translations>
|
||||
<remove_tags>false</remove_tags>
|
||||
<external_command></external_command>
|
||||
</project>
|
||||
</omegat>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
../README.md
|
||||
@@ -0,0 +1 @@
|
||||
../../en/translation.json
|
||||
@@ -0,0 +1,15 @@
|
||||
# 린 4 게임
|
||||
|
||||
[English (영어)](./README.md) | 한국어
|
||||
|
||||
## 기여하기
|
||||
|
||||
`lean4game`의 한국어 번역에 기여하는 활동은 언제든지 환영합니다!
|
||||
|
||||
### 한국어 번역
|
||||
|
||||
저([차불휘][bc])는 [오메가T(OmegaT)][omt]를 이용해 영어 문서를 한국어로 번역합니다. 오메가T 프로젝트는 이 디렉터리, 다시 말해 `client/public/locales/ko`에 있습니다. 오메가T로 JSON 파일을 구문 분석 하려면 [오메가T를 위한 오카피(Okapi) 필터 플러그인][okapi]을 설치해야 됩니다.
|
||||
|
||||
[bc]: https://github.com/chabulhwi
|
||||
[omt]: https://omegat.org/
|
||||
[okapi]: https://okapiframework.org/wiki/index.php/Okapi_Filters_Plugin_for_OmegaT
|
||||
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"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>\n\n참고: '잠금 해제'가 된 전략이나 정리는 다음 두 부류로 정해집니다. (1) 해당 단계를 푸는 데 필요한 최소한의 전략이나 정리의 모임, (2) 다른 단계에서 잠금 해제를 한 전략이나 정리. 따라서 여러분이 <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": "이후 목표",
|
||||
"No Goals": "목표 없음",
|
||||
"Loading goal…": "목표 불러오는 중…",
|
||||
"Click somewhere in the Lean file to enable the infoview.": "린 파일의 아무 곳이나 눌러 정보창을 여십시오.",
|
||||
"Waiting for Lean server to start…": "린 서버가 시작하기를 기다리는 중…",
|
||||
"Level completed! 🎉": "단계 완료! 🎉",
|
||||
"Level completed with warnings 🎭": "경고 있는 채로 단계 완료 🎭",
|
||||
"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>": "<1>린(Lean 4)</1> 증명 보조기와 그 수학 라이브러리 <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>를 이용해 지역[로컬] 폴더에서 게임을 직접 여십시오.",
|
||||
"Prerequisites": "선행 요건",
|
||||
"Worlds": "세계",
|
||||
"Levels": "단계",
|
||||
"Language": "언어",
|
||||
"Server capacity": "서버 이용량",
|
||||
"RAM": "램",
|
||||
"CPU": "CPU",
|
||||
"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": "데스크톱",
|
||||
"<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>여러분이 직접 게임을 작성할 생각이 있다면, <1>GameSkeleton 깃허브 저장소</1>를 양식[템플릿]으로 이용하고 <3>'게임 만들기(Creating a Game)'</3> 문서를 읽으십시오.</0><1>여러분이 작성한 게임을 직접 서버에서 불러오고, 정확한 URL을 이용해 그 게임을 하실 수 있습니다. 여러분의 게임을 서버에서 불러오는 방법에 관한 세부 사항도 위의 <1>설명서</1>에 나와 있습니다. 궁금한 점이 있으면 저희에게 연락해 주십시오.</1><p>이 페이지에 실린 게임들은 수동으로 추가됐습니다. 저희에게 연락하시면 여러분의 게임을 기꺼이 추가하겠습니다.",
|
||||
"Level": "단계",
|
||||
"Introduction": "소개",
|
||||
"Retry proof from here": "여기부터 증명 다시 시도하기",
|
||||
"Retry": "다시 시도하기",
|
||||
"Failed command": "실패한 명령",
|
||||
"<p>As this server runs lean on our university machines, it has a limited capacity. Our current estimate is about 70 simultaneous games.</p>": "",
|
||||
"<0>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!</0>": "",
|
||||
"This server has been developed as part of the project <1>ADAM: Anticipating the Digital Age of Mathematics</1> at Heinrich Heine University Düsseldorf.": "",
|
||||
" used": ""
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
target/translation.json
|
||||
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"Tactics": "策略",
|
||||
"Lean Game Server": "LEAN 游戏服务器",
|
||||
"<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": "进一步目标",
|
||||
"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>": "这是一个为证明助手 <1>Lean</1> <i>(Lean 4)</i> 及其数学库 <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> 从本地文件夹打开游戏。",
|
||||
"<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>如果你打算编写自己的游戏,可以使用 <1>GameSkeleton Github Repo</1> 作为模板,并参阅 <3>如何创建游戏</3>。</0><1>你可以直接将游戏上传至服务器,并通过正确的 URL 进行游戏。上面的 <1>说明</1> 已详细介绍了如何将游戏加载到服务器的步骤。如果你有任何疑问,请随时联系我们。</1><p>本页上的精选游戏都是手动添加的。如果你想添加你的游戏,请与我们联系,我们非常欢迎。</p>",
|
||||
"Prerequisites": "前置条件",
|
||||
"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": "命令失败",
|
||||
"<p>As this server runs lean on our university machines, it has a limited capacity. Our current estimate is about 70 simultaneous games.</p>": "",
|
||||
"<0>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!</0>": "",
|
||||
"This server has been developed as part of the project <1>ADAM: Anticipating the Digital Age of Mathematics</1> at Heinrich Heine University Düsseldorf.": "",
|
||||
"Server capacity": "",
|
||||
"RAM": "",
|
||||
" used": "",
|
||||
"CPU": ""
|
||||
}
|
||||
+12
-21
@@ -8,40 +8,31 @@ import '@fontsource/roboto/700.css';
|
||||
|
||||
import './css/reset.css';
|
||||
import './css/app.css';
|
||||
import { MobileContext } from './components/infoview/context';
|
||||
import { useMobile } from './hooks';
|
||||
import { AUTO_SWITCH_THRESHOLD, getWindowDimensions} from './state/preferences';
|
||||
import { PreferencesContext} from './components/infoview/context';
|
||||
import UsePreferences from "./state/hooks/use_preferences"
|
||||
import i18n from './i18n';
|
||||
|
||||
export const GameIdContext = React.createContext<string>(undefined);
|
||||
|
||||
function App() {
|
||||
const { mobile, setMobile, lockMobile, setLockMobile } = useMobile();
|
||||
|
||||
const params = useParams()
|
||||
const gameId = "g/" + params.owner + "/" + params.repo
|
||||
|
||||
const automaticallyAdjustLayout = () => {
|
||||
const {width} = getWindowDimensions()
|
||||
setMobile(width < AUTO_SWITCH_THRESHOLD)
|
||||
}
|
||||
const {mobile, layout, isSavePreferences, language, setLayout, setIsSavePreferences, setLanguage} = UsePreferences()
|
||||
|
||||
React.useEffect(()=>{
|
||||
if (!lockMobile){
|
||||
void automaticallyAdjustLayout()
|
||||
window.addEventListener('resize', automaticallyAdjustLayout)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', automaticallyAdjustLayout)
|
||||
}
|
||||
}
|
||||
}, [lockMobile])
|
||||
React.useEffect(() => {
|
||||
i18n.changeLanguage(language)
|
||||
}, [language])
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<GameIdContext.Provider value={gameId}>
|
||||
<MobileContext.Provider value={{mobile, setMobile, lockMobile, setLockMobile}}>
|
||||
<Outlet />
|
||||
</MobileContext.Provider>
|
||||
<PreferencesContext.Provider value={{mobile, layout, isSavePreferences, language, setLayout, setIsSavePreferences, setLanguage}}>
|
||||
<React.Suspense>
|
||||
<Outlet />
|
||||
</React.Suspense>
|
||||
</PreferencesContext.Provider>
|
||||
</GameIdContext.Provider>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -5,30 +5,32 @@ import * as React from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faDownload, faUpload, faEraser, faBook, faBookOpen, faGlobe, faHome,
|
||||
faArrowRight, faArrowLeft, faXmark, faBars, faCode,
|
||||
faCircleInfo, faTerminal, faMobileScreenButton, faDesktop, faGear } from '@fortawesome/free-solid-svg-icons'
|
||||
faCircleInfo, faTerminal, faGear } from '@fortawesome/free-solid-svg-icons'
|
||||
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 { changedOpenedIntro, selectCompleted, selectDifficulty, selectProgress } from '../state/progress'
|
||||
import { useAppDispatch, useAppSelector } from '../hooks'
|
||||
import { Button } from './button'
|
||||
import { downloadProgress } from './popup/erase'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
/** navigation buttons for mobile welcome page to switch between intro/tree/inventory. */
|
||||
function MobileNavButtons({pageNumber, setPageNumber}:
|
||||
{ pageNumber: number,
|
||||
setPageNumber: any}) {
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// 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 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
|
||||
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 nextTitle = {0: "World selection", 1: "Inventory", 2: null}[pageNumber]
|
||||
let nextTitle = {0: t("World selection"), 1: t("Inventory"), 2: null}[pageNumber]
|
||||
|
||||
return <>
|
||||
{(prevText || prevIcon) &&
|
||||
@@ -53,7 +55,7 @@ function MobileNavButtons({pageNumber, setPageNumber}:
|
||||
}
|
||||
|
||||
/** 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)}}>
|
||||
{navOpen ? <FontAwesomeIcon icon={faXmark} /> : <FontAwesomeIcon icon={faBars} />}
|
||||
</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.
|
||||
*/
|
||||
function NextButton({worldSize, difficulty, completed, setNavOpen}) {
|
||||
const { t } = useTranslation()
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
|
||||
return (levelId < worldSize ?
|
||||
<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)}
|
||||
onClick={() => setNavOpen(false)}>
|
||||
<FontAwesomeIcon icon={faArrowRight} /> {levelId ? "Next" : "Start"}
|
||||
<FontAwesomeIcon icon={faArrowRight} /> {levelId ? t("Next") : t("Start")}
|
||||
</Button>
|
||||
:
|
||||
<Button to={`/${gameId}`} inverted="true" title="back to world selection" id="home-btn">
|
||||
<FontAwesomeIcon icon={faHome} /> Leave World
|
||||
<Button to={`/${gameId}`} inverted="true" title={t("back to world selection")} id="home-btn">
|
||||
<FontAwesomeIcon icon={faHome} /> {t("Leave World")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -83,66 +86,117 @@ function NextButton({worldSize, difficulty, completed, setNavOpen}) {
|
||||
* only renders if the current level id is > 0.
|
||||
*/
|
||||
function PreviousButton({setNavOpen}) {
|
||||
const { t } = useTranslation()
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
|
||||
return (levelId > 0 && <>
|
||||
<Button disabled={levelId <= 0} inverted="true"
|
||||
to={`/${gameId}/world/${worldId}/level/${levelId - 1}`}
|
||||
title="previous level"
|
||||
title={t("previous level")}
|
||||
onClick={() => setNavOpen(false)}>
|
||||
<FontAwesomeIcon icon={faArrowLeft} /> Previous
|
||||
<FontAwesomeIcon icon={faArrowLeft} /> {t("Previous")}
|
||||
</Button>
|
||||
</>)
|
||||
}
|
||||
|
||||
/** button to toggle between editor and typewriter */
|
||||
function InputModeButton({setNavOpen, isDropdown}) {
|
||||
const { t } = useTranslation()
|
||||
const {levelId} = React.useContext(WorldLevelIdContext)
|
||||
const {typewriterMode, setTypewriterMode, lockInputMode} = React.useContext(InputModeContext)
|
||||
const {typewriterMode, setTypewriterMode, lockEditorMode} = React.useContext(InputModeContext)
|
||||
|
||||
/** toggle input mode if allowed */
|
||||
function toggleInputMode(ev: React.MouseEvent) {
|
||||
if (!lockInputMode){
|
||||
if (!lockEditorMode){
|
||||
setTypewriterMode(!typewriterMode)
|
||||
setNavOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
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=""
|
||||
onClick={(ev) => toggleInputMode(ev)}
|
||||
title={lockInputMode ? "Editor mode is enforced!" : typewriterMode ? "Editor mode" : "Typewriter mode"}>
|
||||
<FontAwesomeIcon icon={typewriterMode ? faCode : faTerminal} />
|
||||
{isDropdown && (typewriterMode ? <> Editor mode</> : <> Typewriter mode</>)}
|
||||
title={lockEditorMode ? t("Editor mode is enforced!") : typewriterMode ? t("Editor mode") : t("Typewriter mode")}>
|
||||
<FontAwesomeIcon icon={(typewriterMode && !lockEditorMode) ? faCode : faTerminal} />
|
||||
{isDropdown && ((typewriterMode && !lockEditorMode) ? <> {t("Editor mode")}</> : <> {t("Typewriter mode")}</>)}
|
||||
</Button>
|
||||
}
|
||||
|
||||
/** button to toggle iimpressum popup */
|
||||
function ImpressumButton({setNavOpen, toggleImpressum, isDropdown}) {
|
||||
return <Button className="btn btn-inverted toggle-width"
|
||||
title="information, Impressum, privacy policy" inverted="true" to="" onClick={(ev) => {toggleImpressum(ev); setNavOpen(false)}}>
|
||||
/** button to toggle iimpressum popup
|
||||
*
|
||||
* Note: Do not translate the word "Impressum"! German GDPR needs this.
|
||||
*/
|
||||
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} />
|
||||
{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 to go back to welcome page */
|
||||
function HomeButton({isDropdown}) {
|
||||
const { t } = useTranslation()
|
||||
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} />
|
||||
{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 in mobile level to toggle inventory.
|
||||
* only displays a button if `setPageNumber` is set.
|
||||
*/
|
||||
function InventoryButton({pageNumber, setPageNumber}) {
|
||||
const { t } = useTranslation()
|
||||
return (setPageNumber &&
|
||||
<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)}}>
|
||||
<FontAwesomeIcon icon={pageNumber ? faBookOpen : faBook} />
|
||||
</Button>
|
||||
@@ -160,59 +214,49 @@ export function WelcomeAppBar({pageNumber, setPageNumber, gameInfo, toggleImpres
|
||||
toggleInfo: any,
|
||||
togglePreferencesPopup: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const gameProgress = useAppSelector(selectProgress(gameId))
|
||||
const {mobile, setMobile} = React.useContext(MobileContext)
|
||||
const {mobile} = React.useContext(PreferencesContext)
|
||||
const [navOpen, setNavOpen] = React.useState(false)
|
||||
|
||||
return <div className="app-bar">
|
||||
<div className='app-bar-left'>
|
||||
<Button inverted="false" title="back to games selection" to="/">
|
||||
<FontAwesomeIcon icon={faArrowLeft} /> <FontAwesomeIcon icon={faGlobe} />
|
||||
</Button>
|
||||
<LandingPageButton />
|
||||
<span className="app-bar-title"></span>
|
||||
</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 className="nav-btns">
|
||||
{mobile && <MobileNavButtons pageNumber={pageNumber} setPageNumber={setPageNumber} />}
|
||||
<MenuButton navOpen={navOpen} setNavOpen={setNavOpen} />
|
||||
</div>
|
||||
<div className={'menu dropdown' + (navOpen ? '' : ' hidden')}>
|
||||
<Button title="Game Info & Credits" inverted="true" to="" onClick={() => {toggleInfo(); setNavOpen(false)}}>
|
||||
<FontAwesomeIcon icon={faCircleInfo} /> Game Info
|
||||
</Button>
|
||||
<Button title="Clear Progress" inverted="true" to="" onClick={() => {toggleEraseMenu(); setNavOpen(false)}}>
|
||||
<FontAwesomeIcon icon={faEraser} /> Erase
|
||||
</Button>
|
||||
<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>
|
||||
<Button title="Preferences" inverted="true" to="" onClick={() => {togglePreferencesPopup(); setNavOpen(false)}}>
|
||||
<FontAwesomeIcon icon={faGear} /> Preferences
|
||||
</Button>
|
||||
<GameInfoButton setNavOpen={setNavOpen} toggleInfo={toggleInfo}/>
|
||||
<EraseButton setNavOpen={setNavOpen} toggleEraseMenu={toggleEraseMenu}/>
|
||||
<DownloadButton setNavOpen={setNavOpen} gameId={gameId} gameProgress={gameProgress}/>
|
||||
<UploadButton setNavOpen={setNavOpen} toggleUploadMenu={toggleUploadMenu}/>
|
||||
<ImpressumButton setNavOpen={setNavOpen} toggleImpressum={toggleImpressum} isDropdown={true} />
|
||||
<PreferencesButton setNavOpen={setNavOpen} togglePreferencesPopup={togglePreferencesPopup}/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
/** 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,
|
||||
levelTitle: string,
|
||||
toggleImpressum: any,
|
||||
toggleInfo: any,
|
||||
togglePreferencesPopup: any,
|
||||
pageNumber?: number,
|
||||
setPageNumber?: any,
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
|
||||
const {mobile} = React.useContext(MobileContext)
|
||||
const {mobile} = React.useContext(PreferencesContext)
|
||||
const [navOpen, setNavOpen] = React.useState(false)
|
||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||
const completed = useAppSelector(selectCompleted(gameId, worldId, levelId))
|
||||
@@ -236,14 +280,16 @@ export function LevelAppBar({isLoading, levelTitle, toggleImpressum, pageNumber=
|
||||
<PreviousButton setNavOpen={setNavOpen} />
|
||||
<HomeButton isDropdown={true} />
|
||||
<InputModeButton setNavOpen={setNavOpen} isDropdown={true}/>
|
||||
<GameInfoButton setNavOpen={setNavOpen} toggleInfo={toggleInfo}/>
|
||||
<ImpressumButton setNavOpen={setNavOpen} toggleImpressum={toggleImpressum} isDropdown={true} />
|
||||
<PreferencesButton setNavOpen={setNavOpen} togglePreferencesPopup={togglePreferencesPopup}/>
|
||||
</div>
|
||||
</> :
|
||||
<>
|
||||
{/* DESKTOP VERSION */}
|
||||
<div className='app-bar-left'>
|
||||
<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>
|
||||
<span className="app-bar-title">{levelTitle}</span>
|
||||
@@ -252,7 +298,12 @@ export function LevelAppBar({isLoading, levelTitle, toggleImpressum, pageNumber=
|
||||
<PreviousButton setNavOpen={setNavOpen} />
|
||||
<NextButton worldSize={gameInfo.data?.worldSize[worldId]} difficulty={difficulty} completed={completed} setNavOpen={setNavOpen} />
|
||||
<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>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,22 +1,46 @@
|
||||
import { GameHint } from "./infoview/rpc_api";
|
||||
import { GameHint, InteractiveGoalsWithHints, ProofState } from "./infoview/rpc_api";
|
||||
import * as React from 'react';
|
||||
import Markdown from './markdown';
|
||||
import { ProofStep } from "./infoview/context";
|
||||
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}) {
|
||||
return <div className={`message information step-${step}` + (step == selected ? ' selected' : '') + (lastLevel ? ' recent' : '')} onClick={toggleSelection}>
|
||||
<Markdown>{hint.text}</Markdown>
|
||||
<Markdown>{getHintText(hint)}</Markdown>
|
||||
</div>
|
||||
}
|
||||
|
||||
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}>
|
||||
<Markdown>{hint.text}</Markdown>
|
||||
<Markdown>{getHintText(hint)}</Markdown>
|
||||
</div>
|
||||
}
|
||||
|
||||
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 hiddenHints = hints.filter(hint => hint.hidden)
|
||||
|
||||
@@ -29,7 +53,7 @@ export function Hints({hints, showHidden, step, selected, toggleSelection, lastL
|
||||
|
||||
export function DeletedHint({hint} : {hint: GameHint}) {
|
||||
return <div className="message information deleted-hint">
|
||||
<Markdown>{hint.text}</Markdown>
|
||||
<Markdown>{getHintText(hint)}</Markdown>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -46,22 +70,56 @@ export function DeletedHints({hints} : {hints: GameHint[]}) {
|
||||
}
|
||||
|
||||
/** Filter hints to not show consequtive identical hints twice.
|
||||
*
|
||||
* This function takes a `ProofStep[]` and extracts the hints in form of an
|
||||
* element of type `GameHint[][]` where it removes hints that are identical to hints
|
||||
* appearing in the previous step. Hidden hints are not filtered.
|
||||
*
|
||||
* This effectively means we prevent consequtive identical hints from being shown.
|
||||
* Hidden hints are not filtered.
|
||||
*/
|
||||
export function filterHints(proof: ProofStep[]): GameHint[][] {
|
||||
return proof.map((step, i) => {
|
||||
if (i == 0){
|
||||
return step.hints
|
||||
} else {
|
||||
// TODO: Writing all fields explicitely is somewhat fragile to changes, is there a
|
||||
// good way to shallow-compare objects?
|
||||
return step.hints.filter((hint) => hint.hidden ||
|
||||
(proof[i-1].hints.find((x) => (x.text == hint.text && x.hidden == hint.hidden)) === undefined))
|
||||
}
|
||||
})
|
||||
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 monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
|
||||
import { InteractiveDiagnostic, InteractiveTermGoal } from '@leanprover/infoview-api';
|
||||
import { GameHint, InteractiveGoal, InteractiveGoals } from './rpc_api';
|
||||
import { InteractiveDiagnostic } from '@leanprover/infoview-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>(
|
||||
null as any)
|
||||
|
||||
export type InfoStatus = 'updating' | 'error' | 'ready';
|
||||
|
||||
/** One step of the proof */
|
||||
export type ProofStep = {
|
||||
/** The command in this step */
|
||||
command : string
|
||||
/** List of goals *after* this command */
|
||||
goals: InteractiveGoal[] // TODO: Add correct type
|
||||
/** Story relevant messages */
|
||||
hints: GameHint[] // TODO: Add correct type
|
||||
/** Errors and warnings */
|
||||
errors: InteractiveDiagnostic[] // TODO: Add correct type
|
||||
}
|
||||
// /** One step of the proof */
|
||||
// export type ProofStep = {
|
||||
// /** The command in this step */
|
||||
// command : string
|
||||
// /** List of goals *after* this command */
|
||||
// goals: InteractiveGoal[] // TODO: Add correct type
|
||||
// /** Story relevant messages */
|
||||
// hints: GameHint[] // TODO: Add correct type
|
||||
// /** Errors and warnings */
|
||||
// errors: InteractiveDiagnostic[] // TODO: Add correct type
|
||||
// }
|
||||
|
||||
/** The context storing the proof step-by-step for the command line mode */
|
||||
export const ProofContext = React.createContext<{
|
||||
/** 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.
|
||||
*
|
||||
* Note that the first step will always have `null` as command
|
||||
* Note that the first step will always have "" as command
|
||||
*/
|
||||
proof: ProofStep[],
|
||||
setProof: React.Dispatch<React.SetStateAction<Array<ProofStep>>>
|
||||
proof: ProofState,
|
||||
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: [],
|
||||
setProof: () => {} // TODO: implement me
|
||||
proof: {steps: [], diagnostics: [], completed: false, completedWithWarnings: false},
|
||||
setProof: () => {},
|
||||
interimDiags: [],
|
||||
setInterimDiags: () => {},
|
||||
crashed: false,
|
||||
setCrashed: () => {}
|
||||
})
|
||||
|
||||
|
||||
// TODO: Do we still need that?
|
||||
export interface ProofStateProps {
|
||||
// pos: DocumentPosition;
|
||||
status: InfoStatus;
|
||||
messages: InteractiveDiagnostic[];
|
||||
goals?: InteractiveGoals;
|
||||
goals?: InteractiveGoalsWithHints;
|
||||
termGoal?: InteractiveTermGoal;
|
||||
error?: string;
|
||||
// userWidgets: UserWidgetInstance[];
|
||||
@@ -49,31 +63,34 @@ export interface ProofStateProps {
|
||||
// triggerUpdate: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const ProofStateContext = React.createContext<{
|
||||
proofState : ProofStateProps,
|
||||
setProofState: React.Dispatch<React.SetStateAction<ProofStateProps>>
|
||||
}>({
|
||||
proofState : {
|
||||
status: 'updating',
|
||||
messages: [],
|
||||
goals: undefined,
|
||||
termGoal: undefined,
|
||||
error: undefined},
|
||||
setProofState: () => {},
|
||||
})
|
||||
// export const ProofStateContext = React.createContext<{
|
||||
// proofState : ProofStateProps,
|
||||
// setProofState: React.Dispatch<React.SetStateAction<ProofStateProps>>
|
||||
// }>({
|
||||
// proofState : {
|
||||
// status: 'updating',
|
||||
// messages: [],
|
||||
// goals: undefined,
|
||||
// termGoal: undefined,
|
||||
// error: undefined},
|
||||
// setProofState: () => {},
|
||||
// })
|
||||
|
||||
export interface IMobileContext {
|
||||
mobile : boolean,
|
||||
setMobile: React.Dispatch<React.SetStateAction<Boolean>>,
|
||||
lockMobile: boolean,
|
||||
setLockMobile: React.Dispatch<React.SetStateAction<Boolean>>,
|
||||
export interface IPreferencesContext extends PreferencesState{
|
||||
mobile: boolean, // The variables that actually control the page 'layout' can only be changed through layout.
|
||||
setLayout: React.Dispatch<React.SetStateAction<PreferencesState["layout"]>>;
|
||||
setIsSavePreferences: React.Dispatch<React.SetStateAction<PreferencesState["isSavePreferences"]>>;
|
||||
setLanguage: React.Dispatch<React.SetStateAction<PreferencesState["language"]>>;
|
||||
}
|
||||
|
||||
export const MobileContext = React.createContext<IMobileContext>({
|
||||
export const PreferencesContext = React.createContext<IPreferencesContext>({
|
||||
mobile: false,
|
||||
setMobile: () => {},
|
||||
lockMobile: false,
|
||||
setLockMobile: () => {}
|
||||
layout: "auto",
|
||||
isSavePreferences: false,
|
||||
language: "en",
|
||||
setLayout: () => {},
|
||||
setIsSavePreferences: () => {},
|
||||
setLanguage: () => {},
|
||||
})
|
||||
|
||||
export const WorldLevelIdContext = React.createContext<{
|
||||
@@ -111,13 +128,13 @@ export const InputModeContext = React.createContext<{
|
||||
setTypewriterMode: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
typewriterInput: string,
|
||||
setTypewriterInput: React.Dispatch<React.SetStateAction<string>>,
|
||||
lockInputMode: boolean,
|
||||
setLockInputMode: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
lockEditorMode: boolean,
|
||||
setLockEditorMode: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
}>({
|
||||
typewriterMode: true,
|
||||
setTypewriterMode: () => {},
|
||||
typewriterInput: "",
|
||||
setTypewriterInput: () => {},
|
||||
lockInputMode: false,
|
||||
setLockInputMode: () => {},
|
||||
lockEditorMode: false,
|
||||
setLockEditorMode: () => {},
|
||||
});
|
||||
|
||||
@@ -10,7 +10,11 @@ import { Locations, LocationsContext, SelectableLocation } from '../../../../nod
|
||||
import { InteractiveCode } from '../../../../node_modules/lean4-infoview/src/infoview/interactiveCode'
|
||||
import { WithTooltipOnHover } from '../../../../node_modules/lean4-infoview/src/infoview/tooltips';
|
||||
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. */
|
||||
function isInaccessibleName(h: string): boolean {
|
||||
@@ -39,7 +43,11 @@ function goalToString(g: InteractiveGoal): 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 {
|
||||
@@ -127,16 +135,12 @@ interface GoalProps {
|
||||
typewriter: boolean
|
||||
}
|
||||
|
||||
interface ProofDisplayProps {
|
||||
proof: string
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Displays the hypotheses, target type and optional case label of a goal according to the
|
||||
* provided `filter`. */
|
||||
export const Goal = React.memo((props: GoalProps) => {
|
||||
const { goal, filter, showHints, typewriter } = props
|
||||
let { t } = useTranslation()
|
||||
|
||||
// TODO: Apparently `goal` can be `undefined`
|
||||
if (!goal) {return <></>}
|
||||
@@ -150,7 +154,7 @@ export const Goal = React.memo((props: GoalProps) => {
|
||||
undefined,
|
||||
[locs, goal.mvarId])
|
||||
const goalLi = <div key={'goal'}>
|
||||
<div className="goal-title">Goal: </div>
|
||||
<div className="goal-title">{t("Goal")}:</div>
|
||||
<LocationsContext.Provider value={goalLocs}>
|
||||
<InteractiveCode fmt={goal.type} />
|
||||
</LocationsContext.Provider>
|
||||
@@ -163,24 +167,23 @@ export const Goal = React.memo((props: GoalProps) => {
|
||||
// const hints = <Hints hints={goal.hints} key={goal.mvarId} />
|
||||
const objectHyps = hyps.filter(hyp => !hyp.isAssumption)
|
||||
const assumptionHyps = hyps.filter(hyp => hyp.isAssumption)
|
||||
const {typewriterMode} = React.useContext(InputModeContext)
|
||||
|
||||
return <div>
|
||||
{/* {goal.userName && <div><strong className="goal-case">case </strong>{goal.userName}</div>} */}
|
||||
{filter.reverse && goalLi}
|
||||
{! 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> }
|
||||
{!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> }
|
||||
{/* {typewriter && typewriterMode && <Typewriter />} */}
|
||||
{!filter.reverse && goalLi}
|
||||
{/* {showHints && hints} */}
|
||||
</div>
|
||||
})
|
||||
|
||||
export const MainAssumptions = React.memo((props: GoalProps2) => {
|
||||
let { t } = useTranslation()
|
||||
const { goals, filter } = props
|
||||
|
||||
const goal = goals[0]
|
||||
@@ -195,7 +198,7 @@ export const MainAssumptions = React.memo((props: GoalProps2) => {
|
||||
[locs, goal.mvarId])
|
||||
|
||||
const goalLi = <div key={'goal'}>
|
||||
<div className="goal-title">Goal: </div>
|
||||
<div className="goal-title">{t("Goal") + ":"}</div>
|
||||
<LocationsContext.Provider value={goalLocs}>
|
||||
<InteractiveCode fmt={goal.type} />
|
||||
</LocationsContext.Provider>
|
||||
@@ -205,25 +208,26 @@ export const MainAssumptions = React.memo((props: GoalProps2) => {
|
||||
const assumptionHyps = hyps.filter(hyp => hyp.isAssumption)
|
||||
|
||||
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}
|
||||
{ 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> }
|
||||
{ assumptionHyps.length > 0 &&
|
||||
<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} />)}
|
||||
</div> }
|
||||
</div>
|
||||
})
|
||||
|
||||
export const OtherGoals = React.memo((props: GoalProps2) => {
|
||||
let { t } = useTranslation()
|
||||
const { goals, filter } = props
|
||||
return <>
|
||||
{goals && goals.length > 1 &&
|
||||
<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) =>
|
||||
<details key={i}>
|
||||
<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 {
|
||||
goals: InteractiveGoals
|
||||
goals: InteractiveGoalsWithHints
|
||||
filter: GoalFilterState
|
||||
}
|
||||
|
||||
export function Goals({ goals, filter }: GoalsProps) {
|
||||
if (goals.goals.length === 0) {
|
||||
return <>No goals</>
|
||||
return <></>
|
||||
} else {
|
||||
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
|
||||
* 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. */
|
||||
goals?: InteractiveGoals
|
||||
goals?: InteractiveGoalsWithHints
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -291,7 +276,7 @@ export const FilteredGoals = React.memo(({ headerChildren, goals }: FilteredGoal
|
||||
data-id="copy-goal-to-comment"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
if (goals) void ec.copyToComment(goalsToString(goals))
|
||||
if (goals) void ec.copyToComment(goalsWithHintsToString(goals))
|
||||
}}
|
||||
title="copy state to comment" />
|
||||
|
||||
@@ -336,3 +321,45 @@ export const FilteredGoals = React.memo(({ headerChildren, goals }: FilteredGoal
|
||||
</details>
|
||||
</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 type { Location, Diagnostic } from 'vscode-languageserver-protocol'
|
||||
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,
|
||||
mapRpcError, useAsyncWithTrigger, PausableProps } from '../../../../node_modules/lean4-infoview/src/infoview/util'
|
||||
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 { AllMessages, lspDiagToInteractive } from './messages'
|
||||
import { goalsToString, Goal, MainAssumptions, OtherGoals, ProofDisplay } from './goals'
|
||||
import { InteractiveGoals } from './rpc_api'
|
||||
import { goalsToString, Goal, MainAssumptions, OtherGoals } from './goals'
|
||||
import { InteractiveTermGoal, InteractiveGoalsWithHints, InteractiveGoals, ProofState } from './rpc_api'
|
||||
import { MonacoEditorContext, ProofStateProps, InfoStatus, ProofContext } from './context'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
// TODO: All about pinning could probably be removed
|
||||
type InfoKind = 'cursor' | 'pin'
|
||||
@@ -83,11 +84,12 @@ interface InfoDisplayContentProps extends PausableProps {
|
||||
error?: string
|
||||
userWidgets: UserWidgetInstance[]
|
||||
triggerUpdate: () => Promise<void>
|
||||
proof? : string
|
||||
proofString? : string
|
||||
}
|
||||
|
||||
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 hasError = !!error
|
||||
@@ -114,7 +116,8 @@ const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => {
|
||||
|
||||
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 */
|
||||
return <>
|
||||
|
||||
return <>
|
||||
{hasError &&
|
||||
<div className='error' key='errors'>
|
||||
Error updating:{' '}{error}.
|
||||
@@ -130,14 +133,14 @@ const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => {
|
||||
<div>
|
||||
{ goals && (goals.goals.length > 0
|
||||
? <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>
|
||||
</LocationsContext.Provider>
|
||||
{userWidgets.map(widget =>
|
||||
<details key={`widget::${widget.id}::${widget.range?.toString()}`} open>
|
||||
<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}/>
|
||||
</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>
|
||||
{' '}to see information.
|
||||
</span> :
|
||||
<><CircularProgress /><div>Loading goal...</div></>)}
|
||||
<><CircularProgress /><div>{t("Loading goal…")}</div></>)}
|
||||
<AllMessages />
|
||||
{/* <LocationsContext.Provider value={locs}>
|
||||
{goals && goals.goals.length > 1 && <div className="goals-section other-goals">
|
||||
@@ -166,6 +169,7 @@ interface InfoDisplayProps {
|
||||
pos: DocumentPosition,
|
||||
status: InfoStatus,
|
||||
messages: InteractiveDiagnostic[],
|
||||
proof?: ProofState,
|
||||
goals?: InteractiveGoals,
|
||||
termGoal?: InteractiveTermGoal,
|
||||
error?: string,
|
||||
@@ -175,7 +179,7 @@ interface InfoDisplayProps {
|
||||
}
|
||||
|
||||
/** 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,
|
||||
// but a display update is triggered
|
||||
const [shouldRefresh, setShouldRefresh] = React.useState<boolean>(false)
|
||||
@@ -214,7 +218,7 @@ function InfoDisplay(props0: ProofStateProps & InfoDisplayProps & InfoPinnable)
|
||||
{/* <details open> */}
|
||||
{/* <InfoStatusBar {...props} triggerUpdate={triggerDisplayUpdate} isPaused={isPaused} setPaused={setPaused} /> */}
|
||||
<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>
|
||||
{/* </details> */}
|
||||
</RpcContext.Provider>
|
||||
@@ -252,7 +256,7 @@ function useIsProcessingAt(p: DocumentPosition): boolean {
|
||||
|
||||
function InfoAux(props: InfoProps) {
|
||||
|
||||
const proofContext = React.useContext(ProofContext)
|
||||
const { setProof } = React.useContext(ProofContext)
|
||||
|
||||
const config = React.useContext(ConfigContext)
|
||||
|
||||
@@ -290,6 +294,10 @@ function InfoAux(props: InfoProps) {
|
||||
// with e.g. a new `pos`.
|
||||
type InfoRequestResult = Omit<InfoDisplayProps, 'triggerUpdate'>
|
||||
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 termGoalReq = getInteractiveTermGoal(rpcSess, DocumentPosition.toTdpp(pos))
|
||||
const widgetsReq = Widget_getWidgets(rpcSess, pos).catch(discardMethodNotFound)
|
||||
@@ -308,6 +316,7 @@ function InfoAux(props: InfoProps) {
|
||||
pos,
|
||||
status: 'updating',
|
||||
messages: lspDiagsHere.map(lspDiagToInteractive),
|
||||
proof: undefined,
|
||||
goals: undefined,
|
||||
termGoal: undefined,
|
||||
error: undefined,
|
||||
@@ -318,11 +327,12 @@ function InfoAux(props: InfoProps) {
|
||||
|
||||
// NB: it is important to await await reqs at once, otherwise
|
||||
// if both throw then one exception becomes unhandled.
|
||||
Promise.all([goalsReq, termGoalReq, widgetsReq, messagesReq]).then(
|
||||
([goals, termGoal, userWidgets, messages]) => resolve({
|
||||
Promise.all([proofReq, goalsReq, termGoalReq, widgetsReq, messagesReq]).then(
|
||||
([proof, goals, termGoal, userWidgets, messages]) => resolve({
|
||||
pos,
|
||||
status: 'ready',
|
||||
messages,
|
||||
proof : proof as any,
|
||||
goals: goals as any,
|
||||
termGoal,
|
||||
error: undefined,
|
||||
@@ -353,6 +363,7 @@ function InfoAux(props: InfoProps) {
|
||||
pos,
|
||||
status: 'error',
|
||||
messages: lspDiagsHere.map(lspDiagToInteractive),
|
||||
proof: undefined,
|
||||
goals: undefined,
|
||||
termGoal: undefined,
|
||||
error: `Error fetching goals: ${errorString}`,
|
||||
@@ -389,6 +400,7 @@ function InfoAux(props: InfoProps) {
|
||||
pos,
|
||||
status: 'updating',
|
||||
messages: [],
|
||||
proof: undefined,
|
||||
goals: undefined,
|
||||
termGoal: undefined,
|
||||
error: undefined,
|
||||
@@ -412,6 +424,11 @@ function InfoAux(props: InfoProps) {
|
||||
// hintContext.setHints(state.value.goals.goals[0].hints)
|
||||
// }
|
||||
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') {
|
||||
// The code inside `useAsyncWithTrigger` may only ever reject with a `retry` exception.
|
||||
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 { DocumentPosition, Keyed, PositionHelpers, useClientNotificationEffect, useClientNotificationState, useEvent, useEventResult } from '../../../../node_modules/lean4-infoview/src/infoview/util';
|
||||
import { Info, InfoProps } from './info';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/** Manages and displays pinned infos, as well as info for the current location. */
|
||||
export function Infos() {
|
||||
let { t } = useTranslation()
|
||||
const ec = React.useContext(EditorContext);
|
||||
|
||||
// Update pins when the document changes. In particular, when edits are made
|
||||
@@ -126,6 +128,6 @@ export function Infos() {
|
||||
|
||||
return <div>
|
||||
{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>;
|
||||
}
|
||||
|
||||
@@ -20,30 +20,35 @@ import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
|
||||
|
||||
import { GameIdContext } from '../../app';
|
||||
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 Markdown from '../markdown';
|
||||
|
||||
import { Infos } from './infos';
|
||||
import { AllMessages, Errors, WithLspDiagnosticsContext } from './messages';
|
||||
import { Goal } from './goals';
|
||||
import { DeletedChatContext, InputModeContext, MobileContext, MonacoEditorContext, ProofContext, ProofStep, SelectionContext, WorldLevelIdContext } from './context';
|
||||
import { Typewriter, hasErrors, hasInteractiveErrors } from './typewriter';
|
||||
import { Goal, isLastStepWithErrors, lastStepHasErrors, loadGoals } from './goals';
|
||||
import { DeletedChatContext, InputModeContext, PreferencesContext, MonacoEditorContext, ProofContext, SelectionContext, WorldLevelIdContext } from './context';
|
||||
import { Typewriter, getInteractiveDiagsAt, hasErrors, hasInteractiveErrors } from './typewriter';
|
||||
import { InteractiveDiagnostic } from '@leanprover/infoview/*';
|
||||
import { Button } from '../button';
|
||||
import { CircularProgress } from '@mui/material';
|
||||
import { GameHint } from './rpc_api';
|
||||
import { GameHint, InteractiveGoalsWithHints, ProofState } from './rpc_api';
|
||||
import { store } from '../../state/store';
|
||||
import { Hints, filterHints } 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
|
||||
* always present, or the monaco editor cannot start.
|
||||
*/
|
||||
export function DualEditor({ level, codeviewRef, levelId, worldId, worldSize }) {
|
||||
const ec = React.useContext(EditorContext)
|
||||
const { typewriterMode } = React.useContext(InputModeContext)
|
||||
const { typewriterMode, lockEditorMode } = React.useContext(InputModeContext)
|
||||
return <>
|
||||
<div className={typewriterMode ? 'hidden' : ''}>
|
||||
<div className={(typewriterMode && !lockEditorMode) ? 'hidden' : ''}>
|
||||
<ExerciseStatement data={level} showLeanStatement={true} />
|
||||
<div ref={codeviewRef} className={'codeview'}></div>
|
||||
</div>
|
||||
@@ -59,38 +64,37 @@ export function DualEditor({ level, codeviewRef, levelId, worldId, worldSize })
|
||||
function DualEditorMain({ worldId, levelId, level, worldSize }: { worldId: string, levelId: number, level: LevelInfo, worldSize: number }) {
|
||||
const ec = React.useContext(EditorContext)
|
||||
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()
|
||||
useServerNotificationEffect(
|
||||
'$/game/completed',
|
||||
(params: any) => {
|
||||
if (ec.events.changedCursorLocation.current &&
|
||||
ec.events.changedCursorLocation.current.uri === params.uri) {
|
||||
dispatch(levelCompleted({ game: gameId, world: worldId, level: levelId }))
|
||||
|
||||
// On completion, add the names of all new items to the local storage
|
||||
let newTiles = [
|
||||
...level?.tactics,
|
||||
...level?.lemmas,
|
||||
...level?.definitions
|
||||
].filter((tile) => tile.new).map((tile) => tile.name)
|
||||
React.useEffect(() => {
|
||||
if (proof?.completed) {
|
||||
dispatch(levelCompleted({ game: gameId, world: worldId, level: levelId }))
|
||||
|
||||
// Add the proven statement to the local storage as well.
|
||||
if (level?.statementName != null) {
|
||||
newTiles.push(level?.statementName)
|
||||
}
|
||||
// On completion, add the names of all new items to the local storage
|
||||
let newTiles = [
|
||||
...level?.tactics,
|
||||
...level?.lemmas,
|
||||
...level?.definitions
|
||||
].filter((tile) => tile.new).map((tile) => tile.name)
|
||||
|
||||
let inv: string[] = selectInventory(gameId)(store.getState())
|
||||
|
||||
// add new items and remove duplicates
|
||||
let newInv = [...inv, ...newTiles].filter((item, i, array) => array.indexOf(item) == i)
|
||||
|
||||
dispatch(changedInventory({ game: gameId, inventory: newInv }))
|
||||
// Add the proven statement to the local storage as well.
|
||||
if (level?.statementName != null) {
|
||||
newTiles.push(level?.statementName)
|
||||
}
|
||||
}, [level]
|
||||
)
|
||||
|
||||
let inv: string[] = selectInventory(gameId)(store.getState())
|
||||
|
||||
// add new items and remove duplicates
|
||||
let newInv = [...inv, ...newTiles].filter((item, i, array) => array.indexOf(item) == i)
|
||||
|
||||
dispatch(changedInventory({ game: gameId, inventory: newInv }))
|
||||
|
||||
}
|
||||
}, [proof, level])
|
||||
|
||||
/* Set up updates to the global infoview state on editor events. */
|
||||
const config = useEventResult(ec.events.changedInfoviewConfig) ?? defaultInfoviewConfig;
|
||||
@@ -110,7 +114,7 @@ function DualEditorMain({ worldId, levelId, level, worldSize }: { worldId: strin
|
||||
<WithRpcSessions>
|
||||
<WithLspDiagnosticsContext>
|
||||
<ProgressContext.Provider value={allProgress}>
|
||||
{typewriterMode ?
|
||||
{(typewriterMode && !lockEditorMode) ?
|
||||
<TypewriterInterfaceWrapper world={worldId} level={levelId} data={level} worldSize={worldSize}/>
|
||||
:
|
||||
<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.
|
||||
*/
|
||||
function ExerciseStatement({ data, showLeanStatement = false }) {
|
||||
let { t } = useTranslation()
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
|
||||
if (!(data?.descrText || data?.descrFormat)) { return <></> }
|
||||
return <>
|
||||
<div className="exercise-statement">
|
||||
{data?.descrText &&
|
||||
<Markdown>
|
||||
{(data?.displayName ? `**Theorem** \`${data?.displayName}\`: ` : '') + data?.descrText}
|
||||
{(data?.displayName ? `**Theorem** \`${data?.displayName}\`: ` : '') + t(data?.descrText, {ns: gameId})}
|
||||
</Markdown>
|
||||
}
|
||||
{data?.descrFormat && showLeanStatement &&
|
||||
@@ -150,12 +157,26 @@ function ExerciseStatement({ data, showLeanStatement = false }) {
|
||||
// TODO: This is only used in `EditorInterface`
|
||||
// while `TypewriterInterface` has this copy-pasted in.
|
||||
export function Main(props: { world: string, level: number, data: LevelInfo}) {
|
||||
let { t } = useTranslation()
|
||||
const ec = React.useContext(EditorContext);
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
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)
|
||||
|
||||
|
||||
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 (() => {
|
||||
@@ -182,6 +203,19 @@ export function Main(props: { world: string, level: number, data: LevelInfo}) {
|
||||
|
||||
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(
|
||||
'textDocument/didClose',
|
||||
(params: DidCloseTextDocumentParams) => {
|
||||
@@ -201,13 +235,22 @@ export function Main(props: { world: string, level: number, data: LevelInfo}) {
|
||||
// that we want to persist.
|
||||
let ret
|
||||
if (!serverVersion) {
|
||||
ret = <p>Waiting for Lean server to start...</p>
|
||||
ret = <p>{t("Waiting for Lean server to start…")}</p>
|
||||
} else if (serverStoppedResult) {
|
||||
ret = <div><p>{serverStoppedResult.message}</p><p className="error">{serverStoppedResult.reason}</p></div>
|
||||
} else {
|
||||
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 />
|
||||
<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>
|
||||
}
|
||||
|
||||
@@ -223,15 +266,26 @@ const goalFilter = {
|
||||
}
|
||||
|
||||
/** 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
|
||||
if (!command) { return <></> }
|
||||
return <div className="command">
|
||||
<div className="command-text">{command}</div>
|
||||
<Button to="" className="undo-button btn btn-inverted" title="Retry proof from here" onClick={deleteProof}>
|
||||
<FontAwesomeIcon icon={faDeleteLeft} /> Retry
|
||||
</Button>
|
||||
</div>
|
||||
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">
|
||||
<div className="command-text">{proof?.steps[i].command}</div>
|
||||
<Button to="" className="undo-button btn btn-inverted" title={t("Retry proof from here")} onClick={deleteProof}>
|
||||
<FontAwesomeIcon icon={faDeleteLeft} /> {t("Retry")}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
// const MessageView = React.memo(({uri, diag}: MessageViewProps) => {
|
||||
@@ -286,21 +340,25 @@ function Command({ command, deleteProof }: { command: string, deleteProof: any }
|
||||
// }, fastIsEqual)
|
||||
|
||||
/** 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)
|
||||
|
||||
if (proofStep.goals.length == 0) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return <div className="goal-tabs" onClick={onClick}>
|
||||
<div className={`tab-bar ${last ? 'current' : ''}`}>
|
||||
{proofStep.goals.map((goal, i) => (
|
||||
// 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() }}>
|
||||
{i ? `Goal ${i + 1}` : "Active Goal"}
|
||||
{i ? t("Goal") + ` ${i + 1}` : t("Active Goal")}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<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>
|
||||
}
|
||||
@@ -340,22 +398,27 @@ export function TypewriterInterfaceWrapper(props: { world: string, level: number
|
||||
|
||||
/** The interface in command line mode */
|
||||
export function TypewriterInterface({props}) {
|
||||
let { t } = useTranslation()
|
||||
const ec = React.useContext(EditorContext)
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const editor = React.useContext(MonacoEditorContext)
|
||||
const model = editor.getModel()
|
||||
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 [loadingProgress, setLoadingProgress] = React.useState<number>(0)
|
||||
const { setDeletedChat, showHelp, setShowHelp } = React.useContext(DeletedChatContext)
|
||||
const {mobile} = React.useContext(MobileContext)
|
||||
const { proof } = React.useContext(ProofContext)
|
||||
const {mobile} = React.useContext(PreferencesContext)
|
||||
const { proof, setProof, crashed, setCrashed, interimDiags } = React.useContext(ProofContext)
|
||||
const { setTypewriterInput } = React.useContext(InputModeContext)
|
||||
const { selectedStep, setSelectedStep } = React.useContext(SelectionContext)
|
||||
|
||||
const proofPanelRef = React.useRef<HTMLDivElement>(null)
|
||||
const completed = useAppSelector(selectCompleted(gameId, props.world, props.level))
|
||||
// const config = useEventResult(ec.events.changedInfoviewConfig) ?? defaultInfoviewConfig;
|
||||
// const curUri = useEventResult(ec.events.changedCursorLocation, loc => loc?.uri);
|
||||
|
||||
@@ -367,12 +430,17 @@ export function TypewriterInterface({props}) {
|
||||
function deleteProof(line: number) {
|
||||
return (ev) => {
|
||||
let deletedChat: Array<GameHint> = []
|
||||
filterHints(proof).slice(line).map((hintsAtStep, 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
|
||||
deletedChat = [...deletedChat, ...hintsAtStep.filter(hint => (!hint.hidden || showHelp.has(line + i)))]
|
||||
deletedChat = [...deletedChat, ...filteredHints.filter(hint => (!hint.hidden || showHelp.has(line + i)))]
|
||||
})
|
||||
setDeletedChat(deletedChat)
|
||||
|
||||
// delete showHelp for deleted steps
|
||||
setShowHelp(new Set(Array.from(showHelp).filter(i => i < line - 1)))
|
||||
|
||||
editor.executeEdits("typewriter", [{
|
||||
range: monaco.Selection.fromPositions(
|
||||
{ lineNumber: line, column: 1 },
|
||||
@@ -382,7 +450,9 @@ export function TypewriterInterface({props}) {
|
||||
forceMoveMarkers: false
|
||||
}])
|
||||
setSelectedStep(undefined)
|
||||
setTypewriterInput(proof[line].command)
|
||||
setTypewriterInput(proof?.steps[line].command)
|
||||
// Reload proof on deleting
|
||||
loadGoals(rpcSess, uri, setProof, setCrashed)
|
||||
ev.stopPropagation()
|
||||
}
|
||||
}
|
||||
@@ -402,7 +472,7 @@ export function TypewriterInterface({props}) {
|
||||
|
||||
// Scroll to the end of the proof if it is updated.
|
||||
React.useEffect(() => {
|
||||
if (proof?.length > 1) {
|
||||
if (proof?.steps.length > 1) {
|
||||
proofPanelRef.current?.lastElementChild?.scrollIntoView() //scrollTo(0,0)
|
||||
} else {
|
||||
proofPanelRef.current?.scrollTo(0,0)
|
||||
@@ -423,38 +493,8 @@ export function TypewriterInterface({props}) {
|
||||
}
|
||||
}, [selectedStep])
|
||||
|
||||
// TODO: This about hidden hints is all copied from `level.tsx`. Can we move that into `hints.tsx`?
|
||||
|
||||
// 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) => {
|
||||
// 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())}`)
|
||||
}
|
||||
|
||||
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
|
||||
// TODO: superfluous, can be replaced with `withErr` from above
|
||||
let lastStepErrors = proof?.steps.length ? hasInteractiveErrors(getInteractiveDiagsAt(proof, proof?.steps.length)) : false
|
||||
|
||||
|
||||
useServerNotificationEffect("$/game/loading", (params : any) => {
|
||||
@@ -470,24 +510,55 @@ export function TypewriterInterface({props}) {
|
||||
return <div className="typewriter-interface">
|
||||
<RpcContext.Provider value={rpcSess}>
|
||||
<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="world-image-container empty">
|
||||
|
||||
</div> */}
|
||||
</div>
|
||||
<div className='proof' ref={proofPanelRef}>
|
||||
<ExerciseStatement data={props.data} />
|
||||
{proof.length ?
|
||||
<>
|
||||
{proof.map((step, i) => {
|
||||
if (i == proof.length - 1 && lastStepErrors) {
|
||||
// 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}`}>
|
||||
<Errors errors={step.errors} typewriterMode={true} />
|
||||
{crashed ? <div>
|
||||
<p className="crashed_message">{t("Crashed! Go to editor mode and fix your proof! Last server response:")}</p>
|
||||
{interimDiags.map(diag => {
|
||||
const severityClass = diag.severity ? {
|
||||
[DiagnosticSeverity.Error]: 'error',
|
||||
[DiagnosticSeverity.Warning]: 'warning',
|
||||
[DiagnosticSeverity.Information]: 'information',
|
||||
[DiagnosticSeverity.Hint]: 'hint',
|
||||
}[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>
|
||||
} 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' : '')}>
|
||||
<Command command={step.command} deleteProof={deleteProof(i)} />
|
||||
<Errors errors={step.errors} typewriterMode={true} />
|
||||
<Command proof={proof} i={i} deleteProof={deleteProof(i)} />
|
||||
<Errors errors={step.diags} typewriterMode={true} />
|
||||
{mobile && i == 0 && props.data?.introduction &&
|
||||
<div className={`message information step-0${selectedStep === 0 ? ' selected' : ''}`} onClick={toggleSelectStep(0)}>
|
||||
<Markdown>{props.data?.introduction}</Markdown>
|
||||
@@ -495,22 +566,21 @@ export function TypewriterInterface({props}) {
|
||||
}
|
||||
{mobile &&
|
||||
<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)}/>
|
||||
}
|
||||
<GoalsTabs proofStep={step} last={i == proof.length - (lastStepErrors ? 2 : 1)} onClick={toggleSelectStep(i)} onGoalChange={i == proof.length - 1 - withErr ? (n) => setDisableInput(n > 0) : (n) => {}}/>
|
||||
|
||||
{mobile && i == proof.length - 1 &&
|
||||
hasHiddenHints(proof.length - 1) && !showHelp.has(k - withErr) &&
|
||||
<Button className="btn btn-help" to="" onClick={activateHiddenHints}>
|
||||
Show more help!
|
||||
</Button>
|
||||
{/* <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) => {}}/>
|
||||
}
|
||||
{mobile && i == proof?.steps.length - 1 &&
|
||||
<MoreHelpButton />
|
||||
}
|
||||
|
||||
{/* Show a message that there are no goals left */}
|
||||
{!step.goals.length && (
|
||||
{/* {!step.goals.length && (
|
||||
<div className="message information">
|
||||
{completed ?
|
||||
{proof?.completed ?
|
||||
<p>Level completed! 🎉</p> :
|
||||
<p>
|
||||
<b>no goals left</b><br />
|
||||
@@ -518,15 +588,21 @@ export function TypewriterInterface({props}) {
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
{mobile && completed &&
|
||||
//}
|
||||
)}
|
||||
{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 ?
|
||||
<Button to={`/${gameId}`}>
|
||||
<FontAwesomeIcon icon={faHome} /> Leave World
|
||||
<FontAwesomeIcon icon={faHome} /> {t("Leave World")}
|
||||
</Button>
|
||||
:
|
||||
<Button to={`/${gameId}/world/${props.world}/level/${props.level + 1}`}>
|
||||
@@ -535,11 +611,14 @@ export function TypewriterInterface({props}) {
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</> : <CircularProgress variant="determinate" value={loadingProgress} />
|
||||
</> : <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>
|
||||
<Typewriter hidden={!withErr && proof[proof.length - 1]?.goals.length == 0} disabled={disableInput || !proof.length}/>
|
||||
<Typewriter disabled={disableInput || !proof?.steps.length}/>
|
||||
</RpcContext.Provider>
|
||||
</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 { InputModeContext } from './context'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface MessageViewProps {
|
||||
uri: DocumentUri;
|
||||
@@ -79,7 +80,7 @@ const MessageView = React.memo(({uri, diag}: MessageViewProps) => {
|
||||
message = diag.message
|
||||
}
|
||||
|
||||
const { typewriterMode } = React.useContext(InputModeContext)
|
||||
const { typewriterMode, lockEditorMode } = React.useContext(InputModeContext)
|
||||
|
||||
return (
|
||||
// <details open>
|
||||
@@ -98,7 +99,7 @@ const MessageView = React.memo(({uri, diag}: MessageViewProps) => {
|
||||
// </span>
|
||||
// </summary>
|
||||
<div className={severityClass + ' ml1 message'}>
|
||||
{!typewriterMode && <p className="mv2">{title}</p>}
|
||||
{!(typewriterMode && !lockEditorMode) && <p className="mv2">{title}</p>}
|
||||
<pre className="font-code pre-wrap">
|
||||
<InteractiveMessage fmt={message} />
|
||||
</pre>
|
||||
@@ -194,17 +195,26 @@ export function AllMessages() {
|
||||
</a>
|
||||
</span>
|
||||
</summary> */}
|
||||
<AllMessagesBody uri={curPos.uri} key={curPos.uri} messages={iDiags0} />
|
||||
<AllMessagesBody uri={curPos.uri} key={curPos.uri} messages={iDiags0} curPos={curPos} />
|
||||
{/* </Details> */}
|
||||
</RpcContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
/** 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)
|
||||
React.useEffect(() => { void messages().then(setMsgs) }, [messages])
|
||||
if (msgs === undefined) return <div>Loading messages...</div>
|
||||
React.useEffect(() => { void messages().then(
|
||||
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}/>
|
||||
}
|
||||
|
||||
|
||||
@@ -3,46 +3,82 @@
|
||||
*
|
||||
* This file is based on `vscode-lean4/vscode-lean4/src/rpcApi.ts`
|
||||
*/
|
||||
import { ContextInfo, FVarId, CodeWithInfos, MVarId } from '@leanprover/infoview-api';
|
||||
|
||||
export interface GameHint {
|
||||
text: string;
|
||||
hidden: boolean;
|
||||
}
|
||||
import type { Range } from 'vscode-languageserver-protocol';
|
||||
import type { ContextInfo, FVarId, CodeWithInfos, MVarId } from '@leanprover/infoview-api';
|
||||
import { InteractiveDiagnostic, TermInfo } from '@leanprover/infoview/*';
|
||||
import type { Diagnostic } from 'vscode-languageserver-protocol';
|
||||
|
||||
export interface InteractiveHypothesisBundle {
|
||||
/** The pretty names of the variables in the bundle. Anonymous names are rendered
|
||||
* as `"[anonymous]"` whereas inaccessible ones have a `✝` appended at the end.
|
||||
* Use `InteractiveHypothesisBundle_nonAnonymousNames` to filter anonymouse ones out. */
|
||||
names: string[];
|
||||
/** Present since server version 1.1.2. */
|
||||
fvarIds?: FVarId[];
|
||||
type: CodeWithInfos;
|
||||
val?: CodeWithInfos;
|
||||
isInstance?: boolean;
|
||||
isType?: boolean;
|
||||
isAssumption?: boolean;
|
||||
isInserted?: boolean;
|
||||
isRemoved?: boolean;
|
||||
isAssumption?: boolean;
|
||||
}
|
||||
|
||||
export interface InteractiveGoalCore {
|
||||
hyps: InteractiveHypothesisBundle[];
|
||||
type: CodeWithInfos;
|
||||
/** Present since server version 1.1.2. */
|
||||
ctx?: ContextInfo;
|
||||
}
|
||||
|
||||
export interface InteractiveGoal extends InteractiveGoalCore {
|
||||
userName?: string;
|
||||
goalPrefix?: string;
|
||||
/** Present since server version 1.1.2. */
|
||||
mvarId?: MVarId;
|
||||
isInserted?: 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[];
|
||||
}
|
||||
|
||||
export interface InteractiveGoals {
|
||||
goals: InteractiveGoal[];
|
||||
export interface InteractiveGoalsWithHints {
|
||||
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 { Registry } from 'monaco-textmate' // peer dependency
|
||||
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 { AbbreviationRewriter } from 'lean4web/client/src/editor/abbreviation/rewriter/AbbreviationRewriter';
|
||||
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 codeblockSyntax from 'lean4web/client/src/syntaxes/codeblock.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 { DocumentPosition } from '../../../../node_modules/lean4-infoview/src/infoview/util';
|
||||
import { RpcContext } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions';
|
||||
import { DeletedChatContext, InputModeContext, MonacoEditorContext, ProofContext, ProofStep } from './context'
|
||||
import { goalsToString } from './goals'
|
||||
import { GameHint, InteractiveGoals } from './rpc_api'
|
||||
import { DeletedChatContext, InputModeContext, MonacoEditorContext, ProofContext } from './context'
|
||||
import { goalsToString, lastStepHasErrors, loadGoals } from './goals'
|
||||
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. */
|
||||
|
||||
@@ -64,7 +70,8 @@ config.autoClosingPairs = config.autoClosingPairs.map(
|
||||
monaco.languages.setLanguageConfiguration('lean4cmd', config);
|
||||
|
||||
/** 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 */
|
||||
const editor = React.useContext(MonacoEditorContext)
|
||||
@@ -79,109 +86,13 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
|
||||
const inputRef = useRef()
|
||||
|
||||
// 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
|
||||
const {setDeletedChat} = React.useContext(DeletedChatContext)
|
||||
|
||||
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
|
||||
const runCommand = React.useCallback(() => {
|
||||
if (processing) {return}
|
||||
@@ -201,6 +112,8 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
|
||||
forceMoveMarkers: false
|
||||
}])
|
||||
setTypewriterInput('')
|
||||
// Load proof after executing edits
|
||||
loadGoals(rpcSess, uri, setProof, setCrashed)
|
||||
}
|
||||
|
||||
editor.setPosition(pos)
|
||||
@@ -212,9 +125,15 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
|
||||
}
|
||||
}, [typewriterInput])
|
||||
|
||||
/* Load proof on start/switching to typewriter */
|
||||
useEffect(() => {
|
||||
if (proof.length && hasInteractiveErrors(proof[proof.length - 1].errors)) {
|
||||
setTypewriterInput(proof[proof.length - 1].command)
|
||||
loadGoals(rpcSess, uri, setProof, setCrashed)
|
||||
}, [])
|
||||
|
||||
/** 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])
|
||||
|
||||
@@ -222,7 +141,14 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
|
||||
useServerNotificationEffect('textDocument/publishDiagnostics', (params: PublishDiagnosticsParams) => {
|
||||
if (params.uri == uri) {
|
||||
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)) {
|
||||
//setTypewriterInput("")
|
||||
editor.setPosition(editor.getModel().getFullModelRange().getEndPosition())
|
||||
@@ -236,6 +162,15 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
|
||||
// loadAllGoals()
|
||||
}, [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(() => {
|
||||
const myEditor = monaco.editor.create(inputRef.current!, {
|
||||
value: typewriterInput,
|
||||
@@ -306,7 +241,8 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
|
||||
useEffect(() => {
|
||||
console.debug(`time to update: ${uri} \n ${rpcSess}`)
|
||||
console.debug(rpcSess)
|
||||
loadAllGoals()
|
||||
// console.debug('LOAD ALL GOALS')
|
||||
// TODO: loadAllGoals()
|
||||
}, [rpcSess])
|
||||
|
||||
/** Process the entered command */
|
||||
@@ -315,13 +251,14 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
|
||||
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}>
|
||||
<div className="typewriter-input-wrapper">
|
||||
<div ref={inputRef} className="typewriter-input" />
|
||||
</div>
|
||||
<button type="submit" disabled={processing} className="btn btn-inverted">
|
||||
<FontAwesomeIcon icon={faWandMagicSparkles} /> Execute
|
||||
<FontAwesomeIcon icon={faWandMagicSparkles} /> {t("Execute")}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -343,3 +280,14 @@ export function hasInteractiveErrors (diags: InteractiveDiagnostic[]) {
|
||||
(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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import { useLoadDocQuery, InventoryTile, LevelInfo, InventoryOverview, useLoadIn
|
||||
import { selectDifficulty, selectInventory } from '../state/progress';
|
||||
import { store } from '../state/store';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { t } from 'i18next';
|
||||
|
||||
export function Inventory({levelInfo, openDoc, lemmaTab, setLemmaTab, enableAll=false} :
|
||||
{
|
||||
@@ -19,20 +21,21 @@ export function Inventory({levelInfo, openDoc, lemmaTab, setLemmaTab, enableAll=
|
||||
setLemmaTab: any,
|
||||
enableAll?: boolean,
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="inventory">
|
||||
{/* TODO: Click on Tactic: show info
|
||||
TODO: click on paste icon -> paste into command line */}
|
||||
<h2>Tactics</h2>
|
||||
<h2>{t("Tactics")}</h2>
|
||||
{levelInfo?.tactics &&
|
||||
<InventoryList items={levelInfo?.tactics} docType="Tactic" openDoc={openDoc} enableAll={enableAll}/>
|
||||
}
|
||||
<h2>Definitions</h2>
|
||||
<h2>{t("Definitions")}</h2>
|
||||
{levelInfo?.definitions &&
|
||||
<InventoryList items={levelInfo?.definitions} docType="Definition" openDoc={openDoc} enableAll={enableAll}/>
|
||||
}
|
||||
<h2>Theorems</h2>
|
||||
<h2>{t("Theorems")}</h2>
|
||||
{levelInfo?.lemmas &&
|
||||
<InventoryList items={levelInfo?.lemmas} docType="Lemma" openDoc={openDoc} level={levelInfo} enableAll={enableAll} tab={lemmaTab} setTab={setLemmaTab}/>
|
||||
}
|
||||
@@ -100,8 +103,8 @@ function InventoryItem({item, name, displayName, locked, disabled, newly, showDo
|
||||
const className = locked ? "locked" : disabled ? "disabled" : newly ? "new" : ""
|
||||
// Note: This is somewhat a hack as the statement of lemmas comes currently in the form
|
||||
// `Namespace.statement_name (x y : Nat) : some type`
|
||||
const title = locked ? "Not unlocked yet" :
|
||||
disabled ? "Not available in this level" : (item.altTitle ? item.altTitle.substring(item.altTitle.indexOf(' ') + 1) : '')
|
||||
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)
|
||||
|
||||
@@ -137,7 +140,7 @@ export function Documentation({name, type, handleClose}) {
|
||||
<h1 className="doc">{doc.data?.displayName}</h1>
|
||||
<p><code>{doc.data?.statement}</code></p>
|
||||
{/* <code>docstring: {doc.data?.docstring}</code> */}
|
||||
<Markdown>{doc.data?.content}</Markdown>
|
||||
<Markdown>{t(doc.data?.content, {ns: gameId})}</Markdown>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import '@fontsource/roboto/300.css';
|
||||
import '@fontsource/roboto/400.css';
|
||||
@@ -14,14 +15,11 @@ import {PrivacyPolicyPopup} from './popup/privacy_policy'
|
||||
import { GameTile, useGetGameInfoQuery } from '../state/api'
|
||||
import path from 'path';
|
||||
|
||||
const flag = {
|
||||
'Dutch': '🇳🇱',
|
||||
'English': '🇬🇧',
|
||||
'French': '🇫🇷',
|
||||
'German': '🇩🇪',
|
||||
'Italian': '🇮🇹',
|
||||
'Spanish': '🇪🇸',
|
||||
}
|
||||
import { PreferencesPopup } from './popup/preferences';
|
||||
import { ImpressumButton, MenuButton, PreferencesButton } from './app_bar';
|
||||
import ReactCountryFlag from 'react-country-flag';
|
||||
import lean4gameConfig from '../config.json'
|
||||
import i18next from 'i18next';
|
||||
|
||||
function GithubIcon({url='https://github.com'}) {
|
||||
|
||||
@@ -35,7 +33,7 @@ function GithubIcon({url='https://github.com'}) {
|
||||
}
|
||||
|
||||
function Tile({gameId, data}: {gameId: string, data: GameTile|undefined}) {
|
||||
|
||||
let { t } = useTranslation()
|
||||
let navigate = useNavigate();
|
||||
const routeChange = () =>{
|
||||
navigate(gameId);
|
||||
@@ -47,29 +45,36 @@ function Tile({gameId, data}: {gameId: string, data: GameTile|undefined}) {
|
||||
|
||||
return <div className="game" onClick={routeChange}>
|
||||
<div className="wrapper">
|
||||
<div className="title">{data.title}</div>
|
||||
<div className="short-description">{data.short}
|
||||
<div className="title">{t(data.title, { ns: gameId })}</div>
|
||||
<div className="short-description">{t(data.short, { ns: gameId })}
|
||||
</div>
|
||||
{ data.image ? <img className="image" src={path.join("data", gameId, data.image)} alt="" /> : <div className="image"/> }
|
||||
<div className="long description"><Markdown>{data.long}</Markdown></div>
|
||||
<div className="long description"><Markdown>{t(data.long, { ns: gameId })}</Markdown></div>
|
||||
</div>
|
||||
<table className="info">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td title="consider playing these games first.">Prerequisites</td>
|
||||
<td><Markdown>{data.prerequisites.join(', ')}</Markdown></td>
|
||||
<td title="consider playing these games first.">{t("Prerequisites")}</td>
|
||||
<td><Markdown>{t(data.prerequisites.join(', '), { ns: gameId })}</Markdown></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Worlds</td>
|
||||
<td>{t("Worlds")}</td>
|
||||
<td>{data.worlds}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Levels</td>
|
||||
<td>{t("Levels")}</td>
|
||||
<td>{data.levels}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Language</td>
|
||||
<td title={`in ${data.languages.join(', ')}`}>{data.languages.map((lan) => flag[lan]).join(', ')}</td>
|
||||
<td>{t("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>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -81,83 +86,78 @@ function LandingPage() {
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [impressum, setImpressum] = React.useState(false);
|
||||
const openImpressum = () => setImpressum(true);
|
||||
const closeImpressum = () => setImpressum(false);
|
||||
const [impressumPopup, setImpressumPopup] = React.useState(false);
|
||||
const [preferencesPopup, setPreferencesPopup] = React.useState(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 [allGames, setAllGames] = React.useState([])
|
||||
// const [allTiles, setAllTiles] = React.useState([])
|
||||
const [usageCPU, setUsageCPU] = React.useState<number>()
|
||||
const [usageMem, setUsageMem] = React.useState<number>()
|
||||
|
||||
// const getTiles=()=>{
|
||||
// fetch('featured_games.json', {
|
||||
// headers : {
|
||||
// 'Content-Type': 'application/json',
|
||||
// 'Accept': 'application/json'
|
||||
// }
|
||||
// }
|
||||
// ).then(function(response){
|
||||
// return response.json()
|
||||
// }).then(function(data) {
|
||||
// setAllGames(data.featured_games)
|
||||
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}`))
|
||||
|
||||
// React.useEffect(()=>{
|
||||
// getTiles()
|
||||
// },[])
|
||||
let allTiles = lean4gameConfig.allGames.map((gameId) => {
|
||||
let q = useGetGameInfoQuery({game: `g/${gameId}`})
|
||||
|
||||
// React.useEffect(()=>{
|
||||
// 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);
|
||||
// }
|
||||
// }
|
||||
|
||||
// Promise.allSettled(
|
||||
// allGames.map((gameId) => (
|
||||
// fetch(`data/g/${gameId}/game.json`).catch(err => {return undefined})))
|
||||
// ).then(responses =>
|
||||
// responses.forEach((result) => console.log(result)))
|
||||
// // Promise.all(responses.map(res => {
|
||||
// // if (res.status == "fulfilled") {
|
||||
// // console.log(res.value.json())
|
||||
// // return res.value.json()
|
||||
// // } else {
|
||||
// // return undefined
|
||||
// // }
|
||||
// // }))
|
||||
// // ).then(allData => {
|
||||
// // setAllTiles(allData.map(data => data?.tile))
|
||||
// // })
|
||||
// },[allGames])
|
||||
return q.data?.tile
|
||||
})
|
||||
|
||||
// TODO: I would like to read the supported games list form a JSON,
|
||||
// Then load all these games in
|
||||
//
|
||||
let allGames = [
|
||||
"leanprover-community/nng4",
|
||||
"hhu-adam/robo",
|
||||
"djvelleman/stg4",
|
||||
"miguelmarco/STG4",
|
||||
]
|
||||
let allTiles = allGames.map((gameId) => (useGetGameInfoQuery({game: `g/${gameId}`}).data?.tile))
|
||||
/** Parse `games/stats.csv` if present and display server capacity. */
|
||||
React.useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
fetch_stats();
|
||||
}, 2000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return <div className="landing-page">
|
||||
<header style={{backgroundImage: `url(${bgImage})`}}>
|
||||
<nav>
|
||||
<nav className="landing-page-nav">
|
||||
<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>
|
||||
<div id="main-title">
|
||||
<h1>Lean Game Server</h1>
|
||||
<h1>{t("Lean Game Server")}</h1>
|
||||
<p>
|
||||
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
|
||||
its mathematical library <a target="_blank" href="https://github.com/leanprover-community/mathlib4">mathlib</a>
|
||||
<Trans>
|
||||
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
|
||||
its mathematical library <a target="_blank" href="https://github.com/leanprover-community/mathlib4">mathlib</a>
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
<div className="game-list">
|
||||
{allTiles.length == 0 ?
|
||||
<p>No Games loaded. Use <a>http://localhost:3000/#/g/local/FOLDER</a> to open a
|
||||
game directly from a local folder.
|
||||
{allTiles.filter(x => x != null).length == 0 ?
|
||||
<p>
|
||||
<Trans>
|
||||
No Games loaded. Use <a>http://localhost:3000/#/g/local/FOLDER</a> to open a
|
||||
game directly from a local folder.
|
||||
</Trans>
|
||||
</p>
|
||||
: allGames.map((id, i) => (
|
||||
: lean4gameConfig.allGames.map((id, i) => (
|
||||
<Tile
|
||||
key={id}
|
||||
gameId={`g/${id}`}
|
||||
@@ -166,58 +166,104 @@ function LandingPage() {
|
||||
))
|
||||
}
|
||||
</div>
|
||||
{ // show server capacity from `games/stats.csv` if present
|
||||
(usageMem >= 0 || usageCPU >= 0 ) &&
|
||||
<section>
|
||||
<div className="wrapper">
|
||||
<h2>{t("Server capacity")}</h2>
|
||||
<Trans>
|
||||
<p>
|
||||
As this server runs lean on our university machines, it has a limited capacity.
|
||||
Our current estimate is about 70 simultaneous games.
|
||||
</p>
|
||||
</Trans>
|
||||
<p>
|
||||
{ usageMem >= 0 && <> {t("RAM")}: <strong>{usageMem.toFixed(2)} %</strong>{t(" used")}.<br/></> }
|
||||
{ usageCPU >= 0 && <> {t("CPU")}: <strong>{usageCPU.toFixed(2)} %</strong>{t(" used")}. </> }
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
<section>
|
||||
<div className="wrapper">
|
||||
<h2>Development notes</h2>
|
||||
<p>
|
||||
As this server runs lean on our university machines, it has a limited capacity.
|
||||
Our current estimate is about 70 simultaneous games.
|
||||
We hope to address and test this limitation better in the future.
|
||||
</p>
|
||||
<p>
|
||||
Most aspects of the games and the infrastructure are still in development. Feel free to
|
||||
file a <a target="_blank" href="https://github.com/leanprover-community/lean4game/issues">GitHub Issue</a> about
|
||||
any problems you experience!
|
||||
</p>
|
||||
<h2>{t("Development notes")}</h2>
|
||||
<Trans>
|
||||
<p>
|
||||
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
|
||||
any problems you experience!
|
||||
</p>
|
||||
</Trans>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div className="wrapper">
|
||||
<h2>Adding new games</h2>
|
||||
<p>
|
||||
If you are considering writing your own game, you should use
|
||||
the <a target="_blank" href="https://github.com/hhu-adam/GameSkeleton">GameSkeleton Github Repo</a> as
|
||||
a template and read <a target="_blank" href="https://github.com/leanprover-community/lean4game/">How to Create a Game</a>.
|
||||
</p>
|
||||
<p>
|
||||
You can directly load your games into the server and play it using
|
||||
the correct URL. The <a target="_blank" href="https://github.com/leanprover-community/lean4game/">instructions above</a> also
|
||||
explain the details for how to load your game to the server.
|
||||
<h2>{t("Adding new games")}</h2>
|
||||
<Trans>
|
||||
<p>
|
||||
If you are considering writing your own game, you should use
|
||||
the <a target="_blank" href="https://github.com/hhu-adam/GameSkeleton">GameSkeleton Github Repo</a> as
|
||||
a template and read <a target="_blank" href="https://github.com/leanprover-community/lean4game/">How to Create a Game</a>.
|
||||
</p>
|
||||
<p>
|
||||
You can directly load your games into the server and play it using
|
||||
the correct URL. The <a target="_blank" href="https://github.com/leanprover-community/lean4game/">instructions above</a> also
|
||||
explain the details for how to load your game to the server.
|
||||
|
||||
We'd like to encourage you to contact us if you have any questions.
|
||||
</p>
|
||||
<p>
|
||||
Featured games on this page are added manually.
|
||||
Please get in contact and we-ll happily add yours.
|
||||
</p>
|
||||
We'd like to encourage you to contact us if you have any questions.
|
||||
</p>
|
||||
<p>
|
||||
Featured games on this page are added manually.
|
||||
Please get in contact and we'll happily add yours.
|
||||
</p>
|
||||
</Trans>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div className="wrapper">
|
||||
<h2>Funding</h2>
|
||||
<h2>{t("Funding")}</h2>
|
||||
<p>
|
||||
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
|
||||
Heinrich-Heine-Universität in Düsseldorf.
|
||||
<Trans>
|
||||
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
|
||||
Heinrich Heine University Düsseldorf.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<footer>
|
||||
{/* Do not translate "Impressum", it's needed for German GDPR */}
|
||||
<a className="link" onClick={openImpressum}>Impressum</a>
|
||||
{impressum? <PrivacyPolicyPopup handleClose={closeImpressum} />: null}
|
||||
{impressumPopup? <PrivacyPolicyPopup handleClose={closeImpressum} />: null}
|
||||
{preferencesPopup ? <PreferencesPopup handleClose={closePreferencesPopup} /> : null}
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
||||
function fetch_stats() {
|
||||
fetch(`${window.location.origin}/data/stats`)
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response.text();
|
||||
} else { throw ""; }
|
||||
})
|
||||
.then(data => {
|
||||
// Parse the CSV content
|
||||
const lines = data.split('\n');
|
||||
const [header, line2] = lines;
|
||||
if (!(header.replace(' ', '').startsWith("CPU,MEM"))) {
|
||||
console.info("Not displaying server stats: received unexpected: ", header);
|
||||
}
|
||||
if (line2) {
|
||||
let values = line2.split(',');
|
||||
setUsageCPU(100 * parseFloat(values[0]));
|
||||
setUsageMem(100 * parseFloat(values[1]));
|
||||
}
|
||||
}).catch(err => {
|
||||
console.info('server stats unavailable');
|
||||
console.debug(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default LandingPage
|
||||
|
||||
+105
-89
@@ -16,6 +16,7 @@ import { InfoviewApi } from '@leanprover/infoview'
|
||||
import { EditorContext } from '../../../node_modules/lean4-infoview/src/infoview/contexts'
|
||||
import { EditorConnection, EditorEvents } from '../../../node_modules/lean4-infoview/src/infoview/editorConnection'
|
||||
import { EventEmitter } from '../../../node_modules/lean4-infoview/src/infoview/event'
|
||||
import { Diagnostic } from 'vscode-languageserver-types'
|
||||
|
||||
import { GameIdContext } from '../app'
|
||||
import { useAppDispatch, useAppSelector } from '../hooks'
|
||||
@@ -27,11 +28,11 @@ import { Button } from './button'
|
||||
import Markdown from './markdown'
|
||||
import {InventoryPanel} from './inventory'
|
||||
import { hasInteractiveErrors } from './infoview/typewriter'
|
||||
import { DeletedChatContext, InputModeContext, MobileContext, MonacoEditorContext,
|
||||
ProofContext, ProofStep, SelectionContext, WorldLevelIdContext } from './infoview/context'
|
||||
import { DeletedChatContext, InputModeContext, PreferencesContext, MonacoEditorContext,
|
||||
ProofContext, SelectionContext, WorldLevelIdContext } from './infoview/context'
|
||||
import { DualEditor } from './infoview/main'
|
||||
import { GameHint } from './infoview/rpc_api'
|
||||
import { DeletedHints, Hint, Hints, filterHints } from './hints'
|
||||
import { GameHint, InteractiveGoalsWithHints, ProofState } from './infoview/rpc_api'
|
||||
import { DeletedHints, Hint, Hints, MoreHelpButton, filterHints } from './hints'
|
||||
import { PrivacyPolicyPopup } from './popup/privacy_policy'
|
||||
import path from 'path';
|
||||
|
||||
@@ -49,6 +50,11 @@ 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()
|
||||
@@ -58,23 +64,41 @@ function Level() {
|
||||
const levelId = parseInt(params.levelId)
|
||||
const worldId = params.worldId
|
||||
|
||||
const [impressum, setImpressum] = React.useState(false)
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
|
||||
const closeImpressum = () => {
|
||||
setImpressum(false)
|
||||
}
|
||||
// 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 [info, setInfo] = React.useState(false)
|
||||
const [preferencesPopup, setPreferencesPopup] = React.useState(false)
|
||||
|
||||
function closeImpressum() {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}}>
|
||||
{levelId == 0 ?
|
||||
<Introduction impressum={impressum} setImpressum={setImpressum} /> :
|
||||
<PlayableLevel key={`${worldId}/${levelId}`} impressum={impressum} setImpressum={setImpressum} />}
|
||||
<Introduction impressum={impressum} setImpressum={setImpressum} toggleInfo={toggleInfo} togglePreferencesPopup={togglePreferencesPopup} /> :
|
||||
<PlayableLevel key={`${worldId}/${levelId}`} impressum={impressum} setImpressum={setImpressum} toggleInfo={toggleInfo} togglePreferencesPopup={togglePreferencesPopup}/>}
|
||||
{impressum ? <PrivacyPolicyPopup handleClose={closeImpressum} /> : null}
|
||||
{info ? <InfoPopup info={gameInfo.data?.info} handleClose={closeInfo}/> : null}
|
||||
{preferencesPopup ? <PreferencesPopup handleClose={closePreferencesPopup} /> : null}
|
||||
</WorldLevelIdContext.Provider>
|
||||
}
|
||||
|
||||
function ChatPanel({lastLevel}) {
|
||||
function ChatPanel({lastLevel, visible = true}) {
|
||||
let { t } = useTranslation()
|
||||
const chatRef = useRef<HTMLDivElement>(null)
|
||||
const {mobile} = useContext(MobileContext)
|
||||
const {mobile} = useContext(PreferencesContext)
|
||||
const gameId = useContext(GameIdContext)
|
||||
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
||||
const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
||||
@@ -83,9 +107,7 @@ function ChatPanel({lastLevel}) {
|
||||
const {selectedStep, setSelectedStep} = useContext(SelectionContext)
|
||||
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.length - 1
|
||||
let withErr = hasInteractiveErrors(proof[k]?.errors) ? 1 : 0
|
||||
let k = proof?.steps.length ? proof?.steps.length - (lastStepHasErrors(proof) ? 2 : 1) : 0
|
||||
|
||||
function toggleSelection(line: number) {
|
||||
return (ev) => {
|
||||
@@ -98,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(() => {
|
||||
// TODO: For some reason this is always called twice
|
||||
console.debug('scroll chat')
|
||||
@@ -144,62 +143,64 @@ function ChatPanel({lastLevel}) {
|
||||
// // chatRef.current!.scrollTo(0,0)
|
||||
// }, [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)+/)
|
||||
|
||||
// experimental: Remove all hints that appeared identically in the previous step
|
||||
// This effectively prevent consequtive hints being shown.
|
||||
let modifiedHints : GameHint[][] = filterHints(proof)
|
||||
|
||||
return <div className="chat-panel">
|
||||
return <div className={`chat-panel ${visible ? '' : 'hidden'}`}>
|
||||
<div ref={chatRef} className="chat">
|
||||
{introText?.filter(t => t.trim()).map(((t, i) =>
|
||||
// Show the level's intro text as hints, too
|
||||
<Hint key={`intro-p-${i}`}
|
||||
hint={{text: t, hidden: false}} step={0} selected={selectedStep} toggleSelection={toggleSelection(0)} />
|
||||
hint={{text: t, hidden: false, rawText: t, varNames: []}} step={0} selected={selectedStep} toggleSelection={toggleSelection(0)} />
|
||||
))}
|
||||
{modifiedHints.map((step, i) => {
|
||||
{proof?.steps.map((step, i) => {
|
||||
let filteredHints = filterHints(step.goals[0]?.hints, proof?.steps[i-1]?.goals[0]?.hints)
|
||||
if (step.goals.length > 0 && !isLastStepWithErrors(proof, i)) {
|
||||
return <Hints key={`hints-${i}`}
|
||||
hints={filteredHints} showHidden={showHelp.has(i)} step={i}
|
||||
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.length - 1 && withErr)) {
|
||||
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.length - 1}/>
|
||||
selected={selectedStep} toggleSelection={toggleSelection(i)} lastLevel={i == proof?.steps.length - 1}/>
|
||||
}
|
||||
})}
|
||||
})} */}
|
||||
<DeletedHints hints={deletedChat}/>
|
||||
{completed &&
|
||||
{proof?.completed &&
|
||||
<>
|
||||
<div className={`message information recent step-${k}${selectedStep == k ? ' selected' : ''}`} onClick={toggleSelection(k)}>
|
||||
Level completed! 🎉
|
||||
{t("Level completed! 🎉")}
|
||||
</div>
|
||||
{level?.data?.conclusion?.trim() &&
|
||||
<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 className="button-row">
|
||||
{completed && (lastLevel ?
|
||||
{proof?.completed && (lastLevel ?
|
||||
<Button to={`/${gameId}`}>
|
||||
<FontAwesomeIcon icon={faHome} /> Leave World
|
||||
<FontAwesomeIcon icon={faHome} /> {t("Leave World")}
|
||||
</Button> :
|
||||
<Button to={`/${gameId}/world/${worldId}/level/${levelId + 1}`}>
|
||||
Next <FontAwesomeIcon icon={faArrowRight} />
|
||||
{t("Next")} <FontAwesomeIcon icon={faArrowRight} />
|
||||
</Button>)
|
||||
}
|
||||
{hasHiddenHints(proof.length - 1) && !showHelp.has(k - withErr) &&
|
||||
<Button to="" onClick={activateHiddenHints}>
|
||||
Show more help!
|
||||
</Button>
|
||||
}
|
||||
<MoreHelpButton />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
function ExercisePanel({codeviewRef, visible=true}) {
|
||||
|
||||
function ExercisePanel({codeviewRef, visible=true}: {codeviewRef: React.MutableRefObject<HTMLDivElement>, visible?: boolean}) {
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
||||
const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
||||
@@ -211,11 +212,12 @@ function ExercisePanel({codeviewRef, visible=true}) {
|
||||
</div>
|
||||
}
|
||||
|
||||
function PlayableLevel({impressum, setImpressum}) {
|
||||
function PlayableLevel({impressum, setImpressum, toggleInfo, togglePreferencesPopup}) {
|
||||
let { t } = useTranslation()
|
||||
const codeviewRef = useRef<HTMLDivElement>(null)
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
||||
const {mobile} = React.useContext(MobileContext)
|
||||
const {mobile} = React.useContext(PreferencesContext)
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
@@ -229,7 +231,11 @@ function PlayableLevel({impressum, setImpressum}) {
|
||||
const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
||||
|
||||
// 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
|
||||
// a new proof has been entered. e.g. to consult messages coming from dead ends
|
||||
const [deletedChat, setDeletedChat] = useState<Array<GameHint>>([])
|
||||
@@ -239,7 +245,7 @@ function PlayableLevel({impressum, setImpressum}) {
|
||||
const [pageNumber, setPageNumber] = useState(0)
|
||||
|
||||
// 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 lastLevel = levelId >= gameInfo.data?.worldSize[worldId]
|
||||
|
||||
@@ -299,10 +305,11 @@ function PlayableLevel({impressum, setImpressum}) {
|
||||
// a hint at the beginning of the proof...
|
||||
const [selectedStep, setSelectedStep] = useState<number>()
|
||||
|
||||
|
||||
useEffect (() => {
|
||||
// Lock editor mode
|
||||
if (level?.data?.template) {
|
||||
setTypewriterMode(false)
|
||||
setLockEditorMode(true)
|
||||
|
||||
if (editor) {
|
||||
let code = editor.getModel().getLinesContent()
|
||||
@@ -329,6 +336,8 @@ function PlayableLevel({impressum, setImpressum}) {
|
||||
console.debug(`not inserting template.`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setLockEditorMode(false)
|
||||
}
|
||||
}, [level, levelId, worldId, gameId, editor])
|
||||
|
||||
@@ -343,7 +352,7 @@ function PlayableLevel({impressum, setImpressum}) {
|
||||
}, [gameId, worldId, levelId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!typewriterMode && editor) {
|
||||
if (!(typewriterMode && !lockEditorMode) && editor) {
|
||||
// Delete last input attempt from command line
|
||||
editor.executeEdits("typewriter", [{
|
||||
range: editor.getSelection(),
|
||||
@@ -352,19 +361,19 @@ function PlayableLevel({impressum, setImpressum}) {
|
||||
}]);
|
||||
editor.focus()
|
||||
}
|
||||
}, [typewriterMode])
|
||||
}, [typewriterMode, lockEditorMode])
|
||||
|
||||
useEffect(() => {
|
||||
// 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))
|
||||
setShowHelp(new Set(Array.from(showHelp).filter(i => (i < proof.length))))
|
||||
setShowHelp(new Set(Array.from(showHelp).filter(i => (i < proof?.steps.length))))
|
||||
}
|
||||
}, [proof])
|
||||
|
||||
// save showed help in store
|
||||
useEffect(() => {
|
||||
if (proof.length) {
|
||||
if (proof?.steps.length) {
|
||||
console.debug(`showHelp:\n ${showHelp}`)
|
||||
dispatch(helpEdited({game: gameId, world: worldId, level: levelId, help: Array.from(showHelp)}))
|
||||
}
|
||||
@@ -372,7 +381,7 @@ function PlayableLevel({impressum, setImpressum}) {
|
||||
|
||||
// Effect when command line mode gets enabled
|
||||
useEffect(() => {
|
||||
if (onigasmH && editor && typewriterMode) {
|
||||
if (onigasmH && editor && (typewriterMode && !lockEditorMode)) {
|
||||
let code = editor.getModel().getLinesContent().filter(line => line.trim())
|
||||
editor.executeEdits("typewriter", [{
|
||||
range: editor.getModel().getFullModelRange(),
|
||||
@@ -395,22 +404,25 @@ function PlayableLevel({impressum, setImpressum}) {
|
||||
// editor.setSelection(monaco.Selection.fromPositions(endPos, endPos))
|
||||
// }
|
||||
}
|
||||
}, [editor, typewriterMode, onigasmH == null])
|
||||
}, [editor, typewriterMode, lockEditorMode, onigasmH == null])
|
||||
|
||||
return <>
|
||||
<div style={level.isLoading ? null : {display: "none"}} className="app-content loading"><CircularProgress /></div>
|
||||
<DeletedChatContext.Provider value={{deletedChat, setDeletedChat, showHelp, setShowHelp}}>
|
||||
<SelectionContext.Provider value={{selectedStep, setSelectedStep}}>
|
||||
<InputModeContext.Provider value={{typewriterMode, setTypewriterMode, typewriterInput, setTypewriterInput, lockInputMode, setLockInputMode}}>
|
||||
<ProofContext.Provider value={{proof, setProof}}>
|
||||
<InputModeContext.Provider value={{typewriterMode, setTypewriterMode, typewriterInput, setTypewriterInput, lockEditorMode, setLockEditorMode}}>
|
||||
<ProofContext.Provider value={{proof, setProof, interimDiags, setInterimDiags, crashed: isCrashed, setCrashed: setIsCrashed}}>
|
||||
<EditorContext.Provider value={editorConnection}>
|
||||
<MonacoEditorContext.Provider value={editor}>
|
||||
<LevelAppBar
|
||||
pageNumber={pageNumber} setPageNumber={setPageNumber}
|
||||
isLoading={level.isLoading}
|
||||
levelTitle={`${mobile ? '' : 'Level '}${levelId} / ${gameInfo.data?.worldSize[worldId]}` +
|
||||
(level?.data?.title && ` : ${level?.data?.title}`)}
|
||||
toggleImpressum={toggleImpressum} />
|
||||
levelTitle={(mobile ? "" : t("Level")) + ` ${levelId} / ${gameInfo.data?.worldSize[worldId]}` +
|
||||
(level?.data?.title && ` : ${t(level?.data?.title, {ns: gameId})}`)}
|
||||
toggleImpressum={toggleImpressum}
|
||||
toggleInfo={toggleInfo}
|
||||
togglePreferencesPopup={togglePreferencesPopup}
|
||||
/>
|
||||
{mobile?
|
||||
// TODO: This is copied from the `Split` component below...
|
||||
<>
|
||||
@@ -439,24 +451,25 @@ function PlayableLevel({impressum, setImpressum}) {
|
||||
}
|
||||
|
||||
function IntroductionPanel({gameInfo}) {
|
||||
let { t } = useTranslation()
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const {worldId} = useContext(WorldLevelIdContext)
|
||||
const {mobile} = React.useContext(MobileContext)
|
||||
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">
|
||||
<div className="chat">
|
||||
{text?.filter(t => t.trim()).map(((t, 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 className={`button-row${mobile ? ' mobile' : ''}`}>
|
||||
{gameInfo.data?.worldSize[worldId] == 0 ?
|
||||
<Button to={`/${gameId}`}><FontAwesomeIcon icon={faHome} /></Button> :
|
||||
<Button to={`/${gameId}/world/${worldId}/level/1`}>
|
||||
Start <FontAwesomeIcon icon={faArrowRight} />
|
||||
{t("Start")} <FontAwesomeIcon icon={faArrowRight} />
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
@@ -466,9 +479,11 @@ function IntroductionPanel({gameInfo}) {
|
||||
export default Level
|
||||
|
||||
/** 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 {mobile} = useContext(MobileContext)
|
||||
const {mobile} = useContext(PreferencesContext)
|
||||
|
||||
const inventory = useLoadInventoryOverviewQuery({game: gameId})
|
||||
|
||||
@@ -484,7 +499,7 @@ function Introduction({impressum, setImpressum}) {
|
||||
}
|
||||
|
||||
return <>
|
||||
<LevelAppBar isLoading={gameInfo.isLoading} levelTitle="Introduction" toggleImpressum={toggleImpressum}/>
|
||||
<LevelAppBar isLoading={gameInfo.isLoading} levelTitle={t("Introduction")} toggleImpressum={toggleImpressum} toggleInfo={toggleInfo} togglePreferencesPopup={togglePreferencesPopup}/>
|
||||
{gameInfo.isLoading ?
|
||||
<div className="app-content loading"><CircularProgress /></div>
|
||||
: mobile ?
|
||||
@@ -492,10 +507,9 @@ function Introduction({impressum, setImpressum}) {
|
||||
:
|
||||
<Split minSize={0} snapOffset={200} sizes={[25, 50, 25]} className={`app-content level`}>
|
||||
<IntroductionPanel gameInfo={gameInfo} />
|
||||
<div className="world-image-container empty">
|
||||
<div className="world-image-container empty center">
|
||||
{image &&
|
||||
// TODO: Temporary for testing
|
||||
<img className={worldId=="Proposition" ? "cover" : "contain"} src={path.join("data", gameId, image)} alt="" />
|
||||
<img className="contain" src={path.join("data", gameId, image)} alt="" />
|
||||
}
|
||||
|
||||
</div>
|
||||
@@ -622,7 +636,9 @@ function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChange
|
||||
}
|
||||
// loadRenderInfoview(imports, [infoProvider.getApi(), div], setInfoviewApi)
|
||||
setInfoProvider(infoProvider)
|
||||
client.restart()
|
||||
|
||||
// TODO: it looks like we get errors "File Changed" here.
|
||||
client.restart("Lean4Game")
|
||||
|
||||
const editorApi = infoProvider.getApi()
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useAppDispatch } from '../../hooks'
|
||||
import { deleteProgress, selectProgress } from '../../state/progress'
|
||||
import { downloadFile } from '../world_tree'
|
||||
import { Button } from '../button'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
/** download the current progress (i.e. what's saved in the browser store) */
|
||||
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.
|
||||
*/
|
||||
export function ErasePopup ({handleClose}) {
|
||||
let { t } = useTranslation()
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const gameProgress = useSelector(selectProgress(gameId))
|
||||
const dispatch = useAppDispatch()
|
||||
@@ -43,17 +45,17 @@ export function ErasePopup ({handleClose}) {
|
||||
<div className="modal-backdrop" onClick={handleClose} />
|
||||
<div className="modal">
|
||||
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
||||
<h2>Delete Progress?</h2>
|
||||
|
||||
<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>
|
||||
|
||||
<Button onClick={eraseProgress} to="">Delete</Button>
|
||||
<Button onClick={downloadAndErase} to="">Download & Delete</Button>
|
||||
<Button onClick={handleClose} to="">Cancel</Button>
|
||||
<h2>{t("Delete Progress?")}</h2>
|
||||
<Trans>
|
||||
<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>
|
||||
</Trans>
|
||||
<Button onClick={eraseProgress} to="">{t("Delete")}</Button>
|
||||
<Button onClick={downloadAndErase} to="">{t("Download & Delete")}</Button>
|
||||
<Button onClick={handleClose} to="">{t("Cancel")}</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import * as React from 'react'
|
||||
import { Typography } from '@mui/material'
|
||||
import Markdown from '../markdown'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { GameIdContext } from '../../app'
|
||||
|
||||
/** Pop-up that is displaying the Game Info.
|
||||
*
|
||||
@@ -11,12 +13,15 @@ import Markdown from '../markdown'
|
||||
* controlled by the containing element.
|
||||
*/
|
||||
export function InfoPopup ({info, handleClose}: {info: string, handleClose: () => void}) {
|
||||
let { t } = useTranslation()
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
|
||||
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="welcome-text">
|
||||
<Markdown>{info}</Markdown>
|
||||
<Markdown>{t(info, {ns: gameId})}</Markdown>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,55 +1,115 @@
|
||||
import * as React from 'react'
|
||||
import { Input, Typography } from '@mui/material'
|
||||
import { Input, MenuItem, Select, SelectChangeEvent, Typography } from '@mui/material'
|
||||
import Markdown from '../markdown'
|
||||
import Switch from '@mui/material/Switch';
|
||||
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 { IMobileContext } from "../infoview/context"
|
||||
import { IPreferencesContext, PreferencesContext } from "../infoview/context"
|
||||
import ReactCountryFlag from 'react-country-flag';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface PreferencesPopupProps extends IMobileContext{
|
||||
handleClose: () => void
|
||||
}
|
||||
export function PreferencesPopup({ handleClose }: { handleClose: () => void }) {
|
||||
let { t } = useTranslation()
|
||||
const {layout, isSavePreferences, language, setLayout, setIsSavePreferences, setLanguage} = React.useContext(PreferencesContext)
|
||||
|
||||
export function PreferencesPopup({ mobile, setMobile, lockMobile, setLockMobile, handleClose }: PreferencesPopupProps) {
|
||||
return <div className="modal-wrapper">
|
||||
<div className="modal-backdrop" onClick={handleClose} />
|
||||
<div className="modal">
|
||||
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
||||
<Typography variant="body1" component="div" className="settings">
|
||||
<div className='preferences-category'>
|
||||
<div className='category-title'>
|
||||
<h3>Mobile layout</h3>
|
||||
</div>
|
||||
<div className='preferences-item'>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={mobile}
|
||||
onChange={() => setMobile(!mobile)}
|
||||
name="checked"
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label="Enable"
|
||||
labelPlacement="start"
|
||||
/>
|
||||
</div>
|
||||
<div className='preferences-item'>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={!lockMobile}
|
||||
onChange={() => setLockMobile(!lockMobile)}
|
||||
name="checked"
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label="Auto"
|
||||
labelPlacement="start"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Typography>
|
||||
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
|
||||
* controlled by the containing element.
|
||||
*
|
||||
* Note: Do not translate the Impressum!
|
||||
*/
|
||||
export function PrivacyPolicyPopup ({handleClose}: {handleClose: () => void}) {
|
||||
return <div className="privacy-policy modal-wrapper">
|
||||
@@ -57,19 +59,3 @@ export function PrivacyPolicyPopup ({handleClose}: {handleClose: () => void}) {
|
||||
</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
|
||||
*/
|
||||
import * as React from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
/** Pop-up that is displayed when opening the help explaining the game rules.
|
||||
*
|
||||
@@ -9,42 +10,46 @@ import * as React from 'react'
|
||||
* controlled by the containing element.
|
||||
*/
|
||||
export function RulesHelpPopup ({handleClose}: {handleClose: () => void}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return <div className="privacy-policy modal-wrapper">
|
||||
<div className="modal-backdrop" onClick={handleClose} />
|
||||
<div className="modal">
|
||||
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
||||
<h2>Game Rules</h2>
|
||||
<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>
|
||||
<p>
|
||||
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 <code>simp</code> in a level, you can use it henceforth in any level.
|
||||
</p>
|
||||
<p>The options are:</p>
|
||||
<h2>{t("Game Rules")}</h2>
|
||||
<Trans>
|
||||
<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>
|
||||
<p>
|
||||
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 <code>simp</code> in a level, you can use it henceforth in any level.
|
||||
</p>
|
||||
<p>The options are:</p>
|
||||
</Trans>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"></th>
|
||||
<th scope="col">levels</th>
|
||||
<th scope="col">tactics</th>
|
||||
<th scope="col">{t("levels")}</th>
|
||||
<th scope="col">{t("tactics")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">regular</th>
|
||||
<th scope="row">{t("regular")}</th>
|
||||
<td>🔐</td>
|
||||
<td>🔐</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">relaxed</th>
|
||||
<th scope="row">{t("relaxed")}</th>
|
||||
<td>🔓</td>
|
||||
<td>🔐</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">none</th>
|
||||
<th scope="row">{t("none")}</th>
|
||||
<td>🔓</td>
|
||||
<td>🔓</td>
|
||||
</tr>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useAppDispatch } from '../../hooks'
|
||||
import { GameProgressState, loadProgress, selectProgress } from '../../state/progress'
|
||||
import { downloadFile } from '../world_tree'
|
||||
import { Button } from '../button'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
/** Pop-up that is displaying the Game Info.
|
||||
*
|
||||
@@ -15,6 +16,8 @@ import { Button } from '../button'
|
||||
* controlled by the containing element.
|
||||
*/
|
||||
export function UploadPopup ({handleClose}) {
|
||||
let { t } = useTranslation()
|
||||
|
||||
const [file, setFile] = React.useState<File>();
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const gameProgress = useSelector(selectProgress(gameId))
|
||||
@@ -54,17 +57,19 @@ export function UploadPopup ({handleClose}) {
|
||||
<div className="modal-backdrop" onClick={handleClose} />
|
||||
<div className="modal">
|
||||
<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!
|
||||
Consider <a className="download-link" onClick={downloadProgress} >downloading your current progress</a> first!</p>
|
||||
<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>
|
||||
</Trans>
|
||||
<p>
|
||||
<input type="file" onChange={handleFileChange}/>
|
||||
</p>
|
||||
|
||||
<Button to="" onClick={uploadProgress}>Load selected file</Button>
|
||||
<Button to="" onClick={uploadProgress}>{t("Load selected file")}</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useAppDispatch, useAppSelector } from '../hooks'
|
||||
import { changedOpenedIntro, selectOpenedIntro } from '../state/progress'
|
||||
import { useGetGameInfoQuery, useLoadInventoryOverviewQuery } from '../state/api'
|
||||
import { Button } from './button'
|
||||
import { MobileContext } from './infoview/context'
|
||||
import { PreferencesContext } from './infoview/context'
|
||||
import { InventoryPanel } from './inventory'
|
||||
import { ErasePopup } from './popup/erase'
|
||||
import { InfoPopup } from './popup/game_info'
|
||||
@@ -23,26 +23,31 @@ import { WorldTreePanel } from './world_tree'
|
||||
import '../css/welcome.css'
|
||||
import { WelcomeAppBar } from './app_bar'
|
||||
import { Hint } from './hints'
|
||||
import i18next from 'i18next'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
|
||||
/** the panel showing the game's introduction text */
|
||||
function IntroductionPanel({introduction, setPageNumber}: {introduction: string, setPageNumber}) {
|
||||
const {mobile} = React.useContext(MobileContext)
|
||||
const {mobile} = React.useContext(PreferencesContext)
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
|
||||
let { t } = useTranslation()
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// TODO: I left the setup for splitting up the introduction in place, but if it's not needed
|
||||
// then this can be simplified.
|
||||
|
||||
// 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">
|
||||
<div className="chat">
|
||||
{text?.map(((t, i) =>
|
||||
t.trim() ?
|
||||
<Hint key={`intro-p-${i}`}
|
||||
hint={{text: t, hidden: false}}
|
||||
hint={{text: t, hidden: false, rawText: t, varNames: []}}
|
||||
step={0} selected={null} toggleSelection={undefined} />
|
||||
: <></>
|
||||
))}
|
||||
@@ -64,7 +69,13 @@ function IntroductionPanel({introduction, setPageNumber}: {introduction: string,
|
||||
/** main page of the game showing among others the tree of worlds/levels */
|
||||
function Welcome() {
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const {mobile, setMobile, lockMobile, setLockMobile} = 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 inventory = useLoadInventoryOverviewQuery({game: gameId})
|
||||
|
||||
@@ -92,7 +103,6 @@ function Welcome() {
|
||||
function toggleUploadMenu() {setUploadMenu(!uploadMenu)}
|
||||
function togglePreferencesPopup() {setPreferencesPopup(!preferencesPopup)}
|
||||
|
||||
|
||||
// set the window title
|
||||
useEffect(() => {
|
||||
if (gameInfo.data?.title) {
|
||||
@@ -134,7 +144,7 @@ function Welcome() {
|
||||
{eraseMenu? <ErasePopup handleClose={closeEraseMenu}/> : null}
|
||||
{uploadMenu? <UploadPopup handleClose={closeUploadMenu}/> : null}
|
||||
{info ? <InfoPopup info={gameInfo.data?.info} handleClose={closeInfo}/> : null}
|
||||
{preferencesPopup ? <PreferencesPopup mobile={mobile} setMobile={setMobile} lockMobile={lockMobile} setLockMobile={setLockMobile} handleClose={closePreferencesPopup}/> : null}
|
||||
{preferencesPopup ? <PreferencesPopup handleClose={closePreferencesPopup} /> : null}
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
@@ -11,11 +11,13 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faXmark, faCircleQuestion } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import { GameIdContext } from '../app'
|
||||
import { useAppDispatch, useMobile } from '../hooks'
|
||||
import { useAppDispatch } from '../hooks'
|
||||
import { selectDifficulty, changedDifficulty, selectCompleted } from '../state/progress'
|
||||
import { store } from '../state/store'
|
||||
|
||||
import '../css/world_tree.css'
|
||||
import { PreferencesContext } from './infoview/context'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
// Settings for the world tree
|
||||
cytoscape.use( klay )
|
||||
@@ -111,6 +113,7 @@ export function WorldIcon({world, title, position, completedLevels, difficulty,
|
||||
difficulty: number,
|
||||
worldSize: number
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
// See level icons. Match radius computed there minus `1.2*r`
|
||||
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 +")"}>
|
||||
<div className={unlocked && !completed ? "playable-world" : ''}>
|
||||
<p className="world-title" style={{fontSize: fontSize + "px"}}>
|
||||
{title ? title : world}
|
||||
{title ? t(title, {ns: gameId}) : world}
|
||||
</p>
|
||||
</div>
|
||||
</foreignObject>
|
||||
@@ -160,7 +163,7 @@ export function WorldIcon({world, title, position, completedLevels, difficulty,
|
||||
>
|
||||
<div className='world-label' style={{backgroundColor: completed ? darkgreen : unlocked ? darkblue : darkgrey}}>
|
||||
<p className='world-title' style={{fontSize: MINFONT + "px"}}>
|
||||
{title ? title : world}
|
||||
{title ? t(title, {ns: gameId}) : world}
|
||||
</p>
|
||||
</div>
|
||||
</foreignObject>}
|
||||
@@ -194,27 +197,28 @@ export const downloadFile = ({ data, fileName, fileType } :
|
||||
|
||||
/** The menu that is shown next to the world selection graph */
|
||||
export function WorldSelectionMenu({rulesHelp, setRulesHelp}) {
|
||||
const { t, i18n } = useTranslation()
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const difficulty = useSelector(selectDifficulty(gameId))
|
||||
const dispatch = useAppDispatch()
|
||||
const { mobile } = useMobile()
|
||||
const { mobile } = React.useContext(PreferencesContext)
|
||||
|
||||
|
||||
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${mobile ? '' : ' desktop'}`}>
|
||||
<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))}/>
|
||||
</span>
|
||||
<Slider
|
||||
orientation="vertical"
|
||||
title="Game Rules"
|
||||
title={t("Game Rules")}
|
||||
min={0} max={2}
|
||||
aria-label="Game Rules"
|
||||
aria-label={t("Game Rules")}
|
||||
value={difficulty}
|
||||
marks={[
|
||||
{value: 0, label: label(0)},
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"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": "中文"
|
||||
},
|
||||
{
|
||||
"iso": "es",
|
||||
"flag": "ES",
|
||||
"name": "Español"
|
||||
},
|
||||
{
|
||||
"iso": "ko",
|
||||
"flag": "KR",
|
||||
"name": "한국어"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -41,6 +41,13 @@
|
||||
.level-completed {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 500;
|
||||
padding-left: .5em;
|
||||
padding-right: .5em;
|
||||
padding-top: .2em;
|
||||
padding-bottom: .2em;
|
||||
border-radius: .5em;
|
||||
background-color: #eee;
|
||||
|
||||
}
|
||||
|
||||
.typewriter {
|
||||
@@ -188,6 +195,18 @@
|
||||
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 {
|
||||
flex: 1;
|
||||
background-color: #fff;
|
||||
@@ -199,3 +218,10 @@
|
||||
.undo-button {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.crashed_message {
|
||||
color: #D8000C;
|
||||
font-weight: bold;
|
||||
padding-left: .5em;
|
||||
padding-right: .5em;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ a {
|
||||
|
||||
@viewport {
|
||||
width: device-width ;
|
||||
zoom: 1.0 ;
|
||||
initial-scale: 1.0 ;
|
||||
}
|
||||
|
||||
.landing-page {
|
||||
@@ -36,6 +36,13 @@ a {
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.landing-page-nav {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#menu-btn {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 440px) {
|
||||
.game-list {
|
||||
@@ -182,6 +189,8 @@ footer .link {
|
||||
|
||||
.github-link {
|
||||
height: 24px; /* TODO: why do I need that? s*/
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.landing-page > section {
|
||||
|
||||
@@ -348,13 +348,19 @@ td code {
|
||||
.world-image-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
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;
|
||||
@@ -368,3 +374,22 @@ td code {
|
||||
min-width: 40px;
|
||||
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-right: 0.3rem;
|
||||
}
|
||||
|
||||
.preferences-category.tail-category{
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
.preferences-item.first{
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.preferences-item.leave-left-gap{
|
||||
margin-left: 3em;
|
||||
}
|
||||
@@ -1,30 +1,6 @@
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
|
||||
import type { RootState, AppDispatch } from './state/store'
|
||||
|
||||
import { setMobile as setMobileState, setLockMobile as setLockMobileState} from "./state/preferences"
|
||||
|
||||
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
||||
export const useAppDispatch: () => AppDispatch = useDispatch
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
|
||||
|
||||
export const useMobile = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const mobile = useAppSelector((state) => state.preferences.mobile);
|
||||
const lockMobile = useAppSelector((state) => state.preferences.lockMobile);
|
||||
|
||||
const setMobile = (val: boolean) => {
|
||||
dispatch(setMobileState(val));
|
||||
};
|
||||
|
||||
const setLockMobile = (val: boolean) => {
|
||||
dispatch(setLockMobileState(val));
|
||||
};
|
||||
|
||||
return {
|
||||
mobile,
|
||||
setMobile,
|
||||
lockMobile,
|
||||
setLockMobile,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
+11
-3
@@ -9,6 +9,7 @@ import ErrorPage from './components/error_page'
|
||||
import Welcome from './components/welcome'
|
||||
import LandingPage from './components/landing_page'
|
||||
import Level from './components/level'
|
||||
import './i18n';
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +21,14 @@ let root_object: RouteObject = single_game ? {
|
||||
loader: () => redirect("/g/local/game")
|
||||
} : {
|
||||
path: "/",
|
||||
element: <LandingPage />,
|
||||
element: <App />,
|
||||
errorElement: <ErrorPage />,
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
element: <LandingPage />,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const router = createHashRouter([
|
||||
@@ -28,12 +36,12 @@ const router = createHashRouter([
|
||||
{
|
||||
// For backwards compatibility
|
||||
path: "/game/nng",
|
||||
loader: () => redirect("/g/hhu-adam/NNG4")
|
||||
loader: () => redirect("/g/leanprover-community/nng4")
|
||||
},
|
||||
{
|
||||
// For backwards compatibility
|
||||
path: "/g/hhu-adam/NNG4",
|
||||
loader: () => redirect("/g/leanprover-community/NNG4")
|
||||
loader: () => redirect("/g/leanprover-community/nng4")
|
||||
},
|
||||
{
|
||||
path: "/g/:owner/:repo",
|
||||
|
||||
@@ -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;
|
||||
@@ -57,3 +57,12 @@ export function savePreferences(state: any) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
export function removePreferences() {
|
||||
try {
|
||||
localStorage.removeItem(PREFERENCES_KEY);
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
import { loadPreferences } from "./local_storage";
|
||||
import { loadPreferences, removePreferences, savePreferences } from "./local_storage";
|
||||
|
||||
interface PreferencesState {
|
||||
mobile: boolean;
|
||||
lockMobile: boolean;
|
||||
export interface PreferencesState {
|
||||
layout: "mobile" | "auto" | "desktop";
|
||||
isSavePreferences: boolean;
|
||||
language: string;
|
||||
}
|
||||
|
||||
export function getWindowDimensions() {
|
||||
@@ -12,26 +13,28 @@ export function getWindowDimensions() {
|
||||
return {width, height}
|
||||
}
|
||||
|
||||
const { width } = getWindowDimensions()
|
||||
|
||||
export const AUTO_SWITCH_THRESHOLD = 800
|
||||
|
||||
const initialState: PreferencesState = loadPreferences() ?? {
|
||||
mobile: width < AUTO_SWITCH_THRESHOLD,
|
||||
lockMobile: false
|
||||
}
|
||||
const initialState: PreferencesState = loadPreferences() ??{
|
||||
layout: "auto",
|
||||
isSavePreferences: false,
|
||||
language: import.meta.env.VITE_CLIENT_DEFAULT_LANGUAGE || "en",
|
||||
};
|
||||
|
||||
export const preferencesSlice = createSlice({
|
||||
name: "preferences",
|
||||
initialState,
|
||||
reducers: {
|
||||
setMobile: (state, action) => {
|
||||
state.mobile = action.payload;
|
||||
setLayout: (state, action) => {
|
||||
state.layout = action.payload;
|
||||
},
|
||||
setLockMobile: (state, action) => {
|
||||
state.lockMobile = action.payload;
|
||||
setIsSavePreferences: (state, action) => {
|
||||
state.isSavePreferences = action.payload;
|
||||
},
|
||||
setLanguage: (state, action) => {
|
||||
state.language = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setMobile, setLockMobile } = preferencesSlice.actions;
|
||||
export const { setLayout, setIsSavePreferences, setLanguage } = preferencesSlice.actions;
|
||||
|
||||
@@ -53,22 +53,22 @@ const initalLevelProgressState: LevelProgressState = {code: "", completed: false
|
||||
|
||||
/** Add an empty skeleton with progress for the current game */
|
||||
function addGameProgress (state: ProgressState, action: PayloadAction<{game: string}>) {
|
||||
if (!state.games[action.payload.game]) {
|
||||
state.games[action.payload.game] = {inventory: [], openedIntro: true, data: {}, difficulty: DEFAULT_DIFFICULTY}
|
||||
if (!state.games[action.payload.game.toLowerCase()]) {
|
||||
state.games[action.payload.game.toLowerCase()] = {inventory: [], openedIntro: true, data: {}, difficulty: DEFAULT_DIFFICULTY}
|
||||
}
|
||||
if (!state.games[action.payload.game].data) {
|
||||
state.games[action.payload.game].data = {}
|
||||
if (!state.games[action.payload.game.toLowerCase()].data) {
|
||||
state.games[action.payload.game.toLowerCase()].data = {}
|
||||
}
|
||||
}
|
||||
|
||||
/** Add an empty skeleton with progress for the current level */
|
||||
function addLevelProgress(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number}>) {
|
||||
addGameProgress(state, action)
|
||||
if (!state.games[action.payload.game].data[action.payload.world]) {
|
||||
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.toLowerCase()].data[action.payload.world] = {}
|
||||
}
|
||||
if (!state.games[action.payload.game].data[action.payload.world][action.payload.level]) {
|
||||
state.games[action.payload.game].data[action.payload.world][action.payload.level] = {...initalLevelProgressState}
|
||||
if (!state.games[action.payload.game.toLowerCase()].data[action.payload.world][action.payload.level]) {
|
||||
state.games[action.payload.game.toLowerCase()].data[action.payload.world][action.payload.level] = {...initalLevelProgressState}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,58 +79,58 @@ export const progressSlice = createSlice({
|
||||
/** put edited code in the state and set completed to false */
|
||||
codeEdited(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number, code: string}>) {
|
||||
addLevelProgress(state, action)
|
||||
state.games[action.payload.game].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].code = action.payload.code
|
||||
state.games[action.payload.game.toLowerCase()].data[action.payload.world][action.payload.level].completed = false
|
||||
},
|
||||
/** TODO: docstring */
|
||||
changedSelection(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number, selections: Selection[]}>) {
|
||||
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 */
|
||||
levelCompleted(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number}>) {
|
||||
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 */
|
||||
helpEdited(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number, help: number[]}>) {
|
||||
addLevelProgress(state, action)
|
||||
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 */
|
||||
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 */
|
||||
deleteLevelProgress(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number}>) {
|
||||
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 */
|
||||
loadProgress(state: ProgressState, action: PayloadAction<{game: string, data:GameProgressState}>) {
|
||||
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 */
|
||||
changedInventory(state: ProgressState, action: PayloadAction<{game: string, inventory: string[]}>) {
|
||||
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 */
|
||||
changedDifficulty(state: ProgressState, action: PayloadAction<{game: string, difficulty: number}>) {
|
||||
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 */
|
||||
changedOpenedIntro(state: ProgressState, action: PayloadAction<{game: string, openedIntro: boolean}>) {
|
||||
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].typewriterMode = action.payload.typewriterMode
|
||||
state.games[action.payload.game.toLowerCase()].typewriterMode = action.payload.typewriterMode
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -138,74 +138,74 @@ export const progressSlice = createSlice({
|
||||
/** if the level does not exist, return default values */
|
||||
export function selectLevel(game: string, world: string, level: number) {
|
||||
return (state) =>{
|
||||
if (!state.progress.games[game]) { return initalLevelProgressState }
|
||||
if (!state.progress.games[game].data[world]) { return initalLevelProgressState }
|
||||
if (!state.progress.games[game].data[world][level]) { return initalLevelProgressState }
|
||||
return state.progress.games[game].data[world][level]
|
||||
if (!state.progress.games[game.toLowerCase()]) { return initalLevelProgressState }
|
||||
if (!state.progress.games[game.toLowerCase()].data[world]) { return initalLevelProgressState }
|
||||
if (!state.progress.games[game.toLowerCase()].data[world][level]) { return initalLevelProgressState }
|
||||
return state.progress.games[game.toLowerCase()].data[world][level]
|
||||
}
|
||||
}
|
||||
|
||||
/** return the code of the current level */
|
||||
export function selectCode(game: string, world: string, level: number) {
|
||||
return (state) => {
|
||||
return selectLevel(game, world, level)(state).code
|
||||
return selectLevel(game.toLowerCase(), world, level)(state).code
|
||||
}
|
||||
}
|
||||
|
||||
/** return the current inventory */
|
||||
export function selectInventory(game: string) {
|
||||
return (state) => {
|
||||
if (!state.progress.games[game]) { return [] }
|
||||
return state.progress.games[game].inventory
|
||||
if (!state.progress.games[game.toLowerCase()]) { return [] }
|
||||
return state.progress.games[game.toLowerCase()].inventory
|
||||
}
|
||||
}
|
||||
|
||||
/** return the code of the current level */
|
||||
export function selectHelp(game: string, world: string, level: number) {
|
||||
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 */
|
||||
export function selectSelections(game: string, world: string, level: number) {
|
||||
return (state) => {
|
||||
return selectLevel(game, world, level)(state).selections
|
||||
return selectLevel(game.toLowerCase(), world, level)(state).selections
|
||||
}
|
||||
}
|
||||
|
||||
/** return whether the current level is clompleted */
|
||||
export function selectCompleted(game: string, world: string, level: number) {
|
||||
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 */
|
||||
export function selectProgress(game: string) {
|
||||
return (state) => {
|
||||
return state.progress.games[game] ?? null
|
||||
return state.progress.games[game.toLowerCase()] ?? null
|
||||
}
|
||||
}
|
||||
|
||||
/** return difficulty for the current game if it exists */
|
||||
export function selectDifficulty(game: string) {
|
||||
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 */
|
||||
export function selectOpenedIntro(game: string) {
|
||||
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]?.typewriterMode ?? true
|
||||
return state.progress.games[game.toLowerCase()]?.typewriterMode ?? true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { connection } from '../connection'
|
||||
import { apiSlice } from './api'
|
||||
import { progressSlice } from './progress'
|
||||
import { preferencesSlice } from "./preferences"
|
||||
import { saveState, savePreferences } from "./local_storage";
|
||||
import { saveState, savePreferences, removePreferences} from "./local_storage";
|
||||
|
||||
|
||||
export const store = configureStore({
|
||||
@@ -29,7 +29,9 @@ export const store = configureStore({
|
||||
store.subscribe(
|
||||
debounce(() => {
|
||||
saveState(store.getState()[progressSlice.name]);
|
||||
savePreferences(store.getState()[preferencesSlice.name]);
|
||||
|
||||
const preferencesState = store.getState()[preferencesSlice.name]
|
||||
preferencesState.isSavePreferences ? savePreferences(preferencesState) : removePreferences()
|
||||
}, 800)
|
||||
);
|
||||
|
||||
|
||||
+53
-321
@@ -1,335 +1,67 @@
|
||||
**NOTE! This document is deprecated! The current documentation is [How To Create A Game](create_game.md)**
|
||||
# Server
|
||||
|
||||
# Creating a game.
|
||||
The server is made out of two parts, named "relay" and "server".
|
||||
|
||||
Ideally one takes the [GameSkeleton template](https://github.com/hhu-adam/GameSkeleton) to create a new game.
|
||||
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.
|
||||
|
||||
## Game Structure
|
||||
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`.
|
||||
|
||||
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.
|
||||
|
||||
### Level
|
||||
## Integration into Games
|
||||
|
||||
A level file is a lean file that imports at least `import GameServer.Commands` and starts with the following Lean commands.
|
||||
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
|
||||
```
|
||||
|
||||
TODO: This is outdated!
|
||||
If you are developing a game other than `Robo` or `NNG4`, adapt the
|
||||
code at the beginning of `lean4game/relay/index.mjs`:
|
||||
```typescript
|
||||
const games = {
|
||||
"g/hhu-adam/robo": {
|
||||
dir: "../../../../Robo",
|
||||
queueLength: 5
|
||||
},
|
||||
"g/hhu-adam/nng4": {
|
||||
dir: "../../../../NNG4",
|
||||
queueLength: 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Run the game:
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
This takes a little time. Eventually, the server is available on http://localhost:3000/
|
||||
and the game is available on http://localhost:3000/#/g/hhu-adam/NNG4.
|
||||
This way any changes to files in `client/` or `relay/` will cause the server to restart automatically.
|
||||
|
||||
### 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
|
||||
+52
-37
@@ -6,11 +6,11 @@ This tutorial walks you through creating a new game for lean4. It covers from wr
|
||||
|
||||
1. Use the [GameSkeleton template](https://github.com/hhu-adam/GameSkeleton) to create a new github repo for your game: On github, click on "Use this template" > "Create a new repository".
|
||||
2. Clone the game repo.
|
||||
3. Call `lake update && lake exe cache get && lake build` to build the Lean project.
|
||||
3. Call `lake update -R && lake build` to build the Lean project.
|
||||
|
||||
Note that you need to host your game's code on github to publish it online later on. If you only
|
||||
want to play it locally, you can simply clone the NNG repo and start modifying that one.
|
||||
### Running the game
|
||||
|
||||
Now you can open the game in VSCode (`cd YourGame/` and `code .`), and start modifying it like a regular Lean project. To run the game consult the section "**5. Testing the Game Locally**" below.
|
||||
|
||||
## 2. Game.lean
|
||||
|
||||
@@ -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. 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
|
||||
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
|
||||
NewTactic induction simp
|
||||
NewLemma Nat.zero_mul
|
||||
NewTheorem Nat.zero_mul
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
"some description"
|
||||
|
||||
/--
|
||||
some description
|
||||
-/
|
||||
DefinitionDoc Pow as "^"
|
||||
"some description"
|
||||
```
|
||||
|
||||
(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:
|
||||
|
||||
```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
|
||||
@@ -161,7 +170,7 @@ disable all items except the ones specified.
|
||||
|
||||
#### 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
|
||||
|
||||
@@ -176,15 +185,16 @@ and only `rw` would show up in the inventory.
|
||||
|
||||
### 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
|
||||
|
||||
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
|
||||
|
||||
Add a docstring that contains the exercise statement in natural language. If you do this, it will appear at the top of the exercise. It supports Latex.
|
||||
Add a docstring that contains the exercise statement in natural language. If you do this, it will appear at the top of the exercise. See [LaTeX in Games](latex.md) for more details on formatting.
|
||||
|
||||
```lean
|
||||
/-- The exercise statement in natural language using latex: $\iff$. -/
|
||||
@@ -192,25 +202,18 @@ Statement ...
|
||||
sorry
|
||||
```
|
||||
|
||||
#### 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 ...
|
||||
```
|
||||
For more details and features, read [Writing Exercises](writing_exercises.md)
|
||||
|
||||
### 6. c) 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:
|
||||
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
|
||||
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
|
||||
helps to set `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
|
||||
@@ -225,23 +228,27 @@ Most important for game development are probably the `Hints`.
|
||||
The hints will be displayed whenever the player's current goal matches the goal the hint is
|
||||
placed at inside the sample proof. You can use `Branch` to place hints in dead ends or alternative proof strands.
|
||||
|
||||
Read [More about Hints](doc/hints.md) for how they work and what the options are.
|
||||
Read [More about Hints](hints.md) for how they work and what the options are.
|
||||
|
||||
### 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.
|
||||
|
||||
The images need to be placed in `images/` and you need to add a command like `Image "images/path/to/myWorldImage.png"`
|
||||
The images need to be placed in `images/` and you need to add a command like `Image "images/path/to/myWorldImage.png"`
|
||||
in one of the files you created in 2), 3), or 4) (i.e. game/world/level).
|
||||
|
||||
NOTE: At present, only the images for a world are displayed. They appear in the introduction of the world.
|
||||
|
||||
## 7. Update your game
|
||||
|
||||
In principle, it is as simple as modifying `lean-toolchain` to update your game to a new Lean version. However, you should read about the details in [Update An Existing Game](doc/update_game.md).
|
||||
In principle, it is as simple as modifying `lean-toolchain` to update your game to a new Lean version. However, you should read about the details in [Update An Existing Game](update_game.md).
|
||||
|
||||
## 8. Publish your game
|
||||
## 8. Add translation
|
||||
|
||||
To publish your game on the official server, see [Publishing a game](doc/publish_game.md)
|
||||
See [Translating a game](translate.md).
|
||||
|
||||
## 9. 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:
|
||||
|
||||
@@ -253,18 +260,26 @@ Prerequisites "NNG"
|
||||
CoverImage "images/cover.png"
|
||||
```
|
||||
* `Languages`: Currently only a single language (capital English name). The tile will show a corresponding flag.
|
||||
* `CaptionShort`: One catch phrase. Appears above the image.
|
||||
* `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
|
||||
## 10. Advanced Topics
|
||||
|
||||
Here are some random further things you should consider designing a new game:
|
||||
### Escaping
|
||||
|
||||
* Inside strings, you need to escape backslashes, but not inside doc-strings, therefore you
|
||||
Inside strings, you need to escape backslashes, but not inside doc-strings, therefore you
|
||||
would write `Introduction "some latex here: $\\iff$."` but
|
||||
`/-- some latex here: $\iff$. -/ Statement ...`
|
||||
* 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 starts getting out
|
||||
of control.
|
||||
|
||||
### LaTeX support
|
||||
|
||||
LaTeX is rendered using the [KaTeX library](https://katex.org/),
|
||||
see [Using LaTeX in the Game](latex.md) for details.
|
||||
|
||||
### Number Of Levels Limit
|
||||
|
||||
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 starts getting out
|
||||
of control.
|
||||
|
||||
+24
-6
@@ -10,6 +10,8 @@ Statement .... := by
|
||||
...
|
||||
```
|
||||
|
||||
Note that hints are only **context-aware but not history-aware**. In particular, they only look at the assumptions and the current goal. Player's might encounter hints in a different order - or not at all - if they decide to go for a unique proof idea. The `Branch` tactic helps 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
|
||||
@@ -17,7 +19,7 @@ sample solutions and the entire context from the sample solutions is present in
|
||||
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
|
||||
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
|
||||
@@ -30,12 +32,12 @@ Hidden hints are not filtered.
|
||||
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.
|
||||
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 proving the goal.
|
||||
|
||||
```
|
||||
Statement .... := by
|
||||
Hint "Huse `rw` or `rewrite`."
|
||||
Hint "use `rw` or `rewrite`."
|
||||
Branch
|
||||
rewrite [h]
|
||||
Hint "now you still need `rfl`"
|
||||
@@ -47,6 +49,9 @@ Statement .... := by
|
||||
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.
|
||||
|
||||
*Note*: This means you need to escape any other uses of **opening** curly brackets (i.e. `\{`). See also [LaTeX in Games](latex.md) for
|
||||
examples of this.
|
||||
|
||||
For example, if the sample proof contains
|
||||
|
||||
```
|
||||
@@ -82,6 +87,19 @@ create new assumptions.
|
||||
|
||||
## 6. Formatting
|
||||
|
||||
You can add use markdown to format your hints, for example you can use KaTex: `$\\iff$`
|
||||
You can use Markdown to format your hints and you can
|
||||
use LaTeX. See [LaTeX in Games](latex.md) for more details.
|
||||
|
||||
TODO: Write a doc about latex/markdown options available.
|
||||
### Images
|
||||
|
||||
Hints and introductions/conclusions can also contain images.
|
||||
|
||||
For remote images, simply add:
|
||||
|
||||
```
|
||||
<img src=\"https://url.com/to/image\"/>
|
||||
```
|
||||
|
||||
Local images can currently only be included with a hack:
|
||||
|
||||
Images in the game's `images/` folder will be accessible at `data/g/[user]/[repo]/[image].[png|jpg|…]` and thus can be included as if they were external images.
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
There are multiple ways how to format the text content of your game. Notably Markdown with KaTeX.
|
||||
|
||||
# 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!
|
||||
|
||||
This means for example you'd write `/-- $\iff$ -/` but `"$\\iff$"`.
|
||||
|
||||
Furthermore, inside `Hint` you need to escape all opening curly brackets as `\{` since `{h}` is syntax for inserting a variable name `h`.
|
||||
|
||||
# KaTeX
|
||||
|
||||
LaTeX syntax is provided trough the [KaTeX library](https://katex.org). KateX supports most but not all of latex and its packages.
|
||||
See [supported](https://katex.org/docs/supported.html).
|
||||
|
||||
## Examples
|
||||
|
||||
### 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}
|
||||
$$
|
||||
```
|
||||
|
||||
Again, note that inside a string like `Hint`/`Introduction`/`Conclusion`/etc. you need to escape `\` and potentially `{`.
|
||||
|
||||
E.g. `\begin` as `\\begin`, `\\` as `\\\\` and inside a
|
||||
`Hint`, `@>{f}>>` as `@>\{f}>>`.
|
||||
|
||||
See https://www.jmilne.org/not/Mamscd.pdf
|
||||
|
||||
### Truth Tables
|
||||
|
||||
KaTeX does not support the tabular environment. You can use the array environment instead.
|
||||
|
||||
$$
|
||||
\begin{array}{|c|c|}
|
||||
\hline
|
||||
P & ¬P \\
|
||||
\hline
|
||||
T & F \\
|
||||
F & T \\
|
||||
\hline
|
||||
\end{array}
|
||||
$$
|
||||
|
||||
```
|
||||
$$
|
||||
\begin{array}{|c|c|}
|
||||
\hline
|
||||
P & ¬P \\
|
||||
\hline
|
||||
T & F \\
|
||||
F & T \\
|
||||
\hline
|
||||
\end{array}
|
||||
$$
|
||||
```
|
||||
@@ -7,3 +7,13 @@ Internally, websocket requests to `ws://localhost:3000/websockets` will be forwa
|
||||
On the server side, the command will set up a docker image containing the Lean server. The two parts can be built separately using `npm run build_client` and `npm run build_server`.
|
||||
|
||||
* `npm run production`: Start the project in production mode. This requires that the build script has been run. It will start a server on the port specified in the `PORT` environment variable or by default on `8080`. You can run on a specific port by running `PORT=80 npm run production`. The server will serve the files in `client/dist` via http and give access to the bubblewrapped Lean server via the web socket protocol.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
The client and server ports, as well as the default language, can be configured using environment variables:
|
||||
|
||||
* `PORT`: Sets the port for the backend server (default: `8080`).
|
||||
* `CLIENT_PORT`: Sets the port for the client server (default: `3000`).
|
||||
* `VITE_CLIENT_DEFAULT_LANGUAGE`: Sets the default language for the application (default: `en`).
|
||||
|
||||
Ensure these environment variables are set appropriately in your environment to configure the project as needed.
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
# Publishing games
|
||||
|
||||
You can publish your game on the official (Lean Game Server)[https://adam.math.hhu.de] in a few simple
|
||||
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
|
||||
@@ -11,7 +11,7 @@ tab.
|
||||
|
||||
## 2. Import the game
|
||||
|
||||
You call the URL that's listed under "What's Next?" in the latest action run. Explicitely you call
|
||||
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}
|
||||
|
||||
+27
-12
@@ -2,6 +2,8 @@
|
||||
|
||||
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:
|
||||
|
||||
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
|
||||
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/>
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
@@ -108,6 +100,29 @@ Run the game:
|
||||
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.
|
||||
|
||||
## Modifying the GameServer
|
||||
|
||||
+22
-3
@@ -6,7 +6,7 @@ In order to set up the server to allow imports, one needs to create a
|
||||
repos will suffice.
|
||||
|
||||
You need to set the environment variables `LEAN4GAME_GITHUB_USER` and `LEAN4GAME_GITHUB_TOKEN`
|
||||
with your user name and access token. For example, you can seet these in `ecosystem.config.cjs` if
|
||||
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:
|
||||
@@ -22,7 +22,26 @@ where you replace:
|
||||
|
||||
> https://{website}/#/g/{owner}/{repo}
|
||||
|
||||
## data management
|
||||
## Data management
|
||||
Everything downloaded remains in the folder `lean4game/games`.
|
||||
the subfolder `tmp` contains downloaded artifacts and can be deleted without loss.
|
||||
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.
|
||||
|
||||
## Server capacity
|
||||
|
||||
If you would like to display the server capacity on the landing page,
|
||||
you can create a file `lean4game/games/stats.csv` of the following form:
|
||||
|
||||
```
|
||||
CPU,MEM
|
||||
0.1,0.8
|
||||
```
|
||||
|
||||
These numbers will be displayed on the landing page ("CPU: 10 % used" and "RAM: 80 % used").
|
||||
|
||||
If you only want one of the numbers, replace the number you don't want with `nan` (or anything
|
||||
else which does not parse as number).
|
||||
|
||||
If you don't want to show either, simply do not create `stats.csv`
|
||||
|
||||
Use your own script or cronjob to update the CSV file as desired.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Translation
|
||||
|
||||
The game server supports internationalisation ("i18n") using [lean-i18n](https://github.com/hhu-adam/lean-i18n) and [i18next](https://www.npmjs.com/package/i18next).
|
||||
|
||||
The intended workflow currently is the following:
|
||||
|
||||
1. When you call `lake build` in your game, it should automatically create a template file `.i18n/en/Game.pot`. Alternatively you can call `lake exe i18n --template` to recreate it.
|
||||
2. Open the file `Game.pot` (the "t" stands for "template") with [Poedit](https://poedit.net/) (or a similar software) and translate all strings. Save your work as `.i18n/{language}/Game.po`.
|
||||
4. Call `lake exe i18n --export-json` to create all Json files `.i18n/{language}/Game.json` which the server needs.
|
||||
5. Add your translations (i.e. `.po` and `.json`, but not the `.mo` files) and push your results, and [publish the game](publish_game.md).
|
||||
|
||||
If you choose the correct language in the "Preferences" of the game, you should see your translations.
|
||||
|
||||
## Alternative: avoiding .po
|
||||
|
||||
Note: This workflow is subject to change, and it might be that in future the server can directly read `.po` files. Until then, there is also an alternative workflow you might choose:
|
||||
|
||||
0. Add `useJson: true` to `.i18n/config.json`
|
||||
1. `lake build` or `lake exe i18n --template` will now create a Json instead: `.i18n/en/Game.json`.
|
||||
2. Add translations to a copy of this Json an save it as `.i18n/{language}/Game.json`
|
||||
|
||||
## Non-English games
|
||||
|
||||
For games written in a language different than English, you should set the correct source language (`sourceLang`) in `.i18n/config.json`. Afterwards, on `lake build` the template should appear under the chosen language, and can be translated (e.g. into English) as described above.
|
||||
|
||||
## New languages
|
||||
|
||||
The server has a set number of languages one can select.
|
||||
If your game has been translated to a language not selectable, [open an issue at lean4game](https://github.com/leanprover-community/lean4game/issues) requesting this new language.
|
||||
Or, even better, create a PR to translate the [server interface](https://github.com/leanprover-community/lean4game/tree/main/client/public/locales) into that new language.
|
||||
@@ -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".
|
||||
+3
-3
@@ -9,8 +9,8 @@ Before you continue, make sure there [exists a `v4.X.0`-tag in this repo](https:
|
||||
|
||||
Then, depending on the setup you use, do one of the following:
|
||||
|
||||
* Dev Container: Rebuild the VSCode Devcontainer.
|
||||
* Local Setup: in your game's folder run the following:
|
||||
* **Dev Container**: Rebuild the VSCode Devcontainer (without Cache!).
|
||||
* **Local Setup**: in your game's folder run the following:
|
||||
```
|
||||
lake update -R
|
||||
lake build
|
||||
@@ -24,7 +24,7 @@ Then, depending on the setup you use, do one of the following:
|
||||
npm install
|
||||
```
|
||||
where `{VERSION_TAG}` is the tag from above of the form `v4.X.0`
|
||||
* Gitpod/Codespaces: Create a fresh one
|
||||
* **Gitpod/Codespaces**: Create a fresh one
|
||||
|
||||
This will update your game (and the mathlib version you might be using) to the new lean version.
|
||||
|
||||
|
||||
@@ -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 ...
|
||||
```
|
||||
+1
-1
@@ -33,7 +33,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</noscript>
|
||||
<script type="module" src="/client/src/index.tsx"></script>
|
||||
<script type="module" src="client/src/index.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
Generated
+3422
-2979
File diff suppressed because it is too large
Load Diff
+10
-3
@@ -27,12 +27,17 @@
|
||||
"cytoscape-klay": "^3.1.4",
|
||||
"debounce": "^1.2.1",
|
||||
"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",
|
||||
"lean4web": "github:hhu-adam/lean4web#b91645a7b88814675ba9f99817436d0a2ce3a0ec",
|
||||
"octokit": "^2.0.14",
|
||||
"lean4web": "github:hhu-adam/lean4web#414d9e62638a392fca278761b4c61a1d2e138bc7",
|
||||
"octokit": "^3.1.2",
|
||||
"path-browserify": "^1.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-country-flag": "^3.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^14.1.0",
|
||||
"react-markdown": "^8.0.4",
|
||||
"react-native": "^0.72.3",
|
||||
"react-redux": "^8.0.5",
|
||||
@@ -58,6 +63,7 @@
|
||||
"concurrently": "^7.6.0",
|
||||
"css-loader": "^6.7.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"i18next-scanner": "^4.4.0",
|
||||
"nodemon": "^3.0.1",
|
||||
"react-refresh": "^0.14.0",
|
||||
"style-loader": "^3.3.1",
|
||||
@@ -73,7 +79,8 @@
|
||||
"preview": "vite preview",
|
||||
"build_server": "cd server && lake build",
|
||||
"build_client": "cross-env NODE_ENV=production vite build",
|
||||
"production": "cross-env NODE_ENV=production node relay/index.mjs"
|
||||
"production": "cross-env NODE_ENV=production node relay/index.mjs",
|
||||
"translate": "npx i18next-scanner --config client/i18next-scanner.config.cjs"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import time
|
||||
|
||||
def measure_proc_stat() -> dict[str, int]:
|
||||
proc_stat_header = open("/proc/stat", "r").readline()
|
||||
proc_stat = proc_stat_header.split(' ')[2:]
|
||||
proc_stat[-1] = proc_stat[-1].removesuffix('\n')
|
||||
proc_stat = list(map(int, proc_stat))
|
||||
|
||||
proc_stat_dict= {'user': proc_stat[0],
|
||||
'nice': proc_stat[1],
|
||||
'system': proc_stat[2],
|
||||
'idle': proc_stat[3],
|
||||
'iowait': proc_stat[4],
|
||||
'irq': proc_stat[5],
|
||||
'softirq': proc_stat[6],
|
||||
'steal': proc_stat[7],
|
||||
'guest': proc_stat[8],
|
||||
'guest_nice': proc_stat[9]}
|
||||
|
||||
return proc_stat_dict
|
||||
|
||||
if __name__ == "__main__":
|
||||
"""
|
||||
Script emulates htop calculation of CPU at the
|
||||
moment of calling.
|
||||
"""
|
||||
prev = measure_proc_stat()
|
||||
prev_idle = prev.get('idle') + prev.get('iowait')
|
||||
prev_non_idle = prev.get('user') + prev.get('nice') + prev.get('system') + prev.get('irq') + prev.get('softirq') + prev.get('steal')
|
||||
prev_total = prev_idle + prev_non_idle
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
curr = measure_proc_stat()
|
||||
curr_idle = curr.get('idle') + curr.get('iowait')
|
||||
curr_non_idle = curr.get('user') + curr.get('nice') + curr.get('system') + curr.get('irq') + curr.get('softirq') + curr.get('steal')
|
||||
curr_total = curr_idle + curr_non_idle
|
||||
|
||||
d_total = curr_total - prev_total
|
||||
d_idle = curr_idle - prev_idle
|
||||
|
||||
cpu_usage = ((d_total - d_idle)/d_total)
|
||||
print(cpu_usage)
|
||||
+50
-2
@@ -9,6 +9,8 @@ import os from 'os';
|
||||
import fs from 'fs';
|
||||
import anonymize from 'ip-anonymize';
|
||||
import { importTrigger, importStatus } from './import.mjs'
|
||||
import process from 'process';
|
||||
import { spawn } from 'child_process'
|
||||
// import fs from 'fs'
|
||||
|
||||
/**
|
||||
@@ -18,8 +20,9 @@ import { importTrigger, importStatus } from './import.mjs'
|
||||
*/
|
||||
const queueLength = {
|
||||
"g/hhu-adam/robo": 2,
|
||||
"g/hhu-adam/nng4": 5,
|
||||
"g/leanprover-community/nng4": 5,
|
||||
"g/djvelleman/stg4": 2,
|
||||
"g/trequetrum/lean4game-logic": 2,
|
||||
}
|
||||
|
||||
const __filename = url.fileURLToPath(import.meta.url);
|
||||
@@ -36,6 +39,30 @@ 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 ip = anonymize(req.headers['x-forwarded-for'] || req.socket.remoteAddress)
|
||||
const log = `${process.cwd()}/logs/game-access.log`
|
||||
const header = "date;anon-ip;game;lang\n"
|
||||
const data = `${new Date()};${ip};${owner}/${repo};${lang}\n`
|
||||
|
||||
fs.writeFile(log, header.concat(data), { flag: 'ax' }, (file_exists) => {
|
||||
if (file_exists) {
|
||||
fs.appendFile(log, data, (err) => {
|
||||
if (err) console.log("Failed to append to log!")
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`[${new Date()}] ${ip} requested translation for ${owner}/${repo} in ${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
|
||||
@@ -43,6 +70,25 @@ const server = app
|
||||
req.url = filename;
|
||||
express.static(path.join(getGameDir(owner,repo),".lake","gamedata"))(req, res, next);
|
||||
})
|
||||
.use('/data/stats', (req, res, next) => {
|
||||
const statsProcess = spawn('/bin/bash', [path.join(__dirname, "stats.sh"), process.pid])
|
||||
let outputData = ''
|
||||
let errorData = ''
|
||||
statsProcess.stdout.on('data', (data) => {
|
||||
outputData += data.toString();
|
||||
})
|
||||
statsProcess.stderr.on('data', (data) => {
|
||||
errorData += data.toString();
|
||||
})
|
||||
statsProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
res.send(outputData);
|
||||
} else {
|
||||
res.status(500).send(`Error executing script: ${errorData}`)
|
||||
console.error(`stats.sh exited with code ${code}. Error: ${errorData}`)
|
||||
}
|
||||
})
|
||||
})
|
||||
.use('/', router)
|
||||
.listen(PORT, () => console.log(`Listening on ${PORT}`));
|
||||
|
||||
@@ -169,7 +215,9 @@ wss.addListener("connection", function(ws, req) {
|
||||
|
||||
socketCounter += 1;
|
||||
const ip = anonymize(req.headers['x-forwarded-for'] || req.socket.remoteAddress)
|
||||
console.log(`[${new Date()}] Socket opened - ${ip}`)
|
||||
|
||||
// TODO (Matvey): extract further information from `req`, for example browser language.
|
||||
console.log(`[${new Date()}] Socket opened - ${ip} - ${owner}/${repo}`)
|
||||
|
||||
const socket = {
|
||||
onMessage: (cb) => { ws.on("message", cb) },
|
||||
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Load python interpreter
|
||||
python=/usr/bin/python3
|
||||
# Load python script
|
||||
cpu_usage=relay/cpu_usage.py
|
||||
# Execute python script
|
||||
cpu=$($python $cpu_usage)
|
||||
# Calculate memory usage by computing 1 - %free_memory
|
||||
mem=$(free | sed '2q;d' | awk '{print 1 - ($4/$2)}')
|
||||
|
||||
printf "CPU, MEM\n%f, %f\n" $cpu $mem
|
||||
@@ -28,3 +28,7 @@ do
|
||||
#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}
|
||||
|
||||
@@ -11,7 +11,7 @@ unsafe def main : List String → IO UInt32 := fun args => do
|
||||
|
||||
-- TODO: remove this argument
|
||||
if args[0]? == some "--server" then
|
||||
MyServer.FileWorker.workerMain {} args
|
||||
GameServer.FileWorker.workerMain {} args
|
||||
else
|
||||
e.putStrLn s!"Expected `--server`"
|
||||
return 1
|
||||
|
||||
@@ -21,7 +21,7 @@ def abstractCtx (goal : MVarId) : MetaM AbstractCtxResult := do
|
||||
def openAbstractCtxResult (res : AbstractCtxResult) (k : Array Expr → Expr → MetaM α) : MetaM α := do
|
||||
let (_mvars, _binderInfo, expr) ← openAbstractMVarsResult res.abstractMVarsResult
|
||||
lambdaLetTelescope (← instantiateMVars expr) k
|
||||
-- TODO: Unfornately, lambdaLetTelescope does not allow us to provide the number of arguments.
|
||||
-- TODO: Unfortunately, lambdaLetTelescope does not allow us to provide the number of arguments.
|
||||
-- If the goal is a function, this will not work.
|
||||
|
||||
end AbstractCtx
|
||||
|
||||
+112
-30
@@ -2,9 +2,15 @@ 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 GameServer
|
||||
|
||||
set_option autoImplicit false
|
||||
|
||||
/-! # Game metadata -/
|
||||
@@ -32,16 +38,17 @@ elab "Level" level:num : command => do
|
||||
|
||||
/-- Define the title of the current game/world/level. -/
|
||||
elab "Title" t:str : command => do
|
||||
let title ← t.getString.translate
|
||||
match ← getCurLayer with
|
||||
| .Level => modifyCurLevel fun level => pure {level with title := t.getString}
|
||||
| .World => modifyCurWorld fun world => pure {world with title := t.getString}
|
||||
| .Level => modifyCurLevel fun level => pure {level with title := title}
|
||||
| .World => modifyCurWorld fun world => pure {world with title := title}
|
||||
| .Game => modifyCurGame fun game => pure {game with
|
||||
title := t.getString
|
||||
tile := {game.tile with title := t.getString}}
|
||||
title := title
|
||||
tile := {game.tile with title := title}}
|
||||
|
||||
/-- Define the introduction of the current game/world/level. -/
|
||||
elab "Introduction" t:str : command => do
|
||||
let intro := t.getString
|
||||
let intro ← t.getString.translate
|
||||
match ← getCurLayer with
|
||||
| .Level => modifyCurLevel fun level => pure {level with introduction := intro}
|
||||
| .World => modifyCurWorld fun world => pure {world with introduction := intro}
|
||||
@@ -49,7 +56,7 @@ elab "Introduction" t:str : command => do
|
||||
|
||||
/-- Define the info of the current game. Used for e.g. credits -/
|
||||
elab "Info" t:str : command => do
|
||||
let info:= t.getString
|
||||
let info ← t.getString.translate
|
||||
match ← getCurLayer with
|
||||
| .Level =>
|
||||
logError "Can't use `Info` in a level!"
|
||||
@@ -81,7 +88,7 @@ elab "Image" t:str : command => do
|
||||
/-- Define the conclusion of the current game or current level if some
|
||||
building a level. -/
|
||||
elab "Conclusion" t:str : command => do
|
||||
let conclusion := t.getString
|
||||
let conclusion ← t.getString.translate
|
||||
match ← getCurLayer with
|
||||
| .Level => modifyCurLevel fun level => pure {level with conclusion := conclusion}
|
||||
| .World => modifyCurWorld fun world => pure {world with conclusion := conclusion}
|
||||
@@ -94,24 +101,25 @@ elab "Prerequisites" t:str* : command => do
|
||||
|
||||
/-- Short caption for the game (1 sentence) -/
|
||||
elab "CaptionShort" t:str : command => do
|
||||
let caption := t.getString
|
||||
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
|
||||
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 "German" "English"`.
|
||||
NOTE: For the time being, only a single language is supported.
|
||||
/-- 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 -/
|
||||
/-- The Image of the game (optional). TODO: Not implemented -/
|
||||
elab "CoverImage" t:str : command => do
|
||||
let file := t.getString
|
||||
if not <| ← System.FilePath.pathExists file then
|
||||
@@ -141,6 +149,7 @@ TacticDoc rw "`rw` stands for rewrite, etc. "
|
||||
-/
|
||||
elab doc:docComment ? "TacticDoc" name:ident content:str ? : command => do
|
||||
let doc ← parseDocCommentLegacy doc content
|
||||
let doc ← doc.translate
|
||||
modifyEnv (inventoryTemplateExt.addEntry · {
|
||||
type := .Tactic
|
||||
name := name.getId
|
||||
@@ -165,6 +174,7 @@ The theorem/definition to have the same fully qualified name as in mathlib.
|
||||
elab doc:docComment ? "TheoremDoc" name:ident "as" displayName:str "in" category:str content:str ? :
|
||||
command => do
|
||||
let doc ← parseDocCommentLegacy doc content
|
||||
let doc ← doc.translate
|
||||
modifyEnv (inventoryTemplateExt.addEntry · {
|
||||
type := .Lemma
|
||||
name := name.getId
|
||||
@@ -194,6 +204,7 @@ The theorem/definition to have the same fully qualified name as in mathlib.
|
||||
-/
|
||||
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 · {
|
||||
type := .Definition
|
||||
name := name.getId,
|
||||
@@ -334,12 +345,40 @@ elab "LemmaTab" category:str : command => do
|
||||
|
||||
/-! # Exercise Statement -/
|
||||
|
||||
/-- You can write `Statement add_comm (preamble := simp) .... := by` which
|
||||
will automatically execute the given tactic sequence before the exercise
|
||||
is handed to the player.
|
||||
|
||||
A common example is to use
|
||||
|
||||
```
|
||||
refine { carrier := M, ?.. }
|
||||
```
|
||||
|
||||
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. -/
|
||||
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
|
||||
|
||||
-- add an optional tactic sequence that the engine executes before the game starts
|
||||
let preambleSeq : TSyntax `Lean.Parser.Tactic.tacticSeq ← match preamble with
|
||||
| none => `(Parser.Tactic.tacticSeq|skip)
|
||||
| some x => match x with
|
||||
| `(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.
|
||||
let initMsgs ← modifyGet fun st => (st.messages, { st with messages := {} })
|
||||
@@ -355,36 +394,40 @@ elab doc:docComment ? attrs:Parser.Term.attributes ?
|
||||
collectUsedInventory proof
|
||||
| _ => 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.
|
||||
match statementName with
|
||||
| some name =>
|
||||
let env ← getEnv
|
||||
|
||||
let fullName := (← getCurrNamespace) ++ name.getId
|
||||
|
||||
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`
|
||||
-- in that case.
|
||||
logWarningAt name (m!"Environment already contains {fullName}! Only the existing " ++
|
||||
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
|
||||
-- Check that statement has a docs entry.
|
||||
checkInventoryDoc .Lemma name (name := fullName) (template := docContent)
|
||||
|
||||
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
|
||||
-- Check that statement has a docs entry.
|
||||
checkInventoryDoc .Lemma name (name := fullName) (template := docContent)
|
||||
|
||||
| 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
|
||||
|
||||
let msgs := (← get).messages
|
||||
|
||||
let mut hints := #[]
|
||||
let mut nonHintMsgs := #[]
|
||||
for msg in msgs.msgs do
|
||||
@@ -396,12 +439,41 @@ elab doc:docComment ? attrs:Parser.Term.attributes ?
|
||||
.nest hidden $
|
||||
.compose (.ofGoal text) (.ofGoal goal) := msg.data then
|
||||
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 {
|
||||
goal := ← abstractCtx goal
|
||||
text := ← instantiateMVars (mkMVar text)
|
||||
text := text
|
||||
rawText := rawText
|
||||
strict := strict == 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
|
||||
else
|
||||
nonHintMsgs := nonHintMsgs.push msg
|
||||
@@ -427,6 +499,7 @@ elab doc:docComment ? attrs:Parser.Term.attributes ?
|
||||
modifyCurLevel fun level => pure { level with
|
||||
module := env.header.mainModule
|
||||
goal := sig,
|
||||
preamble := preambleSeq
|
||||
scope := scope,
|
||||
descrText := docContent
|
||||
statementName := match statementName with
|
||||
@@ -436,10 +509,13 @@ elab doc:docComment ? attrs:Parser.Term.attributes ?
|
||||
hints := hints
|
||||
tactics := {level.tactics with used := usedInventory.tactics.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 -/
|
||||
|
||||
open GameServer in
|
||||
|
||||
/-- 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.
|
||||
-/
|
||||
@@ -501,7 +577,8 @@ elab (name := GameServer.Tactic.Branch) "Branch" t:tacticSeq : tactic => do
|
||||
-- Show an info whether the branch proofs all remaining goals.
|
||||
let gs ← Tactic.getUnsolvedGoals
|
||||
if gs.isEmpty then
|
||||
trace[debug] "This branch finishes the proof."
|
||||
-- trace[debug] "This branch finishes the proof."
|
||||
pure ()
|
||||
else
|
||||
trace[debug] "This branch leaves open goals."
|
||||
|
||||
@@ -533,7 +610,7 @@ where filterArgs (args : List Syntax) : List Syntax :=
|
||||
| Syntax.node _ `GameServer.Tactic.Hint _ :: _ :: r
|
||||
| Syntax.node _ `GameServer.Tactic.Branch _ :: _ :: r =>
|
||||
filterArgs r
|
||||
-- delete `Hint` and `Branch` occurence at the end of the tactic sequence.
|
||||
-- delete `Hint` and `Branch` occurrence at the end of the tactic sequence.
|
||||
| Syntax.node _ `GameServer.Tactic.Hint _ :: []
|
||||
| Syntax.node _ `GameServer.Tactic.Branch _ :: [] =>
|
||||
[]
|
||||
@@ -665,7 +742,8 @@ elab "MakeGame" : command => do
|
||||
| 0 => pure ()
|
||||
| 1 => pure () -- level ids start with 1, so we need to skip 1, too
|
||||
| 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 ()
|
||||
| .num _ _ => panic "Did not expect to get a numerical statement name!"
|
||||
| .str pre s =>
|
||||
@@ -676,7 +754,9 @@ elab "MakeGame" : command => do
|
||||
|
||||
-- if the last level was named, we need to add it as a new lemma
|
||||
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 ()
|
||||
| .num _ _ => panic "Did not expect to get a numerical statement name!"
|
||||
| .str pre s =>
|
||||
@@ -798,7 +878,8 @@ elab "MakeGame" : command => do
|
||||
| 1 => pure () -- level ids start with 1, so we need to skip 1, too.
|
||||
| i₀ + 1 =>
|
||||
-- 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 ()
|
||||
| .num _ _ => panic "Did not expect to get a numerical statement name!"
|
||||
| .str pre s =>
|
||||
@@ -812,7 +893,8 @@ elab "MakeGame" : command => do
|
||||
match i₀ with
|
||||
| 0 => logWarning m!"World `{worldId}` contains no levels."
|
||||
| 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 ()
|
||||
| .num _ _ => panic "Did not expect to get a numerical statement name!"
|
||||
| .str pre s =>
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import GameServer.AbstractCtx
|
||||
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. -/
|
||||
def defaultGameName: String := "MyGame"
|
||||
@@ -18,29 +24,13 @@ defined in this file.
|
||||
|
||||
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)
|
||||
|
||||
The inventory contains documentation that the user can access.
|
||||
There are three inventory types: Lemma, Tactic, Definition. They vary about in the information
|
||||
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
|
||||
`Statement` check if there is a key registered in this extension and might add a default or
|
||||
print a warning if not.
|
||||
@@ -275,6 +265,8 @@ structure GameLevel where
|
||||
template: Option String := none
|
||||
/-- 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`
|
||||
|
||||
+489
-331
@@ -3,8 +3,11 @@ import Lean.Server.FileWorker
|
||||
import GameServer.Game
|
||||
import GameServer.ImportModules
|
||||
import GameServer.SaveData
|
||||
import GameServer.EnvExtensions
|
||||
import GameServer.Tactic.LetIntros
|
||||
|
||||
namespace MyModule
|
||||
|
||||
open Lean
|
||||
open Elab
|
||||
open Parser
|
||||
@@ -48,7 +51,8 @@ partial def parseTactic (inputCtx : InputContext) (pmctx : ParserModuleContext)
|
||||
|
||||
end MyModule
|
||||
|
||||
namespace MyServer.FileWorker
|
||||
namespace GameServer.FileWorker
|
||||
|
||||
open Lean
|
||||
open Lean.Server
|
||||
open Lean.Server.FileWorker
|
||||
@@ -57,103 +61,142 @@ open IO
|
||||
open Snapshots
|
||||
open JsonRpc
|
||||
|
||||
structure GameWorkerState :=
|
||||
/--
|
||||
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
|
||||
/--
|
||||
Check for tactics/theorems that are not unlocked.
|
||||
0: no check
|
||||
1: give warnings
|
||||
2: give errors
|
||||
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
|
||||
deriving ToJson, FromJson
|
||||
|
||||
abbrev GameWorkerM := StateT GameWorkerState Server.FileWorker.WorkerM
|
||||
/--
|
||||
Pack the our custom `WorkerState` on top of the normal worker monad
|
||||
`Server.FileWorker.WorkerM`.
|
||||
-/
|
||||
abbrev WorkerM := StateT WorkerState Server.FileWorker.WorkerM
|
||||
|
||||
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
|
||||
modify fun st => { st with
|
||||
messages := st.messages.add {
|
||||
fileName := inputCtx.fileName
|
||||
severity := MessageSeverity.error
|
||||
pos := inputCtx.fileMap.toPosition (info.getPos?.getD 0)
|
||||
data := s
|
||||
}
|
||||
}
|
||||
messages := st.messages.add {
|
||||
fileName := inputCtx.fileName
|
||||
severity := severity
|
||||
pos := inputCtx.fileMap.toPosition (info.getPos?.getD 0)
|
||||
data := s }}
|
||||
|
||||
-- TODO: use HashSet for allowed tactics?
|
||||
/-- Find all tactics in syntax object that are forbidden according to a
|
||||
set `allowed` of allowed tactics. -/
|
||||
partial def findForbiddenTactics (inputCtx : Parser.InputContext)
|
||||
(gameWorkerState : GameWorkerState) (stx : Syntax) :
|
||||
Elab.Command.CommandElabM Unit := do
|
||||
let levelInfo := gameWorkerState.levelInfo
|
||||
/--
|
||||
Find all tactics in syntax object that are forbidden according to a
|
||||
set `allowed` of allowed tactics.
|
||||
-/
|
||||
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
|
||||
| .missing => return ()
|
||||
| .node _info _kind args =>
|
||||
-- Go inside a node.
|
||||
for arg in args do
|
||||
findForbiddenTactics inputCtx gameWorkerState arg
|
||||
findForbiddenTactics inputCtx workerState arg
|
||||
| .atom info val =>
|
||||
-- ignore syntax elements that do not start with a letter
|
||||
-- and ignore "with" keyword
|
||||
let allowed := ["with", "fun", "at", "only", "by", "to"]
|
||||
-- Atoms might be tactic names or other keywords.
|
||||
-- Note: We whitelisted known keywords because we cannot
|
||||
-- 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
|
||||
let val := val.dropRightWhile (fun c => c == '!' || c == '?') -- treat `simp?` and `simp!` like `simp`
|
||||
-- Treat `simp?` and `simp!` like `simp`
|
||||
let val := val.dropRightWhile (fun c => c == '!' || c == '?')
|
||||
match levelInfo.tactics.find? (·.name.toString == val) with
|
||||
| none =>
|
||||
-- Note: This case means that the tactic will never be introduced in the game.
|
||||
match gameWorkerState.inventory.find? (· == val) with
|
||||
-- Tactic will never be introduced in the game.
|
||||
match workerState.inventory.find? (· == val) with
|
||||
| some _ =>
|
||||
-- Tactic is in the inventory, allow it.
|
||||
-- Note: This case shouldn't be possible...
|
||||
pure ()
|
||||
| none =>
|
||||
addWarningMessage info s!"You have not unlocked the tactic '{val}' yet!"
|
||||
| some _ => pure () -- tactic is in the inventory, allow it.
|
||||
-- Tactic is not in the inventory.
|
||||
addMessageByDifficulty info s!"The tactic '{val}' is not available in this game!"
|
||||
| some tac =>
|
||||
if tac.locked then
|
||||
match gameWorkerState.inventory.find? (· == val) with
|
||||
-- Tactic is introduced at some point in the game.
|
||||
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 =>
|
||||
addWarningMessage info s!"You have not unlocked the tactic '{val}' yet!"
|
||||
| some _ => pure () -- tactic is in the inventory, allow it.
|
||||
else if tac.disabled then
|
||||
addWarningMessage info s!"The tactic '{val}' is disabled in this level!"
|
||||
-- Tactic is marked as locked and not in the inventory.
|
||||
addMessageByDifficulty info s!"You have not unlocked the tactic '{val}' yet!"
|
||||
| some _ =>
|
||||
-- Tactic is in the inventory, allow it.
|
||||
pure ()
|
||||
| .ident info _rawVal val _preresolved =>
|
||||
let ns ←
|
||||
try resolveGlobalConst (mkIdent val)
|
||||
catch | _ => pure [] -- catch "unknown constant" error
|
||||
for n in ns do
|
||||
let some (.thmInfo ..) := (← getEnv).find? n
|
||||
| return () -- not a theorem -> ignore
|
||||
-- Try to resolve the name
|
||||
let ns ←
|
||||
try resolveGlobalConst (mkIdent val)
|
||||
-- Catch "unknown constant" error
|
||||
catch | _ => pure []
|
||||
for n in ns do
|
||||
let some (.thmInfo ..) := (← getEnv).find? n
|
||||
-- Not a theorem, no checks needed.
|
||||
| return ()
|
||||
if some n = levelInfo.statementName then
|
||||
-- Forbid the theorem we are proving currently
|
||||
if some n = levelInfo.statementName then
|
||||
addErrorMessage info inputCtx s!"Structural recursion: you can't use '{n}' to proof itself!"
|
||||
addMessage info inputCtx (severity := .error)
|
||||
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 := levelInfo.lemmas ++ levelInfo.definitions
|
||||
match lemmasAndDefs.find? (fun l => l.name == n) with
|
||||
| none => addWarningMessage info s!"You have not unlocked the lemma/definition '{n}' yet!"
|
||||
| some lem =>
|
||||
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 := gameWorkerState.difficulty
|
||||
where addMessageByDifficulty (info : SourceInfo) (s : MessageData) :=
|
||||
-- See `GameServer.FileWorker.WorkerState.difficulty`. Send nothing/warnings/errors
|
||||
-- depending on difficulty.
|
||||
let difficulty := workerState.difficulty
|
||||
if difficulty > 0 then
|
||||
modify fun st => { st with
|
||||
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
|
||||
}
|
||||
}
|
||||
addMessage info inputCtx (if difficulty > 1 then .error else .warning) s
|
||||
else pure ()
|
||||
-- where addErrorMessage (info : SourceInfo) (s : MessageData) :=
|
||||
-- pure ()
|
||||
|
||||
open Elab Meta Expr in
|
||||
|
||||
def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets : Bool)
|
||||
(couldBeEndSnap : Bool) (gameWorkerState : GameWorkerState)
|
||||
(couldBeEndSnap : Bool) (gameWorkerState : WorkerState)
|
||||
(initParams : Lsp.InitializeParams) : IO Snapshot := do
|
||||
-- Recognize end snap
|
||||
if inputCtx.input.atEnd snap.mpState.pos ∧ couldBeEndSnap then
|
||||
@@ -216,8 +259,11 @@ def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets
|
||||
let tacticStx := (#[skip] ++ tacticStx.getArgs ++ #[done]).map (⟨.⟩)
|
||||
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|
|
||||
theorem the_theorem $(level.goal) := by {$(⟨tacticStx⟩)} )
|
||||
theorem the_theorem $(level.goal) := by {let_intros; $(⟨level.preamble⟩); $(⟨tacticStx⟩)} )
|
||||
Elab.Command.elabCommandTopLevel cmdStx)
|
||||
cmdCtx cmdStateRef
|
||||
let postNew := (← tacticCacheNew.get).post
|
||||
@@ -262,7 +308,7 @@ where
|
||||
private def publishIleanInfo (method : String) (m : DocumentMeta) (hOut : FS.Stream)
|
||||
(snaps : Array Snapshot) : IO Unit := do
|
||||
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 }
|
||||
hOut.writeLspNotification { method, param }
|
||||
|
||||
@@ -276,301 +322,403 @@ where
|
||||
uri : String
|
||||
deriving ToJson, FromJson
|
||||
|
||||
/-- 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
|
||||
for snap in snaps do
|
||||
if snap.diagnostics.any fun d => d.severity? == some .error ∨ d.severity? == some .warning
|
||||
then return
|
||||
let param := { uri := m.uri : GameCompletedParams}
|
||||
hOut.writeLspNotification { method := "$/game/completed", param }
|
||||
structure GameDiagnostics where
|
||||
diagnostics : List Diagnostic
|
||||
deriving ToJson, FromJson
|
||||
|
||||
/-- Elaborates the next command after `parentSnap` and emits diagnostics into `hOut`. -/
|
||||
private def nextSnap (ctx : WorkerContext) (m : DocumentMeta) (cancelTk : CancelToken)
|
||||
(gameWorkerState : GameWorkerState) (initParams : Lsp.InitializeParams)
|
||||
: AsyncElabM (Option Snapshot) := do
|
||||
cancelTk.check
|
||||
let s ← get
|
||||
let .some lastSnap := s.snaps.back? | panic! "empty snapshots"
|
||||
if lastSnap.isAtEnd then
|
||||
publishGameCompleted m ctx.hOut s.snaps
|
||||
publishDiagnostics m lastSnap.diagnostics.toArray 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
|
||||
return none
|
||||
publishProgressAtPos m lastSnap.endPos ctx.hOut
|
||||
-- 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
|
||||
let couldBeEndSnap := s.snaps.size > 1
|
||||
let snap ← compileProof m.mkInputContext lastSnap ctx.clientHasWidgets couldBeEndSnap
|
||||
gameWorkerState initParams
|
||||
set { s with snaps := s.snaps.push snap }
|
||||
-- TODO(MH): check for interrupt with increased precision
|
||||
cancelTk.check
|
||||
/- NOTE(MH): This relies on the client discarding old diagnostics upon receiving new ones
|
||||
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
|
||||
publishIleanInfoUpdate m ctx.hOut #[snap]
|
||||
return some snap
|
||||
structure GameParams where
|
||||
uri : String
|
||||
diagnostics : GameDiagnostics
|
||||
deriving ToJson, FromJson
|
||||
|
||||
/-- Elaborates all commands after the last snap (at least the header snap is assumed to exist), emitting the diagnostics into `hOut`. -/
|
||||
def unfoldSnaps (m : DocumentMeta) (snaps : Array Snapshot) (cancelTk : CancelToken)
|
||||
(startAfterMs : UInt32) (gameWorkerState : GameWorkerState)
|
||||
: ReaderT WorkerContext IO (AsyncList ElabTaskError Snapshot) := do
|
||||
let ctx ← read
|
||||
let some headerSnap := snaps[0]? | panic! "empty snapshots"
|
||||
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)
|
||||
publishIleanInfoFinal m ctx.hOut #[headerSnap]
|
||||
return AsyncList.ofList [headerSnap]
|
||||
else
|
||||
-- This will overwrite existing ilean info for the file since this has a
|
||||
-- higher version number.
|
||||
publishIleanInfoUpdate m ctx.hOut snaps
|
||||
return AsyncList.ofList snaps.toList ++ AsyncList.delayed (← EIO.asTask (ε := ElabTaskError) (prio := .dedicated) do
|
||||
IO.sleep startAfterMs
|
||||
AsyncList.unfoldAsync (nextSnap ctx m cancelTk gameWorkerState ctx.initParams) { snaps })
|
||||
-- `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 errors 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
|
||||
for snap in snaps do
|
||||
if snap.diagnostics.any fun d => d.severity? == some .error ∨ d.severity? == some .warning
|
||||
then return
|
||||
let param := { uri := m.uri : GameCompletedParams}
|
||||
hOut.writeLspNotification { method := "$/game/completed", param }
|
||||
|
||||
/-- copied from `Lean.Server.FileWorker.nextCmdSnap`. -/
|
||||
-- @[inherit_doc Lean.Server.FileWorker.nextCmdSnap] -- cannot inherit from private
|
||||
private def nextCmdSnap (ctx : WorkerContext) (m : DocumentMeta) (cancelTk : CancelToken)
|
||||
(gameWorkerState : WorkerState) (initParams : Lsp.InitializeParams) :
|
||||
AsyncElabM (Option Snapshot) := do
|
||||
cancelTk.check
|
||||
let s ← get
|
||||
let .some lastSnap := s.snaps.back? | panic! "empty snapshots"
|
||||
if lastSnap.isAtEnd then
|
||||
publishDiagnostics m lastSnap.diagnostics.toArray ctx.hOut
|
||||
publishProgressDone m ctx.hOut
|
||||
publishIleanInfoFinal m ctx.hOut s.snaps
|
||||
return none
|
||||
publishProgressAtPos m lastSnap.endPos ctx.hOut
|
||||
|
||||
-- (modified part)
|
||||
-- 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
|
||||
let couldBeEndSnap := s.snaps.size > 1
|
||||
let snap ← compileProof m.mkInputContext lastSnap ctx.clientHasWidgets couldBeEndSnap
|
||||
gameWorkerState initParams
|
||||
|
||||
set { s with snaps := s.snaps.push snap }
|
||||
cancelTk.check
|
||||
-- publishProofState m snap initParams ctx.hOut
|
||||
publishDiagnostics m snap.diagnostics.toArray ctx.hOut
|
||||
publishIleanInfoUpdate m ctx.hOut #[snap]
|
||||
return some snap
|
||||
|
||||
-- Copied from `Lean.Server.FileWorker.unfoldCmdSnaps` using our own `nextCmdSnap`.
|
||||
@[inherit_doc Lean.Server.FileWorker.unfoldCmdSnaps]
|
||||
def unfoldCmdSnaps (m : DocumentMeta) (snaps : Array Snapshot) (cancelTk : CancelToken)
|
||||
(startAfterMs : UInt32) (gameWorkerState : WorkerState)
|
||||
: ReaderT WorkerContext IO (AsyncList ElabTaskError Snapshot) := do
|
||||
let ctx ← read
|
||||
let some headerSnap := snaps[0]? | panic! "empty snapshots"
|
||||
if headerSnap.msgLog.hasErrors then
|
||||
publishProgressAtPos m headerSnap.beginPos ctx.hOut (kind := LeanFileProgressKind.fatalError)
|
||||
publishIleanInfoFinal m ctx.hOut #[headerSnap]
|
||||
return AsyncList.ofList [headerSnap]
|
||||
else
|
||||
publishIleanInfoUpdate m ctx.hOut snaps
|
||||
return AsyncList.ofList snaps.toList ++ AsyncList.delayed (← EIO.asTask (ε := ElabTaskError) (prio := .dedicated) do
|
||||
IO.sleep startAfterMs
|
||||
AsyncList.unfoldAsync (nextCmdSnap ctx m cancelTk gameWorkerState ctx.initParams) { snaps })
|
||||
|
||||
end Elab
|
||||
|
||||
section Updates
|
||||
|
||||
/-- Given the new document, updates editable doc state. -/
|
||||
def updateDocument (newMeta : DocumentMeta) : GameWorkerM Unit := do
|
||||
let s ← get
|
||||
let ctx ← read
|
||||
let oldDoc := (← StateT.lift get).doc
|
||||
oldDoc.cancelTk.set
|
||||
let initHeaderStx := (← StateT.lift get).initHeaderStx
|
||||
let (newHeaderStx, newMpState, _) ← Parser.parseHeader newMeta.mkInputContext
|
||||
let cancelTk ← CancelToken.new
|
||||
let headSnapTask := oldDoc.cmdSnaps.waitHead?
|
||||
let newSnaps ← if initHeaderStx != newHeaderStx then
|
||||
EIO.asTask (ε := ElabTaskError) (prio := .dedicated) do
|
||||
IO.sleep ctx.initParams.editDelay.toUInt32
|
||||
cancelTk.check
|
||||
IO.Process.exit 2
|
||||
else EIO.mapTask (ε := ElabTaskError) (t := headSnapTask) (prio := .dedicated) fun headSnap?? => do
|
||||
-- There is always at least one snapshot absent exceptions
|
||||
let some headSnap ← MonadExcept.ofExcept headSnap?? | panic! "empty snapshots"
|
||||
let newHeaderSnap := { headSnap with stx := newHeaderStx, mpState := newMpState }
|
||||
let changePos := oldDoc.meta.text.source.firstDiffPos newMeta.text.source
|
||||
-- Ignore exceptions, we are only interested in the successful snapshots
|
||||
let (cmdSnaps, _) ← oldDoc.cmdSnaps.getFinishedPrefix
|
||||
-- NOTE(WN): we invalidate eagerly as `endPos` consumes input greedily. To re-elaborate only
|
||||
-- when really necessary, we could do a whitespace-aware `Syntax` comparison instead.
|
||||
let mut validSnaps ← pure (cmdSnaps.takeWhile (fun s => s.endPos < changePos))
|
||||
if h : validSnaps.length ≤ 1 then
|
||||
validSnaps := [newHeaderSnap]
|
||||
else
|
||||
/- When at least one valid non-header snap exists, it may happen that a change does not fall
|
||||
within the syntactic range of that last snap but still modifies it by appending tokens.
|
||||
We check for this here. We do not currently handle crazy grammars in which an appended
|
||||
token can merge two or more previous commands into one. To do so would require reparsing
|
||||
the entire file. -/
|
||||
have : validSnaps.length ≥ 2 := Nat.gt_of_not_le h
|
||||
let mut lastSnap := validSnaps.getLast (by subst ·; simp at h)
|
||||
let preLastSnap :=
|
||||
have : 0 < validSnaps.length := Nat.lt_of_lt_of_le (by decide) this
|
||||
have : validSnaps.length - 2 < validSnaps.length := Nat.sub_lt this (by decide)
|
||||
validSnaps[validSnaps.length - 2]
|
||||
let newLastStx ← parseNextCmd newMeta.mkInputContext preLastSnap
|
||||
if newLastStx != lastSnap.stx then
|
||||
validSnaps := validSnaps.dropLast
|
||||
-- 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)
|
||||
unfoldSnaps newMeta validSnaps.toArray cancelTk s ctx
|
||||
(startAfterMs := ctx.initParams.editDelay.toUInt32)
|
||||
StateT.lift <| modify fun st => { st with
|
||||
doc := { meta := newMeta, cmdSnaps := AsyncList.delayed newSnaps, cancelTk }}
|
||||
def updateDocument (newMeta : DocumentMeta) : WorkerM Unit := do
|
||||
let s ← get
|
||||
let ctx ← read
|
||||
let oldDoc := (← StateT.lift get).doc
|
||||
oldDoc.cancelTk.set
|
||||
let initHeaderStx := (← StateT.lift get).initHeaderStx
|
||||
let (newHeaderStx, newMpState, _) ← Parser.parseHeader newMeta.mkInputContext
|
||||
let cancelTk ← CancelToken.new
|
||||
let headSnapTask := oldDoc.cmdSnaps.waitHead?
|
||||
let newSnaps ← if initHeaderStx != newHeaderStx then
|
||||
EIO.asTask (ε := ElabTaskError) (prio := .dedicated) do
|
||||
IO.sleep ctx.initParams.editDelay.toUInt32
|
||||
cancelTk.check
|
||||
IO.Process.exit 2
|
||||
else EIO.mapTask (ε := ElabTaskError) (t := headSnapTask) (prio := .dedicated) fun headSnap?? => do
|
||||
-- There is always at least one snapshot absent exceptions
|
||||
let some headSnap ← MonadExcept.ofExcept headSnap?? | panic! "empty snapshots"
|
||||
let newHeaderSnap := { headSnap with stx := newHeaderStx, mpState := newMpState }
|
||||
let changePos := oldDoc.meta.text.source.firstDiffPos newMeta.text.source
|
||||
-- Ignore exceptions, we are only interested in the successful snapshots
|
||||
let (cmdSnaps, _) ← oldDoc.cmdSnaps.getFinishedPrefix
|
||||
-- NOTE(WN): we invalidate eagerly as `endPos` consumes input greedily. To re-elaborate only
|
||||
-- when really necessary, we could do a whitespace-aware `Syntax` comparison instead.
|
||||
let mut validSnaps ← pure (cmdSnaps.takeWhile (fun s => s.endPos < changePos))
|
||||
if h : validSnaps.length ≤ 1 then
|
||||
validSnaps := [newHeaderSnap]
|
||||
else
|
||||
/- When at least one valid non-header snap exists, it may happen that a change does not fall
|
||||
within the syntactic range of that last snap but still modifies it by appending tokens.
|
||||
We check for this here. We do not currently handle crazy grammars in which an appended
|
||||
token can merge two or more previous commands into one. To do so would require reparsing
|
||||
the entire file. -/
|
||||
have : validSnaps.length ≥ 2 := Nat.gt_of_not_le h
|
||||
let mut lastSnap := validSnaps.getLast (by subst ·; simp at h)
|
||||
let preLastSnap :=
|
||||
have : 0 < validSnaps.length := Nat.lt_of_lt_of_le (by decide) this
|
||||
have : validSnaps.length - 2 < validSnaps.length := Nat.sub_lt this (by decide)
|
||||
validSnaps[validSnaps.length - 2]
|
||||
let newLastStx ← parseNextCmd newMeta.mkInputContext preLastSnap
|
||||
if newLastStx != lastSnap.stx then
|
||||
validSnaps := validSnaps.dropLast
|
||||
-- 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)
|
||||
unfoldCmdSnaps newMeta validSnaps.toArray cancelTk s ctx
|
||||
(startAfterMs := ctx.initParams.editDelay.toUInt32)
|
||||
StateT.lift <| modify fun st => { st with
|
||||
doc := { meta := newMeta, cmdSnaps := AsyncList.delayed newSnaps, cancelTk }}
|
||||
|
||||
end Updates
|
||||
|
||||
section Initialization
|
||||
|
||||
def DocumentMeta.mkInputContext (doc : DocumentMeta) : Parser.InputContext where
|
||||
input := "" -- No header!
|
||||
fileName := (System.Uri.fileUriToPath? doc.uri).getD doc.uri |>.toString
|
||||
fileMap := default
|
||||
def DocumentMeta.mkInputContext (doc : DocumentMeta) : Parser.InputContext where
|
||||
input := "" -- No header!
|
||||
fileName := (System.Uri.fileUriToPath? doc.uri).getD doc.uri |>.toString
|
||||
fileMap := default
|
||||
|
||||
def compileHeader (m : DocumentMeta) (hOut : FS.Stream) (opts : Options) (hasWidgets : Bool)
|
||||
(gameDir : String) (module : Name):
|
||||
IO (Syntax × Task (Except Error (Snapshot × SearchPath))) := do
|
||||
-- Determine search paths of the game project by running `lake env printenv LEAN_PATH`.
|
||||
let out ← IO.Process.output
|
||||
{ cwd := gameDir, cmd := "lake", args := #["env","printenv","LEAN_PATH"] }
|
||||
if out.exitCode != 0 then
|
||||
throwServerError s!"Error while running Lake: {out.stderr}"
|
||||
/-- `gameDir` and `module` were added.
|
||||
|
||||
-- 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
|
||||
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
|
||||
-- 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}"
|
||||
|
||||
-- Set the search path
|
||||
Lean.searchPathRef.set paths
|
||||
-- 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
|
||||
|
||||
let env ← importModules' #[{ module := `Init : Import }, { module := module : Import }]
|
||||
-- Set the search path
|
||||
Lean.searchPathRef.set paths
|
||||
|
||||
-- use empty header
|
||||
let (headerStx, headerParserState, msgLog) ← Parser.parseHeader
|
||||
{m.mkInputContext with
|
||||
input := ""
|
||||
fileMap := FileMap.ofString ""}
|
||||
(headerStx, ·) <$> EIO.asTask do
|
||||
let mut srcSearchPath : SearchPath := paths --← initSrcSearchPath (← getBuildDir)
|
||||
let headerEnv := env
|
||||
let mut headerEnv := headerEnv
|
||||
try
|
||||
if let some path := System.Uri.fileUriToPath? m.uri then
|
||||
headerEnv := headerEnv.setMainModule (← moduleNameOfFileName path none)
|
||||
catch _ => pure ()
|
||||
let cmdState := Elab.Command.mkState headerEnv {} opts
|
||||
let cmdState := { cmdState with infoState := {
|
||||
enabled := true
|
||||
trees := #[Elab.InfoTree.context ({
|
||||
env := headerEnv
|
||||
fileMap := m.text
|
||||
ngen := { namePrefix := `_worker }
|
||||
}) (Elab.InfoTree.node
|
||||
(Elab.Info.ofCommandInfo { elaborator := `header, stx := headerStx })
|
||||
(headerStx[1].getArgs.toList.map (fun importStx =>
|
||||
Elab.InfoTree.node (Elab.Info.ofCommandInfo {
|
||||
elaborator := `import
|
||||
stx := importStx
|
||||
}) #[].toPArray'
|
||||
)).toPArray'
|
||||
)].toPArray'
|
||||
}}
|
||||
let headerSnap := {
|
||||
beginPos := 0
|
||||
stx := headerStx
|
||||
mpState := {}
|
||||
cmdState := cmdState
|
||||
interactiveDiags := ← cmdState.messages.msgs.mapM (Widget.msgToInteractiveDiagnostic m.text · hasWidgets)
|
||||
tacticCache := (← IO.mkRef {})
|
||||
}
|
||||
publishDiagnostics m headerSnap.diagnostics.toArray hOut
|
||||
return (headerSnap, srcSearchPath)
|
||||
let env ← importModules' #[{ module := `Init : Import }, { module := module : Import }]
|
||||
|
||||
def initializeWorker (meta : DocumentMeta) (i o e : FS.Stream) (initParams : InitializeParams) (opts : Options)
|
||||
(gameDir : String) (gameWorkerState : GameWorkerState) : IO (WorkerContext × WorkerState) := do
|
||||
let clientHasWidgets := initParams.initializationOptions?.bind (·.hasWidgets?) |>.getD false
|
||||
-- use empty header
|
||||
let (headerStx, headerParserState, msgLog) ← Parser.parseHeader
|
||||
{m.mkInputContext with
|
||||
input := ""
|
||||
fileMap := FileMap.ofString ""}
|
||||
(headerStx, ·) <$> EIO.asTask do
|
||||
let mut srcSearchPath : SearchPath := paths --← initSrcSearchPath (← getBuildDir)
|
||||
let headerEnv := env
|
||||
let mut headerEnv := headerEnv
|
||||
try
|
||||
if let some path := System.Uri.fileUriToPath? m.uri then
|
||||
headerEnv := headerEnv.setMainModule (← moduleNameOfFileName path none)
|
||||
catch _ => pure ()
|
||||
let cmdState := Elab.Command.mkState headerEnv {} opts
|
||||
let cmdState := { cmdState with infoState := {
|
||||
enabled := true
|
||||
trees := #[Elab.InfoTree.context (.commandCtx {
|
||||
env := headerEnv
|
||||
fileMap := m.text
|
||||
ngen := { namePrefix := `_worker }
|
||||
}) (Elab.InfoTree.node
|
||||
(Elab.Info.ofCommandInfo { elaborator := `header, stx := headerStx })
|
||||
(headerStx[1].getArgs.toList.map (fun importStx =>
|
||||
Elab.InfoTree.node (Elab.Info.ofCommandInfo {
|
||||
elaborator := `import
|
||||
stx := importStx
|
||||
}) #[].toPArray'
|
||||
)).toPArray'
|
||||
)].toPArray'
|
||||
}}
|
||||
let headerSnap := {
|
||||
beginPos := 0
|
||||
stx := headerStx
|
||||
mpState := {} -- was `headerParserState`
|
||||
cmdState := cmdState
|
||||
interactiveDiags := ← cmdState.messages.msgs.mapM (Widget.msgToInteractiveDiagnostic m.text · hasWidgets)
|
||||
tacticCache := (← IO.mkRef {})
|
||||
}
|
||||
publishDiagnostics m headerSnap.diagnostics.toArray hOut
|
||||
return (headerSnap, srcSearchPath)
|
||||
|
||||
let (headerStx, headerTask) ← compileHeader meta o opts (hasWidgets := clientHasWidgets)
|
||||
gameDir gameWorkerState.levelInfo.module
|
||||
let cancelTk ← CancelToken.new
|
||||
let ctx :=
|
||||
{ hIn := i
|
||||
hOut := o
|
||||
hLog := e
|
||||
headerTask
|
||||
initParams
|
||||
clientHasWidgets
|
||||
}
|
||||
let cmdSnaps ← EIO.mapTask (t := headerTask) (match · with
|
||||
| Except.ok (s, _) => unfoldSnaps meta #[s] cancelTk gameWorkerState ctx (startAfterMs := 0)
|
||||
| Except.error e => throw (e : ElabTaskError))
|
||||
let doc : EditableDocument := { meta, cmdSnaps := AsyncList.delayed cmdSnaps, cancelTk }
|
||||
return (ctx,
|
||||
{ doc := doc
|
||||
initHeaderStx := headerStx
|
||||
pendingRequests := RBMap.empty
|
||||
rpcSessions := RBMap.empty
|
||||
})
|
||||
/-- Copied from `Lean.Server.FileWorker.initializeWorker`. Added `gameDir` and
|
||||
`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 (headerStx, headerTask) ← compileHeader meta o opts (hasWidgets := clientHasWidgets)
|
||||
(gameDir := gameDir) (module := gameWorkerState.levelInfo.module)
|
||||
let cancelTk ← CancelToken.new
|
||||
let ctx := {
|
||||
hIn := i
|
||||
hOut := o
|
||||
hLog := e
|
||||
headerTask
|
||||
initParams
|
||||
clientHasWidgets
|
||||
}
|
||||
let cmdSnaps ← EIO.mapTask (t := headerTask) (match · with
|
||||
| Except.ok (s, _) => unfoldCmdSnaps meta #[s] cancelTk gameWorkerState ctx (startAfterMs := 0)
|
||||
| Except.error e => throw (e : ElabTaskError))
|
||||
let doc : EditableDocument := { meta, cmdSnaps := AsyncList.delayed cmdSnaps, cancelTk }
|
||||
return (ctx, {
|
||||
doc := doc
|
||||
initHeaderStx := headerStx
|
||||
currHeaderStx := headerStx
|
||||
importCachingTask? := none
|
||||
pendingRequests := RBMap.empty
|
||||
rpcSessions := RBMap.empty
|
||||
})
|
||||
|
||||
end Initialization
|
||||
|
||||
section NotificationHandling
|
||||
|
||||
def handleDidChange (p : DidChangeTextDocumentParams) : GameWorkerM Unit := do
|
||||
let docId := p.textDocument
|
||||
let changes := p.contentChanges
|
||||
let oldDoc := (← StateT.lift get).doc
|
||||
let some newVersion ← pure docId.version?
|
||||
| throwServerError "Expected version number"
|
||||
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
|
||||
updateDocument ⟨docId.uri, newVersion, newDocText, .always⟩
|
||||
/-- 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 changes := p.contentChanges
|
||||
let oldDoc := (← StateT.lift get).doc -- needed a lift to our custom `WorkerM`
|
||||
let newVersion := docId.version?.getD 0
|
||||
if ¬ changes.isEmpty then
|
||||
let newDocText := foldDocumentChanges changes oldDoc.meta.text
|
||||
-- modification: set the `DependencyBuildMode` from
|
||||
-- `oldDoc.meta.dependencyBuildMode` to `.always`
|
||||
updateDocument ⟨docId.uri, newVersion, newDocText, .always⟩
|
||||
|
||||
end NotificationHandling
|
||||
|
||||
section MessageHandling
|
||||
|
||||
def handleNotification (method : String) (params : Json) : GameWorkerM Unit := do
|
||||
let handle := fun paramType [FromJson paramType] (handler : paramType → GameWorkerM Unit) =>
|
||||
(StateT.lift <| parseParams paramType params) >>= handler
|
||||
match method with
|
||||
| "textDocument/didChange" => handle DidChangeTextDocumentParams (handleDidChange)
|
||||
| "$/cancelRequest" => handle CancelParams (handleCancelRequest ·)
|
||||
| "$/setTrace" => pure ()
|
||||
| "$/lean/rpc/release" => handle RpcReleaseParams (handleRpcRelease ·)
|
||||
| "$/lean/rpc/keepAlive" => handle RpcKeepAliveParams (handleRpcKeepAlive ·)
|
||||
| _ => throwServerError s!"Got unsupported notification method: {method}"
|
||||
|
||||
/--
|
||||
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
|
||||
match method with
|
||||
-- Modified `textDocument/didChange`, using a custom `handleDidChange`
|
||||
| "textDocument/didChange" => handle DidChangeTextDocumentParams (handleDidChange)
|
||||
-- unmodified
|
||||
| "$/cancelRequest" => handle CancelParams (handleCancelRequest ·)
|
||||
-- unmodified
|
||||
| "$/lean/rpc/release" => handle RpcReleaseParams (handleRpcRelease ·)
|
||||
-- unmodified
|
||||
| "$/lean/rpc/keepAlive" => handle RpcKeepAliveParams (handleRpcKeepAlive ·)
|
||||
-- New. TODO: What is this for?
|
||||
| "$/setTrace" => pure ()
|
||||
| _ => throwServerError s!"Got unsupported notification method: {method}"
|
||||
|
||||
end MessageHandling
|
||||
|
||||
section MainLoop
|
||||
partial def mainLoop : GameWorkerM Unit := do
|
||||
let ctx ← read
|
||||
let mut st ← StateT.lift get
|
||||
let msg ← ctx.hIn.readLspMessage
|
||||
let filterFinishedTasks (acc : PendingRequestMap) (id : RequestID) (task : Task (Except IO.Error Unit))
|
||||
: IO PendingRequestMap := do
|
||||
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
|
||||
throwServerError s!"Failed responding to request {id}: {e}"
|
||||
pure <| acc.erase id
|
||||
else pure acc
|
||||
let pendingRequests ← st.pendingRequests.foldM (fun acc id task => filterFinishedTasks acc id task) st.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
|
||||
let sesh ← seshRef.get
|
||||
if (← sesh.hasExpired) then
|
||||
st := { st with rpcSessions := st.rpcSessions.erase id }
|
||||
/--
|
||||
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 mut st ← StateT.lift get
|
||||
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))
|
||||
: IO PendingRequestMap := do
|
||||
if (← hasFinished task) then
|
||||
if let Except.error e := task.get then
|
||||
throwServerError s!"Failed responding to request {id}: {e}"
|
||||
pure <| acc.erase id
|
||||
else pure acc
|
||||
let pendingRequests ← st.pendingRequests.foldM (fun acc id task => filterFinishedTasks acc id task) st.pendingRequests
|
||||
st := { st with pendingRequests }
|
||||
for (id, seshRef) in st.rpcSessions do
|
||||
let sesh ← seshRef.get
|
||||
if (← sesh.hasExpired) then
|
||||
st := { st with rpcSessions := st.rpcSessions.erase id }
|
||||
|
||||
set st
|
||||
-- Process the RPC-message and restart main-loop.
|
||||
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) =>
|
||||
-- Requests are handled by the unmodified lean server.
|
||||
handleRequest id method (toJson params)
|
||||
mainLoop
|
||||
| Message.notification "exit" none =>
|
||||
let doc := st.doc
|
||||
doc.cancelTk.set
|
||||
doc.cmdSnaps.cancel
|
||||
return ()
|
||||
| Message.notification method (some params) =>
|
||||
-- Custom notification handler
|
||||
handleNotification method (toJson params)
|
||||
mainLoop
|
||||
| _ =>
|
||||
throwServerError s!"Got invalid JSON-RPC message: {toJson msg}"
|
||||
|
||||
set st
|
||||
match msg with
|
||||
| Message.request id method (some params) =>
|
||||
handleRequest id method (toJson params)
|
||||
mainLoop
|
||||
| Message.notification "exit" none =>
|
||||
let doc := st.doc
|
||||
doc.cancelTk.set
|
||||
return ()
|
||||
| Message.request id "shutdown" none =>
|
||||
ctx.hOut.writeLspResponse ⟨id, Json.null⟩
|
||||
mainLoop
|
||||
| Message.notification method (some params) =>
|
||||
handleNotification method (toJson params)
|
||||
mainLoop
|
||||
| _ => throwServerError s!"Got invalid JSON-RPC message: {toJson msg}"
|
||||
end MainLoop
|
||||
|
||||
/-- 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 o ← maybeTee "fwOut.txt" true o
|
||||
|
||||
|
||||
-- BIG MODIFICATION
|
||||
let initRequest ← i.readLspRequestAs "initialize" Game.InitializeParams
|
||||
o.writeLspResponse {
|
||||
id := initRequest.id
|
||||
@@ -586,16 +734,16 @@ def initAndRunWorker (i o e : FS.Stream) (opts : Options) (gameDir : String) : I
|
||||
discard $ i.readLspNotificationAs "initialized" InitializedParams
|
||||
let ⟨_, param⟩ ← i.readLspNotificationAs "textDocument/didOpen" DidOpenTextDocumentParams
|
||||
|
||||
|
||||
let doc := param.textDocument
|
||||
/- NOTE(WN): `toFileMap` marks line beginnings as immediately following
|
||||
"\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. -/
|
||||
-- modification: using `.always`
|
||||
let meta : DocumentMeta := ⟨doc.uri, doc.version, doc.text.toFileMap, .always⟩
|
||||
let e := e.withPrefix s!"[{param.textDocument.uri}] "
|
||||
let _ ← IO.setStderr e
|
||||
try
|
||||
|
||||
|
||||
-- BIG MODIFICATION
|
||||
let game ← loadGameData gameDir
|
||||
-- TODO: We misuse the `rootUri` field to the gameName
|
||||
let rootUri? : Option String := some (toString game.name)
|
||||
@@ -606,14 +754,17 @@ def initAndRunWorker (i o e : FS.Stream) (opts : Options) (gameDir : String) : I
|
||||
let levelInfo ← loadLevelData gameDir levelId.world levelId.level
|
||||
let some initializationOptions := initRequest.param.initializationOptions?
|
||||
| throwServerError "no initialization options found"
|
||||
let gameWorkerState : GameWorkerState:= {
|
||||
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) <|
|
||||
StateT.run (s := gameWorkerState) <| (mainLoop)
|
||||
|
||||
|
||||
return (0 : UInt32)
|
||||
catch e =>
|
||||
IO.eprintln e
|
||||
@@ -623,6 +774,15 @@ def initAndRunWorker (i o e : FS.Stream) (opts : Options) (gameDir : String) : I
|
||||
message := e.toString }] o
|
||||
return (1 : UInt32)
|
||||
|
||||
/--
|
||||
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 o ← IO.getStdout
|
||||
@@ -630,8 +790,6 @@ def workerMain (opts : Options) (args : List String): IO UInt32 := do
|
||||
try
|
||||
let some gameDir := args[1]? | throwServerError "Expected second argument: gameDir"
|
||||
let exitCode ← initAndRunWorker i o e opts gameDir
|
||||
-- HACK: all `Task`s are currently "foreground", i.e. we join on them on main thread exit, but we definitely don't
|
||||
-- want to do that in the case of the worker processes, which can produce non-terminating tasks evaluating user code
|
||||
o.flush
|
||||
e.flush
|
||||
IO.Process.exit exitCode.toUInt8
|
||||
@@ -639,4 +797,4 @@ def workerMain (opts : Options) (args : List String): IO UInt32 := do
|
||||
e.putStrLn s!"worker initialization error: {err}"
|
||||
return (1 : UInt32)
|
||||
|
||||
end MyServer.FileWorker
|
||||
end GameServer.FileWorker
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import Lean
|
||||
import GameServer.Helpers.PrettyPrinter
|
||||
|
||||
/-! This document contains various things which cluttered `Commands.lean`. -/
|
||||
|
||||
open Lean Meta Elab Command
|
||||
|
||||
syntax hintArg := atomic(" (" (&"strict" <|> &"hidden") " := " withoutPosition(term) ")")
|
||||
|
||||
/-! ## 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
|
||||
@@ -61,47 +62,10 @@ def parseDocCommentLegacy (doc: Option (TSyntax `Lean.Parser.Command.docComment)
|
||||
and remove the string following it!"
|
||||
pure <| ← parseDocComment! doc
|
||||
|
||||
/-! ## Statement string -/
|
||||
|
||||
def getStatement (name : Name) : CommandElabM MessageData := do
|
||||
return ← addMessageContextPartial (.ofPPFormat { pp := fun
|
||||
| some ctx => ctx.runMetaM <| PrettyPrinter.ppSignature name
|
||||
| none => return "that's a bug." })
|
||||
|
||||
-- Note: We use `String` because we can't send `MessageData` as json, but
|
||||
-- `MessageData` might be better for interactive highlighting.
|
||||
/-- Get a string of the form `my_lemma (n : ℕ) : n + n = 2 * n`.
|
||||
|
||||
Note: A statement like `theorem abc : ∀ x : Nat, x ≥ 0` would be turned into
|
||||
`theorem abc (x : Nat) : x ≥ 0` by `PrettyPrinter.ppSignature`. -/
|
||||
def getStatementString (name : Name) : CommandElabM String := do
|
||||
try
|
||||
return ← (← getStatement name).toString
|
||||
catch
|
||||
| _ => throwError m!"Could not find {name} in context."
|
||||
-- TODO: I think it would be nicer to unresolve Namespaces as much as possible.
|
||||
|
||||
/-- A `attr := ...` option for `Statement`. Add attributes to the defined theorem. -/
|
||||
syntax statementAttr := "(" &"attr" ":=" Parser.Term.attrInstance,* ")"
|
||||
-- TODO
|
||||
|
||||
|
||||
/-- Remove any spaces at the beginning of a new line -/
|
||||
partial def removeIndentation (s : String) : String :=
|
||||
let rec loop (i : String.Pos) (acc : String) (removeSpaces := false) : String :=
|
||||
let c := s.get i
|
||||
let i := s.next i
|
||||
if s.atEnd i then
|
||||
acc.push c
|
||||
else if removeSpaces && c == ' ' then
|
||||
loop i acc (removeSpaces := true)
|
||||
else if c == '\n' then
|
||||
loop i (acc.push c) (removeSpaces := true)
|
||||
else
|
||||
loop i (acc.push c)
|
||||
loop ⟨0⟩ ""
|
||||
|
||||
|
||||
/-! ## Loops in Graph-like construct
|
||||
|
||||
TODO: Why are we not using graphs here but our own construct `HashMap Name (HashSet Name)`?
|
||||
|
||||
@@ -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⟩ ""
|
||||
@@ -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
|
||||
|
||||
open Lean Lean.Widget Lean.Server
|
||||
|
||||
structure GameHint where
|
||||
text : String
|
||||
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
|
||||
|
||||
-- duplicated with custom `InteractiveGoalCore`
|
||||
-- @[inherit_doc Lean.Widget.InteractiveGoalCore.pretty]
|
||||
def InteractiveGoalCore.pretty (g : InteractiveGoalCore) (userName? : Option String)
|
||||
(goalPrefix : String) : Format := Id.run do
|
||||
let indent := 2 -- Use option
|
||||
@@ -79,8 +24,7 @@ def InteractiveGoalCore.pretty (g : InteractiveGoalCore) (userName? : Option Str
|
||||
ret := addLine ret
|
||||
let names := hyp.names
|
||||
|>.toList
|
||||
|>.filter (not ∘ Name.isAnonymous)
|
||||
|>.map toString
|
||||
|>.filter (· != toString Name.anonymous)
|
||||
|> " ".intercalate
|
||||
match names with
|
||||
| "" =>
|
||||
@@ -97,16 +41,24 @@ where
|
||||
addLine (fmt : Format) : Format :=
|
||||
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 :=
|
||||
g.toInteractiveGoalCore.pretty g.userName? g.goalPrefix
|
||||
|
||||
-- duplicated with custom `InteractiveTermGoal`
|
||||
-- @[inherit_doc Lean.Widget.InteractiveTermGoal.pretty]
|
||||
def InteractiveTermGoal.pretty (g : InteractiveTermGoal) : Format :=
|
||||
g.toInteractiveGoalCore.pretty none "⊢ "
|
||||
|
||||
-- duplicated with custom `InteractiveGoal`
|
||||
-- @[inherit_doc Lean.Widget.InteractiveGoals]
|
||||
structure InteractiveGoals where
|
||||
goals : Array InteractiveGoal
|
||||
deriving RpcEncodable
|
||||
|
||||
-- duplicated with custom `InteractiveGoals`
|
||||
-- @[inherit_doc Lean.Widget.InteractiveGoals.append]
|
||||
def InteractiveGoals.append (l r : InteractiveGoals) : InteractiveGoals where
|
||||
goals := l.goals ++ r.goals
|
||||
|
||||
@@ -114,9 +66,10 @@ instance : Append InteractiveGoals := ⟨InteractiveGoals.append⟩
|
||||
instance : EmptyCollection InteractiveGoals := ⟨{goals := #[]}⟩
|
||||
|
||||
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)
|
||||
(ids : Array (Name × FVarId)) (type : Expr) (value? : Option Expr := none) :
|
||||
(ids : Array (String × FVarId)) (type : Expr) (value? : Option Expr := none) :
|
||||
MetaM (Array InteractiveHypothesisBundle) := do
|
||||
if ids.size == 0 then
|
||||
throwError "Can only add a nonzero number of ids as an InteractiveHypothesisBundle."
|
||||
@@ -125,11 +78,12 @@ def addInteractiveHypothesisBundle (hyps : Array InteractiveHypothesisBundle)
|
||||
return hyps.push {
|
||||
names
|
||||
fvarIds
|
||||
type := (← ppExprTagged type)
|
||||
val? := (← value?.mapM ppExprTagged)
|
||||
isInstance? := if (← isClass? type).isSome then true else none
|
||||
isType? := if (← instantiateMVars type).isSort then true else none
|
||||
isAssumption? := if (← inferType type).isProp then true else none
|
||||
type := (← ppExprTagged type)
|
||||
val? := (← value?.mapM ppExprTagged)
|
||||
isInstance? := if (← isClass? type).isSome then true else none
|
||||
isType? := if (← instantiateMVars type).isSort then true else none
|
||||
-- Added:
|
||||
isAssumption? := if (← inferType type).isProp then true else none
|
||||
}
|
||||
|
||||
open Meta in
|
||||
@@ -142,13 +96,15 @@ def withGoalCtx (goal : MVarId) (action : LocalContext → MetavarDecl → n α)
|
||||
withLCtx lctx mvarDecl.localInstances (action lctx mvarDecl)
|
||||
|
||||
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 ppImplDetailHyps := pp.implementationDetailHyps.get (← getOptions)
|
||||
let showLetValues := pp.showLetValues.get (← getOptions)
|
||||
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) :=
|
||||
if ids.isEmpty then
|
||||
pure hyps
|
||||
@@ -156,7 +112,7 @@ def goalToInteractive (mvarId : MVarId) (hints : Array GameHint): MetaM Interact
|
||||
match type? with
|
||||
| none => pure hyps
|
||||
| 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 hyps : Array InteractiveHypothesisBundle := #[]
|
||||
for localDecl in lctx do
|
||||
@@ -165,7 +121,7 @@ def goalToInteractive (mvarId : MVarId) (hints : Array GameHint): MetaM Interact
|
||||
else
|
||||
match localDecl with
|
||||
| LocalDecl.cdecl _index fvarId varName type _ _ =>
|
||||
let varName := varName.simpMacroScopes
|
||||
let varName := toString varName
|
||||
let type ← instantiateMVars type
|
||||
if prevType? == none || prevType? == some type then
|
||||
varNames := varNames.push (varName, fvarId)
|
||||
@@ -174,7 +130,7 @@ def goalToInteractive (mvarId : MVarId) (hints : Array GameHint): MetaM Interact
|
||||
varNames := #[(varName, fvarId)]
|
||||
prevType? := some type
|
||||
| LocalDecl.ldecl _index fvarId varName type val _ _ => do
|
||||
let varName := varName.simpMacroScopes
|
||||
let varName := toString varName
|
||||
hyps ← pushPending varNames prevType? hyps
|
||||
let type ← instantiateMVars type
|
||||
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 {
|
||||
hyps
|
||||
type := goalFmt
|
||||
ctx := ⟨← Elab.ContextInfo.save⟩
|
||||
ctx := ⟨{← Elab.CommandContextInfo.save with }⟩
|
||||
userName?
|
||||
goalPrefix := getGoalPrefix mvarDecl
|
||||
mvarId
|
||||
hints
|
||||
}
|
||||
|
||||
end GameServer
|
||||
|
||||
@@ -121,7 +121,7 @@ partial def collectUsedInventory (stx : Syntax) (acc : UsedInventory := {}) : Co
|
||||
| .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"]
|
||||
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}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import GameServer.EnvExtensions
|
||||
import GameServer.InteractiveGoal
|
||||
import GameServer.Hints
|
||||
import I18n
|
||||
|
||||
open Lean
|
||||
open Server
|
||||
@@ -7,7 +9,6 @@ open Widget
|
||||
open RequestM
|
||||
open Meta
|
||||
|
||||
|
||||
/-! ## GameGoal -/
|
||||
|
||||
namespace GameServer
|
||||
@@ -103,49 +104,240 @@ def matchDecls (patterns : Array Expr) (fvars : Array Expr) (strict := true) (in
|
||||
then return some bij
|
||||
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
|
||||
/-- 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
|
||||
let some level ← getLevelByFileName? initParams doc.meta.mkInputContext.fileName
|
||||
| throwError "Level not found: {doc.meta.mkInputContext.fileName}"
|
||||
let some level ← getLevelByFileName? initParams m.mkInputContext.fileName
|
||||
| throwError "Level not found: {m.mkInputContext.fileName}"
|
||||
let hints ← level.hints.filterMapM fun hint => do
|
||||
openAbstractCtxResult hint.goal fun hintFVars hintGoal => do
|
||||
if let some fvarBij := matchExpr (← instantiateMVars $ hintGoal) (← instantiateMVars $ ← inferType $ mkMVar goal)
|
||||
then
|
||||
let lctx := (← goal.getDecl).lctx
|
||||
if let some bij ← 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
|
||||
let userFVars := hintFVars.map fun v => bij.forward.findD v.fvarId! v.fvarId!
|
||||
-- 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
|
||||
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
|
||||
return hints
|
||||
|
||||
open RequestM in
|
||||
def getInteractiveGoals (p : Lsp.PlainGoalParams) : RequestM (RequestTask (Option InteractiveGoals)) := do
|
||||
def filterUnsolvedGoal (a : Array InteractiveDiagnostic) :
|
||||
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 rc ← readThe RequestContext
|
||||
let text := doc.meta.text
|
||||
|
||||
withWaitFindSnap
|
||||
doc
|
||||
-- TODO (Alex): I couldn't find a good condition to find the correct snap. So we are looking
|
||||
-- for the first snap with goals here.
|
||||
-- NOTE (Jon): The entire proof is in one snap, so hoped that Position `0` is good enough.
|
||||
(fun snap => ¬ (snap.infoTree.goalsAt? doc.meta.text 0).isEmpty)
|
||||
(notFoundX := return none)
|
||||
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 errors 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
|
||||
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 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
|
||||
-- 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 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 InteractiveGoals ← rs.mapM fun { ctxInfo := ci, tacticInfo := ti, useAfter := useAfter, .. } => do
|
||||
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
|
||||
@@ -153,8 +345,8 @@ def getInteractiveGoals (p : Lsp.PlainGoalParams) : RequestM (RequestTask (Optio
|
||||
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 rc.initParams
|
||||
return ← goalToInteractive goal hints
|
||||
-- let hints ← findHints goal doc.meta rc.initParams
|
||||
return ← goalToInteractive goal
|
||||
-- compute the goal diff
|
||||
-- let goals ← ciAfter.runMetaM {} (do
|
||||
-- try
|
||||
@@ -163,8 +355,8 @@ def getInteractiveGoals (p : Lsp.PlainGoalParams) : RequestM (RequestTask (Optio
|
||||
-- -- fail silently, since this is just a bonus feature
|
||||
-- return goals
|
||||
-- )
|
||||
return {goals}
|
||||
return some <| goals.foldl (· ++ ·) ⟨#[]⟩
|
||||
return goals
|
||||
return some <| ⟨goals.foldl (· ++ ·) #[]⟩
|
||||
else
|
||||
return none
|
||||
|
||||
@@ -172,7 +364,16 @@ builtin_initialize
|
||||
registerBuiltinRpcProcedure
|
||||
`Game.getInteractiveGoals
|
||||
Lsp.PlainGoalParams
|
||||
(Option InteractiveGoals)
|
||||
(Option <| InteractiveGoals
|
||||
)
|
||||
getInteractiveGoals
|
||||
|
||||
builtin_initialize
|
||||
registerBuiltinRpcProcedure
|
||||
`Game.getProofState
|
||||
Lsp.PlainGoalParams
|
||||
(Option ProofState)
|
||||
getProofState
|
||||
|
||||
|
||||
end GameServer
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import GameServer.EnvExtensions
|
||||
import I18n
|
||||
|
||||
open Lean Meta Elab Command
|
||||
|
||||
|
||||
/-! ## Copy images -/
|
||||
|
||||
open IO.FS System FilePath in
|
||||
@@ -59,6 +59,9 @@ def saveGameData (allItemsByType : HashMap InventoryType (HashSet Name))
|
||||
|
||||
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
|
||||
|
||||
@@ -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 preceding 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
|
||||
@@ -4,10 +4,37 @@
|
||||
[{"url": "https://github.com/leanprover/std4.git",
|
||||
"type": "git",
|
||||
"subDir": null,
|
||||
"rev": "2e4a3586a8f16713f16b2d2b3af3d8e65f3af087",
|
||||
"rev": "32983874c1b897d78f20d620fe92fc8fd3f06c3a",
|
||||
"name": "std",
|
||||
"manifestFile": "lake-manifest.json",
|
||||
"inputRev": "v4.3.0",
|
||||
"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",
|
||||
|
||||
@@ -7,6 +7,10 @@ package GameServer
|
||||
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
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
leanprover/lean4:v4.3.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 -/
|
||||
@[simp]
|
||||
Statement My.add_comm (n m : Nat) : n + m = m + n := by
|
||||
rw [Nat.add_comm]
|
||||
Statement My.add_assoc (n m x : Nat) : (m + n) + x = m + (n + x) := by
|
||||
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
|
||||
|
||||
#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]
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"lib": [
|
||||
"ES2021.String",
|
||||
"DOM"
|
||||
]
|
||||
},
|
||||
"exclude": ["server", "relay"]
|
||||
}
|
||||
|
||||
+11
-4
@@ -3,6 +3,9 @@ import react from '@vitejs/plugin-react-swc'
|
||||
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
||||
import svgr from "vite-plugin-svgr"
|
||||
|
||||
const backendPort = process.env.PORT || 8080;
|
||||
const clientPort = process.env.CLIENT_PORT || 3000;
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
//root: 'client/src',
|
||||
@@ -28,21 +31,25 @@ export default defineConfig({
|
||||
})
|
||||
],
|
||||
publicDir: "client/public",
|
||||
base: "/", // setting this to `/leangame/` means the server is now accessible at `localhost:3000/leangame`
|
||||
optimizeDeps: {
|
||||
exclude: ['games']
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
port: Number(clientPort),
|
||||
proxy: {
|
||||
'/websocket': {
|
||||
target: 'ws://localhost:8080',
|
||||
target: `ws://localhost:${backendPort}`,
|
||||
ws: true
|
||||
},
|
||||
'/import': {
|
||||
target: 'http://localhost:8080',
|
||||
target: `http://localhost:${backendPort}`,
|
||||
},
|
||||
'/data': {
|
||||
target: 'http://localhost:8080',
|
||||
target: `http://localhost:${backendPort}`,
|
||||
},
|
||||
'/i18n': {
|
||||
target: `http://localhost:${backendPort}`,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user