Compare commits
245
Commits
cleanup_stuff
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92acdb7f85 | ||
|
|
64ad36dd4f | ||
|
|
3a885245ae | ||
|
|
545ac8b0f7 | ||
|
|
5a09776b5b | ||
|
|
38cac494ab | ||
|
|
5b6f5f0db7 | ||
|
|
fbe48d26c5 | ||
|
|
d05767d74c | ||
|
|
aab6dbb329 | ||
|
|
5310e69d98 | ||
|
|
67090f53fa | ||
|
|
2896237772 | ||
|
|
30d9b881b9 | ||
|
|
937c4714dd | ||
|
|
75b367e3d1 | ||
|
|
b815b86c0d | ||
|
|
8b4215e407 | ||
|
|
a0ecaeeece | ||
|
|
dea5fd0558 | ||
|
|
c03e2f123e | ||
|
|
b1cdcfc113 | ||
|
|
503ea51f95 | ||
|
|
aed2899fb6 | ||
|
|
1f14ad185f | ||
|
|
17d2ba5a2c | ||
|
|
2a070332f2 | ||
|
|
aedf073a33 | ||
|
|
d684f260bb | ||
|
|
925d729725 | ||
|
|
9f4fe656c8 | ||
|
|
72ccdd1478 | ||
|
|
b66954248f | ||
|
|
096bd55f9b | ||
|
|
02d0d57453 | ||
|
|
762c1ef0c4 | ||
|
|
4652f6d50f | ||
|
|
f158250341 | ||
|
|
7dc0a507ed | ||
|
|
80d4b88f5a | ||
|
|
714b4f45b1 | ||
|
|
6da902aede | ||
|
|
cdffe03f9a | ||
|
|
adeed03da8 | ||
|
|
8e3dfdea30 | ||
|
|
71fad5699e | ||
|
|
a7d746a8e5 | ||
|
|
c735211cd8 | ||
|
|
369b77f00f | ||
|
|
895c71dc91 | ||
|
|
8b5d6ff2f3 | ||
|
|
c1642cf09b | ||
|
|
d82ef8af8f | ||
|
|
08875e4415 | ||
|
|
5765c78a1d | ||
|
|
b9112bfb09 | ||
|
|
05fbee9365 | ||
|
|
1daef0d80f | ||
|
|
06cc52bb7e | ||
|
|
eac945e7b5 | ||
|
|
a1b1a33a9b | ||
|
|
02978a38ed | ||
|
|
8cf358e17b | ||
|
|
3ddcc35137 | ||
|
|
4e7c958348 | ||
|
|
237371a77f | ||
|
|
f308e1ad49 | ||
|
|
62f1fb87d1 | ||
|
|
c5df85ce66 | ||
|
|
848b2cddc8 | ||
|
|
d034148bec | ||
|
|
3d9d244b31 | ||
|
|
feaf6dae0c | ||
|
|
6059ff1da7 | ||
|
|
6f95f41d14 | ||
|
|
335209b094 | ||
|
|
de1ff4fadc | ||
|
|
db66cb1fcc | ||
|
|
66aa8e688e | ||
|
|
a523bab1d6 | ||
|
|
499bb00d4f | ||
|
|
67b03d9ccf | ||
|
|
a15dd0a1bd | ||
|
|
ded9c38170 | ||
|
|
d456621875 | ||
|
|
74059bd5af | ||
|
|
ce71bc81c6 | ||
|
|
7cedfc5038 | ||
|
|
a8d3169ebb | ||
|
|
9b05a27888 | ||
|
|
af5426856e | ||
|
|
470a184cac | ||
|
|
1006097e32 | ||
|
|
3124d90cd6 | ||
|
|
5a768c25b7 | ||
|
|
39be34e83a | ||
|
|
dd7a2d1bc1 | ||
|
|
684a1bc72b | ||
|
|
10391b616c | ||
|
|
1431ff8b49 | ||
|
|
d3a55a4dd3 | ||
|
|
aaa6c3b576 | ||
|
|
d60dc3fcb2 | ||
|
|
1a54edffd4 | ||
|
|
ad1add5264 | ||
|
|
a26022e3fc | ||
|
|
7c44d49b2a | ||
|
|
a111814973 | ||
|
|
a7784ef66b | ||
|
|
389e120117 | ||
|
|
ee9fd18a56 | ||
|
|
8b29f88407 | ||
|
|
ebda6cc162 | ||
|
|
7e9514fe96 | ||
|
|
038dbe71b8 | ||
|
|
19d2ea363a | ||
|
|
3ac8fdace7 | ||
|
|
8d0493acb5 | ||
|
|
27c661f08f | ||
|
|
b37f050da5 | ||
|
|
09c81ea43f | ||
|
|
ff12b34295 | ||
|
|
45bc0468df | ||
|
|
c24efb1377 | ||
|
|
830bffaf11 | ||
|
|
a9447a70d4 | ||
|
|
eaa4eecad2 | ||
|
|
1828e73b30 | ||
|
|
ce9f5c7840 | ||
|
|
64d7879c32 | ||
|
|
cb711205a2 | ||
|
|
f42829422d | ||
|
|
ee6741232f | ||
|
|
fa4ae5672d | ||
|
|
9bc0a3de46 | ||
|
|
6cdbfbd9cb | ||
|
|
f55581a5f2 | ||
|
|
381930e547 | ||
|
|
e07570181c | ||
|
|
87689e1c3a | ||
|
|
47297e4194 | ||
|
|
f3f077741d | ||
|
|
edf1085310 | ||
|
|
68f84a3426 | ||
|
|
217f86ce5e | ||
|
|
dd60093dfc | ||
|
|
85347a54d9 | ||
|
|
3b4afd6e0e | ||
|
|
2c12872a6e | ||
|
|
ad819bf7ff | ||
|
|
c0f366abba | ||
|
|
f72ebdf050 | ||
|
|
af8463ca5d | ||
|
|
d0a444205a | ||
|
|
1796c76a84 | ||
|
|
a75a4a81ac | ||
|
|
45d84103c1 | ||
|
|
92e9ed38b2 | ||
|
|
d689c7ec86 | ||
|
|
16c979a6c2 | ||
|
|
2b85386373 | ||
|
|
8008b68fd6 | ||
|
|
2649f985fa | ||
|
|
698a88c545 | ||
|
|
3775ad98c8 | ||
|
|
780514e45a | ||
|
|
19f2ceface | ||
|
|
800d1f3308 | ||
|
|
c0acde14e2 | ||
|
|
976d1c6901 | ||
|
|
5bb6c559bc | ||
|
|
11ee6c1535 | ||
|
|
3998fb2fc9 | ||
|
|
538f74004c | ||
|
|
6aebb8993f | ||
|
|
6472ef5b31 | ||
|
|
72ffab5b46 | ||
|
|
3b660c5185 | ||
|
|
ebb8c98145 | ||
|
|
4abf05b77e | ||
|
|
df423aaace | ||
|
|
a67dcb306f | ||
|
|
37582e04d4 | ||
|
|
9d4a6df139 | ||
|
|
26202e5f36 | ||
|
|
ed017fa605 | ||
|
|
36bc52c960 | ||
|
|
e085f2f106 | ||
|
|
e975c455fe | ||
|
|
c006be5a9f | ||
|
|
49379a0930 | ||
|
|
03076b0161 | ||
|
|
bfb60ffedd | ||
|
|
ca576542ba | ||
|
|
fd5e507541 | ||
|
|
c103eeacfa | ||
|
|
e277a48749 | ||
|
|
16ff701518 | ||
|
|
20ca385e14 | ||
|
|
5a38543e3b | ||
|
|
a4a6f2e725 | ||
|
|
cc89e78e4c | ||
|
|
c533a635c2 | ||
|
|
ab98eaa3ba | ||
|
|
7114a8c4cb | ||
|
|
b7eb15184d | ||
|
|
892c70165a | ||
|
|
8929813e48 | ||
|
|
5d88cd6739 | ||
|
|
d16956da9b | ||
|
|
e15e5af126 | ||
|
|
8c93b3c5b3 | ||
|
|
cd1d212a3c | ||
|
|
406c2799b1 | ||
|
|
e579071a3b | ||
|
|
8cdac88b5a | ||
|
|
7882958f54 | ||
|
|
c9f97b3285 | ||
|
|
614b762b6c | ||
|
|
c4f4dbdc6b | ||
|
|
5a78118bb6 | ||
|
|
a6775d5495 | ||
|
|
03a370464b | ||
|
|
eaa214ec37 | ||
|
|
d7f1f70d41 | ||
|
|
2d0f69d337 | ||
|
|
1ab50710f5 | ||
|
|
e76e287763 | ||
|
|
5fd49abb90 | ||
|
|
4b7d540a80 | ||
|
|
aec196deff | ||
|
|
ae636a03ed | ||
|
|
deec431620 | ||
|
|
9f692ccf61 | ||
|
|
47d7f606c6 | ||
|
|
5aa0764844 | ||
|
|
93c55dc9f7 | ||
|
|
aab7441323 | ||
|
|
2ba36b91d5 | ||
|
|
a2726ae287 | ||
|
|
7515561883 | ||
|
|
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
|
||||
|
||||
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 pimarily been developed by Alexander Bentkamp and Jon Eugster.
|
||||
|
||||
It is based on ideas from the [Lean Game Maker](https://github.com/mpedramfar/Lean-game-maker) and the [Natural Number Game
|
||||
(NNG)](https://www.ma.imperial.ac.uk/~buzzard/xena/natural_number_game/)
|
||||
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();
|
||||
}
|
||||
),
|
||||
|
||||
};
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 607 KiB |
Binary file not shown.
@@ -0,0 +1,93 @@
|
||||
Copyright (c) 2020 - 2023, cormullion
|
||||
with Reserved Font Name JuliaMono.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2021 Google Inc. All Rights Reserved.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"Tactics": "Taktiken",
|
||||
"Lean Game Server": "Lean-Spielserver",
|
||||
"<p>Game rules determine if it is allowed to skip levels and if the games runs checks to only allow unlocked tactics and theorems in proofs.</p><1>Note: \"Unlocked\" tactics (or theorems) are determined by two things: The set of minimal tactics needed to solve a level, plus any tactics you unlocked in another level. That means if you unlock <1>simp</1> in a level, you can use it henceforth in any level.</1><p>The options are:</p>": "<p>Die Spielregeln bestimmen ob es erlaubt ist, Levels zu überspringen und ob das Spiel überprüft welche Taktiken und Theoreme freigeschaltet sind und nur diese im Beweis akzeptiert.</p><1>Bemerkung: \"Freigeschaltete\" Taktiken (und Theoreme) werden durch zwei Faktoren bestimmt: The Menge der Taktiken die minimal notwendig sind um den Level zu lösen und dazu die Menge aller Taktiken, die in einem anderen Level freigeschaltet wurden. Das bedeutet wenn <1>simp</1> in einem Level freigeschaltet wird, kann diese Taktik danach in jeglichen Levels verwendet werden.",
|
||||
"Game Rules": "Spielregeln",
|
||||
"levels": "Levels",
|
||||
"tactics": "Taktiken",
|
||||
"regular": "regulär",
|
||||
"relaxed": "relaxed",
|
||||
"none": "keine",
|
||||
"Rules": "Regeln",
|
||||
"Intro": "Einführung",
|
||||
"Start": "Start",
|
||||
"Next": "Weiter",
|
||||
"Previous": "Zurück",
|
||||
"Editor mode is enforced!": "Editor kann nicht verlassen werden!",
|
||||
"Editor mode": "Editor",
|
||||
"Typewriter mode": "Schreibmaschine",
|
||||
"Preferences": "Einstellungen",
|
||||
"Game Info": "Spielinfo",
|
||||
"Erase": "Löschen",
|
||||
"Download": "Herunterladen",
|
||||
"Upload": "Laden",
|
||||
"Home": "Home",
|
||||
"back to games selection": "Zurück zur Spielauswahl",
|
||||
"World": "Welt",
|
||||
"Show more help!": "Mehr Hilfe",
|
||||
"Goal": "Goal",
|
||||
"Current Goal": "Aktuelles Goal",
|
||||
"Objects": "Objekte",
|
||||
"Assumptions": "Annahmen",
|
||||
"Further Goals": "Weitere Goals",
|
||||
"No Goals": "Keine Goals",
|
||||
"Loading goal…": "Goal wird geladen…",
|
||||
"Click somewhere in the Lean file to enable the infoview.": "Ein Klick in den Lean-Code aktiviert den Infoview.",
|
||||
"Waiting for Lean server to start…": "Warte auf das Starten des Lean-Servers…",
|
||||
"Level completed! 🎉": "Level gelöst! 🎉",
|
||||
"Level completed with warnings 🎭": "Level mit Warnungen abgeschlossen 🎭",
|
||||
"Active Goal": "Aktuelles Goal",
|
||||
"Crashed! Go to editor mode and fix your proof! Last server response:": "",
|
||||
"Line": "Zeile",
|
||||
"Character": "Charakter",
|
||||
"Loading messages…": "",
|
||||
"Execute": "Ausführen",
|
||||
"Definitions": "Definitionen",
|
||||
"Theorems": "Theoreme",
|
||||
"Not unlocked yet": "Noch nicht verfügbar",
|
||||
"Not available in this level": "In diesem Level nicht verfügbar",
|
||||
"A repository of learning games for the proof assistant <1>Lean</1> <i>(Lean 4)</i> and its mathematical library <5>mathlib</5>": "Eine Sammlung von Lernspielen für den Beweisassistenten <1>Lean</1> <i>(Lean 4)</i> und dessen mathematische Bibliothek <5>mathlib</5>",
|
||||
"No Games loaded. Use <1>http://localhost:3000/#/g/local/FOLDER</1> to open a game directly from a local folder.": "Kein Spiel geladen. öffne <1>http://localhost:3000/#/g/local/FOLDER</1> um ein Spiel direkt von einem lokalen Ordner zu laden.",
|
||||
"<p>As this server runs lean on our university machines, it has a limited capacity. Our current estimate is about 70 simultaneous games. We hope to address and test this limitation better in the future.</p><1>Most aspects of the games and the infrastructure are still in development. Feel free to file a <1>GitHub Issue</1> about any problems you experience!</1>": "",
|
||||
"This server has been developed as part of the project <1>ADAM : Anticipating the Digital Age of Mathematics</1> at Heinrich-Heine-Universität in Düsseldorf.": "",
|
||||
"Prerequisites": "Voraussetzungen",
|
||||
"Worlds": "Welten",
|
||||
"Levels": "Levels",
|
||||
"Language": "Sprachen",
|
||||
"Development notes": "Entwicklungshinweis",
|
||||
"Adding new games": "Neue Spiele hinzufügen",
|
||||
"Funding": "Finanzierung",
|
||||
"<p>Do you want to delete your saved progress irreversibly?</p><p>(This deletes your proofs and your collected inventory. Saves from other games are not deleted.)</p>": "<p>Soll der Spielstand unwiderruflich gelöscht werden?</p><p>(Dies löscht sämtliche Beweise und das gesammelte Inventar. Spielstände anderer Spiele werden nicht gelöscht.)</p>",
|
||||
"Delete Progress?": "Spielstand löschen?",
|
||||
"Delete": "Löschen",
|
||||
"Download & Delete": "Herunterladen & Löschen",
|
||||
"Cancel": "Abbrechen",
|
||||
"Layout": "Seitenlayout",
|
||||
"Always visible": "Immer sichtbar",
|
||||
"Save my settings (in the browser store)": "Einstellungen im Browser speichern.",
|
||||
"<p>Select a JSON file with the saved game progress to load your progress.</p><1><0>Warning:</0> This will delete your current game progress! Consider <2>downloading your current progress</2> first!</1>": "<p>Wähle eine JSON-Datei mit dem Spielstand um diesen zu laden.</p><1><0>Achtung:</0> Diese Aktion überschreibt den aktuellen Spielstand! Es wird empfohlen zuerst den <2>aktuellen Spielstand herunterzuladen</2>!</1>",
|
||||
"Upload Saved Progress": "Spielstand hochladen",
|
||||
"Load selected file": "Ausgewählte Datei hochladen",
|
||||
"Mobile": "Mobil",
|
||||
"Auto": "Auto",
|
||||
"Desktop": "Desktop",
|
||||
"<0>If you are considering writing your own game, you should use the <1>GameSkeleton Github Repo</1> as a template and read <3>How to Create a Game</3>.</0><1>You can directly load your games into the server and play it using the correct URL. The <1>instructions above</1> also explain the details for how to load your game to the server. We'd like to encourage you to contact us if you have any questions.</1><p>Featured games on this page are added manually. Please get in contact and we'll happily add yours.</p>": "",
|
||||
"Level": "",
|
||||
"Introduction": "Einführung",
|
||||
"Retry proof from here": "Ab hier erneut versuchen",
|
||||
"Retry": "",
|
||||
"Failed command": "Befehl fehlgeschlagen",
|
||||
"view the Lean game server on Github": "Lean game Server auf Github ansehen",
|
||||
"Theorem": "Theorem",
|
||||
"Impressum": "Impressum",
|
||||
"Privacy Policy": "Datenschutzerklärung",
|
||||
"<0>Impressum</0><1><strong>Contact:</strong><br/>Marcus Zibrowius, Jon Eugster<br/>Mathematisches Institut der Heinrich-Heine-Universität Düsseldorf<br/>Universitätsstr. 1<br/>40225 Düsseldorf<br/>Germany<br/>+49 211 81-14690<br/><14>Contact Details</14></1><2><strong>Legal form:</strong><br/>The Heinrich Heine University Düsseldorf is a corporation under public law. It is legally represented by the Rector Prof. Dr. Anja Steinbeck. The responsible supervisory authority is the Ministry of Culture and Science of North Rhine-Westphalia, Völklinger Straße 49, 40221 Düsseldorf.</2><3><strong>VAT identification number:</strong><br/>according to §27a Sales Tax Act<br/>DE 811222416</3><4><0>Impressum HHU</0></4>": "<0>Impressum</0><1><strong>Kontakt:</strong><br/>Marcus Zibrowius, Jon Eugster<br/>Mathematisches Institut der Heinrich-Heine-Universität Düsseldorf<br/>Universitätsstr. 1<br/>40225 Düsseldorf<br/>Deutschland<br/>+49 211 81-14690<br/><14>Kontaktinformationen</14></1><2><strong>Rechtsform:</strong><br />Die Heinrich-Heine Universität Düsseldorf ist eine Körperschaft des Öffentlichen Rechts. Sie wird durch die Rektorin Prof. Dr. Anja Steinbeck gesetzlich vertreten. Zuständige Aufsichtsbehörde ist das Ministerium für Kultur und Wissenschaft des Landes Nordrhein-Westfalen, Völklinger Straße 49, 40221 Düsseldorf.</2><3><strong>Umsatzsteuer-Identifikationsnummer:</strong><br />gemäß §27a Umsatzsteuergesetz<br />DE 811222416</3><4><0>Impressum der HHU</0></4>",
|
||||
"<0>Progress saving</0><1>The game stores your progress in your local browser storage. If you delete it, your progress will be lost!<br/>Warning: In most browsers, deleting cookies will also clear the local storage (or \"local site data\"). Make sure to download your game progress first!</1><2>Development</2><3>The game engine has been created by <strong>Alexander Bentkamp</strong>, <strong>Jon Eugster</strong>. On a prototype by <strong>Patrick Massot</strong>.</3><4>The source code of this Lean game engine is <1>available on Github</1>. If you experience any problems, please file an <3>Issue on Github</3> or get directly in contact.</4><5>Funding</5><6>The game engine has been developed as part of the project <1>ADAM: Anticipating the Digital Age of Mathematics</1> at Heinrich-Heine-Universität Düsseldorf. It is funded by the <i>Stiftung Innovation in der Hochschullehre</i> as part of project <i>Freiraum 2022</i>.</6>": "<0>Spielstandspeicherung</0><1>Das Spiel speichert den Spielstand im lokalen Browserspeicher. Wird dieser gelöscht, ist der Spielstand nicht wiederherstellbar!<br/>Achtung: In üblichen Browsern bewirkt ein löschen von Cookies auch ein löschen des lokalen Browserspeichers (z.B. \"local site data\"). Der Spielstand sollte vorgehend heruntergeladen werden!</1><2>Entwicklung</2><3>Der Spieleserver wurde entwickelt von <strong>Alexander Bentkamp</strong>, <strong>Jon Eugster</strong>. Basierend auf einem Prototyp von <strong>Patrick Massot</strong>.</3><4>Der Sourcecode dieses Lean-Game-Engine ist <1>auf Github verfügbar</1>. Bei Problemen, bitte einen <3>Github-Issue</3> ausfüllen oder direkt mit uns Kontakt aufnehmen.</4><5>Funding</5><6>Dieser Spielserver wurde im Rahmen des Projekts <1>ADAM: Anticipating the Digital Age of Mathematics</1> an der Heinrich-Heine-Universität Düsseldorf entwickelt. Es wird finanziell durch das Projekt <i>Freiraum 2022</i> der <i>Stiftung Innovation in der Hochschullehre</i> unterstützt.</6>",
|
||||
"<0>Privacy Policy</0><p>Our server collects metadata (such as IP address, browser, operating system) and the data that the user enters into the editor. The data is used to compute the Lean output and display it to the user. The information will be stored as long as the user stays on our website and will be deleted immediately afterwards. We keep logs to improve our software, but the contained data is anonymized.</p><p>We do not use cookies, but your game progress is stored in the browser as site data. Your game progress is not saved on the server; if you delete your browser storage, it is completely gone.</p><p>Our server is located in Germany.</p><4><strong>Contact:</strong><br/>Marcus Zibrowius, Jon Eugster<br/>Mathematisches Institut der Heinrich-Heine-Universität Düsseldorf<br/>Universitätsstr. 1<br/>40225 Düsseldorf<br/>Germany<br/>+49 211 81-14690<br/><14>Contact Details</14></4>": "<0>Datenschutzerklärung</0><p>Unser Server verarbeitet Benutzer-Metadaten (zum Beispiel IP-Addresse, Browser-Version, Betriebssystem) sowie die Benutzereingaben in den Editor. Diese Daten werden verwendet um die Lean-Ausgabe zu berechnen und dem Benutzer darzustellen. Die Informationen werden nur gespeichert, solange der Benutzer auf unserer Webseite bleibt und anschliessend sofort gelöscht. Wir führen Log-Dateien um die Software zu verbessern, aber die enthaltenen Daten sind anonymisiert.</p><p>Wir verwenden keine Cookies, aber der Spielstand wird im Browser als \"Site Data\" gespeichert. Der Spielstand wird nicht auf dem Server gespeichert; wird er lokal gelöscht, kann er nicht wiederhergestellt werden.</p><p>Unser Server ist in Deutschland stationiert.</p><4><strong>Kontakt:</strong><br/>Markus Zibrowius, Jon Eugster<br/>Mathematisches Institut der Heinrich-Heine-Universität Düsseldorf<br/>Universitätsstr. 1<br/>40225 Düsseldorf<br/>Deutschland<br/>+49 211 81-14690<br/><14>Kontaktdetails</14></4>",
|
||||
"home": "",
|
||||
"close language menu": "Sprachmenü schließen",
|
||||
"open language menu": "Sprachmenü öffnen",
|
||||
"close menu": "Menü schließen",
|
||||
"open menu": "Menü öffnen",
|
||||
"Editor Mode": "",
|
||||
"Typewriter Mode": ""
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"Tactics": "Tactics",
|
||||
"Lean Game Server": "Lean Game Server",
|
||||
"<p>Game rules determine if it is allowed to skip levels and if the games runs checks to only allow unlocked tactics and theorems in proofs.</p><1>Note: \"Unlocked\" tactics (or theorems) are determined by two things: The set of minimal tactics needed to solve a level, plus any tactics you unlocked in another level. That means if you unlock <1>simp</1> in a level, you can use it henceforth in any level.</1><p>The options are:</p>": "<p>Game rules determine if it is allowed to skip levels and if the games runs checks to only allow unlocked tactics and theorems in proofs.</p><1>Note: \"Unlocked\" tactics (or theorems) are determined by two things: The set of minimal tactics needed to solve a level, plus any tactics you unlocked in another level. That means if you unlock <1>simp</1> in a level, you can use it henceforth in any level.</1><p>The options are:</p>",
|
||||
"Game Rules": "Game Rules",
|
||||
"levels": "levels",
|
||||
"tactics": "tactics",
|
||||
"regular": "regular",
|
||||
"relaxed": "relaxed",
|
||||
"none": "none",
|
||||
"Rules": "Rules",
|
||||
"Intro": "Intro",
|
||||
"Start": "Start",
|
||||
"Next": "Next",
|
||||
"Previous": "Previous",
|
||||
"Editor mode is enforced!": "Editor mode is enforced!",
|
||||
"Editor mode": "Editor mode",
|
||||
"Typewriter mode": "Typewriter mode",
|
||||
"Preferences": "Preferences",
|
||||
"Game Info": "Game Info",
|
||||
"Erase": "Erase",
|
||||
"Download": "Download",
|
||||
"Upload": "Upload",
|
||||
"Home": "Home",
|
||||
"back to games selection": "back to games selection",
|
||||
"World": "World",
|
||||
"Show more help!": "Show more help!",
|
||||
"Goal": "Goal",
|
||||
"Current Goal": "Current Goal",
|
||||
"Objects": "Objects",
|
||||
"Assumptions": "Assumptions",
|
||||
"Further Goals": "Further Goals",
|
||||
"No Goals": "No Goals",
|
||||
"Loading goal…": "Loading goal…",
|
||||
"Click somewhere in the Lean file to enable the infoview.": "Click somewhere in the Lean file to enable the infoview.",
|
||||
"Waiting for Lean server to start…": "Waiting for Lean server to start…",
|
||||
"Level completed! 🎉": "Level completed! 🎉",
|
||||
"Level completed with warnings 🎭": "Level completed with warnings 🎭",
|
||||
"Active Goal": "Active Goal",
|
||||
"Crashed! Go to editor mode and fix your proof! Last server response:": "Crashed! Go to editor mode and fix your proof! Last server response:",
|
||||
"Line": "Line",
|
||||
"Character": "Character",
|
||||
"Loading messages…": "Loading messages…",
|
||||
"Execute": "Execute",
|
||||
"Definitions": "Definitions",
|
||||
"Theorems": "Theorems",
|
||||
"Not unlocked yet": "Not unlocked yet",
|
||||
"Not available in this level": "Not available in this level",
|
||||
"A repository of learning games for the proof assistant <1>Lean</1> <i>(Lean 4)</i> and its mathematical library <5>mathlib</5>": "A repository of learning games for the proof assistant <1>Lean</1> <i>(Lean 4)</i> and its mathematical library <5>mathlib</5>",
|
||||
"No Games loaded. Use <1>http://localhost:3000/#/g/local/FOLDER</1> to open a game directly from a local folder.": "No Games loaded. Use <1>http://localhost:3000/#/g/local/FOLDER</1> to open a game directly from a local folder.",
|
||||
"<p>As this server runs lean on our university machines, it has a limited capacity. Our current estimate is about 70 simultaneous games. We hope to address and test this limitation better in the future.</p><1>Most aspects of the games and the infrastructure are still in development. Feel free to file a <1>GitHub Issue</1> about any problems you experience!</1>": "<p>As this server runs lean on our university machines, it has a limited capacity. Our current estimate is about 70 simultaneous games. We hope to address and test this limitation better in the future.</p><1>Most aspects of the games and the infrastructure are still in development. Feel free to file a <1>GitHub Issue</1> about any problems you experience!</1>",
|
||||
"This server has been developed as part of the project <1>ADAM : Anticipating the Digital Age of Mathematics</1> at Heinrich-Heine-Universität in Düsseldorf.": "This server has been developed as part of the project <1>ADAM : Anticipating the Digital Age of Mathematics</1> at Heinrich-Heine-Universität in Düsseldorf.",
|
||||
"Prerequisites": "Prerequisites",
|
||||
"Worlds": "Worlds",
|
||||
"Levels": "Levels",
|
||||
"Language": "Language",
|
||||
"Development notes": "Development notes",
|
||||
"Adding new games": "Adding new games",
|
||||
"Funding": "Funding",
|
||||
"<p>Do you want to delete your saved progress irreversibly?</p><p>(This deletes your proofs and your collected inventory. Saves from other games are not deleted.)</p>": "<p>Do you want to delete your saved progress irreversibly?</p><p>(This deletes your proofs and your collected inventory. Saves from other games are not deleted.)</p>",
|
||||
"Delete Progress?": "Delete Progress?",
|
||||
"Delete": "Delete",
|
||||
"Download & Delete": "Download & Delete",
|
||||
"Cancel": "Cancel",
|
||||
"Layout": "Layout",
|
||||
"Always visible": "Always visible",
|
||||
"Save my settings (in the browser store)": "Save my settings (in the browser store)",
|
||||
"<p>Select a JSON file with the saved game progress to load your progress.</p><1><0>Warning:</0> This will delete your current game progress! Consider <2>downloading your current progress</2> first!</1>": "<p>Select a JSON file with the saved game progress to load your progress.</p><1><0>Warning:</0> This will delete your current game progress! Consider <2>downloading your current progress</2> first!</1>",
|
||||
"Upload Saved Progress": "Upload Saved Progress",
|
||||
"Load selected file": "Load selected file",
|
||||
"Mobile": "Mobile",
|
||||
"Auto": "Auto",
|
||||
"Desktop": "Desktop",
|
||||
"<0>If you are considering writing your own game, you should use the <1>GameSkeleton Github Repo</1> as a template and read <3>How to Create a Game</3>.</0><1>You can directly load your games into the server and play it using the correct URL. The <1>instructions above</1> also explain the details for how to load your game to the server. We'd like to encourage you to contact us if you have any questions.</1><p>Featured games on this page are added manually. Please get in contact and we'll happily add yours.</p>": "<0>If you are considering writing your own game, you should use the <1>GameSkeleton Github Repo</1> as a template and read <3>How to Create a Game</3>.</0><1>You can directly load your games into the server and play it using the correct URL. The <1>instructions above</1> also explain the details for how to load your game to the server. We'd like to encourage you to contact us if you have any questions.</1><p>Featured games on this page are added manually. Please get in contact and we'll happily add yours.</p>",
|
||||
"Level": "Level",
|
||||
"Introduction": "Introduction",
|
||||
"Retry proof from here": "Retry proof from here",
|
||||
"Retry": "Retry",
|
||||
"Failed command": "Failed command",
|
||||
"view the Lean game server on Github": "view the Lean game server on Github",
|
||||
"Theorem": "Theorem",
|
||||
"Impressum": "Impressum",
|
||||
"Privacy Policy": "Privacy Policy",
|
||||
"<0>Impressum</0><1><strong>Contact:</strong><br/>Marcus Zibrowius, Jon Eugster<br/>Mathematisches Institut der Heinrich-Heine-Universität Düsseldorf<br/>Universitätsstr. 1<br/>40225 Düsseldorf<br/>Germany<br/>+49 211 81-14690<br/><14>Contact Details</14></1><2><strong>Legal form:</strong><br/>The Heinrich Heine University Düsseldorf is a corporation under public law. It is legally represented by the Rector Prof. Dr. Anja Steinbeck. The responsible supervisory authority is the Ministry of Culture and Science of North Rhine-Westphalia, Völklinger Straße 49, 40221 Düsseldorf.</2><3><strong>VAT identification number:</strong><br/>according to §27a Sales Tax Act<br/>DE 811222416</3><4><0>Impressum HHU</0></4>": "<0>Impressum</0><1><strong>Contact:</strong><br/>Marcus Zibrowius, Jon Eugster<br/>Mathematisches Institut der Heinrich-Heine-Universität Düsseldorf<br/>Universitätsstr. 1<br/>40225 Düsseldorf<br/>Germany<br/>+49 211 81-14690<br/><14>Contact Details</14></1><2><strong>Legal form:</strong><br/>The Heinrich Heine University Düsseldorf is a corporation under public law. It is legally represented by the Rector Prof. Dr. Anja Steinbeck. The responsible supervisory authority is the Ministry of Culture and Science of North Rhine-Westphalia, Völklinger Straße 49, 40221 Düsseldorf.</2><3><strong>VAT identification number:</strong><br/>according to §27a Sales Tax Act<br/>DE 811222416</3><4><0>Impressum HHU</0></4>",
|
||||
"<0>Progress saving</0><1>The game stores your progress in your local browser storage. If you delete it, your progress will be lost!<br/>Warning: In most browsers, deleting cookies will also clear the local storage (or \"local site data\"). Make sure to download your game progress first!</1><2>Development</2><3>The game engine has been created by <strong>Alexander Bentkamp</strong>, <strong>Jon Eugster</strong>. On a prototype by <strong>Patrick Massot</strong>.</3><4>The source code of this Lean game engine is <1>available on Github</1>. If you experience any problems, please file an <3>Issue on Github</3> or get directly in contact.</4><5>Funding</5><6>The game engine has been developed as part of the project <1>ADAM: Anticipating the Digital Age of Mathematics</1> at Heinrich-Heine-Universität Düsseldorf. It is funded by the <i>Stiftung Innovation in der Hochschullehre</i> as part of project <i>Freiraum 2022</i>.</6>": "<0>Progress saving</0><1>The game stores your progress in your local browser storage. If you delete it, your progress will be lost!<br/>Warning: In most browsers, deleting cookies will also clear the local storage (or \"local site data\"). Make sure to download your game progress first!</1><2>Development</2><3>The game engine has been created by <strong>Alexander Bentkamp</strong>, <strong>Jon Eugster</strong>. On a prototype by <strong>Patrick Massot</strong>.</3><4>The source code of this Lean game engine is <1>available on Github</1>. If you experience any problems, please file an <3>Issue on Github</3> or get directly in contact.</4><5>Funding</5><6>The game engine has been developed as part of the project <1>ADAM: Anticipating the Digital Age of Mathematics</1> at Heinrich-Heine-Universität Düsseldorf. It is funded by the <i>Stiftung Innovation in der Hochschullehre</i> as part of project <i>Freiraum 2022</i>.</6>",
|
||||
"<0>Privacy Policy</0><p>Our server collects metadata (such as IP address, browser, operating system) and the data that the user enters into the editor. The data is used to compute the Lean output and display it to the user. The information will be stored as long as the user stays on our website and will be deleted immediately afterwards. We keep logs to improve our software, but the contained data is anonymized.</p><p>We do not use cookies, but your game progress is stored in the browser as site data. Your game progress is not saved on the server; if you delete your browser storage, it is completely gone.</p><p>Our server is located in Germany.</p><4><strong>Contact:</strong><br/>Marcus Zibrowius, Jon Eugster<br/>Mathematisches Institut der Heinrich-Heine-Universität Düsseldorf<br/>Universitätsstr. 1<br/>40225 Düsseldorf<br/>Germany<br/>+49 211 81-14690<br/><14>Contact Details</14></4>": "<0>Privacy Policy</0><p>Our server collects metadata (such as IP address, browser, operating system) and the data that the user enters into the editor. The data is used to compute the Lean output and display it to the user. The information will be stored as long as the user stays on our website and will be deleted immediately afterwards. We keep logs to improve our software, but the contained data is anonymized.</p><p>We do not use cookies, but your game progress is stored in the browser as site data. Your game progress is not saved on the server; if you delete your browser storage, it is completely gone.</p><p>Our server is located in Germany.</p><4><strong>Contact:</strong><br/>Marcus Zibrowius, Jon Eugster<br/>Mathematisches Institut der Heinrich-Heine-Universität Düsseldorf<br/>Universitätsstr. 1<br/>40225 Düsseldorf<br/>Germany<br/>+49 211 81-14690<br/><14>Contact Details</14></4>",
|
||||
"home": "home",
|
||||
"close language menu": "close language menu",
|
||||
"open language menu": "open language menu",
|
||||
"close menu": "close menu",
|
||||
"open menu": "open menu",
|
||||
"Editor Mode": "Editor Mode",
|
||||
"Typewriter Mode": "Typewriter Mode"
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"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": "",
|
||||
"Upload": "",
|
||||
"Home": "",
|
||||
"back to games selection": "",
|
||||
"close inventory": "",
|
||||
"show inventory": "",
|
||||
"World": "",
|
||||
"Show more help!": "",
|
||||
"Goal": "",
|
||||
"Objects": "",
|
||||
"Assumptions": "",
|
||||
"Current Goal": "",
|
||||
"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 🎭": "",
|
||||
"Failed command": "",
|
||||
"Retry proof from here": "",
|
||||
"Retry": "",
|
||||
"Active Goal": "",
|
||||
"Crashed! Go to editor mode and fix your proof! Last server response:": "",
|
||||
"Line": "",
|
||||
"Character": "",
|
||||
"Loading messages…": "",
|
||||
"Execute": "",
|
||||
"Tactics": "",
|
||||
"Definitions": "",
|
||||
"Theorems": "",
|
||||
"Not unlocked yet": "",
|
||||
"Not available in this level": "",
|
||||
"A repository of learning games for the proof assistant <1>Lean</1> <i>(Lean 4)</i> and its mathematical library <5>mathlib</5>": "",
|
||||
"No Games loaded. Use <1>http://localhost:3000/#/g/local/FOLDER</1> to open a game directly from a local folder.": "",
|
||||
"<p>As this server runs lean on our university machines, it has a limited capacity. Our current estimate is about 70 simultaneous games. We hope to address and test this limitation better in the future.</p><1>Most aspects of the games and the infrastructure are still in development. Feel free to file a <1>GitHub Issue</1> about any problems you experience!</1>": "",
|
||||
"<0>If you are considering writing your own game, you should use the <1>GameSkeleton Github Repo</1> as a template and read <3>How to Create a Game</3>.</0><1>You can directly load your games into the server and play it using the correct URL. The <1>instructions above</1> also explain the details for how to load your game to the server. We'd like to encourage you to contact us if you have any questions.</1><p>Featured games on this page are added manually. Please get in contact and we'll happily add yours.</p>": "",
|
||||
"This server has been developed as part of the project <1>ADAM : Anticipating the Digital Age of Mathematics</1> at Heinrich-Heine-Universität in Düsseldorf.": "",
|
||||
"view the Lean game server on Github": "",
|
||||
"Prerequisites": "",
|
||||
"Worlds": "",
|
||||
"Levels": "",
|
||||
"Language": "",
|
||||
"Lean Game Server": "",
|
||||
"Development notes": "",
|
||||
"Adding new games": "",
|
||||
"Funding": "",
|
||||
"Level": "",
|
||||
"Introduction": "",
|
||||
"<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": "",
|
||||
"Download & Delete": "",
|
||||
"Cancel": "",
|
||||
"Mobile": "",
|
||||
"Auto": "",
|
||||
"Desktop": "",
|
||||
"Layout": "",
|
||||
"Always visible": "",
|
||||
"Save my settings (in the browser store)": "",
|
||||
"<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": "",
|
||||
"levels": "",
|
||||
"tactics": "",
|
||||
"regular": "",
|
||||
"relaxed": "",
|
||||
"none": "",
|
||||
"<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": "",
|
||||
"Load selected file": "",
|
||||
"Rules": ""
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"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": "打字机模式",
|
||||
"Preferences": "偏好设置",
|
||||
"Game Info & Credits": "游戏信息和荣誉",
|
||||
"Game Info": "游戏信息",
|
||||
"Clear Progress": "清除进度",
|
||||
"Erase": "删除",
|
||||
"Download Progress": "下载进度",
|
||||
"Download": "下载",
|
||||
"Load Progress from JSON": "从 JSON 加载进度",
|
||||
"Upload": "上传",
|
||||
"Home": "首页",
|
||||
"back to games selection": "返回游戏选择",
|
||||
"close inventory": "关闭定理清单面板",
|
||||
"show inventory": "打开定理清单面板",
|
||||
"World": "世界",
|
||||
"Show more help!": "显示更多帮助!",
|
||||
"Goal": "目标",
|
||||
"Current Goal": "当前目标",
|
||||
"Objects": "对象",
|
||||
"Assumptions": "假设",
|
||||
"Further Goals": "Further Goals",
|
||||
"No Goals": "无目标",
|
||||
"Loading goal…": "加载目标。。。",
|
||||
"Click somewhere in the Lean file to enable the infoview.": "单击Lean文件中的某处以启用信息视图。",
|
||||
"Waiting for Lean server to start…": "等待 Lean 服务器启动。。。",
|
||||
"Level completed! 🎉": "完成关卡!🎉",
|
||||
"Level completed with warnings 🎭": "关卡完成(带有警告) 🎭",
|
||||
"Retry proof from here": "从这里重新试着证明",
|
||||
"Active Goal": "当前目标",
|
||||
"Crashed! Go to editor mode and fix your proof! Last server response:": "程序崩溃!请转到编辑器模式,修复您的证明!最后一次服务器响应:",
|
||||
"Line": "行",
|
||||
"Character": "字符",
|
||||
"Loading messages…": "正在加载信息。。。",
|
||||
"Execute": "执行",
|
||||
"Definitions": "定义",
|
||||
"Theorems": "定理",
|
||||
"Theorem": "定理",
|
||||
"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>直接从本地文件夹打开游戏。",
|
||||
"<p>As this server runs lean on our university machines, it has a limited capacity. Our current estimate is about 70 simultaneous games. We hope to address and test this limitation better in the future.</p><1>Most aspects of the games and the infrastructure are still in development. Feel free to file a <1>GitHub Issue</1> about any problems you experience!</1>": "<p>由于这个服务器在我们大学的机器上运行Lean,它的容量是有限的。我们当前的估计是大约70个同时进行的游戏。我们希望将来能更好地解决和测试这个限制。</p><1>游戏和基础设施的大多数方面仍在开发中。遇到任何问题,请随时提交<1>GitHub Issue</1>!</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>",
|
||||
"This server has been developed as part of the project <1>ADAM : Anticipating the Digital Age of Mathematics</1> at Heinrich-Heine-Universität in Düsseldorf.": "这个服务器是作为杜塞尔多夫海因里希·海涅大学项目<1>ADAM:预见数学的数字时代</1>的一部分而开发的。",
|
||||
"Prerequisites": "前置条件",
|
||||
"Worlds": "世界(Worlds)",
|
||||
"Levels": "关卡",
|
||||
"Language": "语言",
|
||||
"Development notes": "开发信息",
|
||||
"Adding new games": "添加新游戏",
|
||||
"Funding": "赞助",
|
||||
"<p>Do you want to delete your saved progress irreversibly?</p><p>(This deletes your proofs and your collected inventory. Saves from other games are not deleted.)</p>": "<p>您是否想要不可逆转地删除您的游戏进度?</p><p>(这将删除您的证明和您收集的定理和策略。其他游戏的进度不会被删除。)</p>",
|
||||
"Delete Progress?": "删除进度?",
|
||||
"Delete": "删除",
|
||||
"Download & Delete": "下载和删除",
|
||||
"Cancel": "取消",
|
||||
"Layout": "布局",
|
||||
"Always visible": "始终可见",
|
||||
"Save my settings (in the browser store)": "保存我的设置(在浏览器商店中)",
|
||||
"<p>Select a JSON file with the saved game progress to load your progress.</p><1><0>Warning:</0> This will delete your current game progress! Consider <2>downloading your current progress</2> first!</1>": "<p>选择一个包含已保存游戏进度的JSON文件来加载您的进度。</p><1><0>警告:</0>这将删除您当前的游戏进度!首先考虑<2>下载您当前的进度</2>!</1>",
|
||||
"Upload Saved Progress": "上传保存的进度",
|
||||
"Load selected file": "加载所选文件",
|
||||
"Mobile": "移动端",
|
||||
"Auto": "自动",
|
||||
"Desktop": "桌面端",
|
||||
"Level": "关卡",
|
||||
"Introduction": "介绍",
|
||||
"Retry": "重试",
|
||||
"Failed command": "命令失败",
|
||||
"view the Lean game server on Github": "",
|
||||
"Impressum": "版权声明",
|
||||
"Privacy Policy": "隐私政策",
|
||||
"<0>Impressum</0><1><strong>Contact:</strong><br/>Marcus Zibrowius, Jon Eugster<br/>Mathematisches Institut der Heinrich-Heine-Universität Düsseldorf<br/>Universitätsstr. 1<br/>40225 Düsseldorf<br/>Germany<br/>+49 211 81-14690<br/><14>Contact Details</14></1><2><strong>Legal form:</strong><br/>The Heinrich Heine University Düsseldorf is a corporation under public law. It is legally represented by the Rector Prof. Dr. Anja Steinbeck. The responsible supervisory authority is the Ministry of Culture and Science of North Rhine-Westphalia, Völklinger Straße 49, 40221 Düsseldorf.</2><3><strong>VAT identification number:</strong><br/>according to §27a Sales Tax Act<br/>DE 811222416</3><4><0>Impressum HHU</0></4>": "",
|
||||
"<0>Progress saving</0><1>The game stores your progress in your local browser storage. If you delete it, your progress will be lost!<br/>Warning: In most browsers, deleting cookies will also clear the local storage (or \"local site data\"). Make sure to download your game progress first!</1><2>Development</2><3>The game engine has been created by <strong>Alexander Bentkamp</strong>, <strong>Jon Eugster</strong>. On a prototype by <strong>Patrick Massot</strong>.</3><4>The source code of this Lean game engine is <1>available on Github</1>. If you experience any problems, please file an <3>Issue on Github</3> or get directly in contact.</4><5>Funding</5><6>The game engine has been developed as part of the project <1>ADAM: Anticipating the Digital Age of Mathematics</1> at Heinrich-Heine-Universität Düsseldorf. It is funded by the <i>Stiftung Innovation in der Hochschullehre</i> as part of project <i>Freiraum 2022</i>.</6>": "",
|
||||
"<0>Privacy Policy</0><p>Our server collects metadata (such as IP address, browser, operating system) and the data that the user enters into the editor. The data is used to compute the Lean output and display it to the user. The information will be stored as long as the user stays on our website and will be deleted immediately afterwards. We keep logs to improve our software, but the contained data is anonymized.</p><p>We do not use cookies, but your game progress is stored in the browser as site data. Your game progress is not saved on the server; if you delete your browser storage, it is completely gone.</p><p>Our server is located in Germany.</p><4><strong>Contact:</strong><br/>Marcus Zibrowius, Jon Eugster<br/>Mathematisches Institut der Heinrich-Heine-Universität Düsseldorf<br/>Universitätsstr. 1<br/>40225 Düsseldorf<br/>Germany<br/>+49 211 81-14690<br/><14>Contact Details</14></4>": ""
|
||||
}
|
||||
+91
-17
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Outlet, useParams } from "react-router-dom";
|
||||
import { Outlet, useParams, useSearchParams } from "react-router-dom";
|
||||
import { Suspense, useEffect, useState } from 'react';
|
||||
|
||||
import '@fontsource/roboto/300.css';
|
||||
import '@fontsource/roboto/400.css';
|
||||
@@ -8,28 +9,101 @@ import '@fontsource/roboto/700.css';
|
||||
|
||||
import './css/reset.css';
|
||||
import './css/app.css';
|
||||
import { MobileContext } from './components/infoview/context';
|
||||
import { useWindowDimensions } from './window_width';
|
||||
import { connection } from './connection';
|
||||
|
||||
export const GameIdContext = React.createContext<string>(undefined);
|
||||
import { GameIdContext, PageContext, PreferencesContext} from './state/context';
|
||||
import UsePreferences from "./state/hooks/use_preferences"
|
||||
import { Navigation } from './components/navigation';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { changeTypewriterMode, selectReadIntro, selectTypewriterMode } from './state/progress';
|
||||
import { useAppDispatch } from './hooks';
|
||||
import { Popup, PopupContext } from './components/popup/popup';
|
||||
import { useGetGameInfoQuery } from './state/api';
|
||||
import lean4gameConfig from './config.json'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function App() {
|
||||
const params = useParams()
|
||||
const gameId = "g/" + params.owner + "/" + params.repo
|
||||
const {width, height} = useWindowDimensions()
|
||||
const [mobile, setMobile] = React.useState(width < 800)
|
||||
let { t, i18n } = useTranslation()
|
||||
|
||||
React.useEffect(() => {
|
||||
connection.startLeanClient(gameId);
|
||||
}, [gameId])
|
||||
const params = useParams()
|
||||
const gameId = (params.owner && params.repo) ? "g/" + params.owner + "/" + params.repo : null
|
||||
const worldId = params.worldId
|
||||
const levelId = parseInt(params.levelId)
|
||||
|
||||
const {mobile, layout, isSavePreferences, language, setLayout, setIsSavePreferences, setLanguage} = UsePreferences()
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
const typewriterMode = useSelector(selectTypewriterMode(gameId))
|
||||
const setTypewriterMode = (newTypewriterMode: boolean) => dispatch(changeTypewriterMode({game: gameId, typewriterMode: newTypewriterMode}))
|
||||
const [lockEditorMode, setLockEditorMode] = useState(false)
|
||||
const [typewriterInput, setTypewriterInput] = useState("")
|
||||
const [page, setPage] = useState(0)
|
||||
const [popupContent, setPopupContent] = useState(null)
|
||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
|
||||
const readIntro = useSelector(selectReadIntro(gameId, worldId))
|
||||
|
||||
// mobile only: game intro should only be shown once and skipped afterwards
|
||||
useEffect(() => {
|
||||
if (worldId) {
|
||||
console.log('setting page to 1')
|
||||
setPage(1)
|
||||
} else {
|
||||
if (readIntro && page == 0) {
|
||||
console.log('setting page to 1')
|
||||
setPage(1)
|
||||
} else {
|
||||
// setPage(0)
|
||||
}
|
||||
}
|
||||
}, [worldId, levelId])
|
||||
|
||||
// option to pass language as `?lang=de` in the URL
|
||||
useEffect(() => {
|
||||
let urlLang = searchParams.get("lang")
|
||||
let availableLangs = gameInfo.data?.tile?.languages
|
||||
if (gameId) {
|
||||
if (availableLangs?.includes(urlLang)) {
|
||||
setLanguage(urlLang)
|
||||
// Delete the search param as we processed it.
|
||||
searchParams.delete('lang')
|
||||
setSearchParams(searchParams)
|
||||
}
|
||||
} else {
|
||||
if (urlLang in lean4gameConfig.newLanguages) {
|
||||
setLanguage(urlLang)
|
||||
// Delete the search param as we processed it.
|
||||
searchParams.delete('lang')
|
||||
setSearchParams(searchParams)
|
||||
}
|
||||
}
|
||||
}, [gameId, gameInfo.data?.tile?.languages])
|
||||
|
||||
// set the correct language
|
||||
useEffect(() => {
|
||||
let availableLangs = gameInfo.data?.tile?.languages
|
||||
if (gameId && availableLangs?.length > 0 && !(availableLangs.includes(language))) {
|
||||
// if the game is not available in the preferred language, display it in the original
|
||||
// language
|
||||
console.debug(`using default language: ${availableLangs[0]}`)
|
||||
i18n.changeLanguage(availableLangs[0])
|
||||
} else {
|
||||
console.debug(`using language: ${language}`)
|
||||
i18n.changeLanguage(language)
|
||||
}
|
||||
}, [gameId, gameInfo.data?.tile?.languages, language])
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<GameIdContext.Provider value={gameId}>
|
||||
<MobileContext.Provider value={{mobile, setMobile}}>
|
||||
<Outlet />
|
||||
</MobileContext.Provider>
|
||||
<GameIdContext.Provider value={{gameId, worldId, levelId}}>
|
||||
<PreferencesContext.Provider value={{mobile, layout, isSavePreferences, language, setLayout, setIsSavePreferences, setLanguage}}>
|
||||
<PopupContext.Provider value={{popupContent, setPopupContent}}>
|
||||
<PageContext.Provider value={{typewriterMode, setTypewriterMode, typewriterInput, setTypewriterInput, lockEditorMode, setLockEditorMode, page, setPage}}>
|
||||
<Navigation />
|
||||
<Outlet />
|
||||
{ popupContent && <Popup /> }
|
||||
</PageContext.Provider>
|
||||
</PopupContext.Provider>
|
||||
</PreferencesContext.Provider>
|
||||
</GameIdContext.Provider>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
/**
|
||||
* @file contains the navigation bars of the app.
|
||||
*/
|
||||
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 } from '@fortawesome/free-solid-svg-icons'
|
||||
import { GameIdContext } from "../app"
|
||||
import { InputModeContext, MobileContext, 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'
|
||||
|
||||
/** 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 dispatch = useAppDispatch()
|
||||
|
||||
// if `prevText` or `prevIcon` is set, show a button to go back
|
||||
let prevText = {0: null, 1: "Intro", 2: null}[pageNumber]
|
||||
let prevIcon = {0: null, 1: null, 2: faBookOpen}[pageNumber]
|
||||
let prevTitle = {0: null, 1: "Game Introduction", 2: "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 nextIcon = {0: null, 1: faBook, 2: null}[pageNumber]
|
||||
let nextTitle = {0: "World selection", 1: "Inventory", 2: null}[pageNumber]
|
||||
|
||||
return <>
|
||||
{(prevText || prevIcon) &&
|
||||
<Button className="btn btn-inverted toggle-width" to={pageNumber == 0 ? "/" : ""}
|
||||
inverted="true" title={prevTitle}
|
||||
onClick={() => {pageNumber == 0 ? null : setPageNumber(pageNumber - 1)}}>
|
||||
{prevIcon && <FontAwesomeIcon icon={prevIcon} />}
|
||||
{prevText && `${prevText}`}
|
||||
</Button>
|
||||
}
|
||||
{(nextText || nextIcon) &&
|
||||
<Button className="btn btn-inverted toggle-width" to="" inverted="true"
|
||||
title={nextTitle} onClick={() => {
|
||||
console.log(`page number: ${pageNumber}`)
|
||||
setPageNumber(pageNumber+1);
|
||||
dispatch(changedOpenedIntro({game: gameId, openedIntro: true}))}}>
|
||||
{nextText && `${nextText}`}
|
||||
{nextIcon && <FontAwesomeIcon icon={nextIcon} />}
|
||||
</Button>
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
/** button to toggle dropdown menu. */
|
||||
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>
|
||||
}
|
||||
|
||||
/** button to go one level futher.
|
||||
* for the last level, this button turns into a button going back to the welcome page.
|
||||
*/
|
||||
function NextButton({worldSize, difficulty, completed, setNavOpen}) {
|
||||
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"
|
||||
disabled={difficulty >= 2 && !(completed || levelId == 0)}
|
||||
onClick={() => setNavOpen(false)}>
|
||||
<FontAwesomeIcon icon={faArrowRight} /> {levelId ? "Next" : "Start"}
|
||||
</Button>
|
||||
:
|
||||
<Button to={`/${gameId}`} inverted="true" title="back to world selection" id="home-btn">
|
||||
<FontAwesomeIcon icon={faHome} /> Leave World
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
/** button to go one level back.
|
||||
* only renders if the current level id is > 0.
|
||||
*/
|
||||
function PreviousButton({setNavOpen}) {
|
||||
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"
|
||||
onClick={() => setNavOpen(false)}>
|
||||
<FontAwesomeIcon icon={faArrowLeft} /> Previous
|
||||
</Button>
|
||||
</>)
|
||||
}
|
||||
|
||||
/** button to toggle between editor and typewriter */
|
||||
function InputModeButton({setNavOpen, isDropdown}) {
|
||||
const {levelId} = React.useContext(WorldLevelIdContext)
|
||||
const {typewriterMode, setTypewriterMode, lockInputMode} = React.useContext(InputModeContext)
|
||||
|
||||
/** toggle input mode if allowed */
|
||||
function toggleInputMode(ev: React.MouseEvent) {
|
||||
if (!lockInputMode){
|
||||
setTypewriterMode(!typewriterMode)
|
||||
setNavOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
return <Button
|
||||
className={`btn btn-inverted ${isDropdown? '' : 'toggle-width'}`} disabled={levelId <= 0 || lockInputMode}
|
||||
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</>)}
|
||||
</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)}}>
|
||||
<FontAwesomeIcon icon={faCircleInfo} />
|
||||
{isDropdown && <> Info & Impressum</>}
|
||||
</Button>
|
||||
}
|
||||
|
||||
/** button to go back to welcome page */
|
||||
function HomeButton({isDropdown}) {
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
return <Button to={`/${gameId}`} inverted="true" title="back to world selection" id="home-btn">
|
||||
<FontAwesomeIcon icon={faHome} />
|
||||
{isDropdown && <> Home</>}
|
||||
</Button>
|
||||
}
|
||||
|
||||
/** button in mobile level to toggle inventory.
|
||||
* only displays a button if `setPageNumber` is set.
|
||||
*/
|
||||
function InventoryButton({pageNumber, setPageNumber}) {
|
||||
return (setPageNumber &&
|
||||
<Button to="" className="btn btn-inverted toggle-width"
|
||||
title={pageNumber ? "close inventory" : "show inventory"}
|
||||
inverted="true" onClick={() => {setPageNumber(pageNumber ? 0 : 1)}}>
|
||||
<FontAwesomeIcon icon={pageNumber ? faBookOpen : faBook} />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
/** the navigation bar on the welcome page */
|
||||
export function WelcomeAppBar({pageNumber, setPageNumber, gameInfo, toggleImpressum, toggleEraseMenu, toggleUploadMenu, toggleInfo} : {
|
||||
pageNumber: number,
|
||||
setPageNumber: any,
|
||||
gameInfo: GameInfo,
|
||||
toggleImpressum: any,
|
||||
toggleEraseMenu: any,
|
||||
toggleUploadMenu: any,
|
||||
toggleInfo: any
|
||||
}) {
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const gameProgress = useAppSelector(selectProgress(gameId))
|
||||
const {mobile} = React.useContext(MobileContext)
|
||||
const [navOpen, setNavOpen] = React.useState(false)
|
||||
|
||||
return <div className="app-bar">
|
||||
<div>
|
||||
<Button inverted="false" title="back to games selection" to="/">
|
||||
<FontAwesomeIcon icon={faArrowLeft} /> <FontAwesomeIcon icon={faGlobe} />
|
||||
</Button>
|
||||
<span className="app-bar-title"></span>
|
||||
</div>
|
||||
<div>
|
||||
{!mobile && <span className="app-bar-title">{gameInfo?.title}</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>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
/** the navigation bar in a level */
|
||||
export function LevelAppBar({isLoading, levelTitle, toggleImpressum, pageNumber=undefined, setPageNumber=undefined} : {
|
||||
isLoading: boolean,
|
||||
levelTitle: string,
|
||||
toggleImpressum: any,
|
||||
pageNumber?: number,
|
||||
setPageNumber?: any,
|
||||
}) {
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
|
||||
const {mobile} = React.useContext(MobileContext)
|
||||
const [navOpen, setNavOpen] = React.useState(false)
|
||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||
const completed = useAppSelector(selectCompleted(gameId, worldId, levelId))
|
||||
const difficulty = useAppSelector(selectDifficulty(gameId))
|
||||
|
||||
let worldTitle = gameInfo.data?.worlds.nodes[worldId].title
|
||||
|
||||
return <div className="app-bar" style={isLoading ? {display: "none"} : null} >
|
||||
{mobile ?
|
||||
<>
|
||||
{/* MOBILE VERSION */}
|
||||
<div>
|
||||
<span className="app-bar-title">{levelTitle}</span>
|
||||
</div>
|
||||
<div className="nav-btns">
|
||||
<InventoryButton pageNumber={pageNumber} setPageNumber={setPageNumber}/>
|
||||
<MenuButton navOpen={navOpen} setNavOpen={setNavOpen}/>
|
||||
</div>
|
||||
<div className={'menu dropdown' + (navOpen ? '' : ' hidden')}>
|
||||
<NextButton worldSize={gameInfo.data?.worldSize[worldId]} difficulty={difficulty} completed={completed} setNavOpen={setNavOpen} />
|
||||
<PreviousButton setNavOpen={setNavOpen} />
|
||||
<HomeButton isDropdown={true} />
|
||||
<InputModeButton setNavOpen={setNavOpen} isDropdown={true}/>
|
||||
<ImpressumButton setNavOpen={setNavOpen} toggleImpressum={toggleImpressum} isDropdown={true} />
|
||||
</div>
|
||||
</> :
|
||||
<>
|
||||
{/* DESKTOP VERSION */}
|
||||
<div>
|
||||
<HomeButton isDropdown={false} />
|
||||
<span className="app-bar-title">{worldTitle && `World: ${worldTitle}`}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="app-bar-title">{levelTitle}</span>
|
||||
</div>
|
||||
<div className="nav-btns">
|
||||
<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} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Link, LinkProps } from "react-router-dom";
|
||||
|
||||
export interface ButtonProps extends LinkProps {
|
||||
disabled?: boolean
|
||||
inverted?: string // Apparently "inverted" in DOM cannot be `boolean` but must be `inverted`
|
||||
}
|
||||
|
||||
export function Button(props: ButtonProps) {
|
||||
if (props.disabled) {
|
||||
return <span className={`btn btn-disabled ${props.inverted === "true" ? 'btn-inverted' : ''}`} {...props}>{props.children}</span>
|
||||
} else {
|
||||
return <Link className={`btn ${props.inverted === "true" ? 'btn-inverted' : ''}`} {...props}>{props.children}</Link>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
import * as React from 'react'
|
||||
import { useContext, useEffect, useRef, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faArrowRight } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import { changedReadIntro, selectCompleted, selectReadIntro } from '../state/progress'
|
||||
import { useGetGameInfoQuery, useLoadLevelQuery } from '../state/api'
|
||||
import { useAppDispatch, useAppSelector } from '../hooks'
|
||||
import { Button, Markdown } from './utils'
|
||||
import { ChatContext, GameIdContext, PageContext, PreferencesContext, ProofContext } from '../state/context'
|
||||
import { GameHint, InteractiveGoalsWithHints } from './infoview/rpc_api'
|
||||
// import { lastStepHasErrors } from './infoview/goals'
|
||||
// import { AllMessages } from '../../../node_modules/@leanprover/infoview/dist/infoview/messages'
|
||||
// import { LeanDiagnostic, RpcErrorCode, getInteractiveDiagnostics, InteractiveDiagnostic, TaggedText_stripTags } from '@leanprover/infoview-api'
|
||||
import { Location, DocumentUri, Diagnostic, DiagnosticSeverity, PublishDiagnosticsParams } from 'vscode-languageserver-protocol'
|
||||
// import { InteractiveMessage } from '../../../node_modules/lean4-infoview/src/infoview/traceExplorer'
|
||||
|
||||
import '../css/chat.css'
|
||||
import { faHome } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
|
||||
/** Split a string by double newlines and filters out empty segments. */
|
||||
function splitIntro (intro : string) {
|
||||
return intro.split(/\n(\s*\n)+/).filter(t => t.trim())
|
||||
}
|
||||
|
||||
/** Helper to check if a step has any hidden hints. */
|
||||
function hasHiddenHints(step: InteractiveGoalsWithHints): boolean {
|
||||
return step?.goals[0]?.hints.some((hint) => hint.hidden)
|
||||
}
|
||||
|
||||
/** Button which only appears if the current step has hidden hints that are not shown yet. */
|
||||
export function MoreHelpButton({selected=null} : {selected?: number}) {
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { proof } = React.useContext(ProofContext)
|
||||
const { showHelp, setShowHelp } = React.useContext(ChatContext)
|
||||
|
||||
let k = 0
|
||||
// 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>
|
||||
}
|
||||
return <></>
|
||||
}
|
||||
|
||||
/** Placeholder that takes the same space as a button. */
|
||||
function ButtonPlaceholder() {
|
||||
return <div className='btn-placeholder'/>
|
||||
}
|
||||
|
||||
/** The buttons at the bottom of chat. */
|
||||
export function ChatButtons ({counter=undefined, setCounter=()=>{}, introMessages=[]} : {
|
||||
counter?: number
|
||||
setCounter?: React.Dispatch<React.SetStateAction<number>>
|
||||
introMessages?: GameHintWithStep[]
|
||||
}) {
|
||||
let { t } = useTranslation()
|
||||
|
||||
const { mobile } = useContext(PreferencesContext)
|
||||
const { gameId, worldId, levelId } = useContext(GameIdContext)
|
||||
const {setPage} = useContext(PageContext)
|
||||
const dispatch = useAppDispatch()
|
||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||
const { proof } = useContext(ProofContext)
|
||||
|
||||
const readIntro = useSelector(selectReadIntro(gameId, worldId))
|
||||
|
||||
return <div className="button-row">
|
||||
{!levelId && (readIntro || (counter >= introMessages.length) ?
|
||||
((worldId || mobile) &&
|
||||
<>
|
||||
<ButtonPlaceholder />
|
||||
<Button className="btn"
|
||||
title=""
|
||||
to={worldId ? `/${gameId}/world/${worldId}/level/1` : ''}
|
||||
onClick={() => {
|
||||
if (!worldId) {
|
||||
console.log('setting `readIntro` to true')
|
||||
setPage(1)
|
||||
}
|
||||
}} >
|
||||
Start <FontAwesomeIcon icon={faArrowRight}/>
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
:
|
||||
<>
|
||||
<Button className="btn"
|
||||
title=""
|
||||
to=""
|
||||
onClick={() => {
|
||||
if (counter + 1 >= introMessages.length) {
|
||||
dispatch(changedReadIntro({game: gameId, world: worldId, readIntro: true}))
|
||||
}
|
||||
setCounter(counter + 1)
|
||||
}} >
|
||||
{"Read"}
|
||||
</Button>
|
||||
<Button className="btn"
|
||||
title=""
|
||||
to=""
|
||||
onClick={() => {
|
||||
dispatch(changedReadIntro({game: gameId, world: worldId, readIntro: true}))
|
||||
setCounter(introMessages.length)
|
||||
}} >
|
||||
Skip all
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{ (worldId && levelId) ? <MoreHelpButton /> : <></> }
|
||||
{ (worldId && levelId && proof?.completed) ?
|
||||
(levelId == gameInfo.data?.worldSize[worldId] ?
|
||||
<Button className="btn"
|
||||
title=""
|
||||
to={`/${gameId}`} >
|
||||
<FontAwesomeIcon icon={faHome} /> {t("Home")}
|
||||
</Button> :
|
||||
<Button className="btn"
|
||||
title=""
|
||||
to={`/${gameId}/world/${worldId}/level/${levelId + 1}`} >
|
||||
{t("Next")} <FontAwesomeIcon icon={faArrowRight} />
|
||||
|
||||
</Button>
|
||||
) : <></> }
|
||||
</div>
|
||||
}
|
||||
|
||||
/** Insert 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})
|
||||
}
|
||||
}
|
||||
|
||||
/** Hint kinds. Note that number 1-4 are matching the numbers from `DiagnosticSeverity`
|
||||
* from the vscode language server protocol.
|
||||
*/
|
||||
enum HintKind {
|
||||
Error = 1,
|
||||
Warning = 2,
|
||||
Information = 3,
|
||||
Hint = 4,
|
||||
GameHint = 5,
|
||||
Conclusion = 7,
|
||||
}
|
||||
|
||||
/** Bundling a hint with the proof-step it comes from. */
|
||||
type GameHintWithStep = {
|
||||
hint: GameHint
|
||||
kind: HintKind
|
||||
step?: number
|
||||
}
|
||||
|
||||
/** Filter hints to not show consequtive identical hints twice.
|
||||
* Hidden hints are not filtered.
|
||||
*/
|
||||
export function filterHints(hints: GameHint[], prevHints: GameHint[]): GameHint[] {
|
||||
if (!hints) {
|
||||
return []
|
||||
} else if (!prevHints) {
|
||||
return [...hints.filter((hint) => !hint.hidden), ...hints.filter((hint) => hint.hidden)]
|
||||
} else {
|
||||
return [...hints.filter((hint) => !hint.hidden &&
|
||||
(prevHints.find(x => (x.text == hint.text && x.hidden == hint.hidden)) === undefined)
|
||||
), ...hints.filter((hint) => hint.hidden)]
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
// function helper(step, proof, kind, typewriterMode, selectedStep) {
|
||||
// return (step == proof?.steps?.length - (lastStepHasErrors(proof) ? 2 : 1) ? ' recent' : '') +
|
||||
// (!(kind == HintKind.Conclusion) && step >= (typewriterMode ? proof?.steps?.length : selectedStep+1) ? ' deleted-hint' : '')
|
||||
// }
|
||||
|
||||
/** A hint as it is displayed in the chat. */
|
||||
export function Hint({hint, kind, step=null} : GameHintWithStep) {
|
||||
const { levelId } = useContext(GameIdContext)
|
||||
const { selectedStep, setSelectedStep } = useContext(ChatContext)
|
||||
|
||||
const { proof } = useContext(ProofContext)
|
||||
const { typewriterMode } = useContext(PageContext)
|
||||
|
||||
function toggleSelection () {
|
||||
if (!levelId) {return}
|
||||
|
||||
if (selectedStep !== null && selectedStep == step) {
|
||||
setSelectedStep(null)
|
||||
} else if (step < proof?.steps?.length) {
|
||||
setSelectedStep(step)
|
||||
}
|
||||
}
|
||||
|
||||
// "Deleted hints" are marked in grey. They are used two-fold:
|
||||
// In typewriter, deleting parts of the proof stores the removed hints as `deletedChat`
|
||||
// until the next command is submitted; in editor, moving the cursor through the proof will
|
||||
// render all hints
|
||||
return <div className={`message kind-${kind} step-${step}` +
|
||||
((selectedStep !== null && step == selectedStep) ? ' selected' : '') //+ helper(step, proof, kind, typewriterMode, selectedStep)
|
||||
} onClick={toggleSelection}>
|
||||
<Markdown>{getHintText(hint)}</Markdown>
|
||||
</div>
|
||||
}
|
||||
|
||||
/** A collection of hints. If the `counter` is defined, only the first elements will be
|
||||
* shown up to the value of the `counter`.
|
||||
*
|
||||
* Set `conclusion` to true to trigger different style and disable selecting/deleting.
|
||||
*/
|
||||
export function Hints({ hints, counter=undefined } : {
|
||||
hints: GameHintWithStep[],
|
||||
counter?: number
|
||||
}) {
|
||||
|
||||
const { showHelp } = useContext(ChatContext)
|
||||
|
||||
if (!hints) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
// NOTE: This builds on the fact that `.slice(0, undefined)` returns the whole array!
|
||||
// TODO: Should not use index as key.
|
||||
return <>
|
||||
{ hints.slice(0, counter).map((hint, j) =>
|
||||
((!hint.hint.hidden || showHelp.has(hint.step)) &&
|
||||
<Hint key={`hint-${hint.step}-${j}`} hint={hint.hint} kind={hint.kind} step={hint.step} />
|
||||
)
|
||||
)}
|
||||
{/* { //showHelp.has(hint.step) &&
|
||||
hints.filter(hint => hint.hint.hidden).map((hint, j) =>
|
||||
<Hint
|
||||
key={`hidden-hint-${hint.step}-${j}`}
|
||||
hint={hint.hint}
|
||||
step={hint.step}
|
||||
conclusion={hint.conclusion} />
|
||||
)} */}
|
||||
</>
|
||||
}
|
||||
|
||||
/** the panel showing the game's introduction text */
|
||||
export function ChatPanel ({visible = true}) {
|
||||
|
||||
let { t } = useTranslation()
|
||||
|
||||
const { mobile } = useContext(PreferencesContext)
|
||||
const { gameId, worldId, levelId } = useContext(GameIdContext)
|
||||
|
||||
const completed = useAppSelector(selectCompleted(gameId, worldId, levelId))
|
||||
|
||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||
const levelInfo = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
||||
|
||||
const [counter, setCounter] = useState(1)
|
||||
|
||||
const [introText, setIntroText] = useState<Array<GameHintWithStep>>([])
|
||||
const { chatRef, deletedChat, showHelp, selectedStep } = useContext(ChatContext)
|
||||
const { proof } = useContext(ProofContext)
|
||||
|
||||
const readIntro = useSelector(selectReadIntro(gameId, worldId))
|
||||
|
||||
useEffect(() => {
|
||||
setCounter(1)
|
||||
}, [gameId, worldId, levelId])
|
||||
|
||||
// load and display the correct intro text
|
||||
useEffect(() => {
|
||||
if (levelId > 0) {
|
||||
let introText = t(levelInfo.data?.introduction, {ns : gameId}).trim()
|
||||
let introHint: GameHintWithStep = {hint: {text: introText, hidden: false, rawText: introText }, kind: HintKind.GameHint, step: 0}
|
||||
|
||||
// playable level: show the level's intro
|
||||
if (levelInfo.data?.introduction) {
|
||||
setIntroText([introHint])
|
||||
// messages = messages.concat([introHint])
|
||||
}
|
||||
else {
|
||||
setIntroText([])
|
||||
}
|
||||
} else {
|
||||
if (worldId) {
|
||||
let introText = t(gameInfo.data?.worlds.nodes[worldId].introduction, {ns: gameId}).trim()
|
||||
let introHints: GameHintWithStep[] = splitIntro(introText).map( txt => ({hint: {text: txt, hidden: false, rawText: txt }, kind: HintKind.GameHint, step: 0}))
|
||||
|
||||
// Level 0: show the world's intro
|
||||
if (gameInfo.data?.worlds.nodes[worldId].introduction) {
|
||||
// messages = messages.concat(introHints)
|
||||
setIntroText(introHints)
|
||||
} else {
|
||||
setIntroText([])
|
||||
}
|
||||
} else {
|
||||
let introText = t(gameInfo.data?.introduction, {ns : gameId}).trim()
|
||||
let introHints: GameHintWithStep[] = splitIntro(introText).map( txt => ({hint: {text: txt, hidden: false, rawText: txt }, kind: HintKind.GameHint, step: 0}))
|
||||
|
||||
// world overview: show the game's intro
|
||||
if (gameInfo.data?.introduction) {
|
||||
// messages = messages.concat(introHints)
|
||||
setIntroText(introHints)
|
||||
} else {
|
||||
setIntroText([])
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [gameInfo, levelInfo, gameId, worldId, levelId, proof])
|
||||
|
||||
// Scroll the chat
|
||||
useEffect(() => {
|
||||
if (levelId > 0) {
|
||||
|
||||
if (proof) {
|
||||
if (proof?.completed) {
|
||||
// proof currently completed: scroll down
|
||||
console.debug('scroll chat: down to conclusion')
|
||||
chatRef.current!.lastElementChild?.scrollIntoView({block: "center"})
|
||||
} else {
|
||||
// proof currently not completed: first message of last step
|
||||
let lastStep = proof?.steps.length //- (lastStepHasErrors(proof) ? 2 : 1)
|
||||
console.debug(`scroll chat: first message of step ${lastStep}`)
|
||||
chatRef.current?.getElementsByClassName(`step-${lastStep}`)[0]?.scrollIntoView({block: "center"})
|
||||
}
|
||||
} else {
|
||||
// no proof available: scroll to top.
|
||||
console.debug(`scroll chat: top`)
|
||||
chatRef.current!.scrollTo(0,0)
|
||||
}
|
||||
} else {
|
||||
// introduction: scroll to last message
|
||||
console.debug('scroll chat: down')
|
||||
chatRef.current!.lastElementChild?.scrollIntoView({block: "center"})
|
||||
}
|
||||
}, [counter, introText, gameId, worldId, levelId])
|
||||
|
||||
// Scroll down when new hidden hints are triggered
|
||||
useEffect(() => {
|
||||
if (levelId > 0) {
|
||||
let lastStep = proof?.steps.length //- (lastStepHasErrors(proof) ? 2 : 1)
|
||||
if (showHelp.has(lastStep)) {
|
||||
console.debug('scroll chat: down to hidden hints')
|
||||
// TODO: last element of hidden hints?
|
||||
chatRef.current!.lastElementChild?.scrollIntoView({block: "center"})
|
||||
}
|
||||
}
|
||||
}, [showHelp, gameId, worldId, levelId])
|
||||
|
||||
// Scroll to corresponding messages if selected step changes
|
||||
useEffect(() => {
|
||||
if (levelId > 0 && selectedStep !== null) {
|
||||
console.debug(`scroll chat: first message of selected step ${selectedStep}`)
|
||||
chatRef.current?.getElementsByClassName(`step-${selectedStep}`)[0]?.scrollIntoView({block: "center"})
|
||||
// Array.from(chatRef.current?.getElementsByClassName(`step-${selectedStep}`)).map((elem) => {
|
||||
// elem.scrollIntoView({block: "center"})
|
||||
// })
|
||||
}
|
||||
}, [selectedStep, gameId, worldId, levelId])
|
||||
|
||||
/** TODO: What's the magic here? Only needed if diags are displayed in chat. */
|
||||
function diagToString (diag) {
|
||||
// Hide "unsolved goals" messages
|
||||
let message;
|
||||
if ("append" in diag.message && "text" in diag.message.append[0] &&
|
||||
diag.message?.append[0].text === "unsolved goals") {
|
||||
message = diag.message.append[0]
|
||||
} else {
|
||||
message = diag.message
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
return <div className={`column chat-panel${visible ? '' : ' hidden'}`}>
|
||||
<div ref={chatRef} className="chat" >
|
||||
{ gameInfo.error &&
|
||||
<div className="message error">
|
||||
Could not find the game!
|
||||
</div>
|
||||
}
|
||||
<Hints hints={introText} counter={readIntro ? undefined : counter}/>
|
||||
{proof?.steps.map((step, i) => {
|
||||
let x = [].concat(
|
||||
filterHints(step.goals[0]?.hints, proof.steps[i-1]?.goals[0]?.hints).map(hint => ({hint: hint, kind: HintKind.GameHint, step: i})),
|
||||
// // TODO: Uncomment this if you want to see the diags in chat
|
||||
// step.diags.map(diag => ({hint: diagToString(diag), kind: diag.severity, step: i}))
|
||||
)
|
||||
|
||||
return <Hints key={`hints-step-${i}`} hints={x}/>
|
||||
})}
|
||||
|
||||
{/* <Hints hints={chatMessages}/> */}
|
||||
{/* {proof?.steps.map((step, i) =>
|
||||
<Hints hints={step.goals[0]?.hints.map(hint => ({hint: hint, step: i}))}/>
|
||||
)} */}
|
||||
{/* <Hints hints={proof?.steps[proof?.steps?.length - 1]?.goals[0].hints.map(hint => ({hint: hint, step: proof?.steps?.length - 1}))} /> */}
|
||||
|
||||
{ deletedChat &&
|
||||
<Hints hints={deletedChat.map(hint => ({hint: hint, kind: HintKind.GameHint, step: proof?.steps?.length}))} />
|
||||
}
|
||||
{ completed && levelInfo.data?.conclusion &&
|
||||
<Hints hints={[
|
||||
{hint: {text: t("Level completed! 🎉"), rawText: t("Level completed! 🎉"), hidden: false}, kind: HintKind.Conclusion, step: proof?.steps?.length},
|
||||
{hint: {text: levelInfo.data?.conclusion, rawText: levelInfo.data?.conclusion, hidden: false}, kind: HintKind.GameHint, step: proof?.steps?.length}
|
||||
]} />
|
||||
}
|
||||
|
||||
{/* {chatMessages.map(((t, i) =>
|
||||
t.trim() ?
|
||||
<Hint key={`intro-p-${i}`}
|
||||
hint={{text: t, hidden: false, rawText: t, varNames: []}}
|
||||
step={0} />
|
||||
: <></>
|
||||
))} */}
|
||||
</div>
|
||||
{ <ChatButtons counter={counter} setCounter={setCounter} introMessages={introText}/> }
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* @fileOverview Defines the interface for the communication with the server.
|
||||
*
|
||||
*/
|
||||
import type { Range } from 'vscode-languageserver-protocol'
|
||||
import type {
|
||||
CodeWithInfos,
|
||||
ContextInfo,
|
||||
FVarId,
|
||||
InteractiveDiagnostic,
|
||||
MVarId,
|
||||
TermInfo
|
||||
} from '@leanprover/infoview-api'
|
||||
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[]
|
||||
fvarIds?: FVarId[]
|
||||
type: CodeWithInfos
|
||||
val?: CodeWithInfos
|
||||
isInstance?: boolean
|
||||
isType?: boolean
|
||||
isInserted?: boolean
|
||||
isRemoved?: boolean
|
||||
isAssumption?: boolean
|
||||
}
|
||||
|
||||
export interface InteractiveGoalCore {
|
||||
hyps: InteractiveHypothesisBundle[]
|
||||
type: CodeWithInfos
|
||||
ctx?: ContextInfo
|
||||
}
|
||||
|
||||
export interface InteractiveGoal extends InteractiveGoalCore {
|
||||
userName?: string
|
||||
goalPrefix?: string
|
||||
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 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
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import * as React from 'react';
|
||||
import Split from 'react-split'
|
||||
import { useContext, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { GameIdContext, MonacoEditorContext } from '../../state/context';
|
||||
import { useLoadLevelQuery } from '../../state/api';
|
||||
import { Markdown } from '../utils';
|
||||
import * as monaco from 'monaco-editor'
|
||||
import { LeanMonaco, LeanMonacoEditor, LeanMonacoOptions } from 'lean4monaco'
|
||||
|
||||
import '../../css/editor.css'
|
||||
import { useSelector } from 'react-redux';
|
||||
import { selectTypewriterMode } from '../../state/progress';
|
||||
import { TypewriterInterFace } from './Typewriter';
|
||||
|
||||
export function Editor() {
|
||||
let { t } = useTranslation()
|
||||
const {gameId, worldId, levelId } = useContext(GameIdContext)
|
||||
|
||||
const typewriterMode = useSelector(selectTypewriterMode(gameId))
|
||||
|
||||
const editorRef = useRef<HTMLDivElement>(null)
|
||||
const infoviewRef = useRef<HTMLDivElement>(null)
|
||||
const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor>()
|
||||
const [leanMonaco, setLeanMonaco] = useState<LeanMonaco>()
|
||||
const [code, setCode] = useState<string>('')
|
||||
|
||||
const [options, setOptions] = useState<LeanMonacoOptions>({
|
||||
// placeholder. gets set below
|
||||
websocket: {
|
||||
url: ''
|
||||
}
|
||||
})
|
||||
|
||||
// Update LeanMonaco options when preferences are loaded or change
|
||||
useEffect(() => {
|
||||
var socketUrl = ((window.location.protocol === "https:") ? "wss://" : "ws://") +
|
||||
window.location.host + `/websocket/${gameId}`
|
||||
console.log(`[LeanGame] socket url: ${socketUrl}`)
|
||||
var _options: LeanMonacoOptions = {
|
||||
websocket: {
|
||||
url: socketUrl,
|
||||
// @ts-ignore
|
||||
difficulty: 1,
|
||||
inventory: []
|
||||
},
|
||||
// Restrict monaco's extend (e.g. context menu) to the editor itself
|
||||
htmlElement: editorRef.current ?? undefined,
|
||||
vscode: {
|
||||
/* To add settings here, you can open your settings in VSCode (Ctrl+,), search
|
||||
* for the desired setting, select "Copy Setting as JSON" from the "More Actions"
|
||||
* menu next to the selected setting, and paste the copied string here.
|
||||
*/
|
||||
// "workbench.colorTheme": preferences.theme,
|
||||
"editor.tabSize": 2,
|
||||
// "editor.rulers": [100],
|
||||
"editor.lightbulb.enabled": "on",
|
||||
"editor.wordWrap": "on",
|
||||
"editor.wrappingStrategy": "advanced",
|
||||
"editor.semanticHighlighting.enabled": true,
|
||||
"editor.acceptSuggestionOnEnter": "off",
|
||||
"lean4.input.eagerReplacementEnabled": true,
|
||||
// "lean4.input.leader": preferences.abbreviationCharacter
|
||||
}
|
||||
}
|
||||
setOptions(_options)
|
||||
}, [editorRef])
|
||||
|
||||
// Setting up the editor and infoview
|
||||
useEffect(() => {
|
||||
console.debug('[LeanGame] Restarting Editor!')
|
||||
var _leanMonaco = new LeanMonaco()
|
||||
var leanMonacoEditor = new LeanMonacoEditor()
|
||||
|
||||
_leanMonaco.setInfoviewElement(infoviewRef.current!)
|
||||
;(async () => {
|
||||
await _leanMonaco.start(options)
|
||||
console.warn('gameId', gameId)
|
||||
await leanMonacoEditor.start(editorRef.current!, `/${worldId}/L_${levelId}.lean`, code)
|
||||
|
||||
setEditor(leanMonacoEditor.editor)
|
||||
setLeanMonaco(_leanMonaco)
|
||||
|
||||
// Keeping the `code` state up-to-date with the changes in the editor
|
||||
leanMonacoEditor.editor?.onDidChangeModelContent(() => {
|
||||
setCode(leanMonacoEditor.editor?.getModel()?.getValue()!)
|
||||
})
|
||||
})()
|
||||
return () => {
|
||||
leanMonacoEditor.dispose()
|
||||
_leanMonaco.dispose()
|
||||
}
|
||||
}, [options, infoviewRef, editorRef, gameId, worldId, levelId])
|
||||
|
||||
return <MonacoEditorContext.Provider value={editor}>
|
||||
<div className="editor-wrapper"><Split direction='vertical' minSize={200} sizes={[50, 50]}
|
||||
className={`editor-split ${typewriterMode ? 'hidden' : ''}`} >
|
||||
<div ref={editorRef} id="editor" />
|
||||
<div ref={infoviewRef} id="infoview" />
|
||||
</Split>
|
||||
{typewriterMode && <TypewriterInterFace />}
|
||||
</div>
|
||||
</MonacoEditorContext.Provider>
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as React from 'react';
|
||||
import { useContext, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { GameIdContext } from '../../state/context';
|
||||
import { useLoadLevelQuery } from '../../state/api';
|
||||
import { Markdown } from '../utils';
|
||||
|
||||
/** The mathematical formulation of the statement, supporting e.g. Latex
|
||||
* It takes three forms, depending on the precence of name and description:
|
||||
* - Theorem xyz: description
|
||||
* - Theorem xyz
|
||||
* - Exercises: description
|
||||
*
|
||||
* If `showLeanStatement` is true, it will additionally display the lean code.
|
||||
*/
|
||||
export function ExerciseStatement({ showLeanStatement = false }) {
|
||||
let { t } = useTranslation()
|
||||
const {gameId, worldId, levelId } = useContext(GameIdContext)
|
||||
const levelInfo = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
||||
|
||||
if (!(levelInfo.data?.descrText || levelInfo.data?.descrFormat)) { return <></> }
|
||||
return <>
|
||||
<div className="exercise-statement">
|
||||
{levelInfo.data?.descrText ?
|
||||
<Markdown>
|
||||
{(levelInfo.data?.displayName ? `**${t("Theorem")}** \`${levelInfo.data?.displayName}\`: ` : '') + t(levelInfo.data?.descrText, {ns: gameId})}
|
||||
</Markdown> : levelInfo.data?.displayName &&
|
||||
<Markdown>
|
||||
{`**${t("Theorem")}** \`${levelInfo.data?.displayName}\``}
|
||||
</Markdown>
|
||||
}
|
||||
{levelInfo.data?.descrFormat && showLeanStatement &&
|
||||
<p><code className="lean-code">{levelInfo.data?.descrFormat}</code></p>
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
import * as React from 'react'
|
||||
import { useContext } from 'react'
|
||||
import { useRef, useState, useEffect } from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faArrowRight, faHome, faWandMagicSparkles } from '@fortawesome/free-solid-svg-icons'
|
||||
import { DiagnosticSeverity, PublishDiagnosticsParams, DocumentUri } from 'vscode-languageserver-protocol';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import * as monaco from 'monaco-editor'
|
||||
import { ChatContext, GameIdContext, InputModeContext, MonacoEditorContext, PageContext, PreferencesContext, ProofContext } from '../../state/context';
|
||||
|
||||
import { RpcContext, WithRpcSessions, useRpcSessionAtPos } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions';
|
||||
import { useServerNotificationEffect } from '../../../../node_modules/lean4-infoview/src/infoview/util';
|
||||
|
||||
import '../../css/infoview.css'
|
||||
import { useGetGameInfoQuery } from '../../state/api'
|
||||
import { GameHint } from './Defs'
|
||||
import { MoreHelpButton, filterHints } from '../chat'
|
||||
import { isLastStepWithErrors, lastStepHasErrors, loadGoals } from '../infoview/goals'
|
||||
import { getInteractiveDiagsAt, hasInteractiveErrors } from '../infoview/typewriter'
|
||||
import { Errors } from '../infoview/messages'
|
||||
import { Button, Markdown } from '../utils'
|
||||
import { Command, GoalsTabs } from '../infoview/main'
|
||||
import { CircularProgress } from '@mui/material'
|
||||
|
||||
/** The input field */
|
||||
export function TypewriterInput({disabled}: {disabled?: boolean}) {
|
||||
let { t } = useTranslation()
|
||||
|
||||
/** Reference to the hidden multi-line editor */
|
||||
// const editor = React.useContext(MonacoEditorContext)
|
||||
// const model = editor.getModel()
|
||||
// const uri = model.uri.toString()
|
||||
|
||||
const [oneLineEditor, setOneLineEditor] = useState<monaco.editor.IStandaloneCodeEditor>(null)
|
||||
const [processing, setProcessing] = useState(false)
|
||||
|
||||
// const {typewriterInput, setTypewriterInput} = React.useContext(InputModeContext)
|
||||
|
||||
const inputRef = useRef()
|
||||
|
||||
// // The context storing all information about the current proof
|
||||
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)
|
||||
|
||||
// // Run the command
|
||||
// const runCommand = React.useCallback(() => {
|
||||
// if (processing) {return}
|
||||
|
||||
// // TODO: Desired logic is to only reset this after a new *error-free* command has been entered
|
||||
// setDeletedChat([])
|
||||
|
||||
// const pos = editor.getPosition()
|
||||
// if (typewriterInput) {
|
||||
// setProcessing(true)
|
||||
// editor.executeEdits("typewriter", [{
|
||||
// range: monaco.Selection.fromPositions(
|
||||
// pos,
|
||||
// editor.getModel().getFullModelRange().getEndPosition()
|
||||
// ),
|
||||
// text: typewriterInput.trim() + "\n",
|
||||
// forceMoveMarkers: false
|
||||
// }])
|
||||
// setTypewriterInput('')
|
||||
// // Load proof after executing edits
|
||||
// loadGoals(rpcSess, uri, setProof, setCrashed)
|
||||
// }
|
||||
|
||||
// editor.setPosition(pos)
|
||||
// }, [typewriterInput, editor])
|
||||
|
||||
// useEffect(() => {
|
||||
// if (oneLineEditor && oneLineEditor.getValue() !== typewriterInput) {
|
||||
// oneLineEditor.setValue(typewriterInput)
|
||||
// }
|
||||
// }, [typewriterInput])
|
||||
|
||||
// /* Load proof on start/switching to typewriter */
|
||||
// useEffect(() => {
|
||||
// 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])
|
||||
|
||||
// // React when answer from the server comes back
|
||||
// useServerNotificationEffect('textDocument/publishDiagnostics', (params: PublishDiagnosticsParams) => {
|
||||
// if (params.uri == uri) {
|
||||
// setProcessing(false)
|
||||
|
||||
// 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())
|
||||
// }
|
||||
// } else {
|
||||
// // console.debug(`expected uri: ${uri}, got: ${params.uri}`)
|
||||
// // console.debug(params)
|
||||
// }
|
||||
// // TODO: This is the wrong place apparently. Where do wee need to load them?
|
||||
// // TODO: instead of loading all goals every time, we could only load the last one
|
||||
// // 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,
|
||||
// language: "lean4cmd",
|
||||
// quickSuggestions: false,
|
||||
// lightbulb: {
|
||||
// enabled: true
|
||||
// },
|
||||
// unicodeHighlight: {
|
||||
// ambiguousCharacters: false,
|
||||
// },
|
||||
// automaticLayout: true,
|
||||
// minimap: {
|
||||
// enabled: false
|
||||
// },
|
||||
// lineNumbers: 'off',
|
||||
// tabSize: 2,
|
||||
// glyphMargin: false,
|
||||
// folding: false,
|
||||
// lineDecorationsWidth: 0,
|
||||
// lineNumbersMinChars: 0,
|
||||
// 'semanticHighlighting.enabled': true,
|
||||
// overviewRulerLanes: 0,
|
||||
// hideCursorInOverviewRuler: true,
|
||||
// scrollbar: {
|
||||
// vertical: 'hidden',
|
||||
// horizontalScrollbarSize: 3
|
||||
// },
|
||||
// overviewRulerBorder: false,
|
||||
// theme: 'vs-code-theme-converted',
|
||||
// contextmenu: false
|
||||
// })
|
||||
|
||||
// setOneLineEditor(myEditor)
|
||||
|
||||
// const abbrevRewriter = new AbbreviationRewriter(new AbbreviationProvider(), myEditor.getModel(), myEditor)
|
||||
|
||||
// return () => {abbrevRewriter.dispose(); myEditor.dispose()}
|
||||
// }, [])
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!oneLineEditor) return
|
||||
// // Ensure that our one-line editor can only have a single line
|
||||
// const l = oneLineEditor.getModel().onDidChangeContent((e) => {
|
||||
// const value = oneLineEditor.getValue()
|
||||
// setTypewriterInput(value)
|
||||
// const newValue = value.replace(/[\n\r]/g, '')
|
||||
// if (value != newValue) {
|
||||
// oneLineEditor.setValue(newValue)
|
||||
// }
|
||||
// })
|
||||
// return () => { l.dispose() }
|
||||
// }, [oneLineEditor, setTypewriterInput])
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!oneLineEditor) return
|
||||
// // Run command when pressing enter
|
||||
// const l = oneLineEditor.onKeyUp((ev) => {
|
||||
// if (ev.code === "Enter") {
|
||||
// runCommand()
|
||||
// }
|
||||
// })
|
||||
// return () => { l.dispose() }
|
||||
// }, [oneLineEditor, runCommand])
|
||||
|
||||
// // BUG: Causes `file closed` error
|
||||
// //TODO: Intention is to run once when loading, does that work?
|
||||
// useEffect(() => {
|
||||
// console.debug(`time to update: ${uri} \n ${rpcSess}`)
|
||||
// console.debug(rpcSess)
|
||||
// // console.debug('LOAD ALL GOALS')
|
||||
// // TODO: loadAllGoals()
|
||||
// }, [rpcSess])
|
||||
|
||||
/** Process the entered command */
|
||||
const handleSubmit : React.FormEventHandler<HTMLFormElement> = (ev) => {
|
||||
// ev.preventDefault()
|
||||
// runCommand()
|
||||
}
|
||||
|
||||
// do not display if the proof is completed (with potential warnings still present)
|
||||
return <div className={`typewriter-cmd${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} /> {t("Execute")}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
|
||||
export function TypewriterInterFace() {
|
||||
let { t } = useTranslation()
|
||||
const {gameId, worldId, levelId} = useContext(GameIdContext)
|
||||
const editor = useContext(MonacoEditorContext)
|
||||
const model = editor.getModel()
|
||||
const uri = model.uri.toString()
|
||||
|
||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||
let image: string = gameInfo.data?.worlds.nodes[worldId].image
|
||||
|
||||
|
||||
const [disableInput, setDisableInput] = useState<boolean>(false)
|
||||
const [loadingProgress, setLoadingProgress] = useState<number>(0)
|
||||
const { selectedStep, setSelectedStep, setDeletedChat, showHelp, setShowHelp } = useContext(ChatContext)
|
||||
const {mobile} = useContext(PreferencesContext)
|
||||
const { proof, setProof, crashed, setCrashed, interimDiags } = useContext(ProofContext)
|
||||
const { setTypewriterInput } = useContext(PageContext)
|
||||
|
||||
const proofPanelRef = React.useRef<HTMLDivElement>(null)
|
||||
// const config = useEventResult(ec.events.changedInfoviewConfig) ?? defaultInfoviewConfig;
|
||||
// const curUri = useEventResult(ec.events.changedCursorLocation, loc => loc?.uri);
|
||||
|
||||
const rpcSess = useRpcSessionAtPos({uri: uri, line: 0, character: 0})
|
||||
|
||||
/** Delete all proof lines starting from a given line.
|
||||
* Note that the first line (i.e. deleting everything) is `1`!
|
||||
*/
|
||||
function deleteProof(line: number) {
|
||||
return (ev) => {
|
||||
let deletedChat: Array<GameHint> = []
|
||||
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, ...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 },
|
||||
editor.getModel().getFullModelRange().getEndPosition()
|
||||
),
|
||||
text: '',
|
||||
forceMoveMarkers: false
|
||||
}])
|
||||
setSelectedStep(null)
|
||||
setTypewriterInput(proof?.steps[line].command)
|
||||
// Reload proof on deleting
|
||||
loadGoals(rpcSess, uri, setProof, setCrashed)
|
||||
ev.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelectStep(line: number) {
|
||||
return (ev) => {
|
||||
if (mobile) {return}
|
||||
if (selectedStep == line) {
|
||||
setSelectedStep(null)
|
||||
console.debug(`unselected step`)
|
||||
} else {
|
||||
setSelectedStep(line)
|
||||
console.debug(`step ${line} selected`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll to the end of the proof if it is updated.
|
||||
React.useEffect(() => {
|
||||
if (proof?.steps.length > 1) {
|
||||
proofPanelRef.current?.lastElementChild?.scrollIntoView() //scrollTo(0,0)
|
||||
} else {
|
||||
proofPanelRef.current?.scrollTo(0,0)
|
||||
}
|
||||
// also reenable the commandline when the proof changes
|
||||
|
||||
// BUG: If selecting 2nd goal on a intermediate proofstep and then delete proof to there,
|
||||
// the commandline is not displaying disabled even though it should.
|
||||
setDisableInput(false)
|
||||
}, [proof])
|
||||
|
||||
// Scroll to element if selection changes
|
||||
React.useEffect(() => {
|
||||
if (typeof selectedStep !== 'undefined') {
|
||||
Array.from(proofPanelRef.current?.getElementsByClassName(`step-${selectedStep}`)).map((elem) => {
|
||||
elem.scrollIntoView({ block: "center" })
|
||||
})
|
||||
}
|
||||
}, [selectedStep])
|
||||
|
||||
// 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) => {
|
||||
if (params.kind == "loadConstants") {
|
||||
setLoadingProgress(params.counter/100*50)
|
||||
} else if (params.kind == "finalizeExtensions") {
|
||||
setLoadingProgress(50 + params.counter/150*50)
|
||||
} else {
|
||||
console.error(`Unknown loading kind: ${params.kind}`)
|
||||
}
|
||||
})
|
||||
|
||||
return <div className="typewriter-interface">
|
||||
<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 /> */}
|
||||
{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 key={diag.message} >
|
||||
<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}as React
|
||||
</pre>
|
||||
</div>
|
||||
</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' : '')}>
|
||||
{i > 0 && <>
|
||||
<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>
|
||||
</div>
|
||||
}
|
||||
{mobile &&
|
||||
<Hints key={`hints-${i}`}
|
||||
hints={filteredHints.map(hint => ({hint: hint, step: i}))} />
|
||||
} */}
|
||||
{/* <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 goals={step.goals} 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 && (
|
||||
<div className="message information">
|
||||
{proof?.completed ?
|
||||
<p>Level completed! 🎉</p> :
|
||||
<p>
|
||||
<b>no goals left</b><br />
|
||||
<i>This probably means you solved the level with warnings or Lean encountered a parsing error.</i>
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
}
|
||||
//}
|
||||
)}
|
||||
{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} /> {t("Home")}
|
||||
</Button>
|
||||
:
|
||||
<Button to={`/${gameId}/world/${props.world}/level/${props.level + 1}`}>
|
||||
Next <FontAwesomeIcon icon={faArrowRight} />
|
||||
</Button>
|
||||
} */}
|
||||
</div>
|
||||
}
|
||||
</> : <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 disabled={disableInput || !proof?.steps.length}/> */}
|
||||
<TypewriterInput />
|
||||
</RpcContext.Provider>
|
||||
</div>
|
||||
}
|
||||
@@ -1,17 +1,24 @@
|
||||
import * as React from 'react'
|
||||
import { useRouteError } from "react-router-dom";
|
||||
import '../css/error_page.css'
|
||||
|
||||
/** The fallback error page */
|
||||
export default function ErrorPage() {
|
||||
const error: any = useRouteError();
|
||||
console.error(error);
|
||||
const error: any = useRouteError()
|
||||
console.error(error)
|
||||
|
||||
return (
|
||||
<div id="error-page">
|
||||
<h1>Oops!</h1>
|
||||
<p>Sorry, an unexpected error has occurred.</p>
|
||||
<p>
|
||||
<i>{error.statusText || error.message}</i>
|
||||
</p>
|
||||
<div className="error-message">
|
||||
<h1>Oops!</h1>
|
||||
<p>Something unexpected happened:</p>
|
||||
<p><code>({error.status}) {error.statusText || error.message}<br/>{error.data}</code></p>
|
||||
<p>Please create an issue at the <a href="https://github.com/leanprover-community/lean4game/issues" target="_blank">lean4game repo</a>.</p>
|
||||
<div className="thought-bubble" />
|
||||
<div className="thought-bubble" />
|
||||
<div className="thought-bubble" />
|
||||
<div className="thought-bubble" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as React from 'react'
|
||||
import ReactCountryFlag from 'react-country-flag'
|
||||
import lean4gameConfig from '../config.json'
|
||||
|
||||
/** Displays either a flag or the language-code, depending on the settings.
|
||||
* The argument `iso` is an ISO-language code.
|
||||
*/
|
||||
export const Flag : React.FC<{ iso: string, showTitle?: boolean}> = ({iso, showTitle=false}) => {
|
||||
let lang = lean4gameConfig.newLanguages[iso]
|
||||
if (lean4gameConfig.useFlags && lang) {
|
||||
return <ReactCountryFlag countryCode={lang.flag} title={showTitle ? lang.name : null} />
|
||||
}
|
||||
return <span>{iso}</span>
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import * as React from 'react'
|
||||
import { useContext, useEffect, useRef, useState } from 'react'
|
||||
import Split from 'react-split'
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '../hooks'
|
||||
import { changeTypewriterMode, selectCode, selectSelections, selectTypewriterMode } from '../state/progress'
|
||||
import { useGetGameInfoQuery, useLoadInventoryOverviewQuery, useLoadLevelQuery } from '../state/api'
|
||||
import { ChatContext, GameIdContext, PageContext, PreferencesContext, ProofContext } from '../state/context'
|
||||
import { InventoryPanel } from './inventory'
|
||||
import { WorldTreePanel } from './world_tree'
|
||||
|
||||
import i18next from 'i18next'
|
||||
import { ChatPanel } from './chat'
|
||||
import { NewLevel } from './level'
|
||||
import { GameHint, ProofState } from './editor/Defs'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Diagnostic } from 'vscode-languageserver-types'
|
||||
|
||||
import '../css/game.css'
|
||||
import '../css/welcome.css'
|
||||
import '../css/level.css'
|
||||
|
||||
/** main page of the game showing among others the tree of worlds/levels */
|
||||
function Game() {
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
const { gameId, worldId, levelId } = React.useContext(GameIdContext)
|
||||
|
||||
// Load the namespace of the game
|
||||
i18next.loadNamespaces(gameId)
|
||||
|
||||
const {mobile} = useContext(PreferencesContext)
|
||||
const {isSavePreferences, language, setIsSavePreferences, setLanguage} = React.useContext(PreferencesContext)
|
||||
|
||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||
const levelInfo = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
||||
const inventory = useLoadInventoryOverviewQuery({game: gameId})
|
||||
|
||||
const {page, setPage} = useContext(PageContext)
|
||||
|
||||
const chatRef = useRef<HTMLDivElement>(null)
|
||||
// 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>>([])
|
||||
// A set of row numbers where help is displayed
|
||||
const [showHelp, setShowHelp] = useState<Set<number>>(new Set())
|
||||
// Select and highlight proof steps and corresponding hints
|
||||
// TODO: with the new design, there is no difference between the introduction and
|
||||
// a hint at the beginning of the proof...
|
||||
const [selectedStep, setSelectedStep] = useState<number>(null)
|
||||
|
||||
// The state variables for the `ProofContext`
|
||||
const [proof, setProof] = useState<ProofState>({steps: [], diagnostics: [], completed: false, completedWithWarnings: false})
|
||||
const [interimDiags, setInterimDiags] = useState<Array<Diagnostic>>([])
|
||||
const [isCrashed, setIsCrashed] = useState<Boolean>(false)
|
||||
|
||||
const typewriterMode = useSelector(selectTypewriterMode(gameId))
|
||||
const setTypewriterMode = (newTypewriterMode: boolean) => dispatch(changeTypewriterMode({game: gameId, typewriterMode: newTypewriterMode}))
|
||||
|
||||
const initialCode = useAppSelector(selectCode(gameId, worldId, levelId))
|
||||
const initialSelections = useAppSelector(selectSelections(gameId, worldId, levelId))
|
||||
|
||||
// set the window title
|
||||
useEffect(() => {
|
||||
if (gameInfo.data?.title) {
|
||||
window.document.title = gameInfo.data.title
|
||||
}
|
||||
}, [gameInfo.data?.title])
|
||||
|
||||
// Delete the current proof on changing level
|
||||
useEffect(() => {
|
||||
setProof(null)
|
||||
setSelectedStep(null)
|
||||
setDeletedChat([])
|
||||
setShowHelp(new Set())
|
||||
}, [gameId, worldId, levelId])
|
||||
|
||||
return <ChatContext.Provider value={{selectedStep, setSelectedStep, deletedChat, setDeletedChat, showHelp, setShowHelp, chatRef}}>
|
||||
<ProofContext.Provider value={{proof, setProof, interimDiags, setInterimDiags, crashed: isCrashed, setCrashed: setIsCrashed}}>
|
||||
{ mobile ?
|
||||
<div className="app-content mobile">
|
||||
{<>
|
||||
<ChatPanel visible={worldId ? (levelId == 0 && page == 1) :(page == 0)} />
|
||||
{ worldId ?
|
||||
<NewLevel visible={page == 1} /> :
|
||||
<WorldTreePanel visible={page == 1} />
|
||||
}
|
||||
<InventoryPanel visible={page == 2} />
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
:
|
||||
<Split className="app-content" minSize={0} snapOffset={200} sizes={[25, 50, 25]}>
|
||||
<ChatPanel />
|
||||
<div className="column">
|
||||
{/* Note: apparently without this `div` the split panel bugs out. */}
|
||||
{worldId ?
|
||||
<NewLevel />
|
||||
: <WorldTreePanel /> }
|
||||
</div>
|
||||
<InventoryPanel />
|
||||
</Split>
|
||||
}
|
||||
</ProofContext.Provider>
|
||||
</ChatContext.Provider>
|
||||
|
||||
}
|
||||
|
||||
export default Game
|
||||
function useLevelEditor(codeviewRef: React.MutableRefObject<HTMLDivElement>, initialCode: any, initialSelections: any, onDidChangeContent: any, onDidChangeSelection: any): { editor: any; infoProvider: any; editorConnection: any } {
|
||||
throw new Error('Function not implemented.')
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { GameHint } from "./infoview/rpc_api";
|
||||
import * as React from 'react';
|
||||
import Markdown from './markdown';
|
||||
import { ProofStep } from "./infoview/context";
|
||||
|
||||
export function Hint({hint, step, selected, toggleSelection, lastLevel} : {hint: GameHint, step: number, selected: number, toggleSelection: any, lastLevel?: boolean}) {
|
||||
return <div className={`message information step-${step}` + (step == selected ? ' selected' : '') + (lastLevel ? ' recent' : '')} onClick={toggleSelection}>
|
||||
<Markdown>{hint.text}</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>
|
||||
</div>
|
||||
}
|
||||
|
||||
export function Hints({hints, showHidden, step, selected, toggleSelection, lastLevel} : {hints: GameHint[], showHidden: boolean, step: number, selected: number, toggleSelection: any, lastLevel?: boolean}) {
|
||||
|
||||
const openHints = hints.filter(hint => !hint.hidden)
|
||||
const hiddenHints = hints.filter(hint => hint.hidden)
|
||||
|
||||
// TODO: Should not use index as key.
|
||||
return <>
|
||||
{openHints.map((hint, j) => <Hint key={`hint-${step}-${j}`} hint={hint} step={step} selected={selected} toggleSelection={toggleSelection} lastLevel={lastLevel} />)}
|
||||
{showHidden && hiddenHints.map((hint, j) => <HiddenHint key={`hidden-hint-${step}-${j}`} hint={hint} step={step} selected={selected} toggleSelection={toggleSelection} lastLevel={lastLevel} />)}
|
||||
</>
|
||||
}
|
||||
|
||||
export function DeletedHint({hint} : {hint: GameHint}) {
|
||||
return <div className="message information deleted-hint">
|
||||
<Markdown>{hint.text}</Markdown>
|
||||
</div>
|
||||
}
|
||||
|
||||
export function DeletedHints({hints} : {hints: GameHint[]}) {
|
||||
|
||||
const openHints = hints.filter(hint => !hint.hidden)
|
||||
const hiddenHints = hints.filter(hint => hint.hidden)
|
||||
|
||||
// TODO: Should not use index as key.
|
||||
return <>
|
||||
{openHints.map((hint, i) => <DeletedHint key={`deleted-hint-${i}`} hint={hint} />)}
|
||||
{hiddenHints.map((hint, i) => <DeletedHint key={`deleted-hidden-hint-${i}`} hint={hint}/>)}
|
||||
</>
|
||||
}
|
||||
|
||||
/** Filter hints to not show consequtive identical hints twice.
|
||||
*
|
||||
* This function takes a `ProofStep[]` and extracts the hints in form of an
|
||||
* element of type `GameHint[][]` where it removes hints that are identical to hints
|
||||
* appearing in the previous step. Hidden hints are not filtered.
|
||||
*
|
||||
* This effectively means we prevent consequtive identical hints from being shown.
|
||||
*/
|
||||
export function filterHints(proof: ProofStep[]): GameHint[][] {
|
||||
return proof.map((step, i) => {
|
||||
if (i == 0){
|
||||
return step.hints
|
||||
} else {
|
||||
// TODO: Writing all fields explicitely is somewhat fragile to changes, is there a
|
||||
// good way to shallow-compare objects?
|
||||
return step.hints.filter((hint) => hint.hidden ||
|
||||
(proof[i-1].hints.find((x) => (x.text == hint.text && x.hidden == hint.hidden)) === undefined))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
/**
|
||||
* @fileOverview This file contains the the react contexts used in the project.
|
||||
*/
|
||||
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';
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/** 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
|
||||
*/
|
||||
proof: ProofStep[],
|
||||
setProof: React.Dispatch<React.SetStateAction<Array<ProofStep>>>
|
||||
}>({
|
||||
proof: [],
|
||||
setProof: () => {} // TODO: implement me
|
||||
})
|
||||
|
||||
export interface ProofStateProps {
|
||||
// pos: DocumentPosition;
|
||||
status: InfoStatus;
|
||||
messages: InteractiveDiagnostic[];
|
||||
goals?: InteractiveGoals;
|
||||
termGoal?: InteractiveTermGoal;
|
||||
error?: string;
|
||||
// userWidgets: UserWidgetInstance[];
|
||||
// rpcSess: RpcSessionAtPos;
|
||||
// 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 MobileContext = React.createContext<{
|
||||
mobile : boolean,
|
||||
setMobile: React.Dispatch<React.SetStateAction<Boolean>>,
|
||||
}>({
|
||||
mobile : false,
|
||||
setMobile: () => {},
|
||||
})
|
||||
|
||||
export const WorldLevelIdContext = React.createContext<{
|
||||
worldId : string,
|
||||
levelId: number
|
||||
}>({
|
||||
worldId : null,
|
||||
levelId: 0,
|
||||
})
|
||||
|
||||
/** Context to keep highlight selected proof step and corresponding chat messages. */
|
||||
export const SelectionContext = React.createContext<{
|
||||
selectedStep : number,
|
||||
setSelectedStep: React.Dispatch<React.SetStateAction<number>>
|
||||
}>({
|
||||
selectedStep : undefined,
|
||||
setSelectedStep: () => {}
|
||||
})
|
||||
|
||||
/** Context for deleted Hints that are visible just a bit after they've been deleted */
|
||||
export const DeletedChatContext = React.createContext<{
|
||||
deletedChat : GameHint[],
|
||||
setDeletedChat: React.Dispatch<React.SetStateAction<Array<GameHint>>>
|
||||
showHelp : Set<number>,
|
||||
setShowHelp: React.Dispatch<React.SetStateAction<Set<number>>>
|
||||
}>({
|
||||
deletedChat: undefined,
|
||||
setDeletedChat: () => {},
|
||||
showHelp: undefined,
|
||||
setShowHelp: () => {}
|
||||
})
|
||||
|
||||
export const InputModeContext = React.createContext<{
|
||||
typewriterMode: boolean,
|
||||
setTypewriterMode: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
typewriterInput: string,
|
||||
setTypewriterInput: React.Dispatch<React.SetStateAction<string>>,
|
||||
lockInputMode: boolean,
|
||||
setLockInputMode: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
}>({
|
||||
typewriterMode: true,
|
||||
setTypewriterMode: () => {},
|
||||
typewriterInput: "",
|
||||
setTypewriterInput: () => {},
|
||||
lockInputMode: false,
|
||||
setLockInputMode: () => {},
|
||||
});
|
||||
@@ -4,13 +4,17 @@
|
||||
* Mostly copied from https://github.com/leanprover/vscode-lean4/blob/master/lean4-infoview/src/infoview/goals.tsx
|
||||
*/
|
||||
import * as React from 'react'
|
||||
import { InteractiveHypothesisBundle_nonAnonymousNames, MVarId, TaggedText_stripTags } from '@leanprover/infoview-api'
|
||||
import { EditorContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts';
|
||||
import { Locations, LocationsContext, SelectableLocation } from '../../../../node_modules/lean4-infoview/src/infoview/goalLocation';
|
||||
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 { InteractiveHypothesisBundle_nonAnonymousNames, MVarId, TaggedText_stripTags } from '@leanprover/infoview-api'
|
||||
// import { EditorContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts';
|
||||
// import { Locations, LocationsContext, SelectableLocation } from '../../../../node_modules/lean4-infoview/src/infoview/goalLocation';
|
||||
// import { InteractiveCode } from '../../../../node_modules/lean4-infoview/src/infoview/interactiveCode'
|
||||
// import { WithTooltipOnHover } from '../../../../node_modules/lean4-infoview/src/infoview/tooltips';
|
||||
import { PageContext } from '../../state/context';
|
||||
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 {
|
||||
@@ -68,7 +76,7 @@ function getFilteredHypotheses(hyps: InteractiveHypothesisBundle[], filter: Goal
|
||||
|
||||
interface HypProps {
|
||||
hyp: InteractiveHypothesisBundle
|
||||
mvarId?: MVarId
|
||||
mvarId?: any // MVarId
|
||||
}
|
||||
|
||||
function Hyp({ hyp: h, mvarId }: HypProps) {
|
||||
@@ -125,18 +133,15 @@ interface GoalProps {
|
||||
filter: GoalFilterState
|
||||
showHints?: boolean
|
||||
typewriter: boolean
|
||||
unbundle?: boolean /** unbundle `x y : Nat` into `x : Nat` and `y : Nat` */
|
||||
}
|
||||
|
||||
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
|
||||
const { goal, filter, showHints, typewriter, unbundle } = props
|
||||
let { t } = useTranslation()
|
||||
|
||||
// TODO: Apparently `goal` can be `undefined`
|
||||
if (!goal) {return <></>}
|
||||
@@ -149,8 +154,8 @@ export const Goal = React.memo((props: GoalProps) => {
|
||||
{ ...locs, subexprTemplate: { mvarId: goal.mvarId, loc: { target: '' }}} :
|
||||
undefined,
|
||||
[locs, goal.mvarId])
|
||||
const goalLi = <div key={'goal'}>
|
||||
<div className="goal-title">Goal: </div>
|
||||
const goalLi = <div key={'goal'} className="goal">
|
||||
{/* <div className="goal-title">{t("Goal")}:</div> */}
|
||||
<LocationsContext.Provider value={goalLocs}>
|
||||
<InteractiveCode fmt={goal.type} />
|
||||
</LocationsContext.Provider>
|
||||
@@ -160,27 +165,42 @@ export const Goal = React.memo((props: GoalProps) => {
|
||||
// if (props.goal.isInserted) cn += 'b--inserted '
|
||||
// if (props.goal.isRemoved) cn += 'b--removed '
|
||||
|
||||
// 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)
|
||||
function unbundleHyps (hyps: InteractiveHypothesisBundle[]) : InteractiveHypothesisBundle[] {
|
||||
return hyps.flatMap(hyp => (
|
||||
unbundle ? hyp.names.map(name => {return {...hyp, names: [name]}}) : [hyp]
|
||||
))
|
||||
}
|
||||
|
||||
return <div>
|
||||
// const hints = <Hints hints={goal.hints} key={goal.mvarId} />
|
||||
const objectHyps = unbundleHyps(hyps.filter(hyp => !hyp.isAssumption))
|
||||
const assumptionHyps = unbundleHyps(hyps.filter(hyp => hyp.isAssumption))
|
||||
|
||||
return <>
|
||||
{/* {goal.userName && <div><strong className="goal-case">case </strong>{goal.userName}</div>} */}
|
||||
{filter.reverse && goalLi}
|
||||
<div className="hypotheses">
|
||||
{! 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}
|
||||
</div>
|
||||
{!filter.reverse && <>
|
||||
<div className='goal-sign'>
|
||||
<svg width="100%" height="100%">
|
||||
<line x1="0%" y1="0%" x2="0%" y2="100%" />
|
||||
<line x1="0%" y1="50%" x2="100%" y2="50%" />
|
||||
</svg>
|
||||
</div>
|
||||
{goalLi}
|
||||
</>}
|
||||
{/* {showHints && hints} */}
|
||||
</div>
|
||||
</>
|
||||
})
|
||||
|
||||
export const MainAssumptions = React.memo((props: GoalProps2) => {
|
||||
let { t } = useTranslation()
|
||||
const { goals, filter } = props
|
||||
|
||||
const goal = goals[0]
|
||||
@@ -195,7 +215,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 +225,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 +256,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 +278,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 +293,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 +338,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,11 @@ 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 { MonacoEditorContext, ProofStateProps, InfoStatus, ProofContext } from './context'
|
||||
// import { goalsToString, Goal, MainAssumptions, OtherGoals } from './goals'
|
||||
import { InteractiveTermGoal, InteractiveGoalsWithHints, InteractiveGoals, ProofState } from './rpc_api'
|
||||
import { MonacoEditorContext, ProofStateProps, InfoStatus, ProofContext } from '../../state/context'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { GoalsTabs } from './main'
|
||||
|
||||
// TODO: All about pinning could probably be removed
|
||||
type InfoKind = 'cursor' | 'pin'
|
||||
@@ -83,11 +85,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,43 +117,46 @@ 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}.
|
||||
<a className='link pointer dim' onClick={e => { e.preventDefault(); void triggerUpdate() }}>{' '}Try again.</a>
|
||||
</div>}
|
||||
<AllMessages /> {/* TODO: Move error messages to Chat instead */}
|
||||
<LocationsContext.Provider value={locs}>
|
||||
<div className="goals-section">
|
||||
|
||||
{ goals && goals.goals.length > 0 && <>
|
||||
<MainAssumptions filter={goalFilter} key='mainGoal' goals={goals.goals} />
|
||||
<OtherGoals filter={goalFilter} goals={goals.goals} />
|
||||
<GoalsTabs goals={goals.goals.map(goal => ({goal: goal, hints: []}))} last={false} onClick={() => {}} onGoalChange={() => {}}/>
|
||||
{/* <MainAssumptions filter={goalFilter} key='mainGoal' goals={goals.goals} />
|
||||
<OtherGoals filter={goalFilter} goals={goals.goals} /> */}
|
||||
</>}
|
||||
</div>
|
||||
<div>
|
||||
{/* <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>
|
||||
</div> */}
|
||||
</LocationsContext.Provider>
|
||||
{userWidgets.map(widget =>
|
||||
{/* {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>
|
||||
)}
|
||||
{nothingToShow && (
|
||||
)} */}
|
||||
{/* {nothingToShow && (
|
||||
isPaused ?
|
||||
/* Adding {' '} to manage string literals properly: https://reactjs.org/docs/jsx-in-depth.html#string-literals-1 */
|
||||
/* Adding {' '} to manage string literals properly: https://reactjs.org/docs/jsx-in-depth.html#string-literals-1 * /
|
||||
<span>Updating is paused.{' '}
|
||||
<a className='link pointer dim' onClick={e => { e.preventDefault(); void triggerUpdate() }}>Refresh</a>
|
||||
{' '}or <a className='link pointer dim' onClick={e => { e.preventDefault(); setPaused(false) }}>resume updating</a>
|
||||
{' '}to see information.
|
||||
</span> :
|
||||
<><CircularProgress /><div>Loading goal...</div></>)}
|
||||
<AllMessages />
|
||||
<><CircularProgress /><div>{t("Loading goal…")}</div></>)} */}
|
||||
{/* <LocationsContext.Provider value={locs}>
|
||||
{goals && goals.goals.length > 1 && <div className="goals-section other-goals">
|
||||
<div className="goals-section-title">Weitere Goals</div>
|
||||
@@ -166,6 +172,7 @@ interface InfoDisplayProps {
|
||||
pos: DocumentPosition,
|
||||
status: InfoStatus,
|
||||
messages: InteractiveDiagnostic[],
|
||||
proof?: ProofState,
|
||||
goals?: InteractiveGoals,
|
||||
termGoal?: InteractiveTermGoal,
|
||||
error?: string,
|
||||
@@ -175,7 +182,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 +221,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 +259,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 +297,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 +319,7 @@ function InfoAux(props: InfoProps) {
|
||||
pos,
|
||||
status: 'updating',
|
||||
messages: lspDiagsHere.map(lspDiagToInteractive),
|
||||
proof: undefined,
|
||||
goals: undefined,
|
||||
termGoal: undefined,
|
||||
error: undefined,
|
||||
@@ -318,11 +330,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 +366,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 +403,7 @@ function InfoAux(props: InfoProps) {
|
||||
pos,
|
||||
status: 'updating',
|
||||
messages: [],
|
||||
proof: undefined,
|
||||
goals: undefined,
|
||||
termGoal: undefined,
|
||||
error: undefined,
|
||||
@@ -412,6 +427,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>;
|
||||
}
|
||||
|
||||
@@ -18,33 +18,37 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faDeleteLeft, faHome, faArrowRight, faArrowLeft, faRotateLeft } from '@fortawesome/free-solid-svg-icons'
|
||||
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, useLoadLevelQuery } 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 { ChatContext, PageContext, PreferencesContext, MonacoEditorContext, ProofContext, GameIdContext } from '../../state/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, InteractiveGoalWithHints, InteractiveGoalsWithHints, ProofState } from './rpc_api';
|
||||
import { store } from '../../state/store';
|
||||
import { Hints, 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';
|
||||
import { useContext } from 'react';
|
||||
import { Hints, MoreHelpButton, filterHints } from '../chat';
|
||||
import { Button, Markdown } from '../utils'
|
||||
|
||||
|
||||
/** 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(PageContext)
|
||||
return <>
|
||||
<div className={typewriterMode ? 'hidden' : ''}>
|
||||
<ExerciseStatement data={level} showLeanStatement={true} />
|
||||
<div className={(typewriterMode && !lockEditorMode) ? 'hidden' : ''}>
|
||||
{/* <ExerciseStatement showLeanStatement={true} /> */}
|
||||
<div ref={codeviewRef} className={'codeview'}></div>
|
||||
</div>
|
||||
{ec ?
|
||||
@@ -58,39 +62,38 @@ export function DualEditor({ level, codeviewRef, levelId, worldId, worldSize })
|
||||
/** The part of the two editors that needs the editor connection first */
|
||||
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 {gameId} = React.useContext(GameIdContext)
|
||||
const { typewriterMode, lockEditorMode } = React.useContext(PageContext)
|
||||
|
||||
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?.theorems,
|
||||
...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 +113,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} />
|
||||
@@ -131,17 +134,24 @@ function DualEditorMain({ worldId, levelId, level, worldSize }: { worldId: strin
|
||||
*
|
||||
* If `showLeanStatement` is true, it will additionally display the lean code.
|
||||
*/
|
||||
function ExerciseStatement({ data, showLeanStatement = false }) {
|
||||
if (!(data?.descrText || data?.descrFormat)) { return <></> }
|
||||
export function ExerciseStatement({ showLeanStatement = false }) {
|
||||
let { t } = useTranslation()
|
||||
const {gameId, worldId, levelId } = React.useContext(GameIdContext)
|
||||
const levelInfo = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
||||
|
||||
if (!(levelInfo.data?.descrText || levelInfo.data?.descrFormat)) { return <></> }
|
||||
return <>
|
||||
<div className="exercise-statement">
|
||||
{data?.descrText &&
|
||||
{levelInfo.data?.descrText ?
|
||||
<Markdown>
|
||||
{(data?.displayName ? `**Theorem** \`${data?.displayName}\`: ` : '') + data?.descrText}
|
||||
{(levelInfo.data?.displayName ? `**${t("Theorem")}** \`${levelInfo.data?.displayName}\`: ` : '') + t(levelInfo.data?.descrText, {ns: gameId})}
|
||||
</Markdown> : levelInfo.data?.displayName &&
|
||||
<Markdown>
|
||||
{`**${t("Theorem")}** \`${levelInfo.data?.displayName}\``}
|
||||
</Markdown>
|
||||
}
|
||||
{data?.descrFormat && showLeanStatement &&
|
||||
<p><code className="lean-code">{data?.descrFormat}</code></p>
|
||||
{levelInfo.data?.descrFormat && showLeanStatement &&
|
||||
<p><code className="lean-code">{levelInfo.data?.descrFormat}</code></p>
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
@@ -150,12 +160,23 @@ 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 {gameId, worldId, levelId} = React.useContext(GameIdContext)
|
||||
|
||||
const completed = useAppSelector(selectCompleted(gameId, props.world, props.level))
|
||||
const { proof, setProof } = React.useContext(ProofContext)
|
||||
const {selectedStep, setSelectedStep, setDeletedChat, showHelp, setShowHelp} = React.useContext(ChatContext)
|
||||
|
||||
function toggleSelection(line: number) {
|
||||
return (ev) => {
|
||||
console.debug('toggled selection')
|
||||
if (selectedStep == line) {
|
||||
setSelectedStep(null)
|
||||
} 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 }) {
|
||||
export 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) => {
|
||||
@@ -257,7 +311,7 @@ function Command({ command, deleteProof }: { command: string, deleteProof: any }
|
||||
// message = diag.message
|
||||
// }
|
||||
|
||||
// const { typewriterMode } = React.useContext(InputModeContext)
|
||||
// const { typewriterMode } = React.useContext(PageContext)
|
||||
|
||||
// return (
|
||||
// // <details open>
|
||||
@@ -285,22 +339,26 @@ 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 }) {
|
||||
|
||||
/** The tabs of goals that lean has after the command of this step has been processed */
|
||||
export function GoalsTabs({ goals, last, onClick, onGoalChange=(n)=>{}}: { goals: InteractiveGoalWithHints[], last : boolean, onClick? : any, onGoalChange?: (n?: number) => void }) {
|
||||
let { t } = useTranslation()
|
||||
const [selectedGoal, setSelectedGoal] = React.useState<number>(0)
|
||||
|
||||
if (goals.length == 0) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return <div className="goal-tabs" onClick={onClick}>
|
||||
<div className={`tab-bar ${last ? 'current' : ''}`}>
|
||||
{proofStep.goals.map((goal, i) => (
|
||||
{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={goals[selectedGoal]?.goal} unbundle={false} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -308,7 +366,7 @@ function GoalsTabs({ proofStep, last, onClick, onGoalChange=(n)=>{}}: { proofSte
|
||||
// Splitting up Typewriter into two parts is a HACK
|
||||
export function TypewriterInterfaceWrapper(props: { world: string, level: number, data: LevelInfo, worldSize: number }) {
|
||||
const ec = React.useContext(EditorContext)
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const {gameId} = React.useContext(GameIdContext)
|
||||
|
||||
useClientNotificationEffect(
|
||||
'textDocument/didClose',
|
||||
@@ -340,22 +398,25 @@ 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 {gameId,worldId, levelId} = React.useContext(GameIdContext)
|
||||
const editor = React.useContext(MonacoEditorContext)
|
||||
const model = editor.getModel()
|
||||
const uri = model.uri.toString()
|
||||
|
||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||
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 { setTypewriterInput } = React.useContext(InputModeContext)
|
||||
const { selectedStep, setSelectedStep } = React.useContext(SelectionContext)
|
||||
const { selectedStep, setSelectedStep, setDeletedChat, showHelp, setShowHelp } = React.useContext(ChatContext)
|
||||
const {mobile} = React.useContext(PreferencesContext)
|
||||
const { proof, setProof, crashed, setCrashed, interimDiags } = React.useContext(ProofContext)
|
||||
const { setTypewriterInput } = React.useContext(PageContext)
|
||||
|
||||
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 +428,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 },
|
||||
@@ -381,8 +447,10 @@ export function TypewriterInterface({props}) {
|
||||
text: '',
|
||||
forceMoveMarkers: false
|
||||
}])
|
||||
setSelectedStep(undefined)
|
||||
setTypewriterInput(proof[line].command)
|
||||
setSelectedStep(null)
|
||||
setTypewriterInput(proof?.steps[line].command)
|
||||
// Reload proof on deleting
|
||||
loadGoals(rpcSess, uri, setProof, setCrashed)
|
||||
ev.stopPropagation()
|
||||
}
|
||||
}
|
||||
@@ -391,7 +459,7 @@ export function TypewriterInterface({props}) {
|
||||
return (ev) => {
|
||||
if (mobile) {return}
|
||||
if (selectedStep == line) {
|
||||
setSelectedStep(undefined)
|
||||
setSelectedStep(null)
|
||||
console.debug(`unselected step`)
|
||||
} else {
|
||||
setSelectedStep(line)
|
||||
@@ -402,7 +470,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 +491,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,45 +508,78 @@ export function TypewriterInterface({props}) {
|
||||
return <div className="typewriter-interface">
|
||||
<RpcContext.Provider value={rpcSess}>
|
||||
<div className="content">
|
||||
<div className="tmp-pusher">
|
||||
</div>
|
||||
{/* <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} />
|
||||
{/* <ExerciseStatement /> */}
|
||||
{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 key={diag.message} >
|
||||
<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} />
|
||||
{i > 0 && <>
|
||||
<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>
|
||||
</div>
|
||||
}
|
||||
{mobile && <>
|
||||
{mobile &&
|
||||
<Hints key={`hints-${i}`}
|
||||
hints={step.hints} showHidden={showHelp.has(i)} step={i}
|
||||
selected={selectedStep} toggleSelection={toggleSelectStep(i)}/>
|
||||
{i == proof.length - 1 && hasHiddenHints(proof.length - 1) && !showHelp.has(k - withErr) &&
|
||||
<Button className="btn btn-help" to="" onClick={activateHiddenHints}>
|
||||
Show more help!
|
||||
</Button>
|
||||
}
|
||||
</>
|
||||
hints={filteredHints.map(hint => ({hint: hint, step: 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) => {}}/>
|
||||
{/* <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 goals={step.goals} 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 />
|
||||
@@ -516,15 +587,21 @@ export function TypewriterInterface({props}) {
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
{mobile && completed &&
|
||||
<div className="button-row">
|
||||
//}
|
||||
)}
|
||||
{proof?.diagnostics.length > 0 &&
|
||||
<div key={`proof-step-remaining`} className="step step-remaining">
|
||||
<Errors errors={proof?.diagnostics} typewriterMode={true} />
|
||||
</div>
|
||||
}
|
||||
{mobile && proof?.completed &&
|
||||
<div className="button-row mobile">
|
||||
{props.level >= props.worldSize ?
|
||||
<Button to={`/${gameId}`}>
|
||||
<FontAwesomeIcon icon={faHome} /> Leave World
|
||||
<FontAwesomeIcon icon={faHome} /> {t("Home")}
|
||||
</Button>
|
||||
:
|
||||
<Button to={`/${gameId}/world/${props.world}/level/${props.level + 1}`}>
|
||||
@@ -533,11 +610,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>
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ import { Details } from '../../../../node_modules/lean4-infoview/src/infoview/co
|
||||
import { InteractiveMessage } from '../../../../node_modules/lean4-infoview/src/infoview/traceExplorer'
|
||||
import { RpcContext, useRpcSessionAtPos } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions'
|
||||
|
||||
import { InputModeContext } from './context'
|
||||
import { PageContext } from '../../state/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(PageContext)
|
||||
|
||||
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}/>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* @fileOverview Defines the interface for the communication with the server.
|
||||
*
|
||||
* 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
hints: GameHint[];
|
||||
}
|
||||
|
||||
export interface InteractiveGoals {
|
||||
goals: InteractiveGoal[];
|
||||
}
|
||||
@@ -3,23 +3,31 @@ import { useRef, useState, useEffect } from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
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 { Registry } from 'monaco-textmate' // peer dependency
|
||||
// import { wireTmGrammars } from 'monaco-editor-textmate'
|
||||
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';
|
||||
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 { AbbreviationRewriter } from 'lean4web/client/src/editor/abbreviation/rewriter/AbbreviationRewriter';
|
||||
// import { AbbreviationProvider } from 'lean4web/client/src/editor/abbreviation/AbbreviationProvider';
|
||||
// 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, 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 { ChatContext, PageContext, MonacoEditorContext, ProofContext, GameIdContext } from '../../state/context'
|
||||
import { goalsToString, lastStepHasErrors, loadGoals } from './goals'
|
||||
import { GameHint, ProofState } from './rpc_api'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { InputAbbreviationRewriter } from '@leanprover/unicode-input-component'
|
||||
import ContentEditable from 'react-contenteditable'
|
||||
|
||||
export interface GameDiagnosticsParams {
|
||||
uri: DocumentUri;
|
||||
diagnostics: Diagnostic[];
|
||||
}
|
||||
|
||||
/* We register a new language `leancmd` that looks like lean4, but does not use the lsp server. */
|
||||
|
||||
@@ -29,6 +37,12 @@ monaco.languages.register({
|
||||
extensions: ['.leancmd']
|
||||
})
|
||||
|
||||
// register Monaco languages // TODO: JE. I dont understand why I suddenly had to add this when it worked without before.
|
||||
monaco.languages.register({
|
||||
id: 'lean4',
|
||||
extensions: ['.lean']
|
||||
})
|
||||
|
||||
// map of monaco "language id's" to TextMate scopeNames
|
||||
const grammars = new Map()
|
||||
grammars.set('lean4', 'source.lean')
|
||||
@@ -64,124 +78,31 @@ 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)
|
||||
const model = editor.getModel()
|
||||
const uri = model.uri.toString()
|
||||
const model = editor?.getModel()
|
||||
const uri = model?.uri.toString()
|
||||
|
||||
const [oneLineEditor, setOneLineEditor] = useState<monaco.editor.IStandaloneCodeEditor>(null)
|
||||
const [processing, setProcessing] = useState(false)
|
||||
|
||||
const {typewriterInput, setTypewriterInput} = React.useContext(InputModeContext)
|
||||
const {typewriterInput, setTypewriterInput} = React.useContext(PageContext)
|
||||
|
||||
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)
|
||||
|
||||
const {gameId, worldId, levelId} = React.useContext(GameIdContext)
|
||||
|
||||
// state to store the last batch of deleted messages
|
||||
const {setDeletedChat} = React.useContext(DeletedChatContext)
|
||||
const {setDeletedChat} = React.useContext(ChatContext)
|
||||
|
||||
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}
|
||||
@@ -189,21 +110,23 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
|
||||
// TODO: Desired logic is to only reset this after a new *error-free* command has been entered
|
||||
setDeletedChat([])
|
||||
|
||||
const pos = editor.getPosition()
|
||||
const pos = editor?.getPosition()
|
||||
if (typewriterInput) {
|
||||
setProcessing(true)
|
||||
editor.executeEdits("typewriter", [{
|
||||
editor?.executeEdits("typewriter", [{
|
||||
range: monaco.Selection.fromPositions(
|
||||
pos,
|
||||
editor.getModel().getFullModelRange().getEndPosition()
|
||||
editor?.getModel()?.getFullModelRange()?.getEndPosition()
|
||||
),
|
||||
text: typewriterInput.trim() + "\n",
|
||||
forceMoveMarkers: false
|
||||
}])
|
||||
setTypewriterInput('')
|
||||
// Load proof after executing edits
|
||||
loadGoals(rpcSess, uri, setProof, setCrashed)
|
||||
}
|
||||
|
||||
editor.setPosition(pos)
|
||||
editor?.setPosition(pos)
|
||||
}, [typewriterInput, editor])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -212,9 +135,16 @@ 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)
|
||||
setProof(null)
|
||||
loadGoals(rpcSess, uri, setProof, setCrashed)
|
||||
}, [gameId, worldId, levelId])
|
||||
|
||||
/** 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,10 +152,17 @@ 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())
|
||||
editor?.setPosition(editor?.getModel()?.getFullModelRange()?.getEndPosition())
|
||||
}
|
||||
} else {
|
||||
// console.debug(`expected uri: ${uri}, got: ${params.uri}`)
|
||||
@@ -236,6 +173,14 @@ 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,
|
||||
@@ -266,6 +211,7 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
|
||||
},
|
||||
overviewRulerBorder: false,
|
||||
theme: 'vs-code-theme-converted',
|
||||
fontFamily: "JuliaMono",
|
||||
contextmenu: false
|
||||
})
|
||||
|
||||
@@ -294,7 +240,7 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
|
||||
if (!oneLineEditor) return
|
||||
// Run command when pressing enter
|
||||
const l = oneLineEditor.onKeyUp((ev) => {
|
||||
if (ev.code === "Enter") {
|
||||
if (ev.code === "Enter" || ev.code === "NumpadEnter") {
|
||||
runCommand()
|
||||
}
|
||||
})
|
||||
@@ -306,7 +252,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 +262,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 +291,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,146 +1,274 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import * as React from 'react'
|
||||
import { useState, useEffect, createContext, useContext } from 'react';
|
||||
import '../css/inventory.css'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faLock, faBan } from '@fortawesome/free-solid-svg-icons'
|
||||
import { GameIdContext } from '../app';
|
||||
import Markdown from './markdown';
|
||||
import { useLoadDocQuery, InventoryTile, LevelInfo, InventoryOverview, useLoadInventoryOverviewQuery } from '../state/api';
|
||||
import { selectDifficulty, selectInventory } from '../state/progress';
|
||||
import { faLock, faBan, faCheck, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faClipboard } from '@fortawesome/free-regular-svg-icons'
|
||||
import { useLoadDocQuery, InventoryTile, useLoadInventoryOverviewQuery, useLoadLevelQuery } from '../state/api';
|
||||
import { changedInventory, selectDifficulty, selectInventory } from '../state/progress';
|
||||
import { store } from '../state/store';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { t } from 'i18next';
|
||||
import { NavButton } from './navigation';
|
||||
import { LoadingIcon, Markdown } from './utils';
|
||||
import { GameIdContext } from '../state/context';
|
||||
import { useAppDispatch } from '../hooks';
|
||||
|
||||
export function Inventory({levelInfo, openDoc, enableAll=false} :
|
||||
{
|
||||
levelInfo: LevelInfo|InventoryOverview,
|
||||
openDoc: (props: {name: string, type: string}) => void,
|
||||
enableAll?: boolean,
|
||||
}) {
|
||||
|
||||
return (
|
||||
<div className="inventory">
|
||||
{/* TODO: Click on Tactic: show info
|
||||
TODO: click on paste icon -> paste into command line */}
|
||||
<h2>Tactics</h2>
|
||||
{levelInfo?.tactics &&
|
||||
<InventoryList items={levelInfo?.tactics} docType="Tactic" openDoc={openDoc} enableAll={enableAll}/>
|
||||
}
|
||||
<h2>Definitions</h2>
|
||||
{levelInfo?.definitions &&
|
||||
<InventoryList items={levelInfo?.definitions} docType="Definition" openDoc={openDoc} enableAll={enableAll}/>
|
||||
}
|
||||
<h2>Theorems</h2>
|
||||
{levelInfo?.lemmas &&
|
||||
<InventoryList items={levelInfo?.lemmas} docType="Lemma" openDoc={openDoc} defaultTab={levelInfo?.lemmaTab} level={levelInfo} enableAll={enableAll}/>
|
||||
}
|
||||
/** Context which manages the inventory */
|
||||
const InventoryContext = createContext<{
|
||||
theoremTab: string,
|
||||
setTheoremTab: React.Dispatch<React.SetStateAction<string>>,
|
||||
categoryTab: "tactic"|"theorem"|"definition",
|
||||
setCategoryTab: React.Dispatch<React.SetStateAction<"tactic"|"theorem"|"definition">>,
|
||||
docTile: InventoryTile,
|
||||
setDocTile: React.Dispatch<React.SetStateAction<InventoryTile>>
|
||||
}>({
|
||||
theoremTab: null,
|
||||
setTheoremTab: () => {},
|
||||
categoryTab: "tactic",
|
||||
setCategoryTab: () => {},
|
||||
docTile: null,
|
||||
setDocTile: () => {}
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
function InventoryItem({item, name, displayName, locked, disabled, newly, showDoc } :
|
||||
{ item: InventoryTile,
|
||||
name: any,
|
||||
displayName: any,
|
||||
locked: any,
|
||||
disabled: any,
|
||||
newly: any,
|
||||
showDoc: any
|
||||
}) {
|
||||
const icon = locked ? <FontAwesomeIcon icon={faLock} /> :
|
||||
disabled ? <FontAwesomeIcon icon={faBan} /> : <></>
|
||||
const className = locked ? "locked" : disabled ? "disabled" : newly ? "new" : ""
|
||||
// Note: This is somewhat a hack as the statement of theorems comes currently in the form
|
||||
// `Namespace.statement_name (x y : Nat) : some type`
|
||||
const title = locked ? t("Not unlocked yet") :
|
||||
disabled ? t("Not available in this level") : (item.altTitle ? item.altTitle.substring(item.altTitle.indexOf(' ') + 1) : '')
|
||||
|
||||
const { gameId, worldId, levelId } = React.useContext(GameIdContext)
|
||||
const difficulty = useSelector(selectDifficulty(gameId))
|
||||
|
||||
// local state to show checkmark after pressing the copy button
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleClick = () => {
|
||||
// if ((difficulty == 0) || !locked) {
|
||||
showDoc()
|
||||
// }
|
||||
}
|
||||
|
||||
const copyItemName = (ev) => {
|
||||
navigator.clipboard.writeText(displayName)
|
||||
setCopied(true)
|
||||
setInterval(() => {
|
||||
setCopied(false)
|
||||
}, 3000);
|
||||
ev.stopPropagation()
|
||||
}
|
||||
|
||||
return <div className={`item ${className}` +
|
||||
`${(difficulty == 0) ? ' enabled' : ''}` +
|
||||
`${item.world == worldId && item.level == levelId - 1 ? ' recent' : ''}` +
|
||||
`${item.world == worldId && item.level < levelId ? ' current-world' : ''}` } onClick={handleClick} title={title}>
|
||||
{icon} {displayName}
|
||||
<div className="copy-button" onClick={copyItemName}>
|
||||
{copied ? <FontAwesomeIcon icon={faCheck} /> : <FontAwesomeIcon icon={faClipboard} />}
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
}
|
||||
|
||||
function InventoryList({items, docType, openDoc, defaultTab=null, level=undefined, enableAll=false} :
|
||||
|
||||
function InventoryList({ items, tab=null, setTab=()=>{} } :
|
||||
{
|
||||
items: InventoryTile[],
|
||||
docType: string,
|
||||
openDoc(props: {name: string, type: string}): void,
|
||||
defaultTab? : string,
|
||||
level? : LevelInfo|InventoryOverview,
|
||||
enableAll?: boolean,
|
||||
tab?: string,
|
||||
setTab?: React.Dispatch<React.SetStateAction<string>>
|
||||
}) {
|
||||
// TODO: `level` is only used in the `useEffect` below to check if a new level has
|
||||
// been loaded. Is there a better way to observe this?
|
||||
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const { gameId, worldId, levelId } = React.useContext(GameIdContext)
|
||||
const { setDocTile, categoryTab, setCategoryTab } = useContext(InventoryContext)
|
||||
|
||||
const difficulty = useSelector(selectDifficulty(gameId))
|
||||
|
||||
const categorySet = new Set<string>()
|
||||
for (let item of items) {
|
||||
categorySet.add(item.category)
|
||||
}
|
||||
const categories = Array.from(categorySet).sort()
|
||||
const inventory: string[] = selectInventory(gameId)(store.getState())
|
||||
|
||||
const [tab, setTab] = useState(defaultTab)
|
||||
const [categories, setCategories] = useState<Array<string>>([])
|
||||
const [modifiedItems, setModifiedItems] = useState<Array<InventoryTile>>([])
|
||||
const [currentWorldItems, setCurrentWorldItems] = useState<Array<InventoryTile>>([])
|
||||
|
||||
// Add inventory items from local store as unlocked.
|
||||
// Items are unlocked if they are in the local store, or if the server says they should be
|
||||
// given the dependency graph. (OR-connection) (TODO: maybe add different logic for different
|
||||
// modi)
|
||||
let inv: string[] = selectInventory(gameId)(store.getState())
|
||||
let modifiedItems : InventoryTile[] = items.map(tile => inv.includes(tile.name) ? {...tile, locked: false} : tile)
|
||||
|
||||
useEffect(() => {
|
||||
// If the level specifies `LemmaTab "Nat"`, we switch to this tab on loading.
|
||||
// `defaultTab` is `null` or `undefined` otherwise, in which case we don't want to switch.
|
||||
if (defaultTab) {
|
||||
setTab(defaultTab)
|
||||
}}, [level])
|
||||
const categorySet = new Set<string>()
|
||||
|
||||
if (!items) {return}
|
||||
for (let item of items) {
|
||||
categorySet.add(item.category)
|
||||
}
|
||||
setCategories(Array.from(categorySet).sort())
|
||||
|
||||
// Add inventory items from local store as unlocked.
|
||||
// Items are unlocked if they are in the local store, or if the server says they should be
|
||||
// given the dependency graph. (OR-connection) (TODO: maybe add different logic for different
|
||||
// modi)
|
||||
let _modifiedItems : InventoryTile[] = items?.map(tile => inventory.includes(tile.name) ? {...tile, locked: false} : tile)
|
||||
setModifiedItems(_modifiedItems)
|
||||
// Item(s) proved in the preceeding level
|
||||
setCurrentWorldItems(_modifiedItems.filter(x => x.world == worldId && x.level < levelId))
|
||||
// setRecentItems(_modifiedItems.filter(x => x.world == worldId && x.level == levelId - 1))
|
||||
|
||||
}, []) // TODO: had `items, inventory`
|
||||
|
||||
return <>
|
||||
{categories.length > 1 &&
|
||||
{ categories.length > 1 &&
|
||||
<div className="tab-bar">
|
||||
{categories.map((cat) =>
|
||||
<div key={`category-${cat}`} className={`tab ${cat == (tab ?? categories[0]) ? "active": ""}`}
|
||||
onClick={() => { setTab(cat) }}>{cat}</div>)}
|
||||
{categories.map((cat) => {
|
||||
let hasNew = modifiedItems.filter(item => item.new && (cat == item.category)).length > 0
|
||||
return <div key={`category-${cat}`} className={`tab${cat == (tab ?? categories[0]) ? " active": ""}${hasNew ? ' new': ''}${currentWorldItems.map(x => x.category).includes(cat) ? ' recent': ''}`}
|
||||
onClick={() => { setTab(cat) }}>{cat}</div>})}
|
||||
</div>}
|
||||
<div className="inventory-list">
|
||||
{[...modifiedItems].sort(
|
||||
// For lemas, sort entries `available > disabled > locked`
|
||||
// otherwise alphabetically
|
||||
(x, y) => +(docType == "Lemma") * (+x.locked - +y.locked || +x.disabled - +y.disabled) || x.displayName.localeCompare(y.displayName)
|
||||
// alternative approach:
|
||||
// // For theorems, sort entries `available > disabled > locked`
|
||||
// // otherwise alphabetically
|
||||
// (x, y) => +(categoryTab == "theorem") * (+x.locked - +y.locked || +x.disabled - +y.disabled) || x.displayName.localeCompare(y.displayName)
|
||||
|
||||
// sort alphabetically
|
||||
(x, y) => x.displayName.localeCompare(y.displayName)
|
||||
).filter(item => !item.hidden && ((tab ?? categories[0]) == item.category)).map((item, i) => {
|
||||
return <InventoryItem key={`${item.category}-${item.name}`}
|
||||
showDoc={() => {openDoc({name: item.name, type: docType})}}
|
||||
item={item}
|
||||
showDoc={() => {setDocTile(item)}}
|
||||
name={item.name} displayName={item.displayName} locked={difficulty > 0 ? item.locked : false}
|
||||
disabled={item.disabled} newly={item.new} enableAll={enableAll}/>
|
||||
disabled={item.disabled}
|
||||
newly={item.new} />
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
function InventoryItem({name, displayName, locked, disabled, newly, showDoc, enableAll=false}) {
|
||||
const icon = locked ? <FontAwesomeIcon icon={faLock} /> :
|
||||
disabled ? <FontAwesomeIcon icon={faBan} /> : ""
|
||||
const className = locked ? "locked" : disabled ? "disabled" : newly ? "new" : ""
|
||||
const title = locked ? "Not unlocked yet" :
|
||||
disabled ? "Not available in this level" : ""
|
||||
|
||||
const handleClick = () => {
|
||||
if (enableAll || !locked) {
|
||||
showDoc()
|
||||
}
|
||||
|
||||
/** The `Inventory` shows all items present in the game sorted by item type. */
|
||||
export function Inventory () {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { gameId, worldId, levelId } = React.useContext(GameIdContext)
|
||||
const levelInfo = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
||||
|
||||
let { theoremTab, setTheoremTab, categoryTab, setCategoryTab } = useContext(InventoryContext)
|
||||
|
||||
/** Helper function to find if a list of tiles comprises any new elements. */
|
||||
function containsNew(tiles: InventoryTile[]) {
|
||||
return tiles?.filter(item => item.new).length > 0
|
||||
}
|
||||
|
||||
return <div className={`item ${className}${enableAll ? ' enabled' : ''}`} onClick={handleClick} title={title}>{icon} {displayName}</div>
|
||||
return (
|
||||
<div className="inventory">
|
||||
{ levelInfo.data ? <>
|
||||
<div className="tab-bar major">
|
||||
<div className={`tab${(categoryTab == "theorem") ? " active": ""}${containsNew(levelInfo.data?.theorems) ? " new" : ""}`} onClick={() => { setCategoryTab("theorem") }}>{t("Theorems")}</div>
|
||||
<div className={`tab${(categoryTab == "tactic") ? " active": ""}${containsNew(levelInfo.data?.tactics) ? " new" : ""}`} onClick={() => { setCategoryTab("tactic") }}>{t("Tactics")}</div>
|
||||
<div className={`tab${(categoryTab == "definition") ? " active": ""}${containsNew(levelInfo.data?.definitions) ? " new" : ""}`} onClick={() => { setCategoryTab("definition") }}>{t("Definitions")}</div>
|
||||
</div>
|
||||
{ (categoryTab == "theorem") &&
|
||||
<InventoryList items={levelInfo.data?.theorems} tab={theoremTab} setTab={setTheoremTab} />
|
||||
}
|
||||
{ (categoryTab == "tactic") &&
|
||||
<InventoryList items={levelInfo.data?.tactics} />
|
||||
}
|
||||
{ (categoryTab == "definition") &&
|
||||
<InventoryList items={levelInfo.data?.definitions} />
|
||||
}
|
||||
</> : <LoadingIcon /> }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function Documentation({name, type, handleClose}) {
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const doc = useLoadDocQuery({game: gameId, type: type, name: name})
|
||||
/** The `documentation` */
|
||||
export function Documentation() {
|
||||
const dispatch = useAppDispatch()
|
||||
const { gameId } = useContext(GameIdContext)
|
||||
const difficulty = useSelector(selectDifficulty(gameId))
|
||||
|
||||
// const docEntry = useLoadDocQuery({game: gameId, type: type, name: name})
|
||||
let { docTile, setDocTile } = useContext(InventoryContext)
|
||||
|
||||
const docEntry = useLoadDocQuery({game: gameId, name: docTile.name})
|
||||
let inv: string[] = selectInventory(gameId)(store.getState())
|
||||
|
||||
// Set `inventoryDoc` to `null` to close the doc
|
||||
function closeInventoryDoc() { setDocTile(null) }
|
||||
|
||||
return <div className="documentation">
|
||||
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
||||
<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>
|
||||
<NavButton
|
||||
icon={faXmark}
|
||||
onClick={closeInventoryDoc}
|
||||
inverted={true} />
|
||||
{ difficulty == 1 && docTile.locked &&
|
||||
<NavButton
|
||||
icon={faLock}
|
||||
title={t("Unlock this item!")}
|
||||
onClick={() => {
|
||||
console.log(`Adding '${docTile.name}' to the inventory.`)
|
||||
dispatch(changedInventory({ game: gameId, inventory: [...inv, docTile.name] }))
|
||||
closeInventoryDoc() // note: closing seems better than keeping it open without the lock disappearing
|
||||
}}
|
||||
className="lock"
|
||||
inverted={true} />
|
||||
}
|
||||
<h1 className="doc">{docTile.displayName}</h1>
|
||||
<p><code>{docEntry.data?.statement}</code></p>
|
||||
<Markdown>{t(docEntry.data?.content, {ns: gameId})}</Markdown>
|
||||
{/* TODO: The condition below should be updated so that the section
|
||||
is displayed whenever it's non-empty. */}
|
||||
{docTile.proven && <>
|
||||
<h2>Further details</h2>
|
||||
<ul>
|
||||
{docTile.proven && <li>Proven in: <a href={`#/${gameId}/world/${docTile.world}/level/${docTile.level}`}>{docTile.world} level {docTile.level}</a></li>}
|
||||
</ul>
|
||||
</>
|
||||
}
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
/** The panel (on the welcome page) showing the user's inventory with tactics, definitions, and lemmas */
|
||||
export function InventoryPanel({levelInfo, visible = true}) {
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
/** The panel showing the user's inventory with tactics, definitions, and theorems */
|
||||
export function InventoryPanel({visible = true}) {
|
||||
const {gameId, worldId, levelId} = React.useContext(GameIdContext)
|
||||
const levelInfo = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
||||
const inventory = useLoadInventoryOverviewQuery({game: gameId})
|
||||
|
||||
const [theoremTab, setTheoremTab] = useState<string>(null)
|
||||
const [categoryTab, setCategoryTab] = useState<"tactic"|"theorem"|"definition">('tactic')
|
||||
// The inventory is overlayed by the doc entry of a clicked item
|
||||
const [inventoryDoc, setInventoryDoc] = useState<{name: string, type: string}>(null)
|
||||
// Set `inventoryDoc` to `null` to close the doc
|
||||
function closeInventoryDoc() {setInventoryDoc(null)}
|
||||
const [docTile, setDocTile] = useState<InventoryTile>(null)
|
||||
|
||||
useEffect(() => {
|
||||
// If the level specifies `TheoremTab "Nat"`, we switch to this tab on loading.
|
||||
// `defaultTab` is `null` or `undefined` otherwise, in which case we don't want to switch.
|
||||
if (levelInfo?.data?.theoremTab) {
|
||||
setTheoremTab(levelInfo?.data?.theoremTab)
|
||||
}}, [levelInfo])
|
||||
|
||||
return <div className={`column inventory-panel ${visible ? '' : 'hidden'}`}>
|
||||
{inventoryDoc ?
|
||||
<Documentation name={inventoryDoc.name} type={inventoryDoc.type} handleClose={closeInventoryDoc}/>
|
||||
<InventoryContext.Provider value={{theoremTab, setTheoremTab, categoryTab, setCategoryTab, docTile, setDocTile }}>
|
||||
{docTile ?
|
||||
<Documentation />
|
||||
:
|
||||
<Inventory levelInfo={levelInfo} openDoc={setInventoryDoc} enableAll={true}/>
|
||||
<Inventory />
|
||||
}
|
||||
</InventoryContext.Provider>
|
||||
</div>
|
||||
}
|
||||
|
||||
// HERE: next up: locked items should not be disabled!
|
||||
|
||||
@@ -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';
|
||||
@@ -9,33 +10,19 @@ import '@fontsource/roboto/700.css';
|
||||
import '../css/landing_page.css'
|
||||
import bgImage from '../assets/bg.jpg'
|
||||
|
||||
import Markdown from './markdown';
|
||||
import {PrivacyPolicyPopup} from './popup/privacy_policy'
|
||||
import { GameTile, useGetGameInfoQuery } from '../state/api'
|
||||
import path from 'path';
|
||||
|
||||
const flag = {
|
||||
'Dutch': '🇳🇱',
|
||||
'English': '🇬🇧',
|
||||
'French': '🇫🇷',
|
||||
'German': '🇩🇪',
|
||||
'Italian': '🇮🇹',
|
||||
'Spanish': '🇪🇸',
|
||||
}
|
||||
|
||||
function GithubIcon({url='https://github.com'}) {
|
||||
|
||||
return <div className="github-link">
|
||||
<a title="view the Lean game server on Github" href={url}>
|
||||
<svg height="24" aria-hidden="true" viewBox="0 0 16 16" version="1.1" width="24" className="">
|
||||
<path fill="#fff" d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
import ReactCountryFlag from 'react-country-flag';
|
||||
import lean4gameConfig from '../config.json'
|
||||
import i18next from 'i18next';
|
||||
import { useContext } from 'react';
|
||||
import { PopupContext } from './popup/popup';
|
||||
import { Flag } from './flag';
|
||||
import { Markdown } from './utils';
|
||||
|
||||
function Tile({gameId, data}: {gameId: string, data: GameTile|undefined}) {
|
||||
|
||||
let { t } = useTranslation()
|
||||
let navigate = useNavigate();
|
||||
const routeChange = () =>{
|
||||
navigate(gameId);
|
||||
@@ -47,29 +34,34 @@ 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>
|
||||
<tr className="languages">
|
||||
<td>{t("Language")}</td>
|
||||
|
||||
<td>
|
||||
{data.languages.map((lang) => (
|
||||
<Flag key={lang} iso={lang} showTitle={true} />
|
||||
))}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -80,141 +72,108 @@ function Tile({gameId, data}: {gameId: string, data: GameTile|undefined}) {
|
||||
function LandingPage() {
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { setPopupContent } = useContext(PopupContext)
|
||||
|
||||
const [impressum, setImpressum] = React.useState(false);
|
||||
const openImpressum = () => setImpressum(true);
|
||||
const closeImpressum = () => setImpressum(false);
|
||||
const { t, i18n } = useTranslation()
|
||||
|
||||
// const [allGames, setAllGames] = React.useState([])
|
||||
// const [allTiles, setAllTiles] = React.useState([])
|
||||
// Load the namespaces of all games
|
||||
// TODO: should `allGames` contain game-ids starting with `g/`?
|
||||
i18next.loadNamespaces(lean4gameConfig.allGames.map(id => `g/${id}`))
|
||||
|
||||
// const getTiles=()=>{
|
||||
// fetch('featured_games.json', {
|
||||
// headers : {
|
||||
// 'Content-Type': 'application/json',
|
||||
// 'Accept': 'application/json'
|
||||
// }
|
||||
// }
|
||||
// ).then(function(response){
|
||||
// return response.json()
|
||||
// }).then(function(data) {
|
||||
// setAllGames(data.featured_games)
|
||||
|
||||
// })
|
||||
// }
|
||||
|
||||
// React.useEffect(()=>{
|
||||
// getTiles()
|
||||
// },[])
|
||||
|
||||
// React.useEffect(()=>{
|
||||
|
||||
// Promise.allSettled(
|
||||
// allGames.map((gameId) => (
|
||||
// fetch(`data/g/${gameId}/game.json`).catch(err => {return undefined})))
|
||||
// ).then(responses =>
|
||||
// responses.forEach((result) => console.log(result)))
|
||||
// // Promise.all(responses.map(res => {
|
||||
// // if (res.status == "fulfilled") {
|
||||
// // console.log(res.value.json())
|
||||
// // return res.value.json()
|
||||
// // } else {
|
||||
// // return undefined
|
||||
// // }
|
||||
// // }))
|
||||
// // ).then(allData => {
|
||||
// // setAllTiles(allData.map(data => data?.tile))
|
||||
// // })
|
||||
// },[allGames])
|
||||
|
||||
// TODO: I would like to read the supported games list form a JSON,
|
||||
// Then load all these games in
|
||||
//
|
||||
let allGames = [
|
||||
"leanprover-community/nng4",
|
||||
"hhu-adam/robo",
|
||||
"djvelleman/stg4",
|
||||
"miguelmarco/STG4",
|
||||
]
|
||||
let allTiles = allGames.map((gameId) => (useGetGameInfoQuery({game: `g/${gameId}`}).data?.tile))
|
||||
let allTiles = lean4gameConfig.allGames.map((gameId) => {
|
||||
let q = useGetGameInfoQuery({game: `g/${gameId}`})
|
||||
return q.data?.tile
|
||||
})
|
||||
|
||||
return <div className="landing-page">
|
||||
<header style={{backgroundImage: `url(${bgImage})`}}>
|
||||
<nav>
|
||||
<GithubIcon url="https://github.com/leanprover-community/lean4game"/>
|
||||
</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.
|
||||
</p>
|
||||
: allGames.map((id, i) => (
|
||||
<Tile
|
||||
key={id}
|
||||
gameId={`g/${id}`}
|
||||
data={allTiles[i]}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<section>
|
||||
<div className="wrapper">
|
||||
<h2>Development notes</h2>
|
||||
<p>
|
||||
As this server runs lean on our university machines, it has a limited capacity.
|
||||
Our current estimate is about 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>
|
||||
</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.
|
||||
<React.Suspense>
|
||||
<div className="game-list">
|
||||
|
||||
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>
|
||||
{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>
|
||||
: lean4gameConfig.allGames.map((id, i) => (
|
||||
<Tile
|
||||
key={id}
|
||||
gameId={`g/${id}`}
|
||||
data={allTiles[i]}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</React.Suspense>
|
||||
<section>
|
||||
<div className="wrapper">
|
||||
<h2>{t("Development notes")}</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.
|
||||
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>
|
||||
</Trans>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div className="wrapper">
|
||||
<h2>Funding</h2>
|
||||
<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>
|
||||
</Trans>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div className="wrapper">
|
||||
<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-Universität in Düsseldorf.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<footer>
|
||||
<a className="link" onClick={openImpressum}>Impressum</a>
|
||||
{impressum? <PrivacyPolicyPopup handleClose={closeImpressum} />: null}
|
||||
{/* Do not translate "Impressum", it's needed for German GDPR */}
|
||||
<a className="link" onClick={() => {setPopupContent("impressum")}}>{t("Impressum")}</a>
|
||||
<a className="link" onClick={() => {setPopupContent("privacy")}}>{t("Privacy Policy")}</a>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
||||
+516
-569
File diff suppressed because it is too large
Load Diff
@@ -1,19 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkMath from 'remark-math'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
import 'katex/dist/katex.min.css' // `rehype-katex` does not import the CSS for you
|
||||
import gfm from "remark-gfm";
|
||||
|
||||
function Markdown(props) {
|
||||
const newProps = {
|
||||
...props,
|
||||
remarkPlugins: [...props.remarkPlugins ?? [], remarkMath, gfm],
|
||||
rehypePlugins: [...props.remarkPlugins ?? [], rehypeKatex],
|
||||
};
|
||||
return (
|
||||
<ReactMarkdown {...newProps} className="markdown" />
|
||||
);
|
||||
}
|
||||
|
||||
export default Markdown
|
||||
@@ -5,7 +5,7 @@ import '@fontsource/roboto/500.css';
|
||||
import '@fontsource/roboto/700.css';
|
||||
|
||||
import { Button, Dialog, DialogContent, DialogContentText, DialogActions } from '@mui/material';
|
||||
import Markdown from './markdown';
|
||||
import { Markdown } from './utils';
|
||||
|
||||
function Message({ isOpen, content, close }) {
|
||||
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
import * as React from 'react'
|
||||
import { createContext, useContext, useState } from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faDownload, faUpload, faEraser, faBook, faBookOpen, faGlobe, faHome,
|
||||
faArrowRight, faArrowLeft, faXmark, faBars, faCode,
|
||||
faCircleInfo, faTerminal, faGear, IconDefinition, faShield } from '@fortawesome/free-solid-svg-icons'
|
||||
import { GameIdContext, PageContext, PreferencesContext } from "../state/context"
|
||||
import { useGetGameInfoQuery, useLoadLevelQuery } from '../state/api'
|
||||
import { downloadProgress } from './popup/erase'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import '../css/navigation.css'
|
||||
import { PopupContext } from './popup/popup'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { selectCompleted, selectDifficulty, selectProgress, selectReadIntro } from '../state/progress'
|
||||
import lean4gameConfig from '../config.json'
|
||||
import { Flag } from './flag'
|
||||
import { useAppSelector } from '../hooks'
|
||||
|
||||
/** SVG github icon */
|
||||
function GithubIcon () {
|
||||
return <svg className="svg-inline--fa" height="24" aria-hidden="true" viewBox="0 0 16 16" version="1.1" width="24" >
|
||||
<path fill="#fff" d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
/** A button to appear in the navigation (both, top bar or dropdown). */
|
||||
export const NavButton: React.FC<{
|
||||
icon?: IconDefinition
|
||||
iconElement?: JSX.Element
|
||||
text?: string
|
||||
onClick?: React.MouseEventHandler<HTMLAnchorElement>
|
||||
title?: string
|
||||
href?: string
|
||||
inverted?: boolean
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
}> = ({icon, iconElement, text, onClick=()=>{}, title, href=null, inverted=false, disabled=false, className=''}) => {
|
||||
return <a className={`${className} nav-button btn${inverted?' btn-inverted':''}${disabled?' btn-disabled':''}`} onClick={disabled?null:onClick} href={disabled?null:href} title={title}>
|
||||
{iconElement ?? (icon && <FontAwesomeIcon icon={icon} />)}{text && <> {text}</>}
|
||||
</a>
|
||||
}
|
||||
|
||||
/** Context which manages the dropdown navigation */
|
||||
const NavigationContext = createContext<{
|
||||
navOpen: boolean,
|
||||
setNavOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}>({navOpen: false, setNavOpen: () => {}})
|
||||
|
||||
/** Content of the navigation during game selection. */
|
||||
function NavigationLandingPage () {
|
||||
return <div className="nav-content">
|
||||
<div className="nav-title-left"></div>
|
||||
<div className="nav-title-middle"></div>
|
||||
<div className="nav-title-right"></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
/** Content of the navigation on Desktop during world selection. */
|
||||
function DesktopNavigationOverview () {
|
||||
const { t } = useTranslation()
|
||||
const { gameId } = useContext(GameIdContext)
|
||||
const { setPopupContent } = useContext(PopupContext)
|
||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||
|
||||
return <div className="nav-content">
|
||||
<div className="nav-title-left">
|
||||
<NavButton
|
||||
text={t("Rules")}
|
||||
onClick={() => {setPopupContent("rules")}}
|
||||
inverted={true} />
|
||||
</div>
|
||||
<div className="nav-title-middle">
|
||||
<span className="nav-title">{t(gameInfo.data?.title, {ns: gameId})}</span>
|
||||
</div>
|
||||
<div className="nav-title-right"></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
/** Content of the navigation on Mobile during world selection. */
|
||||
function MobileNavigationOverview () {
|
||||
const { t } = useTranslation()
|
||||
const {page, setPage} = useContext(PageContext)
|
||||
const { setPopupContent } = useContext(PopupContext)
|
||||
|
||||
const { gameId, worldId } = useContext(GameIdContext)
|
||||
const readIntro = useSelector(selectReadIntro(gameId, worldId))
|
||||
|
||||
return <div className="nav-content">
|
||||
<div className="nav-title-left">
|
||||
<NavButton
|
||||
text={t("Rules")}
|
||||
onClick={() => {setPopupContent("rules")}}
|
||||
inverted={true} />
|
||||
</div>
|
||||
<div className="nav-title-middle">
|
||||
<span className="nav-title">
|
||||
</span>
|
||||
</div>
|
||||
<div className="nav-title-right">
|
||||
{page > 0 &&
|
||||
<NavButton
|
||||
text={page == 1 ? t("Intro") : null}
|
||||
icon={page == 1 ? null : faBookOpen}
|
||||
onClick={() => setPage(page - 1)}
|
||||
inverted={true} />
|
||||
}
|
||||
{ page < 2 &&
|
||||
<NavButton
|
||||
text={(page==0) ? t("Start") : null}
|
||||
icon={(page==0) ? null : faBook}
|
||||
onClick={() => setPage(page+1)}
|
||||
disabled={!readIntro}
|
||||
inverted={true} />
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
/** Content of the navigation on Desktop in a level. */
|
||||
function DesktopNavigationLevel () {
|
||||
const { t } = useTranslation()
|
||||
const { gameId, worldId, levelId } = useContext(GameIdContext)
|
||||
const { typewriterMode, setTypewriterMode, lockEditorMode } = useContext(PageContext)
|
||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||
const levelInfo = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
||||
const difficulty = useSelector(selectDifficulty(gameId))
|
||||
const completed = useAppSelector(selectCompleted(gameId, worldId, levelId))
|
||||
|
||||
const readIntro = useSelector(selectReadIntro(gameId, worldId))
|
||||
|
||||
const worldTitle = gameInfo.data?.worlds.nodes[worldId]?.title
|
||||
const levelTitle = ((levelId == 0) ?
|
||||
t("Introduction") :
|
||||
(
|
||||
t("Level") +
|
||||
` ${levelId}` +
|
||||
(gameInfo.data?.worldSize[worldId] ? ` / ${gameInfo.data?.worldSize[worldId]}` : '') +
|
||||
(levelInfo.data?.title ? ` : ${t(levelInfo?.data?.title, {ns: gameId})}` : '')
|
||||
)
|
||||
)
|
||||
|
||||
return <div className="nav-content">
|
||||
<div className="nav-title-left">
|
||||
<span className="nav-title">{worldTitle ? `${t(worldTitle, {ns: gameId})}` : '' /* ${t("World")}: */ }
|
||||
</span>
|
||||
</div>
|
||||
<div className="nav-title-middle">
|
||||
<span className="nav-title">
|
||||
{ levelTitle
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div className="nav-title-right" >
|
||||
{ levelId > 0 &&
|
||||
<NavButton
|
||||
icon={faArrowLeft}
|
||||
text={t("Previous")}
|
||||
inverted={true}
|
||||
href={`#/${gameId}/world/${worldId}/level/${levelId - 1}`} />
|
||||
}
|
||||
{ levelId == gameInfo.data?.worldSize[worldId] ?
|
||||
<NavButton
|
||||
icon={faHome}
|
||||
text={t("Home")}
|
||||
inverted={true}
|
||||
disabled={levelId > 0 && difficulty == 2 && !completed}
|
||||
href={`#/${gameId}`} /> :
|
||||
<NavButton
|
||||
icon={faArrowRight}
|
||||
text={levelId == 0 ? t("Start") : t("Next")} inverted={true}
|
||||
disabled={levelId == 0 ? !readIntro : (difficulty == 2 && !completed)}
|
||||
href={`#/${gameId}/world/${worldId}/level/${levelId + 1}`} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
/** Content of the navigation on Mobile in a level. */
|
||||
function MobileNavigationLevel () {
|
||||
const { t } = useTranslation()
|
||||
const {gameId, worldId, levelId} = useContext(GameIdContext)
|
||||
const {page, setPage} = useContext(PageContext)
|
||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||
const levelInfo = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
||||
|
||||
let title = worldId ?
|
||||
` ${levelId} / ${gameInfo.data?.worldSize[worldId]}`+ (levelInfo?.data?.title && ` : ${t(levelInfo?.data?.title, {ns: gameId})}`)
|
||||
:
|
||||
''
|
||||
|
||||
return <div className="nav-content">
|
||||
<div className="nav-title-left"></div>
|
||||
<div className="nav-title-middle">
|
||||
<span className="nav-title">
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
<div className="nav-title-right">
|
||||
<NavButton
|
||||
icon={(page == 1) ? faBook : faBookOpen}
|
||||
onClick={() => setPage((page == 1) ? 2 : 1)}
|
||||
inverted={true} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
/** The skeleton of the navigation which is the same across all layouts. */
|
||||
export function Navigation () {
|
||||
const { t, i18n } = useTranslation()
|
||||
const { gameId, worldId, levelId } = useContext(GameIdContext)
|
||||
const { mobile, language, setLanguage } = useContext(PreferencesContext)
|
||||
const { setPopupContent } = useContext(PopupContext)
|
||||
const { typewriterMode, setTypewriterMode, lockEditorMode } = useContext(PageContext)
|
||||
const gameProgress = useSelector(selectProgress(gameId))
|
||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||
const levelInfo = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
||||
const difficulty = useSelector(selectDifficulty(gameId))
|
||||
const completed = useAppSelector(selectCompleted(gameId, worldId, levelId))
|
||||
|
||||
const readIntro = useSelector(selectReadIntro(gameId, worldId))
|
||||
|
||||
const [navOpen, setNavOpen] = useState(false)
|
||||
const [langNavOpen, setLangNavOpen] = useState(false)
|
||||
function toggleNav () {setNavOpen(!navOpen); setLangNavOpen(false)}
|
||||
function toggleLangNav () {setLangNavOpen(!langNavOpen); setNavOpen(false)}
|
||||
|
||||
/** toggle input mode if allowed */
|
||||
function toggleInputMode(ev: React.MouseEvent) {
|
||||
if (!lockEditorMode) {
|
||||
setTypewriterMode(!typewriterMode)
|
||||
console.log('test')
|
||||
}
|
||||
}
|
||||
|
||||
return <nav>
|
||||
<NavigationContext.Provider value={{navOpen, setNavOpen}}>
|
||||
{ gameId && <>
|
||||
<NavButton
|
||||
icon={worldId ? faHome : faGlobe}
|
||||
title={worldId ? t("home") : t("back to games selection")}
|
||||
href={worldId ? `#/${gameId}` : `#`} />
|
||||
</>}
|
||||
{ gameId ?
|
||||
worldId ?
|
||||
(mobile ? <MobileNavigationLevel /> : <DesktopNavigationLevel />) :
|
||||
(mobile ? <MobileNavigationOverview /> : <DesktopNavigationOverview />) :
|
||||
<NavigationLandingPage />
|
||||
}
|
||||
{ !gameId &&
|
||||
<NavButton
|
||||
iconElement={<GithubIcon />}
|
||||
title={t("view the Lean game server on Github")}
|
||||
href='https://github.com/leanprover-community/lean4game' />
|
||||
}
|
||||
{(!gameId || gameInfo.data?.tile?.languages.length > 1) &&
|
||||
// Language button only visible if the game exists in `>1` languages
|
||||
<NavButton
|
||||
iconElement={langNavOpen ? null : <Flag iso={i18n.language} />}
|
||||
icon={langNavOpen ? faXmark : null}
|
||||
title={langNavOpen ? t('close language menu') : t('open language menu')}
|
||||
onClick={toggleLangNav}
|
||||
/>
|
||||
}
|
||||
<NavButton
|
||||
icon={navOpen ? faXmark : faBars}
|
||||
title={navOpen ? t('close menu') : t('open menu')}
|
||||
onClick={toggleNav} />
|
||||
{ langNavOpen &&
|
||||
<div className='dropdown' onClick={toggleLangNav} >
|
||||
{gameId && gameInfo.data?.tile?.languages ?
|
||||
// Show all languages the game is available in
|
||||
gameInfo.data?.tile?.languages.map(iso =>
|
||||
<NavButton
|
||||
key={`lang-selection-${iso}`}
|
||||
iconElement={<Flag iso={iso} />}
|
||||
text={lean4gameConfig.newLanguages[iso]?.name}
|
||||
onClick={() => {setLanguage(iso)}}
|
||||
inverted={true} />) :
|
||||
// Show all languages the interface is available in (e.g. landing page)
|
||||
Object.entries(lean4gameConfig.newLanguages).map(([iso, val]) =>
|
||||
<NavButton
|
||||
key={`lang-selection-${iso}`}
|
||||
iconElement={<Flag iso={iso} />}
|
||||
text={lean4gameConfig.newLanguages[iso]?.name}
|
||||
onClick={() => {setLanguage(iso)}}
|
||||
inverted={true} />)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{ navOpen &&
|
||||
<div className='dropdown' onClick={toggleNav} >
|
||||
{ gameId && <>
|
||||
{ mobile && (levelId == gameInfo.data?.worldSize[worldId] ?
|
||||
<NavButton
|
||||
icon={faHome}
|
||||
text={t("Home")}
|
||||
inverted={true}
|
||||
disabled={levelId > 0 && difficulty == 2 && !completed}
|
||||
href={`#/${gameId}`} /> :
|
||||
<NavButton
|
||||
icon={faArrowRight}
|
||||
text={levelId == 0 ? t("Start") : t("Next")} inverted={true}
|
||||
disabled={levelId == 0 ? !readIntro : (difficulty == 2 && !completed)}
|
||||
href={`#/${gameId}/world/${worldId}/level/${levelId + 1}`} />
|
||||
)}
|
||||
{mobile && levelId > 0 &&
|
||||
<NavButton
|
||||
icon={faArrowLeft}
|
||||
text={t("Previous")}
|
||||
inverted={true}
|
||||
href={`#/${gameId}/world/${worldId}/level/${levelId - 1}`} />
|
||||
}
|
||||
{ mobile && levelId > 0 &&
|
||||
<NavButton
|
||||
icon={(typewriterMode && !lockEditorMode) ? faCode : faTerminal}
|
||||
inverted={true}
|
||||
text={typewriterMode ? t("Editor Mode") : t("Typewriter Mode")}
|
||||
disabled={levelId == 0 || lockEditorMode}
|
||||
onClick={(ev) => toggleInputMode(ev)}
|
||||
title={lockEditorMode ? t("Editor mode is enforced!") : typewriterMode ? t("Editor mode") : t("Typewriter mode")} />
|
||||
}
|
||||
<NavButton
|
||||
icon={faCircleInfo}
|
||||
text={t("Game Info")}
|
||||
onClick={() => {setPopupContent("info")}}
|
||||
inverted={true} />
|
||||
<NavButton
|
||||
icon={faEraser}
|
||||
text={t("Erase")}
|
||||
onClick={() => {setPopupContent("erase")}}
|
||||
inverted={true} />
|
||||
<NavButton
|
||||
icon={faDownload}
|
||||
text={t("Download")}
|
||||
onClick={() => {downloadProgress(gameId, gameProgress)}}
|
||||
inverted={true} />
|
||||
<NavButton
|
||||
icon={faUpload}
|
||||
text={t("Upload")}
|
||||
onClick={() => {setPopupContent("upload")}}
|
||||
inverted={true} />
|
||||
</>}
|
||||
<NavButton
|
||||
icon={faCircleInfo}
|
||||
text={t("Impressum")}
|
||||
onClick={() => {setPopupContent("impressum")}}
|
||||
inverted={true} />
|
||||
<NavButton
|
||||
icon={faShield}
|
||||
text={t("Privacy Policy")}
|
||||
onClick={() => {setPopupContent("privacy")}}
|
||||
inverted={true} />
|
||||
<NavButton
|
||||
icon={faGear}
|
||||
text={t("Preferences")}
|
||||
onClick={() => {setPopupContent("preferences")}}
|
||||
inverted={true} />
|
||||
</div>
|
||||
}
|
||||
</NavigationContext.Provider>
|
||||
</nav>
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
/**
|
||||
* @fileOverview
|
||||
*/
|
||||
import * as React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { GameIdContext } from '../../app'
|
||||
import { useAppDispatch } from '../../hooks'
|
||||
import { deleteProgress, selectProgress } from '../../state/progress'
|
||||
import { deleteLevelProgress, deleteProgress, deleteWorldProgress, selectProgress } from '../../state/progress'
|
||||
import { downloadFile } from '../world_tree'
|
||||
import { Button } from '../button'
|
||||
import { Button } from '../utils'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'react'
|
||||
import { PopupContext } from './popup'
|
||||
import { GameIdContext, PageContext } from '../../state/context'
|
||||
|
||||
/** download the current progress (i.e. what's saved in the browser store) */
|
||||
export function downloadProgress(gameId: string, gameProgress: any, ev: React.MouseEvent) {
|
||||
ev.preventDefault()
|
||||
export function downloadProgress(gameId: string, gameProgress) {
|
||||
|
||||
// ev.preventDefault()
|
||||
downloadFile({
|
||||
data: JSON.stringify(gameProgress, null, 2),
|
||||
fileName: `lean4game-${gameId}-${new Date().toLocaleDateString()}.json`,
|
||||
@@ -24,36 +25,60 @@ export function downloadProgress(gameId: string, gameProgress: any, ev: React.Mo
|
||||
* `handleClose` is the function to close it again because it's open/closed state is
|
||||
* controlled by the containing element.
|
||||
*/
|
||||
export function ErasePopup ({handleClose}) {
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
export function ErasePopup () {
|
||||
let { t } = useTranslation()
|
||||
const { gameId, worldId, levelId } = React.useContext(GameIdContext)
|
||||
const { setPage } = useContext(PageContext)
|
||||
const gameProgress = useSelector(selectProgress(gameId))
|
||||
const dispatch = useAppDispatch()
|
||||
const { setPopupContent } = useContext(PopupContext)
|
||||
|
||||
const eraseProgress = () => {
|
||||
const eraseProgress = (ev) => {
|
||||
dispatch(deleteProgress({game: gameId}))
|
||||
handleClose()
|
||||
setPopupContent(null)
|
||||
setPage(0)
|
||||
// ev.preventDefault() // TODO: this is a hack to prevent the buttons below from opening a link
|
||||
}
|
||||
|
||||
function eraseLevel (ev) {
|
||||
dispatch(deleteLevelProgress({game: gameId, world: worldId, level: levelId}))
|
||||
setPopupContent(null)
|
||||
ev.preventDefault()
|
||||
}
|
||||
|
||||
function eraseWorld (ev) {
|
||||
dispatch(deleteWorldProgress({game: gameId, world: worldId}))
|
||||
setPopupContent(null)
|
||||
ev.preventDefault()
|
||||
}
|
||||
|
||||
const downloadAndErase = (ev) => {
|
||||
downloadProgress(gameId, gameProgress, ev)
|
||||
eraseProgress()
|
||||
downloadProgress(gameId, gameProgress)
|
||||
eraseProgress(ev)
|
||||
}
|
||||
|
||||
return <div className="modal-wrapper">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
return <>
|
||||
<h2>{t("Delete Progress?")}</h2>
|
||||
<Trans>
|
||||
<p>Do you want to delete your saved progress irreversibly?</p>
|
||||
</Trans>
|
||||
<div className='settings-buttons'>
|
||||
<Button onClick={levelId && eraseLevel} to="" disabled={!levelId} >{t("Delete this Level")}</Button>
|
||||
<Button onClick={worldId && eraseWorld} to="" disabled={!worldId} >{t("Delete this World")}</Button>
|
||||
<Button onClick={eraseProgress} to={`/${gameId}/`}>{t("Delete Everything")}</Button>
|
||||
</div>
|
||||
<Trans>
|
||||
<p>
|
||||
Deleting everything will delete all your proofs and your collected inventory! It's recommended
|
||||
to download your progress first.
|
||||
</p>
|
||||
<p>
|
||||
(Saves from other games are not deleted.)
|
||||
</p>
|
||||
</Trans>
|
||||
<div className='settings-buttons'>
|
||||
<Button onClick={downloadAndErase} to={`/${gameId}/`}>{t("Download & Delete everything")}</Button>
|
||||
<Button onClick={(ev) => {setPopupContent(null); ev.preventDefault()}} to="">{t("Cancel")}</Button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* @fileOverview
|
||||
*/
|
||||
import * as React from 'react'
|
||||
import { Typography } from '@mui/material'
|
||||
import Markdown from '../markdown'
|
||||
|
||||
/** Pop-up that is displaying the Game Info.
|
||||
*
|
||||
* `handleClose` is the function to close it again because it's open/closed state is
|
||||
* controlled by the containing element.
|
||||
*/
|
||||
export function InfoPopup ({info, handleClose}: {info: string, handleClose: () => void}) {
|
||||
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>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import * as React from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
/** Pop-up that is displayed when opening the privacy policy. */
|
||||
export function ImpressumPopup () {
|
||||
let {t, i18n} = useTranslation()
|
||||
|
||||
function content (lng = i18n.language) {
|
||||
const tt = i18n.getFixedT(lng);
|
||||
return <Trans t={tt} >
|
||||
<h2>Impressum</h2>
|
||||
<p>
|
||||
<strong>Contact:</strong><br />
|
||||
Marcus Zibrowius, Jon Eugster<br />
|
||||
Mathematisches Institut der Heinrich-Heine-Universität Düsseldorf<br />
|
||||
Universitätsstr. 1<br />
|
||||
40225 Düsseldorf<br />
|
||||
Germany<br />
|
||||
+49 211 81-14690<br />
|
||||
<a href="https://www.math.hhu.de/en/lehrstuehle-/-personen-/-ansprechpartner/innen/lehrstuehle-des-mathematischen-instituts/lehrstuhl-fuer-algebraische-geometrie/team">Contact Details</a>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Legal form:</strong><br />
|
||||
The Heinrich Heine University Düsseldorf is a corporation under public law. It is legally represented by the Rector Prof. Dr. Anja Steinbeck. The responsible supervisory authority is the Ministry of Culture and Science of North Rhine-Westphalia, Völklinger Straße 49, 40221 Düsseldorf.
|
||||
</p>
|
||||
<p>
|
||||
<strong>VAT identification number:</strong><br />
|
||||
according to §27a Sales Tax Act<br />
|
||||
DE 811222416
|
||||
</p>
|
||||
<p><a href="https://www.hhu.de/impressum" target="_blank">Impressum HHU</a></p>
|
||||
</Trans>
|
||||
}
|
||||
|
||||
return <>
|
||||
{i18n.language != 'en' && <>
|
||||
<p><i>(English version below)</i></p>
|
||||
{content()}
|
||||
<hr />
|
||||
</>}
|
||||
{content('en')}
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import * as React from 'react'
|
||||
import { Typography } from '@mui/material'
|
||||
import { Markdown } from '../utils'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useGetGameInfoQuery } from '../../state/api'
|
||||
import { GameIdContext } from '../../state/context'
|
||||
|
||||
/** Pop-up that is displaying the Game Info.
|
||||
*
|
||||
* `handleClose` is the function to close it again because it's open/closed state is
|
||||
* controlled by the containing element.
|
||||
*/
|
||||
export function InfoPopup () {
|
||||
let { t } = useTranslation()
|
||||
const {gameId} = React.useContext(GameIdContext)
|
||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||
|
||||
return <>
|
||||
<Typography variant="body1" component="div" className="welcome-text">
|
||||
<Markdown>{t(gameInfo.data?.info, {ns: gameId})}</Markdown>
|
||||
<hr />
|
||||
<Trans>
|
||||
<h2>Progress saving</h2>
|
||||
<p>
|
||||
The game stores your progress in your local browser storage. If you delete it, your progress will be lost!<br />
|
||||
Warning: In most browsers, deleting cookies will also clear the local storage (or "local site data").
|
||||
Make sure to download your game progress first!
|
||||
</p>
|
||||
<h2>Accessibility</h2>
|
||||
<p>
|
||||
If you experience any accessibilty barriers, please get in contact with us!
|
||||
We are dedicated to address such barriers to the best of our abilities.
|
||||
</p>
|
||||
<h2>Development</h2>
|
||||
<p>The game engine has been created by <strong>Alexander Bentkamp</strong>, <strong>Jon Eugster</strong>.
|
||||
On a prototype by <strong>Patrick Massot</strong>.
|
||||
</p>
|
||||
<p>
|
||||
The source code of this Lean game engine
|
||||
is <a href="https://github.com/leanprover-community/lean4game" target="_blank">available on Github</a>.
|
||||
If you experience any problems, please
|
||||
file an <a href="https://github.com/leanprover-community/lean4game/issues" target="_blank">Issue on Github</a> or
|
||||
get directly in contact.
|
||||
</p>
|
||||
<h2>Funding</h2>
|
||||
<p>
|
||||
The game engine has been developed as part of the
|
||||
project <a href="https://hhu-adam.github.io/" target="_blank">ADAM: Anticipating the Digital
|
||||
Age of Mathematics</a> at
|
||||
Heinrich-Heine-Universität Düsseldorf. It is funded by
|
||||
the <i>Stiftung Innovation in der Hochschullehre</i> as part of project <i>Freiraum 2022</i>.
|
||||
</p>
|
||||
</Trans>
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import * as React from 'react'
|
||||
import { useContext } from 'react'
|
||||
import { PrivacyPolicyPopup } from './privacy'
|
||||
import { ImpressumPopup } from './impressum'
|
||||
import { InfoPopup } from './info'
|
||||
import { ErasePopup } from './erase'
|
||||
import { PreferencesPopup } from './preferences'
|
||||
import { UploadPopup } from './upload'
|
||||
import { RulesPopup } from './rules'
|
||||
import '../../css/popup.css'
|
||||
import { NavButton } from '../navigation'
|
||||
import { faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
/** The context which manages if a popup is shown.
|
||||
* If `popupContent` is `null`, the popup is closed.
|
||||
*/
|
||||
export const PopupContext = React.createContext<{
|
||||
popupContent: string,
|
||||
setPopupContent: React.Dispatch<React.SetStateAction<string>>
|
||||
}>({
|
||||
popupContent: null,
|
||||
setPopupContent: () => {}
|
||||
})
|
||||
|
||||
/** To create a new Popup, one needs to add its content as `React.JSX.Element` here
|
||||
* and then call `setPopupConent(key)` at the place where to popup should be opened.
|
||||
*
|
||||
* TODO: The drawback of this design is that there is no check for key missmatches.
|
||||
* How could that be achieved?
|
||||
*/
|
||||
export const Popups = {
|
||||
"erase": <ErasePopup />,
|
||||
"impressum": <ImpressumPopup />,
|
||||
"info": <InfoPopup />,
|
||||
"preferences": <PreferencesPopup />,
|
||||
"privacy": <PrivacyPolicyPopup />,
|
||||
"rules": <RulesPopup />,
|
||||
"upload": <UploadPopup />,
|
||||
}
|
||||
|
||||
/** The skeleton for the popups. */
|
||||
export function Popup () {
|
||||
const {popupContent, setPopupContent} = useContext(PopupContext)
|
||||
function closePopup() {
|
||||
setPopupContent(null)
|
||||
}
|
||||
|
||||
return <div className="modal-wrapper">
|
||||
<div className="modal-backdrop" onClick={closePopup} />
|
||||
<div className="modal">
|
||||
{/* <NavButton icon={faXmark}
|
||||
onClick={closePopup}
|
||||
inverted={true} /> */}
|
||||
<div className="codicon codicon-close modal-close" onClick={closePopup}></div>
|
||||
{Popups[popupContent]}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import * as React from 'react'
|
||||
import { Input, MenuItem, Select, SelectChangeEvent, Typography } from '@mui/material'
|
||||
import { Markdown } from '../utils'
|
||||
import { Switch, Button, ButtonGroup } from '@mui/material';
|
||||
import Box from '@mui/material/Box';
|
||||
import Slider from '@mui/material/Slider';
|
||||
import lean4gameConfig from '../../config.json'
|
||||
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
|
||||
import { IPreferencesContext, PreferencesContext } from "../../state/context"
|
||||
import ReactCountryFlag from 'react-country-flag';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function PreferencesPopup () {
|
||||
let { t } = useTranslation()
|
||||
const {layout, isSavePreferences, language, setLayout, setIsSavePreferences, setLanguage} = React.useContext(PreferencesContext)
|
||||
|
||||
|
||||
const marks = [
|
||||
{
|
||||
value: 0,
|
||||
label: t('Mobile'),
|
||||
key: "mobile"
|
||||
},
|
||||
{
|
||||
value: 1,
|
||||
label: t('Auto'),
|
||||
key: "auto"
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
label: t('Desktop'),
|
||||
key: "desktop"
|
||||
},
|
||||
];
|
||||
|
||||
const handlerChangeLayout = (_: Event, value: number) => {
|
||||
setLayout(marks[value].key as IPreferencesContext["layout"])
|
||||
}
|
||||
|
||||
const handlerChangeLanguage = (ev: SelectChangeEvent<string>) => {
|
||||
setLanguage(ev.target.value as IPreferencesContext["language"])
|
||||
}
|
||||
|
||||
return <>
|
||||
<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}>
|
||||
{lean4gameConfig.useFlags && <ReactCountryFlag countryCode={lang.flag}/>}
|
||||
|
||||
{lang.name}
|
||||
</MenuItem>
|
||||
})}
|
||||
</Select>
|
||||
</Box>
|
||||
}
|
||||
label=""
|
||||
/>
|
||||
<p>
|
||||
If a game does not exist in the language selected, this setting has no effect
|
||||
and the game's default language is used.
|
||||
</p>
|
||||
|
||||
</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>
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import * as React from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
/** Pop-up that is displayed when opening the privacy policy.
|
||||
*
|
||||
* Note: Do not translate the Impressum!
|
||||
*/
|
||||
export function PrivacyPolicyPopup () {
|
||||
let {t, i18n} = useTranslation()
|
||||
function content (lng = i18n.language) {
|
||||
const tt = i18n.getFixedT(lng);
|
||||
return <Trans t={tt} >
|
||||
<h2>Privacy Policy</h2>
|
||||
<p>
|
||||
Our server collects metadata (such as IP address, browser, operating system)
|
||||
and the data that the user enters into the editor. The data is used to
|
||||
compute the Lean output and display it to the user. The information will be stored
|
||||
as long as the user stays on our website and will be deleted immediately afterwards.
|
||||
We keep logs to improve our software, but the contained data is anonymized.
|
||||
</p>
|
||||
<p>
|
||||
We do not use cookies, but your game progress is stored in the browser
|
||||
as site data. Your game progress is not saved on the server; if you delete
|
||||
your browser storage, it is completely gone.
|
||||
</p>
|
||||
<p>Our server is located in Germany.</p>
|
||||
<p>
|
||||
<strong>Contact:</strong><br />
|
||||
Marcus Zibrowius, Jon Eugster<br />
|
||||
Mathematisches Institut der Heinrich-Heine-Universität Düsseldorf<br />
|
||||
Universitätsstr. 1<br />
|
||||
40225 Düsseldorf<br />
|
||||
Germany<br />
|
||||
+49 211 81-14690<br />
|
||||
<a href="https://www.math.hhu.de/en/lehrstuehle-/-personen-/-ansprechpartner/innen/lehrstuehle-des-mathematischen-instituts/lehrstuhl-fuer-algebraische-geometrie/team">Contact Details</a>
|
||||
</p>
|
||||
</Trans>
|
||||
}
|
||||
|
||||
return <>
|
||||
{i18n.language != 'en' && <>
|
||||
<p><i>(English version below)</i></p>
|
||||
{content()}
|
||||
<hr />
|
||||
</>}
|
||||
{content('en')}
|
||||
</>
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
/**
|
||||
* @fileOverview The impressum/privacy policy
|
||||
*/
|
||||
import { faShield } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import * as React from 'react'
|
||||
|
||||
/** Pop-up that is displayed when opening the privacy policy.
|
||||
*
|
||||
* `handleClose` is the function to close it again because it's open/closed state is
|
||||
* controlled by the containing element.
|
||||
*/
|
||||
export function PrivacyPolicyPopup ({handleClose}: {handleClose: () => void}) {
|
||||
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>Privacy Policy & Impressum</h2>
|
||||
<p>
|
||||
Our server collects metadata (such as IP address, browser, operating system)
|
||||
and the data that the user enters into the editor. The data is used to
|
||||
compute the Lean output and display it to the user. The information will be stored
|
||||
as long as the user stays on our website and will be deleted immediately afterwards.
|
||||
We keep logs to improve our software, but the contained data is anonymized.
|
||||
</p>
|
||||
<p>
|
||||
We do not use cookies, but your game progress is stored in the browser
|
||||
as site data. Your game progress is not saved on the server; if you delete
|
||||
your browser storage, it is completely gone.
|
||||
</p>
|
||||
<p>Our server is located in Germany.</p>
|
||||
<p>
|
||||
<strong>Contact information:</strong><br />
|
||||
Alexander Bentkamp, Jon Eugster<br />
|
||||
Mathematisches Institut der Heinrich-Heine-Universität Düsseldorf<br />
|
||||
Universitätsstr. 1<br />
|
||||
40225 Düsseldorf<br />
|
||||
Germany<br />
|
||||
+49 211 81-12173<br />
|
||||
<a href="https://www.math.hhu.de/en/lehrstuehle-/-personen-/-ansprechpartner/innen/lehrstuehle-des-mathematischen-instituts/lehrstuhl-fuer-algebraische-geometrie/team/jon-eugster">Contact Details</a>
|
||||
</p>
|
||||
<h2>Development & Funding</h2>
|
||||
<p>
|
||||
The source code of this Lean game engine
|
||||
is <a href="https://github.com/leanprover-community/lean4game" target="_blank">available on Github</a>.
|
||||
If you experience any problems, please
|
||||
file an <a href="https://github.com/leanprover-community/lean4game/issues" target="_blank">Issue on Github</a> or
|
||||
get directly in contact.
|
||||
</p>
|
||||
<p>
|
||||
The game engine has been developed as part of the
|
||||
project <a href="https://hhu-adam.github.io/" target="_blank">ADAM: Anticipating the Digital
|
||||
Age of Mathematics</a> at
|
||||
Heinrich-Heine-Universität Düsseldorf. It is funded by
|
||||
the <i>Stiftung Innovation in der Hochschullehre</i> as part of project <i>Freiraum 2022</i>.
|
||||
</p>
|
||||
</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}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Box, Slider } from '@mui/material'
|
||||
import * as React from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { changedDifficulty, selectDifficulty } from '../../state/progress'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useContext } from 'react'
|
||||
import { useAppDispatch } from '../../hooks'
|
||||
import { GameIdContext } from '../../state/context'
|
||||
|
||||
/** Pop-up that is displayed when opening the help explaining the game rules.
|
||||
*
|
||||
*/
|
||||
export function RulesPopup () {
|
||||
const { t } = useTranslation()
|
||||
const { gameId } = useContext(GameIdContext)
|
||||
const difficulty = useSelector(selectDifficulty(gameId))
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
function label(x : number) {
|
||||
return x == 0 ? t("none") : x == 1 ? t("relaxed") : t("regular")
|
||||
}
|
||||
|
||||
return <>
|
||||
<h2>{t("Game Rules")}</h2>
|
||||
|
||||
{/* <span className="difficulty-label">{t("Rules")}
|
||||
<FontAwesomeIcon icon={rulesHelp ? faXmark : faCircleQuestion} className='helpButton' onClick={() => (setRulesHelp(!rulesHelp))}/>
|
||||
</span> */}
|
||||
<Box className="slider-wrapper">
|
||||
<Slider
|
||||
orientation="horizontal"
|
||||
title={t("Game Rules")}
|
||||
min={0} max={2}
|
||||
aria-label={t("Game Rules")}
|
||||
value={difficulty}
|
||||
marks={[
|
||||
{value: 0, label: label(0)},
|
||||
{value: 1, label: label(1)},
|
||||
{value: 2, label: label(2)}
|
||||
]}
|
||||
valueLabelFormat={label}
|
||||
getAriaValueText={label}
|
||||
valueLabelDisplay="off"
|
||||
onChange={(ev, val: number) => {
|
||||
dispatch(changedDifficulty({game: gameId, difficulty: val}))
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<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">{t("levels")}</th>
|
||||
<th scope="col">{t("tactics")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">{t("regular")}</th>
|
||||
<td>🔐</td>
|
||||
<td>🔐</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{t("relaxed")}</th>
|
||||
<td>🔓</td>
|
||||
<td>🔐</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{t("none")}</th>
|
||||
<td>🔓</td>
|
||||
<td>🔓</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/**
|
||||
* @fileOverview
|
||||
*/
|
||||
import * as React from 'react'
|
||||
|
||||
/** Pop-up that is displayed when opening the help explaining the game rules.
|
||||
*
|
||||
* `handleClose` is the function to close it again because it's open/closed state is
|
||||
* controlled by the containing element.
|
||||
*/
|
||||
export function RulesHelpPopup ({handleClose}: {handleClose: () => void}) {
|
||||
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>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"></th>
|
||||
<th scope="col">levels</th>
|
||||
<th scope="col">tactics</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">regular</th>
|
||||
<td>🔐</td>
|
||||
<td>🔐</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">relaxed</th>
|
||||
<td>🔓</td>
|
||||
<td>🔐</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">none</th>
|
||||
<td>🔓</td>
|
||||
<td>🔓</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -3,23 +3,30 @@
|
||||
*/
|
||||
import * as React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { GameIdContext } from '../../app'
|
||||
import { useAppDispatch } from '../../hooks'
|
||||
import { GameProgressState, loadProgress, selectProgress } from '../../state/progress'
|
||||
import { downloadFile } from '../world_tree'
|
||||
import { Button } from '../button'
|
||||
import { Button } from '../utils'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { PopupContext } from './popup'
|
||||
import { useContext } from 'react'
|
||||
import { GameIdContext } from '../../state/context'
|
||||
|
||||
/** Pop-up that is displaying the Game Info.
|
||||
*
|
||||
* `handleClose` is the function to close it again because it's open/closed state is
|
||||
* controlled by the containing element.
|
||||
*/
|
||||
export function UploadPopup ({handleClose}) {
|
||||
export function UploadPopup () {
|
||||
let { t } = useTranslation()
|
||||
|
||||
const [file, setFile] = React.useState<File>();
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const {gameId} = React.useContext(GameIdContext)
|
||||
const gameProgress = useSelector(selectProgress(gameId))
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const { setPopupContent } = useContext(PopupContext)
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
if (e.target.files) {
|
||||
setFile(e.target.files[0])
|
||||
@@ -36,7 +43,7 @@ export function UploadPopup ({handleClose}) {
|
||||
console.debug("Json Data", data)
|
||||
dispatch(loadProgress({game: gameId, data: data}))
|
||||
}
|
||||
handleClose()
|
||||
setPopupContent(null) // close the popup
|
||||
}
|
||||
|
||||
/** Download the current progress (i.e. what's saved in the browser store) */
|
||||
@@ -50,21 +57,19 @@ export function UploadPopup ({handleClose}) {
|
||||
}
|
||||
|
||||
|
||||
return <div className="modal-wrapper">
|
||||
<div className="modal-backdrop" onClick={handleClose} />
|
||||
<div className="modal">
|
||||
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
||||
<h2>Upload Saved Progress</h2>
|
||||
return <>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<Button to="" onClick={uploadProgress}>{t("Load selected file")}</Button>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import * as React from 'react'
|
||||
import { Link, LinkProps } from "react-router-dom"
|
||||
import { Box, CircularProgress } from "@mui/material"
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkMath from 'remark-math'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
import 'katex/dist/katex.min.css' // `rehype-katex` does not import the CSS for you
|
||||
import gfm from "remark-gfm"
|
||||
|
||||
/** Simple loading icon */
|
||||
export function LoadingIcon () {
|
||||
return <Box display="flex" alignItems="center" justifyContent="center" sx={{ flex: 1, height: "calc(100vh - 64px)" }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
}
|
||||
|
||||
export interface ButtonProps extends LinkProps {
|
||||
disabled?: boolean
|
||||
inverted?: string // Apparently "inverted" in DOM cannot be `boolean` but must be `inverted`
|
||||
}
|
||||
|
||||
|
||||
/** Our own button class */
|
||||
export function Button(props: ButtonProps) {
|
||||
if (props.disabled) {
|
||||
return <span className={`btn btn-disabled ${props.inverted === "true" ? 'btn-inverted' : ''}`} {...props}>{props.children}</span>
|
||||
} else {
|
||||
return <Link className={`btn ${props.inverted === "true" ? 'btn-inverted' : ''}`} {...props}>{props.children}</Link>
|
||||
}
|
||||
}
|
||||
|
||||
/** Spiced-up markdown */
|
||||
export function Markdown(props) {
|
||||
const newProps = {
|
||||
...props,
|
||||
remarkPlugins: [...props.remarkPlugins ?? [], remarkMath, gfm],
|
||||
rehypePlugins: [...props.remarkPlugins ?? [], rehypeKatex, rehypeRaw],
|
||||
};
|
||||
return (
|
||||
<ReactMarkdown {...newProps} className="markdown" />
|
||||
);
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import Split from 'react-split'
|
||||
import { Box, CircularProgress } from '@mui/material'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faArrowRight } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import { GameIdContext } from '../app'
|
||||
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 { InventoryPanel } from './inventory'
|
||||
import { ErasePopup } from './popup/erase'
|
||||
import { InfoPopup } from './popup/game_info'
|
||||
import { PrivacyPolicyPopup } from './popup/privacy_policy'
|
||||
import { RulesHelpPopup } from './popup/rules_help'
|
||||
import { UploadPopup } from './popup/upload'
|
||||
import { WorldTreePanel } from './world_tree'
|
||||
|
||||
import '../css/welcome.css'
|
||||
import { WelcomeAppBar } from './app_bar'
|
||||
import { Hint } from './hints'
|
||||
|
||||
|
||||
/** the panel showing the game's introduction text */
|
||||
function IntroductionPanel({introduction, setPageNumber}: {introduction: string, setPageNumber}) {
|
||||
const {mobile} = React.useContext(MobileContext)
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
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] : []
|
||||
|
||||
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}}
|
||||
step={0} selected={null} toggleSelection={undefined} />
|
||||
: <></>
|
||||
))}
|
||||
</div>
|
||||
{mobile &&
|
||||
<div className="button-row">
|
||||
<Button className="btn" to=""
|
||||
title="" onClick={() => {
|
||||
setPageNumber(1);
|
||||
dispatch(changedOpenedIntro({game: gameId, openedIntro: true}))
|
||||
}}>
|
||||
Start <FontAwesomeIcon icon={faArrowRight}/>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
/** main page of the game showing among others the tree of worlds/levels */
|
||||
function Welcome() {
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const {mobile} = React.useContext(MobileContext)
|
||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||
const inventory = useLoadInventoryOverviewQuery({game: gameId})
|
||||
|
||||
// For mobile only
|
||||
const openedIntro = useAppSelector(selectOpenedIntro(gameId))
|
||||
const [pageNumber, setPageNumber] = React.useState(openedIntro ? 1 : 0)
|
||||
|
||||
// pop-ups
|
||||
const [eraseMenu, setEraseMenu] = React.useState(false)
|
||||
const [impressum, setImpressum] = React.useState(false)
|
||||
const [info, setInfo] = React.useState(false)
|
||||
const [rulesHelp, setRulesHelp] = React.useState(false)
|
||||
const [uploadMenu, setUploadMenu] = React.useState(false)
|
||||
function closeEraseMenu() {setEraseMenu(false)}
|
||||
function closeImpressum() {setImpressum(false)}
|
||||
function closeInfo() {setInfo(false)}
|
||||
function closeRulesHelp() {setRulesHelp(false)}
|
||||
function closeUploadMenu() {setUploadMenu(false)}
|
||||
function toggleEraseMenu() {setEraseMenu(!eraseMenu)}
|
||||
function toggleImpressum() {setImpressum(!impressum)}
|
||||
function toggleInfo() {setInfo(!info)}
|
||||
function toggleUploadMenu() {setUploadMenu(!uploadMenu)}
|
||||
|
||||
// set the window title
|
||||
useEffect(() => {
|
||||
if (gameInfo.data?.title) {
|
||||
window.document.title = gameInfo.data.title
|
||||
}
|
||||
}, [gameInfo.data?.title])
|
||||
|
||||
return gameInfo.isLoading ?
|
||||
<Box display="flex" alignItems="center" justifyContent="center" sx={{ height: "calc(100vh - 64px)" }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
: <>
|
||||
<WelcomeAppBar pageNumber={pageNumber} setPageNumber={setPageNumber} gameInfo={gameInfo.data} toggleImpressum={toggleImpressum}
|
||||
toggleEraseMenu={toggleEraseMenu} toggleUploadMenu={toggleUploadMenu}
|
||||
toggleInfo={toggleInfo} />
|
||||
<div className="app-content">
|
||||
{ mobile ?
|
||||
<div className="welcome mobile">
|
||||
{(pageNumber == 0 ?
|
||||
<IntroductionPanel introduction={gameInfo.data?.introduction} setPageNumber={setPageNumber} />
|
||||
: pageNumber == 1 ?
|
||||
<WorldTreePanel worlds={gameInfo.data?.worlds} worldSize={gameInfo.data?.worldSize}
|
||||
rulesHelp={rulesHelp} setRulesHelp={setRulesHelp} />
|
||||
:
|
||||
<InventoryPanel levelInfo={inventory?.data} />
|
||||
)}
|
||||
</div>
|
||||
:
|
||||
<Split className="welcome" minSize={0} snapOffset={200} sizes={[25, 50, 25]}>
|
||||
<IntroductionPanel introduction={gameInfo.data?.introduction} setPageNumber={setPageNumber} />
|
||||
<WorldTreePanel worlds={gameInfo.data?.worlds} worldSize={gameInfo.data?.worldSize}
|
||||
rulesHelp={rulesHelp} setRulesHelp={setRulesHelp} />
|
||||
<InventoryPanel levelInfo={inventory?.data} />
|
||||
</Split>
|
||||
}
|
||||
</div>
|
||||
{impressum ? <PrivacyPolicyPopup handleClose={closeImpressum} /> : null}
|
||||
{rulesHelp ? <RulesHelpPopup handleClose={closeRulesHelp} /> : null}
|
||||
{eraseMenu? <ErasePopup handleClose={closeEraseMenu}/> : null}
|
||||
{uploadMenu? <UploadPopup handleClose={closeUploadMenu}/> : null}
|
||||
{info ? <InfoPopup info={gameInfo.data?.info} handleClose={closeInfo}/> : null}
|
||||
</>
|
||||
}
|
||||
|
||||
export default Welcome
|
||||
@@ -3,19 +3,18 @@
|
||||
*/
|
||||
import * as React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useStore, useSelector } from 'react-redux'
|
||||
import { Slider } from '@mui/material'
|
||||
import { useSelector } from 'react-redux'
|
||||
import cytoscape, { LayoutOptions } from 'cytoscape'
|
||||
import klay from 'cytoscape-klay'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faXmark, faCircleQuestion } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import { GameIdContext } from '../app'
|
||||
import { useAppDispatch } from '../hooks'
|
||||
import { selectDifficulty, changedDifficulty, selectCompleted } from '../state/progress'
|
||||
import { selectDifficulty, selectCompleted } from '../state/progress'
|
||||
import { store } from '../state/store'
|
||||
|
||||
import '../css/world_tree.css'
|
||||
import { GameIdContext } from '../state/context'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useGetGameInfoQuery } from '../state/api'
|
||||
import { LoadingIcon } from './utils'
|
||||
|
||||
// Settings for the world tree
|
||||
cytoscape.use( klay )
|
||||
@@ -63,7 +62,7 @@ export function LevelIcon({ world, level, position, completed, unlocked, worldSi
|
||||
// Sinus-Satz: (1.1*r) / sin(β/2) = R / sin(π/2)
|
||||
let R = 1.1 * r / Math.sin(beta/2)
|
||||
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const {gameId} = React.useContext(GameIdContext)
|
||||
const difficulty = useSelector(selectDifficulty(gameId))
|
||||
const levelDisabled = (difficulty >= 2 && !(unlocked || completed))
|
||||
|
||||
@@ -111,6 +110,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)
|
||||
@@ -134,7 +134,7 @@ export function WorldIcon({world, title, position, completedLevels, difficulty,
|
||||
nextLevel = 0
|
||||
}
|
||||
let playable = difficulty <= 1 || completed || unlocked
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const {gameId} = React.useContext(GameIdContext)
|
||||
|
||||
return <Link
|
||||
to={playable ? `/${gameId}/world/${world}/level/${nextLevel}` : ''}
|
||||
@@ -149,7 +149,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 +160,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>}
|
||||
@@ -192,44 +192,6 @@ export const downloadFile = ({ data, fileName, fileType } :
|
||||
a.remove()
|
||||
}
|
||||
|
||||
/** The menu that is shown next to the world selection graph */
|
||||
export function WorldSelectionMenu({rulesHelp, setRulesHelp}) {
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const difficulty = useSelector(selectDifficulty(gameId))
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
function label(x : number) {
|
||||
return x == 0 ? 'none' : x == 1 ? 'relaxed' : 'regular'
|
||||
}
|
||||
|
||||
|
||||
return <nav className="world-selection-menu">
|
||||
<div className="slider-wrap">
|
||||
<span className="difficulty-label">Rules
|
||||
<FontAwesomeIcon icon={rulesHelp ? faXmark : faCircleQuestion} className='helpButton' onClick={() => (setRulesHelp(!rulesHelp))}/>
|
||||
</span>
|
||||
<Slider
|
||||
orientation="vertical"
|
||||
title="Game Rules"
|
||||
min={0} max={2}
|
||||
aria-label="Game Rules"
|
||||
defaultValue={difficulty}
|
||||
marks={[
|
||||
{value: 0, label: label(0)},
|
||||
{value: 1, label: label(1)},
|
||||
{value: 2, label: label(2)}
|
||||
]}
|
||||
valueLabelFormat={label}
|
||||
getAriaValueText={label}
|
||||
valueLabelDisplay="off"
|
||||
onChange={(ev, val: number) => {
|
||||
dispatch(changedDifficulty({game: gameId, difficulty: val}))
|
||||
}}
|
||||
></Slider>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
|
||||
export function computeWorldLayout(worlds) {
|
||||
let elements = []
|
||||
for (let id in worlds.nodes) {
|
||||
@@ -265,15 +227,14 @@ export function computeWorldLayout(worlds) {
|
||||
}
|
||||
|
||||
|
||||
export function WorldTreePanel({worlds, worldSize, rulesHelp, setRulesHelp}:
|
||||
{ worlds: any,
|
||||
worldSize: any,
|
||||
rulesHelp: boolean,
|
||||
setRulesHelp: any,
|
||||
}) {
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
export function WorldTreePanel ({visible = true}) {
|
||||
const {gameId} = React.useContext(GameIdContext)
|
||||
const difficulty = useSelector(selectDifficulty(gameId))
|
||||
const {nodes, bounds}: any = worlds ? computeWorldLayout(worlds) : {nodes: []}
|
||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||
|
||||
|
||||
const {nodes, bounds}: any = gameInfo.data?.worlds ? computeWorldLayout(gameInfo.data?.worlds) : {nodes: []}
|
||||
|
||||
|
||||
// scroll to playable world
|
||||
React.useEffect(() => {
|
||||
@@ -286,7 +247,7 @@ export function WorldTreePanel({worlds, worldSize, rulesHelp, setRulesHelp}:
|
||||
console.debug(`scrolling to ${elem.textContent}`)
|
||||
elem.scrollIntoView({block: "center"})
|
||||
}
|
||||
}, [worlds, worldSize])
|
||||
}, [gameInfo])
|
||||
|
||||
let svgElements = []
|
||||
|
||||
@@ -295,18 +256,18 @@ export function WorldTreePanel({worlds, worldSize, rulesHelp, setRulesHelp}:
|
||||
// Indices `1, …, n` indicate if the corresponding level is completed
|
||||
var completed = {}
|
||||
|
||||
if (worlds && worldSize) {
|
||||
if (gameInfo.data?.worlds && gameInfo.data?.worldSize) {
|
||||
// Fill `completed` with the level data.
|
||||
for (let worldId in nodes) {
|
||||
completed[worldId] = Array.from({ length: worldSize[worldId] + 1 }, (_, i) => {
|
||||
completed[worldId] = Array.from({ length: gameInfo.data?.worldSize[worldId] + 1 }, (_, i) => {
|
||||
// index `0` starts off as `true` but can be set to `false` by any edge with non-completed source
|
||||
return i == 0 || selectCompleted(gameId, worldId, i)(store.getState())
|
||||
})
|
||||
}
|
||||
|
||||
// draw all connecting paths
|
||||
for (let i in worlds.edges) {
|
||||
const edge = worlds.edges[i]
|
||||
for (let i in gameInfo.data?.worlds.edges) {
|
||||
const edge = gameInfo.data?.worlds.edges[i]
|
||||
let sourceCompleted = completed[edge[0]].slice(1).every(Boolean)
|
||||
// if the origin world is not completed, mark the target world as non-playable
|
||||
if (!sourceCompleted) {completed[edge[1]][0] = false}
|
||||
@@ -326,11 +287,11 @@ export function WorldTreePanel({worlds, worldSize, rulesHelp, setRulesHelp}:
|
||||
completedLevels={completed[worldId]}
|
||||
difficulty={difficulty}
|
||||
key={`${gameId}-${worldId}`}
|
||||
worldSize={worldSize[worldId]}
|
||||
worldSize={gameInfo.data?.worldSize[worldId]}
|
||||
/>
|
||||
)
|
||||
|
||||
for (let i = 1; i <= worldSize[worldId]; i++) {
|
||||
for (let i = 1; i <= gameInfo.data?.worldSize[worldId]; i++) {
|
||||
svgElements.push(
|
||||
<LevelIcon
|
||||
world={worldId}
|
||||
@@ -339,7 +300,7 @@ export function WorldTreePanel({worlds, worldSize, rulesHelp, setRulesHelp}:
|
||||
completed={completed[worldId][i]}
|
||||
unlocked={completed[worldId][i-1]}
|
||||
key={`${gameId}-${worldId}-${i}`}
|
||||
worldSize={worldSize[worldId]}
|
||||
worldSize={gameInfo.data?.worldSize[worldId]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -353,13 +314,14 @@ export function WorldTreePanel({worlds, worldSize, rulesHelp, setRulesHelp}:
|
||||
|
||||
let dx = bounds ? s*(bounds.x2 - bounds.x1) + 2*padding : null
|
||||
|
||||
return <div className="column">
|
||||
<WorldSelectionMenu rulesHelp={rulesHelp} setRulesHelp={setRulesHelp} />
|
||||
return <div className={`${visible ? '' : 'hidden'}`}>
|
||||
{/* <WorldSelectionMenu rulesHelp={rulesHelp} setRulesHelp={setRulesHelp} /> */}
|
||||
{ gameInfo.data ?
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
width={bounds ? `${ds * dx}` : ''}
|
||||
viewBox={bounds ? `${s*bounds.x1 - padding} ${s*bounds.y1 - padding} ${dx} ${s*(bounds.y2 - bounds.y1) + 2 * padding}` : ''}
|
||||
className="world-selection" >
|
||||
{svgElements}
|
||||
</svg>
|
||||
</svg> : <LoadingIcon/> }
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"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": "中文"
|
||||
}
|
||||
],
|
||||
"newLanguages": {
|
||||
"en": {
|
||||
"flag": "GB",
|
||||
"name": "English"
|
||||
},
|
||||
"de": {
|
||||
"flag": "DE",
|
||||
"name": "Deutsch"
|
||||
},
|
||||
"zh": {
|
||||
"flag": "CN",
|
||||
"name": "中文"
|
||||
}
|
||||
},
|
||||
"useFlags": false
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/**
|
||||
* @fileOverview todo
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
|
||||
import { LeanClient } from 'lean4web/client/src/editor/leanclient';
|
||||
|
||||
export class Connection {
|
||||
private game: string = undefined // We only keep a connection to a single game at a time
|
||||
private leanClient: LeanClient = null
|
||||
|
||||
getLeanClient(game): LeanClient {
|
||||
if (this.game !== game) {
|
||||
if (this.leanClient) {
|
||||
this.leanClient.stop() // Stop previous Lean client
|
||||
}
|
||||
this.game = game
|
||||
// Start a new Lean client for the new `gameId`.
|
||||
const socketUrl = ((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + '/websocket/' + game
|
||||
const uri = monaco.Uri.parse('file:///')
|
||||
this.leanClient = new LeanClient(socketUrl, undefined, uri, () => {})
|
||||
}
|
||||
|
||||
return this.leanClient
|
||||
}
|
||||
|
||||
/** If not already started, starts the Lean client. resolves the returned promise as soon as a
|
||||
* Lean client is running.
|
||||
*/
|
||||
startLeanClient = (game) => {
|
||||
return new Promise<LeanClient>((resolve) => {
|
||||
const leanClient = this.getLeanClient(game)
|
||||
if (leanClient.isRunning()) {
|
||||
resolve(leanClient)
|
||||
} else {
|
||||
if (!leanClient.isStarted()) {
|
||||
leanClient.start()
|
||||
}
|
||||
leanClient.restarted(() => {
|
||||
// This keep alive message is not recognized by the server,
|
||||
// but it makes sure that the websocket connection does not
|
||||
// time out after 60 seconds.
|
||||
setInterval(() => {leanClient.sendNotification('$/keepAlive', {}) }, 5000)
|
||||
resolve(leanClient)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const connection = new Connection()
|
||||
|
||||
export const ConnectionContext = React.createContext(null);
|
||||
|
||||
export const useLeanClient = (gameId) => {
|
||||
const leanClient = connection.getLeanClient(gameId)
|
||||
const [leanClientStarted, setLeanClientStarted] = React.useState(leanClient.isStarted())
|
||||
|
||||
React.useEffect(() => {
|
||||
const t1 = leanClient.restarted(() => { console.log("START"); setLeanClientStarted(true) })
|
||||
const t2 = leanClient.stopped(() => { console.log("STOP"); setLeanClientStarted(false) })
|
||||
|
||||
return () => {t1.dispose(); t2.dispose()}
|
||||
}, [leanClient, setLeanClientStarted])
|
||||
|
||||
return {leanClientStarted, leanClient}
|
||||
}
|
||||
+40
-16
@@ -6,18 +6,25 @@
|
||||
--clr-light-gray: #ddd;
|
||||
--clr-dark-gray: #aaa;
|
||||
--clr-darker-gray: #555;
|
||||
--ff-primary: Roboto;
|
||||
}
|
||||
|
||||
|
||||
/* General styling */
|
||||
|
||||
html, body, #root, .app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--ff-primary);
|
||||
font-family: "Roboto", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.markdown code {
|
||||
color: rgba(0, 32, 90, 0.87);
|
||||
background-color: rgba(223, 227, 234);
|
||||
@@ -75,6 +82,7 @@ body {
|
||||
.btn-disabled, .btn[disabled] {
|
||||
color: var(--clr-dark-gray);
|
||||
background: var(--clr-light-gray);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-inverted.btn-disabled, .btn-inverted.btn[disabled] {
|
||||
@@ -88,16 +96,12 @@ em {
|
||||
|
||||
/* App Bar */
|
||||
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.app {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* TODO: remove */
|
||||
.app-bar {
|
||||
flex: 0;
|
||||
background: var(--clr-primary);
|
||||
@@ -105,11 +109,18 @@ em {
|
||||
position: relative;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.1em;
|
||||
filter: drop-shadow(0 0 5px rgba(0,0,0,0.5));
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.app-bar > .app-bar-left{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5em;
|
||||
}
|
||||
|
||||
.app-bar-title, .app-bar-subtitle {
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
@@ -119,14 +130,6 @@ em {
|
||||
/* margin: 0 1em; */
|
||||
}
|
||||
|
||||
.app-content {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
.markdown li ul, .markdown li ol {
|
||||
margin:0 1.5em;
|
||||
}
|
||||
@@ -135,6 +138,27 @@ em {
|
||||
margin:0 1.5em 1.5em 1.5em;
|
||||
}
|
||||
|
||||
.privacy-policy {
|
||||
.modal-wrapper {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.modal hr {
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 3rem;
|
||||
border-color: var(--vscode-breadcrumb-foreground);
|
||||
}
|
||||
|
||||
/* TODO: should not be #root */
|
||||
#root code {
|
||||
font-family: "JuliaMono", monospace;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'JuliaMono';
|
||||
src: local('JuliaMono'), url('/fonts/JuliaMono-Regular.ttf') format('truetype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: local('Roboto'), url('/fonts/Roboto-Regular.ttf') format('truetype');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
.chat {
|
||||
flex: 1;
|
||||
overflow-y: scroll;
|
||||
margin-left: .5em;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.chat-panel {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
}
|
||||
|
||||
.button-row {
|
||||
/* width:100%; */
|
||||
/* margin-left: .5em;
|
||||
margin-right: .5em; */
|
||||
min-height: 2.5em;
|
||||
border-top: 0.1em solid #aaa;
|
||||
display: flex;
|
||||
padding-top: .2rem;
|
||||
margin: .5rem;
|
||||
|
||||
}
|
||||
|
||||
.button-row > .btn:not(:last-child), .button-row > .btn-placeholder:not(:last-child) {
|
||||
/* display: block; */
|
||||
margin-right: .2rem;
|
||||
}
|
||||
|
||||
.button-row .btn-placeholder {
|
||||
display: inline-block;
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.button-row .btn {
|
||||
flex: 1;
|
||||
/* margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
margin-left: .5rem;
|
||||
margin-right: .5rem; */
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: .5em;
|
||||
}
|
||||
|
||||
|
||||
.message {
|
||||
margin: 10px 0;
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px 3px 3px 3px;
|
||||
}
|
||||
|
||||
.message.information, .message.info, .message.kind-5, .message.kind-3, .message.kind-4 {
|
||||
/* color: #059; */
|
||||
color: #000;
|
||||
background-color: #DDF6FF;
|
||||
}
|
||||
|
||||
.message.warning, .message.kind-2 {
|
||||
color: #9F6000;
|
||||
background-color: #FEEFB3;
|
||||
}
|
||||
|
||||
.message.error, .message.kind-1 {
|
||||
color: #D8000C;
|
||||
background-color: #FFBABA;
|
||||
}
|
||||
|
||||
.message.deleted-hint {
|
||||
background-color: #EEE;
|
||||
color: #777;
|
||||
box-shadow: .0em .0em .5em .2em #EEE;
|
||||
}
|
||||
|
||||
.message.success, , .message.kind-6 {
|
||||
color: #000;
|
||||
background-color: #E5FFDD;
|
||||
}
|
||||
|
||||
.chat .message {
|
||||
margin-left: .5em;
|
||||
margin-right: .5em;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.chat .recent {
|
||||
box-shadow: .0em .0em .4em .1em #8cbbe9;
|
||||
}
|
||||
|
||||
.chat .selected {
|
||||
/* border: 3px solid #5191d1; */
|
||||
|
||||
box-shadow: .0em .0em .4em .1em var(--clr-primary);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
.editor-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.editor-wrapper .editor-split {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#editor {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
#infoview {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
#infoview iframe {
|
||||
width: 100%;
|
||||
height: 95%; /* TODO: setting this to 100% makes it a few pixels too high... */
|
||||
border: unset;
|
||||
}
|
||||
|
||||
/* .typewriter {
|
||||
background-color: lightpink;
|
||||
min-height: 400px;
|
||||
} */
|
||||
@@ -0,0 +1,60 @@
|
||||
#error-page {
|
||||
height: 100vh;
|
||||
background-image: url("/RoboSurprised.png");
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
background-position: right bottom;
|
||||
}
|
||||
|
||||
/* these colours are matching the ones of `.messages.error` */
|
||||
.error-message{
|
||||
border: 2px solid #D8000C;
|
||||
background-color: #FFBABA;
|
||||
border-radius: 1rem;
|
||||
display: inline-block;
|
||||
margin: 3rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
/* color: #D8000C; */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
.thought-bubble {
|
||||
border: 2px solid #D8000C;
|
||||
background-color: #FFBABA;
|
||||
position: absolute;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thought-bubble:nth-of-type(1) {
|
||||
bottom: -3.5rem;
|
||||
left: 1rem;
|
||||
border-radius: 1.2rem;
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
}
|
||||
|
||||
.thought-bubble:nth-of-type(2) {
|
||||
bottom: -6rem;
|
||||
left: 2.5rem;
|
||||
border-radius: 1rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.thought-bubble:nth-of-type(3) {
|
||||
bottom: -7.5rem;
|
||||
left: 4.5rem;
|
||||
border-radius: 0.8rem;
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
}
|
||||
|
||||
.thought-bubble:nth-of-type(4) {
|
||||
bottom: -8.2rem;
|
||||
left: 6.8rem;
|
||||
border-radius: 0.6rem;
|
||||
width: 1.4rem;
|
||||
height: 1.4rem;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
.app-content {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.gutter {
|
||||
background-color: #eee;
|
||||
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50%;
|
||||
}
|
||||
|
||||
.gutter.gutter-vertical {
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=');
|
||||
}
|
||||
|
||||
.gutter.gutter-horizontal {
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==');
|
||||
}
|
||||
|
||||
|
||||
.column {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.slider .column {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
+69
-44
@@ -1,28 +1,4 @@
|
||||
|
||||
.message {
|
||||
margin: 10px 0;
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px 3px 3px 3px;
|
||||
}
|
||||
.message.information, .message.info {
|
||||
/* color: #059; */
|
||||
color: #000;
|
||||
background-color: #DDF6FF;
|
||||
}
|
||||
.message.warning {
|
||||
color: #9F6000;
|
||||
background-color: #FEEFB3;
|
||||
}
|
||||
.message.error {
|
||||
color: #D8000C;
|
||||
background-color: #FFBABA;
|
||||
}
|
||||
|
||||
.message.deleted-hint {
|
||||
background-color: #eee;
|
||||
color: #777;
|
||||
box-shadow: .0em .0em .5em .2em #eee;
|
||||
}
|
||||
|
||||
.hyp-group {
|
||||
margin-bottom: 0.3em;
|
||||
@@ -41,6 +17,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 {
|
||||
@@ -87,13 +70,6 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.typewriter-interface .proof .MuiCircularProgress-root {
|
||||
left: 50%;
|
||||
position: relative;
|
||||
margin-left: -20px;
|
||||
margin-bottom: 0.6em;
|
||||
}
|
||||
|
||||
.typewriter .typewriter-input {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -115,8 +91,8 @@
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.goals-section div {
|
||||
flex-grow: 1;
|
||||
.goal-tabs {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#current-proof, #main-assumptions {
|
||||
@@ -160,22 +136,12 @@
|
||||
}
|
||||
|
||||
|
||||
/* Push the goals to the bottom for now, until we insert the proof history above. */
|
||||
.typewriter-interface .content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* TODO this is in the wrong file */
|
||||
.chat {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.typewriter-interface .content .tmp-pusher {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.exercise .command {
|
||||
background-color: #bbb;
|
||||
padding: .5em;
|
||||
@@ -188,14 +154,73 @@
|
||||
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;
|
||||
font-family: "Droid Sans Mono", "monospace", monospace;
|
||||
font-size: 14px;
|
||||
font-family: "JuliaMono", monospace;
|
||||
padding: 0.4em .6em 0;
|
||||
}
|
||||
|
||||
.undo-button {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.crashed_message {
|
||||
color: #D8000C;
|
||||
font-weight: bold;
|
||||
padding-left: .5em;
|
||||
padding-right: .5em;
|
||||
}
|
||||
|
||||
/* Apparently this prevents the layout from jumping around when hovering over expressions. */
|
||||
.tooltip-arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.goal-tab {
|
||||
/* border: 2px dotted darkgreen; */
|
||||
padding: .2rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-family: "JuliaMono", monospace;
|
||||
}
|
||||
|
||||
.goal-tab .hyp-group-title {
|
||||
font-family: "Roboto", sans-serif;
|
||||
}
|
||||
|
||||
.goal-tab .goal, .goal-tab .hypotheses {
|
||||
/* border: 1px solid lightblue; */
|
||||
display: inline-block;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.goal-sign {
|
||||
/* border: 1px solid lightblue; */
|
||||
width: 1rem;
|
||||
margin-left: .5rem;
|
||||
margin-right: .5rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.goal-sign line {
|
||||
stroke: var(--clr-dark-gray);
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
.documentation h1 {
|
||||
font-weight: 900;
|
||||
font-family: "JuliaMono", monospace;
|
||||
}
|
||||
|
||||
.documentation.hidden {
|
||||
@@ -26,12 +27,22 @@
|
||||
.inventory .item {
|
||||
background: #fff;
|
||||
border: solid 1px #777;
|
||||
padding: .1em .5em;
|
||||
padding-left: .5rem;
|
||||
padding-right: 1.0rem;
|
||||
padding-top: .1rem;
|
||||
padding-bottom: .1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.inventory .item, .inventory .tab-bar:not(.major) .tab {
|
||||
font-family: "JuliaMono", monospace;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.inventory .item.locked {
|
||||
border: solid 1px #ccc;
|
||||
color: #ccc;
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
.inventory .item.disabled {
|
||||
@@ -42,13 +53,21 @@
|
||||
background-color: rgb(255, 242, 190);
|
||||
}
|
||||
|
||||
.inventory .item:not(.locked), .inventory .item.enabled {
|
||||
.inventory .item.current-world {
|
||||
background-color: rgb(250, 231, 255);
|
||||
}
|
||||
|
||||
.inventory .item.recent {
|
||||
background-color: rgb(242, 190, 255);
|
||||
}
|
||||
|
||||
.inventory .item, .inventory .item.enabled {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tab-bar {
|
||||
border-bottom: 0.1em solid var(--clr-dark-gray);
|
||||
margin-bottom: 0.5em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.tab {
|
||||
@@ -63,12 +82,72 @@
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.tab-bar.major {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
padding-top: .5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tab-bar.major .tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.tab-bar.current .tab.active {
|
||||
color: black;
|
||||
border-bottom: 0.3em solid var(--clr-primary);
|
||||
}
|
||||
|
||||
.inventory .tab.recent {
|
||||
background-image: linear-gradient(to bottom, rgba(255,0,0,0), rgb(242, 190, 255));
|
||||
}
|
||||
|
||||
.inventory .tab.recent:not(.active) {
|
||||
border-bottom: 0.3em solid rgb(242, 190, 255);
|
||||
}
|
||||
|
||||
.inventory .tab.new {
|
||||
background-image: linear-gradient(to bottom, rgba(255,0,0,0), rgb(255, 242, 190));
|
||||
}
|
||||
|
||||
.inventory .tab.new:not(.active) {
|
||||
border-bottom: 0.3em solid rgb(255, 242, 190);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: black;
|
||||
border-bottom: 0.3em solid #999;
|
||||
}
|
||||
|
||||
|
||||
.inventory .item .copy-button {
|
||||
min-width: 3px;
|
||||
min-height: 3px;
|
||||
display: inline-block;
|
||||
color: #ccc;
|
||||
font-size: 0.6em;
|
||||
padding-right: .2rem;
|
||||
vertical-align: top;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
width: 1rem;
|
||||
align-items: end;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.documentation .nav-button {
|
||||
float: right;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.documentation .nav-button.lock {
|
||||
float: left;
|
||||
font-size: .7rem;
|
||||
}
|
||||
|
||||
@@ -9,17 +9,13 @@ html {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@viewport {
|
||||
width: device-width ;
|
||||
zoom: 1.0 ;
|
||||
initial-scale: 1.0 ;
|
||||
}
|
||||
|
||||
.landing-page {
|
||||
@@ -36,6 +32,13 @@ a {
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.landing-page-nav {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#menu-btn {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 440px) {
|
||||
.game-list {
|
||||
@@ -158,12 +161,12 @@ header {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
header nav {
|
||||
/* header nav {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 8px;
|
||||
background-color: rgba(0, 133, 162, .7);
|
||||
}
|
||||
} */
|
||||
|
||||
footer {
|
||||
background-color: rgba(0, 133, 162, 1);
|
||||
@@ -182,6 +185,8 @@ footer .link {
|
||||
|
||||
.github-link {
|
||||
height: 24px; /* TODO: why do I need that? s*/
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.landing-page > section {
|
||||
@@ -222,3 +227,7 @@ footer .link {
|
||||
margin-right: calc(50% - 650px);
|
||||
}
|
||||
}
|
||||
|
||||
.info .languages span + span {
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
+95
-83
@@ -5,31 +5,12 @@
|
||||
/* display: flex; */
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.gutter {
|
||||
background-color: #eee;
|
||||
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50%;
|
||||
}
|
||||
|
||||
.gutter.gutter-vertical {
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=');
|
||||
}
|
||||
|
||||
.gutter.gutter-horizontal {
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==');
|
||||
}
|
||||
|
||||
.inventory-panel, .exercise-panel, .doc-panel, .introduction-panel {
|
||||
/* .inventory-panel, .exercise-panel, .doc-panel, .introduction-panel {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
}
|
||||
} */
|
||||
|
||||
.infoview {
|
||||
padding-top: 1em;
|
||||
@@ -63,11 +44,11 @@
|
||||
padding-top: 0em;
|
||||
}
|
||||
|
||||
.exercise {
|
||||
/* .exercise {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
} */
|
||||
|
||||
.codeview {
|
||||
flex: 1 1 auto;
|
||||
@@ -193,56 +174,21 @@ td code {
|
||||
|
||||
.exercise {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chat {
|
||||
flex: 1;
|
||||
overflow-y: scroll;
|
||||
margin-left: .5em;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.chat-panel {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
}
|
||||
|
||||
.chat-panel .button-row {
|
||||
/* width:100%; */
|
||||
margin-left: .5em;
|
||||
margin-right: .5em;
|
||||
min-height: 2.5em;
|
||||
border-top: 0.1em solid #aaa;
|
||||
}
|
||||
|
||||
.chat-panel .btn {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
margin-left: .5rem;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
/* .exercise-panel {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
height: 100%;
|
||||
} */
|
||||
|
||||
.typewriter-interface {
|
||||
.exercise-content {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.typewriter {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.typewriter-interface .content {
|
||||
.exercise-content {
|
||||
flex: 1 1 auto;
|
||||
overflow-y: scroll;
|
||||
padding: 0;
|
||||
@@ -262,16 +208,7 @@ td code {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.chat .message {
|
||||
margin-left: .5em;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.chat .recent {
|
||||
box-shadow: .0em .0em .4em .1em #8cbbe9;
|
||||
}
|
||||
|
||||
.exercise .step.selected .goal-tabs, .chat .selected {
|
||||
.exercise .step.selected .goal-tab {
|
||||
/* border: 3px solid #5191d1; */
|
||||
box-shadow: .0em .0em .4em .1em var(--clr-primary);
|
||||
}
|
||||
@@ -317,11 +254,6 @@ td code {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
#home-btn {
|
||||
margin-right: .5em;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.menu.dropdown .svg-inline--fa {
|
||||
width: 1.8rem;
|
||||
}
|
||||
@@ -332,30 +264,110 @@ td code {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.typewriter-interface .content, .world-image-container.empty {
|
||||
.exercise {
|
||||
background-color: #eee;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.world-image-container {
|
||||
.world-image {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
/* min-height: 0px;*/ /* somehow this has a desired affect, but why? */
|
||||
}
|
||||
|
||||
.world-image img.contain {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
object-position: top center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.world-image.center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.world-image-container img.contain {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.world-image-container img.cover {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.typewriter-interface .proof {
|
||||
.proof {
|
||||
z-index: 2;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.exercise-content .tmp-pusher {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.exercise-content {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.editor-mode .proof {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.editor-mode .tmp-pusher {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* Push the goals to the bottom for now, until we insert the proof history above. */
|
||||
.exercise-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.exercise-content .proof .MuiCircularProgress-root {
|
||||
left: 50%;
|
||||
position: relative;
|
||||
margin-left: -20px;
|
||||
margin-bottom: 0.6em;
|
||||
}
|
||||
|
||||
.toggle-width {
|
||||
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;
|
||||
}
|
||||
|
||||
#unicode-input, .lean-abbrev-input {
|
||||
min-width: 0;
|
||||
white-space: pre;
|
||||
flex: 1;
|
||||
/* padding: 0.4em .6em 0; */
|
||||
font-size: var(--vscode-editor-font-size);
|
||||
background-color: white;
|
||||
border: 2px solid orange;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: JuliaMono, monospace;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
|
||||
nav {
|
||||
flex: 0;
|
||||
background: var(--clr-primary);
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.1em;
|
||||
filter: drop-shadow(0 0 5px rgba(0,0,0,0.5));
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.nav-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nav-content > div {
|
||||
/* border: 1px solid red; */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-title-left, .nav-title-right {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-title-middle {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
margin-left: .5rem;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
font-size: 1.3rem;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
/* margin: 0 1em; */
|
||||
}
|
||||
|
||||
/* fix to make toggle buttons work */
|
||||
.svg-inline--fa {
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
/* TODO */
|
||||
.nav-button:not(.btn-inverted) {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
background-color: #fff;
|
||||
z-index: 5;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow: -.1rem .3rem .3rem 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dropdown .svg-inline--fa {
|
||||
width: 1.8rem;
|
||||
}
|
||||
|
||||
.btn-right {
|
||||
float: right;
|
||||
margin: 0.2rem;
|
||||
}
|
||||
|
||||
.btn-input-mode {
|
||||
position: absolute;
|
||||
right: 0.4rem;
|
||||
z-index: 5;
|
||||
top: 0.4rem;
|
||||
margin: 0;
|
||||
border: 1px solid var(--clr-primary);
|
||||
background-color: #eee;
|
||||
font-size: .7rem;
|
||||
/* box-shadow: .0em .0em .4em .1em var(--clr-primary); */
|
||||
/* box-shadow: -.1rem .3rem .3rem 0 var(--clr-primary); */
|
||||
/* border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0; */
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
|
||||
/* For the "Rules" popup. */
|
||||
.slider-wrapper {
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
float: right;
|
||||
scale: 2;
|
||||
color: var(--vscode-breadcrumb-foreground);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
float: right;
|
||||
scale: 2;
|
||||
color: var(--vscode-breadcrumb-focusForeground);
|
||||
}
|
||||
|
||||
.modal .nav-button {
|
||||
float: right;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.settings-buttons .btn {
|
||||
margin-top: 0.2rem;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
+12
-16
@@ -14,9 +14,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.welcome .column {
|
||||
height: 100%;
|
||||
@@ -159,19 +156,6 @@ h5, h6 {
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
float: right;
|
||||
scale: 2;
|
||||
color: var(--vscode-breadcrumb-foreground);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
float: right;
|
||||
scale: 2;
|
||||
color: var(--vscode-breadcrumb-focusForeground);
|
||||
}
|
||||
|
||||
.modal table {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -187,3 +171,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;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ svg .disabled {
|
||||
}
|
||||
|
||||
.world-selection-menu {
|
||||
position: absolute;
|
||||
right: 1em;
|
||||
top: 1em;
|
||||
/* margin: 1em; */
|
||||
@@ -60,6 +59,10 @@ svg .disabled {
|
||||
filter: drop-shadow(4px 4px 5px rgba(0,0,0,0.5));
|
||||
}
|
||||
|
||||
.world-selection-menu.desktop {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.world-selection-menu .btn, .welcome .btn {
|
||||
min-width: 5em;
|
||||
text-align: center;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import i18n from "i18next";
|
||||
import Backend from "i18next-http-backend"
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
.use(Backend)
|
||||
.init({
|
||||
ns: ['translation'],
|
||||
backend: {
|
||||
// > see https://github.com/i18next/i18next-http-backend
|
||||
loadPath: function(lngs, namespaces: Array<string>) {
|
||||
if (namespaces[0].startsWith("g/")) {
|
||||
return '/i18n/{{ns}}/{{lng}}/Game.json';
|
||||
} else {
|
||||
return '/locales/{{lng}}/{{ns}}.json';
|
||||
}
|
||||
}
|
||||
},
|
||||
// > language to use, more information here:
|
||||
// > https://www.i18next.com/overview/configuration-options#languages-namespaces-resources
|
||||
lng: "en",
|
||||
// we use natural language keys, so we don't need a fallback language.
|
||||
fallbackLng: false,
|
||||
// > you can use the i18n.changeLanguage function to change the language manually:
|
||||
// > https://www.i18next.com/overview/api#changelanguage
|
||||
// > if you're using a language detector, do not define the lng option
|
||||
returnEmptyString: false,
|
||||
interpolation: {
|
||||
// > react already safes from xss
|
||||
escapeValue: false
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
+20
-19
@@ -1,7 +1,6 @@
|
||||
import * as React from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './app'
|
||||
import { ConnectionContext, connection } from './connection'
|
||||
import { store } from './state/store'
|
||||
import { Provider } from 'react-redux'
|
||||
import type { RouteObject } from "react-router"
|
||||
@@ -9,26 +8,29 @@ import { createHashRouter, RouterProvider, Route, redirect } from "react-router-
|
||||
import ErrorPage from './components/error_page'
|
||||
import Welcome from './components/welcome'
|
||||
import LandingPage from './components/landing_page'
|
||||
import Level from './components/level'
|
||||
import { monacoSetup } from 'lean4web/client/src/monacoSetup'
|
||||
import './i18n';
|
||||
import Game from './components/game'
|
||||
|
||||
|
||||
monacoSetup()
|
||||
|
||||
|
||||
// If `VITE_LEAN4GAME_SINGLE` is set to true, then `/` should be redirected to
|
||||
// `/g/local/game`. This is used for the devcontainer setup
|
||||
let single_game = (import.meta.env.VITE_LEAN4GAME_SINGLE == "true")
|
||||
let root_object: RouteObject = single_game ? {
|
||||
path: "/",
|
||||
loader: () => redirect("/g/local/game")
|
||||
} : {
|
||||
path: "/",
|
||||
element: <LandingPage />,
|
||||
}
|
||||
// let root_object: RouteObject = single_game ? {
|
||||
// path: "/",
|
||||
// loader: () => redirect("/g/local/game")
|
||||
// }
|
||||
|
||||
let landing_page: RouteObject = single_game ? {
|
||||
path: "/",
|
||||
loader: () => redirect("/g/local/game")
|
||||
} : {
|
||||
path: "/",
|
||||
element: <LandingPage />,
|
||||
}
|
||||
|
||||
const router = createHashRouter([
|
||||
root_object,
|
||||
// root_object,
|
||||
{
|
||||
// For backwards compatibility
|
||||
path: "/game/nng",
|
||||
@@ -40,17 +42,18 @@ const router = createHashRouter([
|
||||
loader: () => redirect("/g/leanprover-community/NNG4")
|
||||
},
|
||||
{
|
||||
path: "/g/:owner/:repo",
|
||||
path: "/",
|
||||
element: <App />,
|
||||
errorElement: <ErrorPage />,
|
||||
children: [
|
||||
landing_page,
|
||||
{
|
||||
path: "/g/:owner/:repo",
|
||||
element: <Welcome />,
|
||||
element: <Game />,
|
||||
},
|
||||
{
|
||||
path: "/g/:owner/:repo/world/:worldId/level/:levelId",
|
||||
element: <Level />,
|
||||
element: <Game />,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -61,9 +64,7 @@ const root = createRoot(container!);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<ConnectionContext.Provider value={connection}>
|
||||
<RouterProvider router={router} />
|
||||
</ConnectionContext.Provider>
|
||||
<RouterProvider router={router} />
|
||||
</Provider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
+17
-7
@@ -35,6 +35,10 @@ export interface InventoryTile {
|
||||
locked: boolean,
|
||||
new: boolean,
|
||||
hidden: boolean
|
||||
altTitle: string,
|
||||
world : string|null,
|
||||
level : number|null,
|
||||
proven : boolean
|
||||
}
|
||||
|
||||
export interface LevelInfo {
|
||||
@@ -43,11 +47,11 @@ export interface LevelInfo {
|
||||
conclusion: null|string,
|
||||
index: number,
|
||||
tactics: InventoryTile[],
|
||||
lemmas: InventoryTile[],
|
||||
theorems: InventoryTile[],
|
||||
definitions: InventoryTile[],
|
||||
descrText: null|string,
|
||||
descrFormat: null|string,
|
||||
lemmaTab: null|string,
|
||||
theoremTab: null|string,
|
||||
statementName: null|string,
|
||||
displayName: null|string,
|
||||
template: null|string,
|
||||
@@ -57,9 +61,9 @@ export interface LevelInfo {
|
||||
/** Used to display the inventory on the welcome page */
|
||||
export interface InventoryOverview {
|
||||
tactics: InventoryTile[],
|
||||
lemmas: InventoryTile[],
|
||||
theorems: InventoryTile[],
|
||||
definitions: InventoryTile[],
|
||||
lemmaTab: null,
|
||||
theoremTab: null,
|
||||
}
|
||||
|
||||
interface Doc {
|
||||
@@ -80,13 +84,19 @@ export const apiSlice = createApi({
|
||||
query: ({game}) => `${game}/game.json`,
|
||||
}),
|
||||
loadLevel: builder.query<LevelInfo, {game: string, world: string, level: number}>({
|
||||
query: ({game, world, level}) => `${game}/level__${world}__${level}.json`,
|
||||
query: ({game, world, level}) => {
|
||||
if (world && level > 0) {
|
||||
return `${game}/level__${world}__${level}.json`
|
||||
} else {
|
||||
return `${game}/inventory.json`
|
||||
}
|
||||
},
|
||||
}),
|
||||
loadInventoryOverview: builder.query<InventoryOverview, {game: string}>({
|
||||
query: ({game}) => `${game}/inventory.json`,
|
||||
}),
|
||||
loadDoc: builder.query<Doc, {game: string, name: string, type: "lemma"|"tactic"}>({
|
||||
query: ({game, type, name}) => `${game}/doc__${type}__${name}.json`,
|
||||
loadDoc: builder.query<Doc, {game: string, name: string }>({
|
||||
query: ({game, name}) => `${game}/doc__${name}.json`,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* @fileOverview This file contains the the react contexts used in the project.
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
|
||||
import { InteractiveDiagnostic } from '@leanprover/infoview-api';
|
||||
import { Diagnostic } from 'vscode-languageserver-types'
|
||||
import { GameHint, InteractiveGoal, InteractiveTermGoal,InteractiveGoalsWithHints, ProofState } from '../components/infoview/rpc_api';
|
||||
import { PreferencesState } from './preferences';
|
||||
|
||||
export const MonacoEditorContext = React.createContext<monaco.editor.IStandaloneCodeEditor>(
|
||||
null as any)
|
||||
|
||||
export type InfoStatus = 'updating' | 'error' | 'ready';
|
||||
|
||||
|
||||
export const GameIdContext = React.createContext<{
|
||||
gameId: string,
|
||||
worldId: string|null,
|
||||
levelId: number|null}>({gameId: null, worldId: null, levelId: null});
|
||||
|
||||
// /** 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 "" as command
|
||||
*/
|
||||
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: {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?: InteractiveGoalsWithHints;
|
||||
termGoal?: InteractiveTermGoal;
|
||||
error?: string;
|
||||
// userWidgets: UserWidgetInstance[];
|
||||
// rpcSess: RpcSessionAtPos;
|
||||
// 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 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 PreferencesContext = React.createContext<IPreferencesContext>({
|
||||
mobile: false,
|
||||
layout: "auto",
|
||||
isSavePreferences: false,
|
||||
language: "en",
|
||||
setLayout: () => {},
|
||||
setIsSavePreferences: () => {},
|
||||
setLanguage: () => {},
|
||||
})
|
||||
|
||||
|
||||
export const ChatContext = React.createContext<{
|
||||
selectedStep : number
|
||||
setSelectedStep: React.Dispatch<React.SetStateAction<number>>
|
||||
deletedChat : GameHint[]
|
||||
setDeletedChat: React.Dispatch<React.SetStateAction<Array<GameHint>>>
|
||||
showHelp : Set<number>
|
||||
setShowHelp: React.Dispatch<React.SetStateAction<Set<number>>>
|
||||
chatRef: React.MutableRefObject<HTMLDivElement>
|
||||
}>({
|
||||
selectedStep : undefined,
|
||||
setSelectedStep: () => {},
|
||||
deletedChat: undefined,
|
||||
setDeletedChat: () => {},
|
||||
showHelp: undefined,
|
||||
setShowHelp: () => {},
|
||||
chatRef: null
|
||||
})
|
||||
|
||||
export const PageContext = React.createContext<{
|
||||
typewriterMode: boolean,
|
||||
setTypewriterMode: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
typewriterInput: string,
|
||||
setTypewriterInput: React.Dispatch<React.SetStateAction<string>>,
|
||||
lockEditorMode: boolean,
|
||||
setLockEditorMode: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
page: number, /* only for mobile */
|
||||
setPage: React.Dispatch<React.SetStateAction<number>>,
|
||||
}>({
|
||||
typewriterMode: true,
|
||||
setTypewriterMode: () => {},
|
||||
typewriterInput: "",
|
||||
setTypewriterInput: () => {},
|
||||
lockEditorMode: false,
|
||||
setLockEditorMode: () => {},
|
||||
page: 0,
|
||||
setPage: () => {}
|
||||
});
|
||||
|
||||
export const InputModeContext = React.createContext<{
|
||||
typewriterMode: boolean,
|
||||
setTypewriterMode: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
typewriterInput: string,
|
||||
setTypewriterInput: React.Dispatch<React.SetStateAction<string>>,
|
||||
lockEditorMode: boolean,
|
||||
setLockEditorMode: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
}>({
|
||||
typewriterMode: true,
|
||||
setTypewriterMode: () => {},
|
||||
typewriterInput: "",
|
||||
setTypewriterInput: () => {},
|
||||
lockEditorMode: false,
|
||||
setLockEditorMode: () => {},
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import React, { useState } from "react";
|
||||
import { useAppDispatch, useAppSelector } from "../../hooks";
|
||||
import {
|
||||
PreferencesState,
|
||||
setLayout as setPreferencesLayout,
|
||||
setIsSavePreferences as setPreferencesIsSavePreferences,
|
||||
setLanguage as setLanguagePreferences,
|
||||
getWindowDimensions,
|
||||
AUTO_SWITCH_THRESHOLD
|
||||
} from "../preferences";
|
||||
|
||||
|
||||
const UsePreferences = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
const [mobile, setMobile] = React.useState<boolean>()
|
||||
|
||||
const layout = useAppSelector((state) => state.preferences.layout);
|
||||
const setLayout = (layout: PreferencesState["layout"]) => dispatch(setPreferencesLayout(layout))
|
||||
|
||||
const isSavePreferences = useAppSelector((state) => state.preferences.isSavePreferences);
|
||||
const setIsSavePreferences = (isSave: boolean) => dispatch(setPreferencesIsSavePreferences(isSave))
|
||||
|
||||
const language = useAppSelector((state) => state.preferences.language);
|
||||
const setLanguage = (lang: string) => dispatch(setLanguagePreferences(lang))
|
||||
|
||||
const automaticallyAdjustLayout = () => {
|
||||
const {width} = getWindowDimensions()
|
||||
setMobile(width < AUTO_SWITCH_THRESHOLD)
|
||||
}
|
||||
|
||||
React.useEffect(()=>{
|
||||
if (layout === "auto"){
|
||||
void automaticallyAdjustLayout()
|
||||
window.addEventListener('resize', automaticallyAdjustLayout)
|
||||
|
||||
return () => window.removeEventListener('resize', automaticallyAdjustLayout)
|
||||
} else {
|
||||
setMobile(layout === "mobile")
|
||||
}
|
||||
}, [layout])
|
||||
|
||||
return {mobile, layout, isSavePreferences, language, setLayout, setIsSavePreferences, setLanguage}
|
||||
}
|
||||
|
||||
export default UsePreferences;
|
||||
@@ -36,3 +36,33 @@ export async function saveState(state: any) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
const PREFERENCES_KEY = "preferences"
|
||||
|
||||
/** Load from browser storage */
|
||||
export function loadPreferences() {
|
||||
try {
|
||||
const serializedState = localStorage.getItem(PREFERENCES_KEY);
|
||||
return JSON.parse(serializedState)
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function savePreferences(state: any) {
|
||||
try {
|
||||
const serializedState = JSON.stringify(state)
|
||||
localStorage.setItem(PREFERENCES_KEY, serializedState);
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
export function removePreferences() {
|
||||
try {
|
||||
localStorage.removeItem(PREFERENCES_KEY);
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
import { loadPreferences, removePreferences, savePreferences } from "./local_storage";
|
||||
|
||||
export interface PreferencesState {
|
||||
layout: "mobile" | "auto" | "desktop";
|
||||
isSavePreferences: boolean;
|
||||
language: string;
|
||||
}
|
||||
|
||||
export function getWindowDimensions() {
|
||||
const {innerWidth: width, innerHeight: height } = window
|
||||
return {width, height}
|
||||
}
|
||||
|
||||
export const AUTO_SWITCH_THRESHOLD = 800
|
||||
|
||||
const initialState: PreferencesState = loadPreferences() ??{
|
||||
layout: "auto",
|
||||
isSavePreferences: false,
|
||||
language: "en",
|
||||
}
|
||||
|
||||
export const preferencesSlice = createSlice({
|
||||
name: "preferences",
|
||||
initialState,
|
||||
reducers: {
|
||||
setLayout: (state, action) => {
|
||||
state.layout = action.payload;
|
||||
},
|
||||
setIsSavePreferences: (state, action) => {
|
||||
state.isSavePreferences = action.payload;
|
||||
},
|
||||
setLanguage: (state, action) => {
|
||||
state.language = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setLayout, setIsSavePreferences, setLanguage } = preferencesSlice.actions;
|
||||
@@ -19,13 +19,13 @@ interface LevelProgressState {
|
||||
help: number[], // A set of rows where hidden hints have been displayed
|
||||
}
|
||||
interface WorldProgressState {
|
||||
[world: string] : {[level: number]: LevelProgressState},
|
||||
[world: string] : {[level: number]: LevelProgressState, readIntro: boolean},
|
||||
}
|
||||
|
||||
export interface GameProgressState {
|
||||
inventory: string[],
|
||||
difficulty: number,
|
||||
openedIntro: boolean,
|
||||
readIntro: boolean,
|
||||
data: WorldProgressState,
|
||||
typewriterMode?: boolean
|
||||
}
|
||||
@@ -53,22 +53,27 @@ 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: [], readIntro: false, 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 = {}
|
||||
}
|
||||
}
|
||||
|
||||
function addWorldProgress(state: ProgressState, action: PayloadAction<{game: string, world: string}>) {
|
||||
addGameProgress(state, action)
|
||||
if (!state.games[action.payload.game.toLowerCase()].data[action.payload.world]) {
|
||||
state.games[action.payload.game.toLowerCase()].data[action.payload.world] = {readIntro: false}
|
||||
}
|
||||
}
|
||||
|
||||
/** 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].data[action.payload.world][action.payload.level]) {
|
||||
state.games[action.payload.game].data[action.payload.world][action.payload.level] = {...initalLevelProgressState}
|
||||
addWorldProgress(state, action)
|
||||
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,137 +84,149 @@ 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: {}, readIntro: false, difficulty: DEFAULT_DIFFICULTY}
|
||||
},
|
||||
/** delete progress for this world */
|
||||
deleteWorldProgress(state: ProgressState, action: PayloadAction<{game: string, world: string}>) {
|
||||
// addWorldProgress(state, action)
|
||||
state.games[action.payload.game.toLowerCase()].data[action.payload.world] = {readIntro: false}
|
||||
},
|
||||
/** 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
|
||||
// addLevelProgress(state, action)
|
||||
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
|
||||
changedReadIntro(state: ProgressState, action: PayloadAction<{game: string, world: string, readIntro: boolean}>) {
|
||||
addWorldProgress(state, action)
|
||||
if (action.payload.world) {
|
||||
state.games[action.payload.game.toLowerCase()].data[action.payload.world].readIntro = action.payload.readIntro
|
||||
} else {
|
||||
state.games[action.payload.game.toLowerCase()].readIntro = action.payload.readIntro
|
||||
}
|
||||
},
|
||||
/** 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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/** 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]
|
||||
return (state) => {
|
||||
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) {
|
||||
export function selectReadIntro(game: string, worldId: string) {
|
||||
return (state) => {
|
||||
return state.progress.games[game]?.openedIntro
|
||||
if (worldId) {
|
||||
return state.progress.games[game?.toLowerCase()]?.data[worldId]?.readIntro
|
||||
}
|
||||
return state.progress.games[game?.toLowerCase()]?.readIntro
|
||||
}
|
||||
}
|
||||
|
||||
/** 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
|
||||
}
|
||||
}
|
||||
|
||||
/** Export actions to modify the progress */
|
||||
export const { changedSelection, codeEdited, levelCompleted, deleteProgress,
|
||||
deleteLevelProgress, loadProgress, helpEdited, changedInventory, changedOpenedIntro,
|
||||
deleteLevelProgress, deleteWorldProgress, loadProgress, helpEdited, changedInventory, changedReadIntro,
|
||||
changedDifficulty, changeTypewriterMode} = progressSlice.actions
|
||||
|
||||
@@ -7,21 +7,19 @@ import { debounce } from "debounce";
|
||||
import { connection } from '../connection'
|
||||
import { apiSlice } from './api'
|
||||
import { progressSlice } from './progress'
|
||||
import { saveState } from "./local_storage";
|
||||
import { preferencesSlice } from "./preferences"
|
||||
import { saveState, savePreferences, removePreferences} from "./local_storage";
|
||||
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
[apiSlice.reducerPath]: apiSlice.reducer,
|
||||
[progressSlice.name]: progressSlice.reducer,
|
||||
[preferencesSlice.name]: preferencesSlice.reducer,
|
||||
},
|
||||
// Make connection available in thunks:
|
||||
middleware: getDefaultMiddleware =>
|
||||
getDefaultMiddleware({
|
||||
thunk: {
|
||||
extraArgument: { connection }
|
||||
}
|
||||
}).concat(apiSlice.middleware),
|
||||
getDefaultMiddleware().concat(apiSlice.middleware),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -31,6 +29,9 @@ export const store = configureStore({
|
||||
store.subscribe(
|
||||
debounce(() => {
|
||||
saveState(store.getState()[progressSlice.name]);
|
||||
|
||||
const preferencesState = store.getState()[preferencesSlice.name]
|
||||
preferencesState.isSavePreferences ? savePreferences(preferencesState) : removePreferences()
|
||||
}, 800)
|
||||
);
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import {useState, useEffect} from 'react'
|
||||
|
||||
function getWindowDimensions() {
|
||||
const {innerWidth: width, innerHeight: height } = window
|
||||
return {width, height}
|
||||
}
|
||||
|
||||
export function useWindowDimensions() {
|
||||
const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions())
|
||||
|
||||
useEffect(() => {
|
||||
function handleResize() {
|
||||
setWindowDimensions(getWindowDimensions())
|
||||
}
|
||||
window.addEventListener('resize', handleResize)
|
||||
return () => window.removeEventListener('resize', handleResize)
|
||||
|
||||
}, [])
|
||||
|
||||
return windowDimensions
|
||||
}
|
||||
+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
|
||||
@@ -0,0 +1,9 @@
|
||||
# Client
|
||||
|
||||
This document describes features of the frontent/client side.
|
||||
|
||||
|
||||
## URL
|
||||
|
||||
You can add `?lang=de` at the end of the URL to specify the language that should be set. This
|
||||
will change the user's preferred language to the one specified.
|
||||
+50
-27
@@ -46,6 +46,22 @@ This game has been developed by me.
|
||||
MakeGame
|
||||
```
|
||||
|
||||
### Local notations
|
||||
|
||||
**Important**: If you use any local notations in your game, you need to open these
|
||||
namespaces manually before `MakeGame` in order to see the notation in the inventory.
|
||||
|
||||
For example (using mathlib), write
|
||||
|
||||
```
|
||||
open BigOperators
|
||||
|
||||
MakeGame
|
||||
```
|
||||
|
||||
in order to see `∑ x in s, f x` instead of `Finset.sum s fun x => f x`.
|
||||
|
||||
|
||||
## 3. Creating a Level
|
||||
|
||||
In this tutorial we first learn about Levels. A game consists of a tree of worlds, each world has
|
||||
@@ -98,7 +114,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 +141,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 +178,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 +186,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,11 +201,12 @@ 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
|
||||
|
||||
@@ -192,25 +218,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 +244,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,7 +276,7 @@ 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.
|
||||
|
||||
+51
-3
@@ -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
|
||||
A proof inside a `Branch`-block is normally evaluated by lean, but it's discarded at the end
|
||||
so that no progress has been made on proofing the goal.
|
||||
|
||||
```
|
||||
Statement .... := by
|
||||
Hint "Huse `rw` or `rewrite`."
|
||||
Hint "use `rw` or `rewrite`."
|
||||
Branch
|
||||
rewrite [h]
|
||||
Hint "now you still need `rfl`"
|
||||
@@ -84,4 +86,50 @@ create new assumptions.
|
||||
|
||||
You can add use markdown to format your hints, for example you can use KaTex: `$\\iff$`
|
||||
|
||||
**Escaping**: Generally, if you add text inside quotes `" "` (e.g. in `Hint`) you need to escape
|
||||
backslashes, but if you provide text inside a doc comment
|
||||
`/-- -/` (e.g. in the `Statement` description) you do not!
|
||||
|
||||
TODO: Write a doc about latex/markdown options available.
|
||||
|
||||
### Commutative diagrams
|
||||
|
||||
Here is an example of how to write a commutative diagram in KaTeX:
|
||||
|
||||
$$
|
||||
\begin{CD}
|
||||
A @>{f}>> B @<{g}<< C \\
|
||||
@V{h}VV @V{i}VV @V{j}VV \\
|
||||
D @<{k}<< E @>{l}>> F \\
|
||||
@A{m}AA @A{n}AA @V{p}VV \\
|
||||
G @<{q}<< H @>{r}>> I
|
||||
\end{CD}
|
||||
$$
|
||||
|
||||
```
|
||||
$$
|
||||
\\begin{CD}
|
||||
A @>{f}>> B @<{g}<< C \\\\
|
||||
@V{h}VV @V{i}VV @V{j}VV \\\\
|
||||
D @<{k}<< E @>{l}>> F \\\\
|
||||
@A{m}AA @A{n}AA @V{p}VV \\\\
|
||||
G @<{q}<< H @>{r}>> I
|
||||
\\end{CD}
|
||||
$$
|
||||
```
|
||||
|
||||
See https://www.jmilne.org/not/Mamscd.pdf
|
||||
|
||||
### 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.
|
||||
|
||||
+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
|
||||
|
||||
+3
-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,7 @@ 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.
|
||||
|
||||
@@ -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` theorem:
|
||||
|
||||
```lean
|
||||
@[simp]
|
||||
Statement my_simp_theorem ...
|
||||
```
|
||||
+9
-8
@@ -5,11 +5,6 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Lean Game Server</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -23,13 +18,19 @@
|
||||
</p>
|
||||
<p>
|
||||
<strong>Impressum:</strong><br />
|
||||
Jon Eugster<br />
|
||||
Marcus Zibrowius, Jon Eugster<br />
|
||||
Mathematisches Institut der Heinrich-Heine-Universität Düsseldorf<br />
|
||||
Universitätsstr. 1<br />
|
||||
40225 Düsseldorf<br />
|
||||
Germany<br />
|
||||
+49 211 81-12173<br />
|
||||
<a href="https://www.math.hhu.de/en/lehrstuehle-/-personen-/-ansprechpartner/innen/lehrstuehle-des-mathematischen-instituts/lehrstuhl-fuer-algebraische-geometrie/team/jon-eugster">Contact Details</a>
|
||||
+49 211 81-14690<br />
|
||||
<a href="https://www.math.hhu.de/en/lehrstuehle-/-personen-/-ansprechpartner/innen/lehrstuehle-des-mathematischen-instituts/lehrstuhl-fuer-algebraische-geometrie/team">Contact Details</a>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Datenschutzerklärung:</strong><br />
|
||||
Without JavaScript this website cannot be used and therefore no personal data is collected
|
||||
or stored.<br />
|
||||
Our Server is located in Germany.<br />
|
||||
</p>
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
Generated
+5963
-9402
File diff suppressed because it is too large
Load Diff
+20
-7
@@ -10,7 +10,10 @@
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@fontsource/roboto": "^4.5.8",
|
||||
"@fontsource/roboto-mono": "^4.5.8",
|
||||
"@leanprover/infoview": "^0.4.3",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.5.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@mui/icons-material": "^5.11.0",
|
||||
"@mui/material": "^5.11.1",
|
||||
"@reduxjs/toolkit": "^1.9.1",
|
||||
@@ -23,23 +26,31 @@
|
||||
"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",
|
||||
"ip-anonymize": "^0.1.0",
|
||||
"lean4-infoview": "https://gitpkg.now.sh/leanprover/vscode-lean4/lean4-infoview?de0062c",
|
||||
"lean4web": "github:hhu-adam/lean4web",
|
||||
"octokit": "^2.0.14",
|
||||
"lean4monaco": "^1.0.29",
|
||||
"memfs": "^4.11.1",
|
||||
"octokit": "^3.1.2",
|
||||
"path-browserify": "^1.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-contenteditable": "^3.3.7",
|
||||
"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",
|
||||
"react-router-dom": "^6.5.0",
|
||||
"react-split": "^2.0.14",
|
||||
"rehype-katex": "^6.0.2",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-math": "^5.1.1",
|
||||
"request": "^2.88.2",
|
||||
"request-progress": "^3.0.0",
|
||||
"vite": "^4.5.0",
|
||||
"vite-plugin-node-polyfills": "0.17.0",
|
||||
"vite-plugin-static-copy": "^0.17.0",
|
||||
"vite-plugin-svgr": "^4.1.0",
|
||||
"vscode-ws-jsonrpc": "^2.0.1",
|
||||
@@ -47,6 +58,7 @@
|
||||
"ws": "^8.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codingame/esbuild-import-meta-url-plugin": "https://gitpkg.vercel.app/hhu-adam/lean4monaco/esbuild-import-meta-url-plugin?main",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
|
||||
"@redux-devtools/core": "^3.13.1",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
@@ -54,11 +66,11 @@
|
||||
"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",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^4.9.4",
|
||||
"url-loader": "^4.1.1"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -69,7 +81,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": [
|
||||
|
||||
+17
-7
@@ -1,16 +1,21 @@
|
||||
#/bin/bash
|
||||
|
||||
ELAN_HOME=$(lake env printenv ELAN_HOME)
|
||||
# Note: This fails if there is no default toolchain installed
|
||||
LEAN_ROOT="$(cd $1 && lean --print-prefix)"
|
||||
LEAN_PATH="$(cd $1 && lake env printenv LEAN_PATH)"
|
||||
|
||||
# $1 : the game directory
|
||||
# $2 : the lean4game folder
|
||||
# $3 : the gameserver executable
|
||||
|
||||
# # print commands as they are executed
|
||||
# set -x
|
||||
|
||||
(exec bwrap\
|
||||
--bind $2 /lean4game \
|
||||
--bind $1 /game \
|
||||
--bind $ELAN_HOME /elan \
|
||||
--bind /usr /usr \
|
||||
--ro-bind $2 /lean4game \
|
||||
--ro-bind $1 /game \
|
||||
--ro-bind "$LEAN_ROOT" /lean \
|
||||
--ro-bind /usr /usr \
|
||||
--dev /dev \
|
||||
--proc /proc \
|
||||
--symlink usr/lib /lib\
|
||||
@@ -18,8 +23,9 @@ ELAN_HOME=$(lake env printenv ELAN_HOME)
|
||||
--symlink usr/bin /bin\
|
||||
--symlink usr/sbin /sbin\
|
||||
--clearenv \
|
||||
--setenv PATH "/elan/bin:/bin" \
|
||||
--setenv ELAN_HOME "/elan" \
|
||||
--setenv PATH "/lean/bin" \
|
||||
--setenv LAKE "/no" `# tries to invoke git otherwise` \
|
||||
--setenv LEAN_PATH "$LEAN_PATH" \
|
||||
--unshare-user \
|
||||
--unshare-pid \
|
||||
--unshare-net \
|
||||
@@ -29,3 +35,7 @@ ELAN_HOME=$(lake env printenv ELAN_HOME)
|
||||
--chdir "/game/.lake/packages/GameServer/server/.lake/build/bin/" \
|
||||
./gameserver --server /game
|
||||
)
|
||||
|
||||
# TODO
|
||||
# --chdir "/game/.lake/packages/GameServer/server/.lake/build/bin/" \
|
||||
# ./gameserver --server /game
|
||||
|
||||
+146
-95
@@ -1,15 +1,18 @@
|
||||
import { WebSocketServer } from 'ws';
|
||||
import { WebSocketServer } from 'ws'
|
||||
import express from 'express'
|
||||
import path from 'path'
|
||||
import * as cp from 'child_process';
|
||||
import * as url from 'url';
|
||||
import * as rpc from 'vscode-ws-jsonrpc';
|
||||
import * as jsonrpcserver from 'vscode-ws-jsonrpc/server';
|
||||
import os from 'os';
|
||||
import fs from 'fs';
|
||||
import anonymize from 'ip-anonymize';
|
||||
import * as cp from 'child_process'
|
||||
import * as url from 'url'
|
||||
import * as rpc from 'vscode-ws-jsonrpc'
|
||||
import * as path from 'path'
|
||||
import * as jsonrpcserver from 'vscode-ws-jsonrpc/server'
|
||||
// import nocache from 'nocache'
|
||||
import anonymize from 'ip-anonymize'
|
||||
import os from 'os'
|
||||
import fs from 'fs'
|
||||
import http from 'http'
|
||||
import https from 'https'
|
||||
|
||||
import { importTrigger, importStatus } from './import.mjs'
|
||||
// import fs from 'fs'
|
||||
|
||||
/**
|
||||
* Add a game here if the server should keep a queue of pre-loaded games ready at all times.
|
||||
@@ -19,40 +22,75 @@ import { importTrigger, importStatus } from './import.mjs'
|
||||
const queueLength = {
|
||||
"g/hhu-adam/robo": 2,
|
||||
"g/hhu-adam/nng4": 5,
|
||||
"g/djvelleman/stg4": 2,
|
||||
"g/djvelleman/stg4": 0,
|
||||
"g/trequetrum/lean4game-logic": 0,
|
||||
}
|
||||
|
||||
const __filename = url.fileURLToPath(import.meta.url);
|
||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
||||
let socketCounter = 0
|
||||
|
||||
const app = express()
|
||||
function logStats() {
|
||||
console.log(`[${new Date()}] Number of open sockets - ${socketCounter}`)
|
||||
console.log(`[${new Date()}] Free RAM - ${Math.round(os.freemem() / 1024 / 1024)} / ${Math.round(os.totalmem() / 1024 / 1024)} MB`)
|
||||
}
|
||||
|
||||
const PORT = process.env.PORT || 8080;
|
||||
const __filename = url.fileURLToPath(import.meta.url)
|
||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
||||
|
||||
var router = express.Router();
|
||||
|
||||
router.get('/import/status/:owner/:repo', importStatus)
|
||||
router.get('/import/trigger/:owner/:repo', importTrigger)
|
||||
|
||||
const server = app
|
||||
.use(express.static(path.join(__dirname, '..', 'client', 'dist'))) // TODO: add a dist folder from inside the game
|
||||
.use('/data/g/:owner/:repo/*', (req, res, next) => {
|
||||
const owner = req.params.owner;
|
||||
const repo = req.params.repo
|
||||
const filename = req.params[0];
|
||||
req.url = filename;
|
||||
express.static(path.join(getGameDir(owner,repo),".lake","gamedata"))(req, res, next);
|
||||
})
|
||||
.use('/', router)
|
||||
.listen(PORT, () => console.log(`Listening on ${PORT}`));
|
||||
|
||||
const wss = new WebSocketServer({ server })
|
||||
|
||||
var socketCounter = 0
|
||||
|
||||
const environment = process.env.NODE_ENV
|
||||
const isDevelopment = environment === 'development'
|
||||
|
||||
const crtFile = process.env.SSL_CRT_FILE
|
||||
const keyFile = process.env.SSL_KEY_FILE
|
||||
|
||||
const app = express()
|
||||
|
||||
var router = express.Router()
|
||||
|
||||
// Paths for game import logic
|
||||
router.get('/import/status/:owner/:repo', importStatus)
|
||||
router.get('/import/trigger/:owner/:repo', importTrigger)
|
||||
|
||||
app.use(express.static(path.join(__dirname, '..', 'client', 'dist'))) // TODO: add a dist folder from inside the game
|
||||
app.use('/i18n/g/:owner/:repo/:lang/*', (req, res, next) => {
|
||||
const owner = req.params.owner
|
||||
const repo = req.params.repo
|
||||
const lang = req.params.lang
|
||||
const filename = req.params[0]
|
||||
req.url = filename
|
||||
express.static(path.join(getGameDir(owner,repo),".i18n",lang))(req, res, next)
|
||||
})
|
||||
app.use('/data/g/:owner/:repo/*', (req, res, next) => {
|
||||
const owner = req.params.owner
|
||||
const repo = req.params.repo
|
||||
const filename = req.params[0]
|
||||
req.url = filename
|
||||
express.static(path.join(getGameDir(owner,repo),".lake","gamedata"))(req, res, next)
|
||||
})
|
||||
app.use('/', router)
|
||||
|
||||
let server
|
||||
if (crtFile && keyFile) {
|
||||
var privateKey = fs.readFileSync(keyFile, 'utf8')
|
||||
var certificate = fs.readFileSync(crtFile, 'utf8')
|
||||
var credentials = {key: privateKey, cert: certificate}
|
||||
|
||||
const PORT = process.env.PORT ?? 443
|
||||
server = https.createServer(credentials, app).listen(PORT,
|
||||
() => console.log(`HTTPS on port ${PORT}`))
|
||||
|
||||
// redirect http to https
|
||||
express().get('*', function(req, res) {
|
||||
res.redirect('https://' + req.headers.host + req.url).listen(80)
|
||||
})
|
||||
} else {
|
||||
const PORT = process.env.PORT ?? 8080
|
||||
server = app.listen(PORT,
|
||||
() => console.log(`HTTP on port ${PORT}`))
|
||||
}
|
||||
|
||||
const wss = new WebSocketServer({ server })
|
||||
|
||||
/** We keep queues of started Lean Server processes to be ready when a user arrives */
|
||||
const queue = {}
|
||||
|
||||
@@ -76,7 +114,8 @@ function getGameDir(owner, repo) {
|
||||
}
|
||||
|
||||
let game_dir = (owner == 'local') ?
|
||||
path.join(__dirname, '..', '..', repo) : // note: here we need `repo` to be case sensitive
|
||||
// note: in the local case we need `repo` to be case sensitive
|
||||
path.join(__dirname, '..', '..', repo) :
|
||||
path.join(__dirname, '..', 'games', `${owner}`, `${repo.toLowerCase()}`)
|
||||
|
||||
if(!fs.existsSync(game_dir)) {
|
||||
@@ -84,16 +123,20 @@ function getGameDir(owner, repo) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return game_dir;
|
||||
return game_dir
|
||||
}
|
||||
|
||||
function startServerProcess(owner, repo) {
|
||||
|
||||
let game_dir = getGameDir(owner, repo)
|
||||
if (!game_dir) return;
|
||||
if (!game_dir) { return }
|
||||
|
||||
let serverProcess
|
||||
if (isDevelopment) {
|
||||
console.warn("Running without Bubblewrap container!")
|
||||
|
||||
// TODO: use the gameserver
|
||||
// serverProcess = cp.spawn("lean", ["--server"], { cwd: game_dir })
|
||||
|
||||
let args = ["--server", game_dir]
|
||||
let binDir = path.join(game_dir, ".lake", "packages", "GameServer", "server", ".lake", "build", "bin")
|
||||
// Note: `cwd` is important to be the `bin` directory as `Watchdog` calls `./gameserver` again
|
||||
@@ -106,19 +149,24 @@ function startServerProcess(owner, repo) {
|
||||
{ cwd: path.join(__dirname, "..", "server", ".lake", "build", "bin") })
|
||||
}
|
||||
} else {
|
||||
serverProcess = cp.spawn("./bubblewrap.sh",
|
||||
console.info("Running with Bubblewrap container.")
|
||||
serverProcess = cp.spawn(
|
||||
"./bubblewrap.sh",
|
||||
[ game_dir, path.join(__dirname, '..')],
|
||||
{ cwd: __dirname })
|
||||
{ cwd: __dirname }
|
||||
)
|
||||
}
|
||||
|
||||
serverProcess.stderr.on('data', data =>
|
||||
console.error(`Lean Server: ${data}`)
|
||||
)
|
||||
serverProcess.on('error', error =>
|
||||
console.error(`Launching Lean Server failed: ${error}`)
|
||||
)
|
||||
if (serverProcess.stderr !== null) {
|
||||
serverProcess.stderr.on('data', data =>
|
||||
console.error(`Lean Server: ${data}`)
|
||||
)
|
||||
}
|
||||
serverProcess.on('close', (code, _signal) => {
|
||||
console.log(`Lean server exited with code ${code}`)
|
||||
})
|
||||
|
||||
return serverProcess
|
||||
}
|
||||
|
||||
@@ -143,61 +191,64 @@ function fillQueue(tag) {
|
||||
// }
|
||||
// }
|
||||
|
||||
const urlRegEx = /^\/websocket\/g\/([\w.-]+)\/([\w.-]+)$/
|
||||
|
||||
wss.addListener("connection", function(ws, req) {
|
||||
const reRes = urlRegEx.exec(req.url)
|
||||
if (!reRes) { console.error(`Connection refused because of invalid URL: ${req.url}`); return; }
|
||||
const owner = reRes[1]
|
||||
const repo = reRes[2]
|
||||
// server expects URL of the form `/websocket/g/{owner}/{repo}`
|
||||
const urlRegEx = /^\/websocket\/g\/([\w.-]+)\/([\w.-]+)\/?$/
|
||||
const reRes = urlRegEx.exec(req.url)
|
||||
if (!reRes) { console.error(`Connection refused because of invalid URL: ${req.url}`); return }
|
||||
const owner = reRes[1]
|
||||
const repo = reRes[2]
|
||||
const tag = getTag(owner, repo)
|
||||
|
||||
const tag = getTag(owner, repo)
|
||||
const ip = anonymize(req.headers['x-forwarded-for'] || req.socket.remoteAddress)
|
||||
let ps
|
||||
if (!queue[tag] || queue[tag].length == 0) {
|
||||
ps = startServerProcess(owner, repo)
|
||||
} else {
|
||||
console.info('Got process from the queue')
|
||||
ps = queue[tag].shift() // Pick the first Lean process; it's likely to be ready immediately
|
||||
// TODO
|
||||
// async () => {
|
||||
// fillQueue(tag)
|
||||
// }
|
||||
}
|
||||
if (ps == null) {
|
||||
console.error('server process is undefined/null')
|
||||
return
|
||||
}
|
||||
|
||||
let ps
|
||||
if (!queue[tag] || queue[tag].length == 0) {
|
||||
ps = startServerProcess(owner, repo)
|
||||
} else {
|
||||
console.info('Got process from the queue')
|
||||
ps = queue[tag].shift() // Pick the first Lean process; it's likely to be ready immediately
|
||||
fillQueue(tag)
|
||||
const socket = {
|
||||
onMessage: (cb) => { ws.on("message", cb) },
|
||||
onError: (cb) => { ws.on("error", cb) },
|
||||
onClose: (cb) => { ws.on("close", cb) },
|
||||
send: (data, cb) => { ws.send(data,cb) }
|
||||
}
|
||||
const reader = new rpc.WebSocketMessageReader(socket)
|
||||
const writer = new rpc.WebSocketMessageWriter(socket)
|
||||
const socketConnection = jsonrpcserver.createConnection(reader, writer, () => ws.close())
|
||||
const serverConnection = jsonrpcserver.createProcessStreamConnection(ps)
|
||||
socketConnection.forward(serverConnection, message => {
|
||||
if (isDevelopment) {
|
||||
console.log(`CLIENT: ${JSON.stringify(message)}`)
|
||||
}
|
||||
|
||||
if (ps == null) {
|
||||
console.error('server process is undefined/null')
|
||||
return
|
||||
return message
|
||||
})
|
||||
serverConnection.forward(socketConnection, message => {
|
||||
if (isDevelopment) {
|
||||
console.log(`SERVER: ${JSON.stringify(message)}`)
|
||||
}
|
||||
return message
|
||||
})
|
||||
|
||||
socketCounter += 1;
|
||||
const ip = anonymize(req.headers['x-forwarded-for'] || req.socket.remoteAddress)
|
||||
console.log(`[${new Date()}] Socket opened - ${ip}`)
|
||||
ws.on('close', () => {
|
||||
console.log(`[${new Date()}] Socket closed - ${ip}`)
|
||||
socketCounter -= 1
|
||||
})
|
||||
|
||||
const socket = {
|
||||
onMessage: (cb) => { ws.on("message", cb) },
|
||||
onError: (cb) => { ws.on("error", cb) },
|
||||
onClose: (cb) => { ws.on("close", cb) },
|
||||
send: (data, cb) => { ws.send(data,cb) }
|
||||
}
|
||||
const reader = new rpc.WebSocketMessageReader(socket)
|
||||
const writer = new rpc.WebSocketMessageWriter(socket)
|
||||
const socketConnection = jsonrpcserver.createConnection(reader, writer, () => ws.close())
|
||||
const serverConnection = jsonrpcserver.createProcessStreamConnection(ps)
|
||||
socketConnection.forward(serverConnection, message => {
|
||||
if (isDevelopment) {console.log(`CLIENT: ${JSON.stringify(message)}`)}
|
||||
return message;
|
||||
})
|
||||
serverConnection.forward(socketConnection, message => {
|
||||
if (isDevelopment) {console.log(`SERVER: ${JSON.stringify(message)}`)}
|
||||
return message;
|
||||
});
|
||||
socketConnection.onClose(() => serverConnection.dispose())
|
||||
serverConnection.onClose(() => socketConnection.dispose())
|
||||
|
||||
console.log(`[${new Date()}] Number of open sockets - ${socketCounter}`)
|
||||
console.log(`[${new Date()}] Free RAM - ${Math.round(os.freemem() / 1024 / 1024)} / ${Math.round(os.totalmem() / 1024 / 1024)} MB`)
|
||||
|
||||
ws.on('close', () => {
|
||||
console.log(`[${new Date()}] Socket closed - ${ip}`)
|
||||
socketCounter -= 1
|
||||
})
|
||||
|
||||
socketConnection.onClose(() => serverConnection.dispose())
|
||||
serverConnection.onClose(() => socketConnection.dispose())
|
||||
console.log(`[${new Date()}] Socket opened - ${ip}`)
|
||||
socketCounter += 1
|
||||
logStats()
|
||||
})
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import GameServer.FileWorker
|
||||
import GameServer.Watchdog
|
||||
import GameServer.Commands
|
||||
|
||||
-- TODO: The only reason we import `Commands` is so that it gets built to on `lake build`
|
||||
@@ -10,13 +9,9 @@ unsafe def main : List String → IO UInt32 := fun args => do
|
||||
|
||||
Lean.enableInitializersExecution
|
||||
|
||||
-- TODO: remove this argument
|
||||
if args[0]? == some "--server" then
|
||||
MyServer.Watchdog.watchdogMain args
|
||||
else if args[0]? == some "--worker" then
|
||||
MyServer.FileWorker.workerMain {}
|
||||
GameServer.FileWorker.workerMain {} args
|
||||
else
|
||||
e.putStrLn s!"Expected `--server` or `--worker`"
|
||||
e.putStrLn s!"Expected `--server`"
|
||||
return 1
|
||||
|
||||
|
||||
-- TODO: Potentially it could be useful to pass in the `gameName` via the websocket connection
|
||||
|
||||
+212
-89
@@ -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,18 +101,19 @@ 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
|
||||
@@ -124,7 +132,7 @@ elab "CoverImage" t:str : command => do
|
||||
|
||||
/-! # Inventory
|
||||
|
||||
The inventory contains docs for tactics, lemmas, and definitions. These are all locked
|
||||
The inventory contains docs for tactics, theorems, and definitions. These are all locked
|
||||
in the first level and get enabled during the game.
|
||||
-/
|
||||
|
||||
@@ -141,38 +149,40 @@ 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
|
||||
displayName := name.getId.toString
|
||||
content := doc })
|
||||
|
||||
/-- Documentation entry of a lemma. Example:
|
||||
/-- Documentation entry of a theorem. Example:
|
||||
|
||||
```
|
||||
LemmaDoc Nat.succ_pos as "succ_pos" in "Nat" "says `0 < n.succ`, etc."
|
||||
TheoremDoc Nat.succ_pos as "succ_pos" in "Nat" "says `0 < n.succ`, etc."
|
||||
```
|
||||
|
||||
* The first identifier is used in the commands `[New/Only/Disabled]Lemma`.
|
||||
It is preferably the true name of the lemma. However, this is not required.
|
||||
* The first identifier is used in the commands `[New/Only/Disabled]Theorem`.
|
||||
It is preferably the true name of the theorem. However, this is not required.
|
||||
* The string following `as` is the displayed name (in the Inventory).
|
||||
* The identifier after `in` is the category to group lemmas by (in the Inventory).
|
||||
* The identifier after `in` is the category to group theorems by (in the Inventory).
|
||||
* The description is a string supporting Markdown.
|
||||
|
||||
Use `[[mathlib_doc]]` in the string to insert a link to the mathlib doc page. This requires
|
||||
The lemma/definition to have the same fully qualified name as in mathlib.
|
||||
The theorem/definition to have the same fully qualified name as in mathlib.
|
||||
-/
|
||||
elab doc:docComment ? "LemmaDoc" name:ident "as" displayName:str "in" category:str content:str ? :
|
||||
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
|
||||
type := .Theorem
|
||||
name := name.getId
|
||||
category := category.getString
|
||||
displayName := displayName.getString
|
||||
content := doc })
|
||||
-- TODO: Catch the following behaviour.
|
||||
-- 1. if `LemmaDoc` appears in the same file as `Statement`, it will silently use
|
||||
-- 1. if `TheoremDoc` appears in the same file as `Statement`, it will silently use
|
||||
-- it but display the info that it wasn't found in `Statement`
|
||||
-- 2. if it appears in a later file, however, it will silently not do anything and keep
|
||||
-- the first one.
|
||||
@@ -190,10 +200,11 @@ DefinitionDoc Function.Bijective as "Bijective" "defined as `Injective f ∧ Sur
|
||||
* The description is a string supporting Markdown.
|
||||
|
||||
Use `[[mathlib_doc]]` in the string to insert a link to the mathlib doc page. This requires
|
||||
The lemma/definition to have the same fully qualified name as in mathlib.
|
||||
The theorem/definition to have the same fully qualified name as in mathlib.
|
||||
-/
|
||||
elab 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,
|
||||
@@ -202,8 +213,13 @@ elab doc:docComment ? "DefinitionDoc" name:ident "as" displayName:str template:s
|
||||
|
||||
/-! ## Add inventory items -/
|
||||
|
||||
def checkCommandNotDuplicated (items : Array Name) (cmd := "Command") : CommandElabM Unit := do
|
||||
if ¬ items.isEmpty then
|
||||
logWarning s!"You should only use one `{cmd}` per level, but it takes multiple arguments: `{cmd} obj₁ obj₂ obj₃`!"
|
||||
|
||||
/-- Declare tactics that are introduced by this level. -/
|
||||
elab "NewTactic" args:ident* : command => do
|
||||
checkCommandNotDuplicated ((←getCurLevel).tactics.new) "NewTactic"
|
||||
for name in ↑args do
|
||||
checkInventoryDoc .Tactic name -- TODO: Add (template := "[docstring]")
|
||||
modifyCurLevel fun level => pure {level with
|
||||
@@ -211,23 +227,26 @@ elab "NewTactic" args:ident* : command => do
|
||||
|
||||
/-- Declare tactics that are introduced by this level but do not show up in inventory. -/
|
||||
elab "NewHiddenTactic" args:ident* : command => do
|
||||
checkCommandNotDuplicated ((←getCurLevel).tactics.hidden) "NewHiddenTactic"
|
||||
for name in ↑args do
|
||||
checkInventoryDoc .Tactic name (template := "")
|
||||
modifyCurLevel fun level => pure {level with
|
||||
tactics := {level.tactics with new := level.tactics.new ++ args.map (·.getId),
|
||||
hidden := level.tactics.hidden ++ args.map (·.getId)}}
|
||||
|
||||
/-- Declare lemmas that are introduced by this level. -/
|
||||
elab "NewLemma" args:ident* : command => do
|
||||
/-- Declare theorems that are introduced by this level. -/
|
||||
elab "NewTheorem" args:ident* : command => do
|
||||
checkCommandNotDuplicated ((←getCurLevel).theorems.new) "NewTheorem"
|
||||
for name in ↑args do
|
||||
try let _decl ← getConstInfo name.getId catch
|
||||
| _ => logErrorAt name m!"unknown identifier '{name}'."
|
||||
checkInventoryDoc .Lemma name -- TODO: Add (template := "[mathlib]")
|
||||
checkInventoryDoc .Theorem name -- TODO: Add (template := "[mathlib]")
|
||||
modifyCurLevel fun level => pure {level with
|
||||
lemmas := {level.lemmas with new := args.map (·.getId)}}
|
||||
theorems := {level.theorems with new := args.map (·.getId)}}
|
||||
|
||||
/-- Declare definitions that are introduced by this level. -/
|
||||
elab "NewDefinition" args:ident* : command => do
|
||||
checkCommandNotDuplicated ((←getCurLevel).definitions.new) "NewDefinition"
|
||||
for name in ↑args do checkInventoryDoc .Definition name -- TODO: Add (template := "[mathlib]")
|
||||
modifyCurLevel fun level => pure {level with
|
||||
definitions := {level.definitions with new := args.map (·.getId)}}
|
||||
@@ -235,55 +254,89 @@ elab "NewDefinition" args:ident* : command => do
|
||||
/-- Declare tactics that are temporarily disabled in this level.
|
||||
This is ignored if `OnlyTactic` is set. -/
|
||||
elab "DisabledTactic" args:ident* : command => do
|
||||
checkCommandNotDuplicated ((←getCurLevel).tactics.disabled) "DisabledTactic"
|
||||
for name in ↑args do checkInventoryDoc .Tactic name
|
||||
modifyCurLevel fun level => pure {level with
|
||||
tactics := {level.tactics with disabled := args.map (·.getId)}}
|
||||
|
||||
/-- Declare lemmas that are temporarily disabled in this level.
|
||||
This is ignored if `OnlyLemma` is set. -/
|
||||
elab "DisabledLemma" args:ident* : command => do
|
||||
for name in ↑args do checkInventoryDoc .Lemma name
|
||||
/-- Declare theorems that are temporarily disabled in this level.
|
||||
This is ignored if `OnlyTheorem` is set. -/
|
||||
elab "DisabledTheorem" args:ident* : command => do
|
||||
checkCommandNotDuplicated ((←getCurLevel).theorems.disabled) "DisabledTheorem"
|
||||
for name in ↑args do checkInventoryDoc .Theorem name
|
||||
modifyCurLevel fun level => pure {level with
|
||||
lemmas := {level.lemmas with disabled := args.map (·.getId)}}
|
||||
theorems := {level.theorems with disabled := args.map (·.getId)}}
|
||||
|
||||
/-- Declare definitions that are temporarily disabled in this level -/
|
||||
elab "DisabledDefinition" args:ident* : command => do
|
||||
checkCommandNotDuplicated ((←getCurLevel).definitions.disabled) "DisabledDefinition"
|
||||
for name in ↑args do checkInventoryDoc .Definition name
|
||||
modifyCurLevel fun level => pure {level with
|
||||
definitions := {level.definitions with disabled := args.map (·.getId)}}
|
||||
|
||||
/-- Temporarily disable all tactics except the ones declared here -/
|
||||
elab "OnlyTactic" args:ident* : command => do
|
||||
checkCommandNotDuplicated ((←getCurLevel).tactics.only) "OnlyTactic"
|
||||
for name in ↑args do checkInventoryDoc .Tactic name
|
||||
modifyCurLevel fun level => pure {level with
|
||||
tactics := {level.tactics with only := args.map (·.getId)}}
|
||||
|
||||
/-- Temporarily disable all lemmas except the ones declared here -/
|
||||
elab "OnlyLemma" args:ident* : command => do
|
||||
for name in ↑args do checkInventoryDoc .Lemma name
|
||||
/-- Temporarily disable all theorems except the ones declared here -/
|
||||
elab "OnlyTheorem" args:ident* : command => do
|
||||
checkCommandNotDuplicated ((←getCurLevel).theorems.only) "OnlyTheorem"
|
||||
for name in ↑args do checkInventoryDoc .Theorem name
|
||||
modifyCurLevel fun level => pure {level with
|
||||
lemmas := {level.lemmas with only := args.map (·.getId)}}
|
||||
theorems := {level.theorems with only := args.map (·.getId)}}
|
||||
|
||||
/-- Temporarily disable all definitions except the ones declared here.
|
||||
This is ignored if `OnlyDefinition` is set. -/
|
||||
elab "OnlyDefinition" args:ident* : command => do
|
||||
checkCommandNotDuplicated ((←getCurLevel).definitions.only) "OnlyDefinition"
|
||||
for name in ↑args do checkInventoryDoc .Definition name
|
||||
modifyCurLevel fun level => pure {level with
|
||||
definitions := {level.definitions with only := args.map (·.getId)}}
|
||||
|
||||
/-- Define which tab of Lemmas is opened by default. Usage: `LemmaTab "Nat"`.
|
||||
/-- Define which tab of Theorems is opened by default. Usage: `TheoremTab "Nat"`.
|
||||
If omitted, the current tab will remain open. -/
|
||||
elab "LemmaTab" category:str : command =>
|
||||
modifyCurLevel fun level => pure {level with lemmaTab := category.getString}
|
||||
elab "TheoremTab" category:str : command =>
|
||||
modifyCurLevel fun level => pure {level with theoremTab := category.getString}
|
||||
|
||||
/-! # 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
|
||||
|
||||
let docContent ← parseDocComment doc
|
||||
-- Add an optional tactic sequence that the engine executes before the game starts
|
||||
let preambleSeq : TSyntax `Lean.Parser.Tactic.tacticSeq ← match preamble with
|
||||
| some arg => match arg with
|
||||
| `(preambleArg| (preamble := $tac)) => pure tac
|
||||
| _ => `(Parser.Tactic.tacticSeq| skip)
|
||||
| none => `(Parser.Tactic.tacticSeq| skip)
|
||||
|
||||
-- Translate the docstring of the `Statement`
|
||||
let docComment : Option String ← match (← parseDocComment doc) 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 := {} })
|
||||
@@ -293,42 +346,45 @@ elab doc:docComment ? attrs:Parser.Term.attributes ?
|
||||
let defaultDeclName : Ident := mkIdent <| (← getCurGame).name ++ (← getCurWorld).name ++
|
||||
("level" ++ toString lvlIdx : String)
|
||||
|
||||
-- Collect all used tactics/lemmas in the sample proof:
|
||||
let usedInventory ← match val with
|
||||
-- Collect all used tactics/theorems in the sample proof:
|
||||
let usedInventory : UsedInventory ← match val with
|
||||
| `(Parser.Command.declVal| := $proof:term) => do
|
||||
collectUsedInventory proof
|
||||
| _ => throwError "expected `:=`"
|
||||
|
||||
-- modify the proof to start with `let_intros` and any specified preamble sequence.
|
||||
let modifiedVal ← match val with
|
||||
| `(Parser.Command.declVal| := by $proof) =>
|
||||
`(Parser.Command.declVal| := by {let_intros; $(⟨preambleSeq⟩); $(⟨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 $modifiedVal)
|
||||
elabCommand thmStatement
|
||||
-- Check that statement has a docs entry.
|
||||
checkInventoryDoc .Lemma name (name := fullName) (template := docContent)
|
||||
|
||||
checkInventoryDoc .Theorem name (name := fullName) (template := docComment)
|
||||
else
|
||||
let thmStatement ← `(command| $[$doc]? $[$attrs:attributes]? theorem $name $sig $val)
|
||||
let thmStatement ← `(command| $[$doc]? $[$attrs:attributes]? theorem $name $sig $modifiedVal)
|
||||
elabCommand thmStatement
|
||||
-- Check that statement has a docs entry.
|
||||
checkInventoryDoc .Lemma name (name := fullName) (template := docContent)
|
||||
|
||||
checkInventoryDoc .Theorem name (name := fullName) (template := docComment)
|
||||
| none =>
|
||||
let thmStatement ← `(command| $[$doc]? $[$attrs:attributes]? theorem $defaultDeclName $sig $val)
|
||||
let thmStatement ← `(command| $[$doc]? $[$attrs:attributes]? theorem $defaultDeclName $sig $modifiedVal)
|
||||
elabCommand thmStatement
|
||||
|
||||
let msgs := (← get).messages
|
||||
|
||||
let mut hints := #[]
|
||||
let mut nonHintMsgs := #[]
|
||||
for msg in msgs.msgs do
|
||||
@@ -338,14 +394,45 @@ elab doc:docComment ? attrs:Parser.Term.attributes ?
|
||||
.tagged `Hint $
|
||||
.nest strict $
|
||||
.nest hidden $
|
||||
.nest defeq $
|
||||
.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
|
||||
defeq := defeq == 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
|
||||
@@ -371,8 +458,9 @@ 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
|
||||
descrText := docComment
|
||||
statementName := match statementName with
|
||||
| none => default
|
||||
| some name => currNamespace ++ name.getId
|
||||
@@ -380,16 +468,20 @@ 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} }
|
||||
theorems := {level.theorems with used := usedInventory.theorems.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.
|
||||
-/
|
||||
elab (name := GameServer.Tactic.Hint) "Hint" args:hintArg* msg:interpolatedStr(term) : tactic => do
|
||||
let mut strict := false
|
||||
let mut hidden := false
|
||||
let mut defeq := true
|
||||
|
||||
-- remove spaces at the beginning of new lines
|
||||
let msg := TSyntax.mk $ msg.raw.setArgs $ ← msg.raw.getArgs.mapM fun m => do
|
||||
@@ -411,6 +503,8 @@ elab (name := GameServer.Tactic.Hint) "Hint" args:hintArg* msg:interpolatedStr(t
|
||||
| `(hintArg| (strict := false)) => strict := false
|
||||
| `(hintArg| (hidden := true)) => hidden := true
|
||||
| `(hintArg| (hidden := false)) => hidden := false
|
||||
| `(hintArg| (defeq := true)) => defeq := true
|
||||
| `(hintArg| (defeq := false)) => defeq := false
|
||||
| _ => throwUnsupportedSyntax
|
||||
|
||||
let goal ← Tactic.getMainGoal
|
||||
@@ -434,6 +528,7 @@ elab (name := GameServer.Tactic.Hint) "Hint" args:hintArg* msg:interpolatedStr(t
|
||||
.tagged `Hint $
|
||||
.nest (if strict then 1 else 0) $
|
||||
.nest (if hidden then 1 else 0) $
|
||||
.nest (if defeq then 1 else 0) $
|
||||
.compose (.ofGoal textmvar.mvarId!) (.ofGoal goal)
|
||||
|
||||
/-- This tactic allows us to execute an alternative sequence of tactics, but without affecting the
|
||||
@@ -445,7 +540,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."
|
||||
|
||||
@@ -505,7 +601,7 @@ elab "Template" tacs:tacticSeq : tactic => do
|
||||
-- open Lean Meta Elab Command Tactic Simp
|
||||
|
||||
-- def Lean.Meta.SimpTheorems.hasAttribute (d : SimpTheorems) (decl : Name) :=
|
||||
-- d.isLemma (.decl decl) || d.isDeclToUnfold decl
|
||||
-- d.isTheorem (.decl decl) || d.isDeclToUnfold decl
|
||||
|
||||
-- def isInSimpset (simpAttr decl: Name) : CoreM Bool := do
|
||||
-- let .some simpDecl ←getSimpExtension? simpAttr | return false
|
||||
@@ -523,8 +619,8 @@ def Parser.dependency := Parser.sepBy1Indent Parser.ident "→"
|
||||
/-- Manually add a dependency between two worlds.
|
||||
|
||||
Normally, the dependencies are computed automatically by the
|
||||
tactics & lemmas used in the example
|
||||
proof and the ones introduced by `NewLemma`/`NewTactic`.
|
||||
tactics & theorems used in the example
|
||||
proof and the ones introduced by `NewTheorem`/`NewTactic`.
|
||||
Use the command `Dependency World₁ → World₂` to add a manual edge to the graph,
|
||||
for example if the only dependency between the worlds is given by
|
||||
the narrative. -/
|
||||
@@ -569,10 +665,10 @@ elab "MakeGame" : command => do
|
||||
s!"[mathlib doc](https://leanprover-community.github.io/mathlib4_docs/find/?pattern={name}#doc)"
|
||||
|
||||
match item.type with
|
||||
| .Lemma =>
|
||||
| .Theorem =>
|
||||
modifyEnv (inventoryExt.addEntry · { item with
|
||||
content := content
|
||||
-- Add the lemma statement to the doc
|
||||
-- Add the theorem statement to the doc
|
||||
statement := (← getStatementString name)
|
||||
})
|
||||
| _ =>
|
||||
@@ -597,30 +693,33 @@ elab "MakeGame" : command => do
|
||||
for (worldId, world) in allWorlds do
|
||||
let mut usedItems : HashSet Name := {}
|
||||
let mut newItems : HashSet Name := {}
|
||||
for inventoryType in #[.Tactic, .Definition, .Lemma] do
|
||||
for inventoryType in #[.Tactic, .Definition, .Theorem] do
|
||||
for (levelId, level) in world.levels.toArray do
|
||||
usedItems := usedItems.insertMany (level.getInventory inventoryType).used
|
||||
newItems := newItems.insertMany (level.getInventory inventoryType).new
|
||||
hiddenItems := hiddenItems.insertMany (level.getInventory inventoryType).hidden
|
||||
|
||||
-- if the previous level was named, we need to add it as a new lemma
|
||||
if inventoryType == .Lemma then
|
||||
-- if the previous level was named, we need to add it as a new theorem
|
||||
if inventoryType == .Theorem then
|
||||
match levelId with
|
||||
| 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 =>
|
||||
let name := Name.str pre s
|
||||
newItems := newItems.insert name
|
||||
|
||||
if inventoryType == .Lemma then
|
||||
if inventoryType == .Theorem then
|
||||
|
||||
-- if the last level was named, we need to add it as a new lemma
|
||||
-- if the last level was named, we need to add it as a new theorem
|
||||
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 =>
|
||||
@@ -721,42 +820,44 @@ elab "MakeGame" : command => do
|
||||
|
||||
let mut allItemsByType : HashMap InventoryType (HashSet Name) := {}
|
||||
-- Compute which inventory items are available in which level:
|
||||
for inventoryType in #[.Tactic, .Definition, .Lemma] do
|
||||
for inventoryType in #[.Tactic, .Definition, .Theorem] do
|
||||
|
||||
-- Which items are introduced in which world?
|
||||
let mut lemmaStatements : HashMap (Name × Nat) Name := {}
|
||||
let mut theoremStatements : HashMap (Name × Nat) Name := {}
|
||||
-- TODO: I believe `newItemsInWorld` has way to many elements in it which we iterate over
|
||||
-- e.g. we iterate over `ring` for `Lemma`s as well, but so far that seems to cause no problems
|
||||
-- e.g. we iterate over `ring` for `Theorem`s as well, but so far that seems to cause no problems
|
||||
let mut allItems : HashSet Name := {}
|
||||
for (worldId, world) in game.worlds.nodes.toArray do
|
||||
let mut newItems : HashSet Name := {}
|
||||
for (levelId, level) in world.levels.toArray do
|
||||
let newLemmas := (level.getInventory inventoryType).new
|
||||
newItems := newItems.insertMany newLemmas
|
||||
allItems := allItems.insertMany newLemmas
|
||||
if inventoryType == .Lemma then
|
||||
let newTheorems := (level.getInventory inventoryType).new
|
||||
newItems := newItems.insertMany newTheorems
|
||||
allItems := allItems.insertMany newTheorems
|
||||
if inventoryType == .Theorem then
|
||||
-- For levels `2, 3, …` we check if the previous level was named
|
||||
-- in which case we add it as available lemma.
|
||||
-- in which case we add it as available theorem.
|
||||
match levelId with
|
||||
| 0 => pure ()
|
||||
| 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
|
||||
-- add named statement from previous level to the available theorems.
|
||||
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 =>
|
||||
let name := Name.str pre s
|
||||
newItems := newItems.insert name
|
||||
allItems := allItems.insert name
|
||||
lemmaStatements := lemmaStatements.insert (worldId, levelId) name
|
||||
if inventoryType == .Lemma then
|
||||
-- if named, add the lemma from the last level of the world to the inventory
|
||||
theoremStatements := theoremStatements.insert (worldId, levelId) name
|
||||
if inventoryType == .Theorem then
|
||||
-- if named, add the theorem from the last level of the world to the inventory
|
||||
let i₀ := world.levels.size
|
||||
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 =>
|
||||
@@ -772,7 +873,7 @@ elab "MakeGame" : command => do
|
||||
-- Using a match statement because the error message of `Option.get!` is not helpful.
|
||||
match (← getInventoryItem? item inventoryType) with
|
||||
| none =>
|
||||
-- Note: we did have a panic here before because lemma statement and doc entry
|
||||
-- Note: we did have a panic here before because theorem statement and doc entry
|
||||
-- had mismatching namespaces
|
||||
logError m!"There is no inventory item ({inventoryType}) for: {item}."
|
||||
panic s!"Inventory item {item} not found!"
|
||||
@@ -781,6 +882,7 @@ elab "MakeGame" : command => do
|
||||
name := item
|
||||
displayName := data.displayName
|
||||
category := data.category
|
||||
altTitle := data.statement
|
||||
hidden := hiddenItems.contains item })
|
||||
|
||||
|
||||
@@ -800,6 +902,7 @@ elab "MakeGame" : command => do
|
||||
displayName := data.displayName
|
||||
category := data.category
|
||||
locked := false
|
||||
altTitle := data.statement
|
||||
hidden := hiddenItems.contains item }
|
||||
itemsInWorld := itemsInWorld.insert worldId items
|
||||
|
||||
@@ -819,12 +922,13 @@ elab "MakeGame" : command => do
|
||||
displayName := data.displayName
|
||||
category := data.category
|
||||
locked := false
|
||||
hidden := levelInfo.hidden.contains item }
|
||||
altTitle := data.statement
|
||||
hidden := hiddenItems.contains item }
|
||||
|
||||
-- add the exercise statement from the previous level
|
||||
-- if it was named
|
||||
if inventoryType == .Lemma then
|
||||
match lemmaStatements.find? (worldId, levelId) with
|
||||
if inventoryType == .Theorem then
|
||||
match theoremStatements.find? (worldId, levelId) with
|
||||
| none => pure ()
|
||||
| some name =>
|
||||
let data := (← getInventoryItem? name inventoryType).get!
|
||||
@@ -832,10 +936,17 @@ elab "MakeGame" : command => do
|
||||
name := name
|
||||
displayName := data.displayName
|
||||
category := data.category
|
||||
world := worldId
|
||||
-- from the previous level. This is fine b/c in practise levels start at 1
|
||||
level := (levelId - 1 : Nat)
|
||||
proven := true
|
||||
altTitle := data.statement
|
||||
locked := false }
|
||||
|
||||
-- add marks for `disabled` and `new` lemmas here, so that they only apply to
|
||||
-- add marks for `disabled` and `new` theorems here, so that they only apply to
|
||||
-- the current level.
|
||||
|
||||
|
||||
let itemsArray := items.toArray
|
||||
|>.insertionSort (fun a b => a.1.toString < b.1.toString)
|
||||
|>.map (·.2)
|
||||
@@ -851,4 +962,16 @@ elab "MakeGame" : command => do
|
||||
return level.setComputedInventory inventoryType itemsArray
|
||||
allItemsByType := allItemsByType.insert inventoryType allItems
|
||||
|
||||
saveGameData allItemsByType
|
||||
let getTiles (type : InventoryType) : CommandElabM (Array InventoryTile) := do
|
||||
(allItemsByType.findD type {}).toArray.mapM (fun name => do
|
||||
let some item ← getInventoryItem? name type
|
||||
| throwError "Expected item to exist: {name}"
|
||||
return item.toTile)
|
||||
let inventory : InventoryOverview := {
|
||||
theorems := (← getTiles .Theorem).map (fun tile => {tile with hidden := hiddenItems.contains tile.name})
|
||||
tactics := (← getTiles .Tactic).map (fun tile => {tile with hidden := hiddenItems.contains tile.name})
|
||||
definitions := (← getTiles .Definition).map (fun tile => {tile with hidden := hiddenItems.contains tile.name})
|
||||
theoremTab := none
|
||||
}
|
||||
|
||||
saveGameData allItemsByType inventory
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user