Compare commits

..
64 Commits
Author SHA1 Message Date
matlorr 92665b7521 Merge branch 'main' into server_capacity 2025-01-24 13:52:18 +01:00
matlorr 1845644e86 Extract fetching of stats and perform it every 2 seconds 2025-01-24 13:51:28 +01:00
Matvey Lorkish 341a9ae14b Merge pull request #281 from leanprover-community/server_capacity
Adjust memory to be approx. the same as by htop
2025-01-10 12:18:14 +01:00
matlorr 0895821baa Merge branch 'main' into server_capacity 2025-01-10 12:15:28 +01:00
matlorr 5b0715629a Adjust memory to be approx. the same as by htop 2025-01-10 12:11:06 +01:00
Matvey Lorkish 39e08194f6 Merge pull request #280 from leanprover-community/server_capacity
Fixed display error of server capacity by performing rounding as last…
2025-01-10 10:16:09 +01:00
matlorr 451dc262b2 Fixed display error of server capacity by performing rounding as last step. 2025-01-10 10:13:36 +01:00
matlorr 52898d93e7 Fix turning stats to zero by casting them to int before multiplying. 2024-12-28 11:13:11 +01:00
matlorr 5f30572741 Merge branch 'main' of github.com:leanprover-community/lean4game 2024-12-28 10:10:14 +01:00
matlorr 76f2414bd5 Refactor stats.sh to be more efficient. 2024-12-28 10:08:53 +01:00
Jon Eugster f1f9325c54 update gitignore 2024-12-22 11:18:00 +01:00
matlorr bd3375ada7 Create CPU-usage script 2024-12-20 17:09:12 +01:00
ADAM 64f34acd7a TMP: set github token 2024-12-10 17:47:10 +00:00
TentativeConvert ae38ad977a update redirects 2024-12-10 14:35:27 +01:00
TentativeConvert a191c47b8e update queue of pre-loaded games 2024-12-10 14:28:30 +01:00
Jon Eugster 0e58e81875 parse CPU/MEM usage as integer. 2024-11-25 12:38:16 +01:00
Marcus Zibrowius f0aa6b58ed Edits on landing page, with updates to German & Spanish translations.
modified:   ../de/translation.json
	modified:   ../en/translation.json
	modified:   ../es/translation.json
	modified:   ../ko/target/translation.json
	modified:   translation.json
	modified:   ../../../src/components/landing_page.tsx
2024-11-08 16:51:38 +01:00
Matvey Lorkish 045b1ea3fb Merge pull request #261 from leanprover-community/stats
feat: improved stat logging
2024-10-25 08:08:18 +02:00
matlorr dc6f7b2822 Specify that logs should be created relative to the current working directory. 2024-10-21 08:02:08 +00:00
Jon Eugster db5ac7ed15 Merge pull request #269 from chabulhwi/fix-korean
fix `config.json` and add Korean translation
2024-10-20 15:02:13 +02:00
Jon Eugster 6a0e739301 Merge pull request #267 from chabulhwi/update-npm-deps
update npm deps
2024-10-20 14:55:14 +02:00
Jon Eugster ed96cf9534 Merge pull request #268 from chabulhwi/add-translation-keys
add some translation keys
2024-10-20 14:54:13 +02:00
Bulhwi Cha 64670d1579 fix config.json and add Korean translation
* In the `config.json` file, the ISO code representing Korean should be
  `ko`, not `kr`.
* Remove the `client/public/locales/kr` subdirectory.
* Add the `client/public/locales/ko` subdirectory, which itself is the
  OmegaT project for the Korean translation of `lean4game`.

OmegaT[0] is a translation memory application intended for professional
translators. I use it to translate English documentation into Korean.

[0] https://omegat.org/
2024-10-18 13:10:10 +09:00
Bulhwi Cha 25141b9613 add some translation keys 2024-10-17 20:03:46 +09:00
Bulhwi Cha ab9d0d0679 add some translation keys 2024-10-17 19:38:41 +09:00
Bulhwi Cha 6dca770dbf update npm deps 2024-10-17 19:32:36 +09:00
matlorr 2022fa9a44 Added logging game-access data 2024-10-11 09:10:39 +00:00
Jon Eugster b77afebe0a add sample 2024-10-01 14:54:13 +02:00
matlorr 4ac38ef7dd Refactored stats.sh to display approx. cpu usage and mem usage in .csv format 2024-10-01 08:28:07 +00:00
Jon Eugster 6a8abf41bd implement stats script 2024-09-29 14:02:56 +02:00
Jon Eugster 1466a41169 fix fetch url for stats 2024-09-29 12:43:51 +02:00
Jon Eugster 3cdb9a026b fix stats 2024-09-29 12:39:09 +02:00
Jon Eugster ebd7268421 feat: add option to display server capacity 2024-09-26 18:07:25 +02:00
Jon Eugster af15982804 Merge pull request #260 from 0417taehyun/feat/generate-structure-of-korean-document
feat: Generate a structure of Korean document
2024-09-07 20:15:16 +02:00
Taehyun Lee 5a404a9a58 Generate translation.json of Korean 2024-09-07 21:16:18 +09:00
Taehyun Lee cebea6a6aa Add Korean on config.json 2024-09-07 21:14:59 +09:00
Jon Eugster 36499c0257 add note for servers with different base url 2024-08-27 14:52:20 +02:00
Jon EugsterandJadAbouHawili 8c5e47dd7b improve doc, adaptation of #250
Co-authored-by: JadAbouHawili <jad-abou-hawili@hotmail.com>
2024-07-31 17:13:24 +02:00
Jon Eugster a46840d327 Merge pull request #253 from pitmonticone/fix-typos
Fix typos
2024-07-25 06:50:11 +02:00
Pietro Monticone f518efc81c Update LetIntros.lean 2024-07-24 11:06:26 +02:00
Pietro Monticone 9b93e3817d Update RpcHandlers.lean 2024-07-24 11:06:24 +02:00
Pietro Monticone 54c1e0dcaf Update FileWorker.lean 2024-07-24 11:06:22 +02:00
Pietro Monticone 2c1e69611b Update Commands.lean 2024-07-24 11:06:21 +02:00
Pietro Monticone 2c22c445b1 Update AbstractCtx.lean 2024-07-24 11:06:19 +02:00
Pietro Monticone b6b31a06ac Update README.md 2024-07-24 11:06:16 +02:00
Jon Eugster 255839fea7 Merge pull request #248 from Lean-zh/main
Environment Variable for Default Language Setting
2024-07-12 10:00:26 +02:00
Jon Eugster b961030db2 Update hints.md 2024-07-12 09:54:02 +02:00
rexwzh c20d807d5c add default language configuration 2024-07-11 01:51:17 +08:00
Jon Eugster 23c8099401 Merge pull request #244 from RexWzh/main
Update ZH-Translation for Lean Game Server
2024-06-25 17:01:18 +02:00
rexwzh 6bd9e95db9 update zh-translation 2024-06-23 13:53:19 +08:00
Jon Eugster 1febc51791 Update create_game.md 2024-06-13 11:46:01 +02:00
Jon Eugster d53b57a764 Update create_game.md 2024-06-13 11:45:33 +02:00
Jon Eugster 020b4f7803 Update create_game.md 2024-06-13 11:43:07 +02:00
Jon Eugster 0ae099414c Update create_game.md 2024-06-13 10:39:31 +02:00
Jon Eugster b091ec579b Merge pull request #236 from RexWzh/main
Fix Bug with Environment Variables for Ports
2024-06-04 20:38:30 +02:00
cubenlp 2a14f48f45 use environment variable for port 2024-06-04 23:34:33 +08:00
Jon Eugster 4ed0753bb0 Merge pull request #234 from miguelmarco/patch-1
Spanish translation of the UI
2024-06-02 10:36:46 +02:00
Jon Eugster 6b5fc80896 Update client/public/locales/es/translation.json 2024-06-02 10:34:18 +02:00
Jon Eugster e56c7a0670 Update client/public/locales/es/translation.json 2024-06-02 10:32:59 +02:00
Jon Eugster 5b710da197 Update client/public/locales/es/translation.json 2024-06-02 10:32:51 +02:00
miguelmarco b275bbb94f Update translation.json
Spanish translation of the UI.
2024-05-31 18:55:00 +02:00
Jon Eugster 29eb90e6c8 add spanish 2024-05-31 13:00:43 +02:00
Jon Eugster 4e9ac54cde Merge pull request #224 from JadAbouHawili/patch-1
Typo in documentation, hints.md
2024-05-03 14:28:29 +02:00
JadAbouHawili 18f21fa324 Typo in documentation, hints.md 2024-05-02 17:38:30 +00:00
110 changed files with 12656 additions and 9689 deletions
+3
View File
@@ -3,3 +3,6 @@ client/dist
games/
server/.lake
**/.DS_Store
logs/
relay/prev_cpu_metric
test.ecosystem.config.cjs
+1 -1
View File
@@ -54,7 +54,7 @@ Providing the use access to a Lean instance running on the server is a severe se
## Credits
The project has pimarily been developed by Alexander Bentkamp and Jon Eugster.
The project has primarily been developed by Alexander Bentkamp and Jon Eugster.
It is based on ideas from the [Lean Game Maker](https://github.com/mpedramfar/Lean-game-maker) and the [Natural Number Game
(NNG)](https://www.ma.imperial.ac.uk/~buzzard/xena/natural_number_game/)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 607 KiB

Binary file not shown.
-93
View File
@@ -1,93 +0,0 @@
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.
@@ -1,93 +0,0 @@
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.
-202
View File
@@ -1,202 +0,0 @@
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.
+46 -41
View File
@@ -1,94 +1,99 @@
{
"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.",
"Lean Game Server": "Lean-Lern-Spiel-Server",
"<p>Game rules determine if it is allowed to skip levels and if the games runs checks to only allow unlocked tactics and theorems in proofs.</p><1>Note: \"Unlocked\" tactics (or theorems) are determined by two things: The set of minimal tactics needed to solve a level, plus any tactics you unlocked in another level. That means if you unlock <1>simp</1> in a level, you can use it henceforth in any level.</1><p>The options are:</p>": "<p>Die Spielregeln bestimmen ob es erlaubt ist, Levels zu überspringen und ob das Spiel überprüft welche Taktiken und Theoreme freigeschaltet sind und nur diese im Beweis akzeptiert.</p><1>Bemerkung: \"Freigeschaltete\" Taktiken (und Theoreme) werden durch zwei Faktoren bestimmt: The Menge der Taktiken die minimal notwending sind um den Level zu lösen und dazu die Menge aller Taktiken, die in einem anderen Level freigeschaltet wurden. Das bedeutet wenn <1>simp</1> in einem Level freigeschaltet wird, kann diese Taktik danach in jeglichen Levels verwendet werden.",
"Game Rules": "Spielregeln",
"levels": "Levels",
"levels": "Level",
"tactics": "Taktiken",
"regular": "regulär",
"relaxed": "relaxed",
"none": "keine",
"Rules": "Regeln",
"Intro": "Einführung",
"Intro": "Prolog",
"Game Introduction": "Prolog",
"World selection": "Übersicht",
"Start": "Start",
"Inventory": "Inventar",
"next level": "nächstes Level",
"Next": "Weiter",
"back to world selection": "Zurück zur Übersicht",
"Leave World": "Welt verlassen",
"previous level": "voheriges Level",
"Previous": "Zurück",
"Editor mode is enforced!": "Editor kann nicht verlassen werden!",
"Editor mode": "Editor",
"Typewriter mode": "Schreibmaschine",
"information, Impressum, privacy policy": "Informationen, Impressum, Privacy Policy",
"Preferences": "Einstellungen",
"Game Info & Credits": "Spielinfo & Credits",
"Game Info": "Spielinfo",
"Clear Progress": "Spielstand löschen",
"Erase": "Löschen",
"Download Progress": "Spielstand herunterladen",
"Download": "Herunterladen",
"Load Progress from JSON": "Spielstand aus JSON laden",
"Upload": "Laden",
"Home": "Home",
"back to games selection": "Zurück zur Spielauswahl",
"close inventory": "Inventar schließen",
"show inventory": "Inventar öffnen",
"World": "Welt",
"Show more help!": "Mehr Hilfe",
"Goal": "Goal",
"Current Goal": "Aktuelles Goal",
"Goal": "Beweisziel",
"Current Goal": "Aktuelles Beweisziel",
"Objects": "Objekte",
"Assumptions": "Annahmen",
"Further Goals": "Weitere Goals",
"No Goals": "Keine Goals",
"Loading goal…": "Goal wird geladen…",
"Further Goals": "Weitere Ziele",
"No Goals": "Keine Beweisziele",
"Loading goal…": "Beweisziel wird geladen…",
"Click somewhere in the Lean file to enable the infoview.": "Ein Klick in den Lean-Code aktiviert den Infoview.",
"Waiting for Lean server to start…": "Warte auf das Starten des Lean-Servers…",
"Waiting for Lean server to start…": "Warte auf den Lean-Server …",
"Level completed! 🎉": "Level gelöst! 🎉",
"Level completed with warnings 🎭": "Level mit Warnungen abgeschlossen 🎭",
"Active Goal": "Aktuelles Goal",
"Crashed! Go to editor mode and fix your proof! Last server response:": "",
"Active Goal": "Aktuelles Ziel",
"Crashed! Go to editor mode and fix your proof! Last server response:": "Abgestürzt! Wechsle in den Editor-Modus, um deinen Beweis zu repariaeren. Letzte Meldung vom Server:",
"Line": "Zeile",
"Character": "Charakter",
"Loading messages…": "",
"Loading messages…": "Lade Meldungen …",
"Execute": "Ausführen",
"Definitions": "Definitionen",
"Theorems": "Theoreme",
"Not unlocked yet": "Noch nicht verfügbar",
"Not available in this level": "In diesem Level nicht verfügbar",
"A repository of learning games for the proof assistant <1>Lean</1> <i>(Lean 4)</i> and its mathematical library <5>mathlib</5>": "Eine Sammlung von Lernspielen für den Beweisassistenten <1>Lean</1> <i>(Lean 4)</i> und dessen mathematische Bibliothek <5>mathlib</5>",
"No Games loaded. Use <1>http://localhost:3000/#/g/local/FOLDER</1> to open a game directly from a local folder.": "Kein Spiel geladen. öffne <1>http://localhost:3000/#/g/local/FOLDER</1> um ein Spiel direkt 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.": "",
"No Games loaded. Use <1>http://localhost:3000/#/g/local/FOLDER</1> to open a game directly from a local folder.": "Kein Spiel geladen. Öffne <1>http://localhost:3000/#/g/local/FOLDER</1> um ein Spiel direkt aus einem lokalen Ordner zu laden.",
"Prerequisites": "Voraussetzungen",
"Worlds": "Welten",
"Levels": "Levels",
"Language": "Sprachen",
"Development notes": "Entwicklungshinweis",
"Levels": "Level",
"Language": "Sprache",
"Development notes": "Entwicklungsstand",
"Adding new games": "Neue Spiele hinzufügen",
"Funding": "Finanzierung",
"<p>Do you want to delete your saved progress irreversibly?</p><p>(This deletes your proofs and your collected inventory. Saves from other games are not deleted.)</p>": "<p>Soll der Spielstand unwiderruflich gelöscht werden?</p><p>(Dies löscht sämtliche Beweise und das gesammelte Inventar. Spielstände anderer Spiele werden nicht gelöscht.)</p>",
"Delete Progress?": "Spielstand löschen?",
"Delete": "Löschen",
"Download & Delete": "Herunterladen & Löschen",
"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>",
"<p>Select a JSON file with the saved game progress to load your progress.</p><1><0>Warning:</0> This will delete your current game progress! Consider <2>downloading your current progress</2> first!</1>": "<p>Wähle eine JSON-Datei mit einem Spielstand, um diesen zu laden.</p><1><0>Achtung:</0> Deraktuelle Spielstand wird dabei überschrieben! Wenn du noch einmal zum aktuellen Spielstand zurückkehren möchtest, solltest du zunächst den <2>aktuellen Spielstand herunterladen</2>!</1>",
"Upload Saved Progress": "Spielstand hochladen",
"Load selected file": "Ausgewählte Datei hochladen",
"Mobile": "Mobil",
"Auto": "Auto",
"Desktop": "Desktop",
"<0>If you are considering writing your own game, you should use the <1>GameSkeleton Github Repo</1> as a template and read <3>How to Create a Game</3>.</0><1>You can directly load your games into the server and play it using the correct URL. The <1>instructions above</1> also explain the details for how to load your game to the server. We'd like to encourage you to contact us if you have any questions.</1><p>Featured games on this page are added manually. Please get in contact and we'll happily add yours.</p>": "",
"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>If you are considering writing your own game, you should use the <1>GameSkeleton Github Repo</1> as a template and read <3>How to Create a Game</3>.</0><1>You can directly load your games into the server and play it using the correct URL. The <1>instructions above</1> also explain the details for how to load your game to the server. We'd like to encourage you to contact us if you have any questions.</1><p>Featured games on this page are added manually. Please get in contact and we'll happily add yours.</p>": "<0>Für alle, die selbst Spiel entwickeln möchten, gibt es ein <1>GameSkeleton Github Repo</1> als Vorlage und die Anleitung <3>How to Create a Game</3>.</0><1>Die <1>Anleitung</1> erklärt auch, wie ein solches Spiel mittels einer passenden URL auf den Sever geladen und gespiel werden kann. Fragen dazu beantworten wir gern.</1><p>Als Kacheln sichtbar ist auf dieser Seite nur eine kuratierte Auswahl an existierenden Spielen. Wir erweitern diese Auswahl auf Anfrage sehr gerne.</p>",
"Level": "Level",
"Introduction": "Prolog",
"Retry proof from here": "Ab hier neu ansetzen",
"Retry": "Noch einmal",
"Failed command": "Gescheiterter Befehl",
"<p>As this server runs lean on our university machines, it has a limited capacity. Our current estimate is about 70 simultaneous games.</p>": "Diese Server läuft auf universitäter Infrastruktur mit begrenzten Kapazitäten. Wir schätzen, dass die Belastungsgrenze bei rund 70 gleichzeitig laufenden Spielen besteht.",
"<0>Most aspects of the games and the infrastructure are still in development. Feel free to file a <1>GitHub Issue</1> about any problems you experience!</0>": "Der Spieleserver und die alle Spiele befinden sich in fortlaufender Entwicklung. Wir bitten darum, Fehler und Ungereimtheiten als <1>GitHub Issue</1> zu melden.",
"This server has been developed as part of the project <1>ADAM: Anticipating the Digital Age of Mathematics</1> at Heinrich Heine University Düsseldorf.": "Die Lean-Spiele-Software und dieser Spiele-Server werden als Teils der Projekts <1>ADAM: Anticipating the Digital Age of Mathematics</1> an der Heinrich-Heine-Universität Düsseldorf entwickelt.",
"Server capacity": "Server-Auslastung",
"RAM": "RAM",
" used": "",
"CPU": "CPU"
}
+21 -16
View File
@@ -10,19 +10,33 @@
"none": "none",
"Rules": "Rules",
"Intro": "Intro",
"Game Introduction": "Game Introduction",
"World selection": "World selection",
"Start": "Start",
"Inventory": "Inventory",
"next level": "next level",
"Next": "Next",
"back to world selection": "back to world selection",
"Leave World": "Leave World",
"previous level": "previous level",
"Previous": "Previous",
"Editor mode is enforced!": "Editor mode is enforced!",
"Editor mode": "Editor mode",
"Typewriter mode": "Typewriter mode",
"information, Impressum, privacy policy": "information, Impressum, privacy policy",
"Preferences": "Preferences",
"Game Info & Credits": "Game Info & Credits",
"Game Info": "Game Info",
"Clear Progress": "Clear Progress",
"Erase": "Erase",
"Download Progress": "Download Progress",
"Download": "Download",
"Load Progress from JSON": "Load Progress from JSON",
"Upload": "Upload",
"Home": "Home",
"back to games selection": "back to games selection",
"close inventory": "close inventory",
"show inventory": "show inventory",
"World": "World",
"Show more help!": "Show more help!",
"Goal": "Goal",
@@ -48,12 +62,13 @@
"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",
"Server capacity": "Server capacity",
"RAM": "RAM",
"CPU": "CPU",
"Development notes": "Development notes",
"Adding new games": "Adding new games",
"Funding": "Funding",
@@ -77,18 +92,8 @@
"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"
"<p>As this server runs lean on our university machines, it has a limited capacity. Our current estimate is about 70 simultaneous games.</p>": "<p>As this server runs lean on our university machines, it has a limited capacity. Our current estimate is about 70 simultaneous games.</p>",
"<0>Most aspects of the games and the infrastructure are still in development. Feel free to file a <1>GitHub Issue</1> about any problems you experience!</0>": "<0>Most aspects of the games and the infrastructure are still in development. Feel free to file a <1>GitHub Issue</1> about any problems you experience!</0>",
"This server has been developed as part of the project <1>ADAM: Anticipating the Digital Age of Mathematics</1> at Heinrich Heine University Düsseldorf.": "This server has been developed as part of the project <1>ADAM: Anticipating the Digital Age of Mathematics</1> at Heinrich Heine University Düsseldorf.",
" used": " used"
}
+97 -93
View File
@@ -1,95 +1,99 @@
{
"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": ""
"Intro": "Introducción",
"Game Introduction": "Introducción del juego",
"World selection": "Seleccionar mundo",
"Start": "Empezar",
"Inventory": "Inventario",
"next level": "siguiente nivel",
"Next": "Siguiente",
"back to world selection": "volver a la selección de mundos",
"Leave World": "Abandonar mundo",
"previous level": "nivel anterior",
"Previous": "Anterior",
"Editor mode is enforced!": "¡El modo editor es obligatorio!",
"Editor mode": "Modo editor",
"Typewriter mode": "Modo línea a línea",
"information, Impressum, privacy policy": "información, Impressum, política de privacidad",
"Preferences": "Preferencias",
"Game Info & Credits": "Información del juego y reconocimientos",
"Game Info": "Información del juego",
"Clear Progress": "Limpiar el progreso",
"Erase": "Borrar",
"Download Progress": "Descargar progreso",
"Download": "Descargar",
"Load Progress from JSON": "Cargar progreso desde JSON",
"Upload": "Subir",
"Home": "Inicio",
"back to games selection": "volver a la selección de juegos",
"close inventory": "cerrar inventario",
"show inventory": "mostrar inventario",
"World": "Mundo",
"Show more help!": "¡Mostrar más ayuda!",
"Goal": "Objetivo",
"Objects": "Objetos",
"Assumptions": "Hipótesis",
"Current Goal": "Objetivo actual",
"Further Goals": "Objetivos pendientes",
"No Goals": "Sin objetivos",
"Loading goal…": "Cargando objetivo…",
"Click somewhere in the Lean file to enable the infoview.": "Pulsa en algún lugar del archivo Lean para habilitar la vista de información.",
"Waiting for Lean server to start…": "Esperando a que el servidor Lean se inicie…",
"Level completed! 🎉": "Nivel completado 🎉",
"Level completed with warnings 🎭": "Nivel completado con advertencias 🎭",
"Failed command": "Comando fallido",
"Retry proof from here": "Reintentar la prueba desde aquí",
"Retry": "Reintentar",
"Active Goal": "Objetivo activo",
"Crashed! Go to editor mode and fix your proof! Last server response:": "¡Error! Vaya al modo editor y corrija su prueba. Última respuesta del servidor:",
"Line": "Línea",
"Character": "Carácter",
"Loading messages…": "Cargando mensajes…",
"Execute": "Ejecutar",
"Tactics": "Tácticas",
"Definitions": "Definiciones",
"Theorems": "Teoremas",
"Not unlocked yet": "No desbloqueado aún",
"Not available in this level": "No disponible en este nivel",
"A repository of learning games for the proof assistant <1>Lean</1> <i>(Lean 4)</i> and its mathematical library <5>mathlib</5>": "Un repositorio de juegos para aprender el asistente de demostración <1>Lean</1>, <i>(Lean 4)</i> y su biblioteca matemática <5>mathlib</5> ",
"No Games loaded. Use <1>http://localhost:3000/#/g/local/FOLDER</1> to open a game directly from a local folder.": "No se ha cargado ningún juego. Use <1>http://localhost:3000/#/g/local/FOLDER</1> para abrir un juego directamente desde una carpeta local",
"<0>If you are considering writing your own game, you should use the <1>GameSkeleton Github Repo</1> as a template and read <3>How to Create a Game</3>.</0><1>You can directly load your games into the server and play it using the correct URL. The <1>instructions above</1> also explain the details for how to load your game to the server. We'd like to encourage you to contact us if you have any questions.</1><p>Featured games on this page are added manually. Please get in contact and we'll happily add yours.</p>": "<0>Si está considerando escribir su propio juego, use el <1>GameSkeleton Github Repo</1> como plantilla y lea <3>How to Create a Game</3>.</0><1>Puede cargar directamente los juegos en el servidor y jugarlo usando la URL adecuada. Las <1>instrucciones anteriores</1> también explican los detalles sobre cómo cargar su juego en el servidor. Le animamos a ponerse en contacto con nosotros si tiene preguntas.</1><p>Los juegos incluidos en esta página son añadidos manualmente. Por favor, contactenos y añadiremos el suyo encantados.</p>",
"Prerequisites": "Requisitos previos",
"Worlds": "Mundos",
"Levels": "Niveles",
"Language": "Idioma",
"Lean Game Server": "Servidor de Juegos de Lean",
"Development notes": "Notas de desarrollo",
"Adding new games": "Añadir nuevos juegos",
"Funding": "Financiación",
"Level": "Nivel",
"Introduction": "Introducción",
"<p>Do you want to delete your saved progress irreversibly?</p><p>(This deletes your proofs and your collected inventory. Saves from other games are not deleted.)</p>": "<p>¿Desea eliminar su progreso guardado definitivamente?</p><p>(Esto elimina sus pruebas y su inventario recopilado. Los progresos guardados de otros juegos no se eliminan.)</p>",
"Delete Progress?": "¿Borrar Progreso?",
"Delete": "Borrar",
"Download & Delete": "Descargar y Borrar",
"Cancel": "Cancelar",
"Mobile": "Móvil",
"Auto": "Automático",
"Desktop": "Escritorio",
"Layout": "Diseño",
"Always visible": "Siempre visible",
"Save my settings (in the browser store)": "Guardar mis ajustes (en el almacenamiento del navegador)",
"<p>Game rules determine if it is allowed to skip levels and if the games runs checks to only allow unlocked tactics and theorems in proofs.</p><1>Note: \"Unlocked\" tactics (or theorems) are determined by two things: The set of minimal tactics needed to solve a level, plus any tactics you unlocked in another level. That means if you unlock <1>simp</1> in a level, you can use it henceforth in any level.</1><p>The options are:</p>": "<p>Las reglas del juego determinan si se permite saltarse niveles y si el juego realiza comprobaciones para permitir únicamente tácticas y teoremas desbloqueados en las pruebas.</p><1>Nota: las tácticas (o teoremas) \"Desbloqueadas\" está determinadas por dos cosas: el conjunto mínimo de tácticas necesarias para resolver un nivel, más cualquier táctica que hayas desbloqueado en otro nivel. Esto significa que si desbloqueas <1>simp</1> en un nivel, puedes usarlo a partir de entonces en cualquier nivel.</1><p>Las opciones son:</p>",
"Game Rules": "Reglas del juego",
"levels": "niveles",
"tactics": "tácticas",
"regular": "normal",
"relaxed": "relajado",
"none": "ninguno",
"<p>Select a JSON file with the saved game progress to load your progress.</p><1><0>Warning:</0> This will delete your current game progress! Consider <2>downloading your current progress</2> first!</1>": "<p>Seleccione un archivo JSON con el progreso del juego guardado para cargar su progreso.</p><1><0>Advertencia:</0> Esto borrará su progreso actual en el juego. Considere <2>descargar su progreso actual</2> antes</1>",
"Upload Saved Progress": "Subir progreso guardado",
"Load selected file": "Cargar archivo seleccionado",
"Rules": "Reglas",
"<p>As this server runs lean on our university machines, it has a limited capacity. Our current estimate is about 70 simultaneous games.</p>": "<p>Como este servidor corre en máquinas de nuestra universidad, tiene una capacidad limitada. Nuestra estimación actual es de unos 70 juegos simultaneos.</p>.",
"<0>Most aspects of the games and the infrastructure are still in development. Feel free to file a <1>GitHub Issue</1> about any problems you experience!</0>": "<1>Muchos aspectos de los juegos y la infrastructura están aún en desarrollo. No dude en abrir una <1>GitHub Issue</1> sobre cualquier problema que experimente.</1>",
"This server has been developed as part of the project <1>ADAM: Anticipating the Digital Age of Mathematics</1> at Heinrich Heine University Düsseldorf.": "Este servidor se ha desarrollado como parte del proyecto <1>ADAM: Anticipating the Digital Age of Mathematics</1> en la Heinrich-Heine-Universität de Düsseldorf.",
"Server capacity": "",
"RAM": "",
" used": "",
"CPU": ""
}
+8
View File
@@ -0,0 +1,8 @@
/ko-level1.tmx
/ko-level2.tmx
/ko-omegat.tmx
/**/*.bak
/omegat/last_entry.properties
/omegat/files_order.txt
/omegat/project_stats.txt
/tm/*.tmx
+15
View File
@@ -0,0 +1,15 @@
# 린 4 게임
[English (영어)](./README.md) | 한국어
## 기여하기
`lean4game`의 한국어 번역에 기여하는 활동은 언제든지 환영합니다!
### 한국어 번역
저([차불휘][bc])는 [오메가T(OmegaT)][omt]를 이용해 영어 문서를 한국어로 번역합니다. 오메가T 프로젝트는 이 디렉터리, 다시 말해 `client/public/locales/ko`에 있습니다. 오메가T로 JSON 파일을 구문 분석 하려면 [오메가T를 위한 오카피(Okapi) 필터 플러그인][okapi]을 설치해야 됩니다.
[bc]: https://github.com/chabulhwi
[omt]: https://omegat.org/
[okapi]: https://okapiframework.org/wiki/index.php/Okapi_Filters_Plugin_for_OmegaT
+18
View File
@@ -0,0 +1,18 @@
# Lean 4 Game
English | [한국어[Korean]](./README.ko.md)
## Contributing
Contributions to the Korean translation of `lean4game` are always welcome!
### Korean Translation
I ([Bulhwi Cha][bc]) use [OmegaT][omt] to translate English documentation into
Korean. The OmegaT project is in this directory, that is,
`client/public/locales/ko`. You need to install the [Okapi filters plugin for
OmegaT][okapi] to make OmegaT parse JSON files.
[bc]: https://github.com/chabulhwi
[omt]: https://omegat.org/
[okapi]: https://okapiframework.org/wiki/index.php/Okapi_Filters_Plugin_for_OmegaT
@@ -0,0 +1,167 @@
# Glossary in tab-separated format -*- coding: utf-8 -*-
St. Anselm 안셀무스
ontological 존재론적
argument 논증
argument 인수
Lean 린
Lean community 린 커뮤니티
God 신
Alvin Plantinga 앨빈 플랜팅아
exist 존재하다
existence 존재
the understanding 지성
reality 현실
assumption 가정
reductio 귀류법
great 큰
premise 전제
being 존재자
conceive 생각하다
definition 정의
true 참
true 참인
false 거짓
false 거짓인
formulate 정식화하다
formulation 정식화
formalize 형식화하다
formalization 형식화
property 속성
redundant 불필요한
sentence 문장
state 진술하다
statement 진술
proposition 명제
negation 부정
axiom 공리
theorem 정리
theory 이론
conclude 결론하다
prove 증명하다
SEP 스탠퍼드 철학 백과사전
Eric Wieser 에릭 비저
Alistair Tucker 앨리스터 터커
philosophy 철학
Owl of Sogang 서강올빼미
type theory 유형론
universe level 유형 세계 변수
type 유형
type 입력하다
type check 유형을 확인하다
type check 유형이 확인되다
type check 유형 확인이 잘되다
type class 유형 클래스
instance 사례
category mistake 범주 실수
class 클래스
example 보기
example 예
inductive type 귀납형
define 정의하다
constructor 구성자
Apache License, Version 2.0 아파치 라이선스, 버전 2.0
under the terms of 의 조건에 따라
OmegaT 오메가T
symbolic link 심벌릭 링크
directory 디렉터리
root directory 최상위 디렉터리
documentation 문서
English 영어
Korean 한국어
chapter 장
quiz 퀴즈
question 문제
command 명령어
error 오류
code 코드
evaluate 계산하다
keyword 핵심어
declare 선언하다
constant 상수
reference 참고 문헌
universe-polymorphic 세계 다형적
parametrically polymorphic 매개 변수 다형적
function 함수
identifier 식별자
definitionally equal 정의상 같은
natural number 자연수
input 입력값
return 반환하다
non-zero 영이 아닌
zero 영
alpha-equivalent 알파 동등한
less-than-or-equal-to sign 작거나 같음 부호
dependent function 의존 함수
dependent function type 의존 함수형
dependent product 의존곱
dependent product type 의존곱형
underscore 밑줄
implicit 암시적
explicit 명시적
sigma type 시그마
propositional logic 명제 논리
term 항
contemporary 동시대
term 용어
truth-value 진릿값
bearer 담지자
propositional attitudes 명제적 태도
believe 믿다
doubt 의심하다
South Korea 한국
school mathematics 학교 수학
sentence 문장
high school mathematics 고등학교 수학
teacher 교사
Stanford Encyclopedia of Philosophy 스탠퍼드 철학 백과사전
ordered triple 순서세짝
conjecture 추측
Goldbach's conjecture 골드바흐의 추측
interactive theorem prover 상호 작용 정리 증명기
truth 참
falsity 거짓
rule of inference 추론 규칙
introduction rule 도입 규칙
elimination rule 제거 규칙
ex falso quodlibet 엑스 팔소 세퀴투르 쿠오들리베트
principle of explosion 폭발 원리
propositional connective 명제 연결사
symbol 기호
refutation by contradiction 모순에 따른 부인
contradiction 모순
conjunction 연언
disjunction 선언(選言)
implication 함의
material implication 내용적 함의
conditional 조건문
modus ponens 긍정 논법[모더스 포넨스]
necessary condition 필요조건
conversion 역
inversion 이(裏)
contraposition 대우(對偶)
equivalence 동등
biconditional 쌍조건문
abbreviation 준말
if and only if …일 때 그리고 그럴 때만
editor 편집기
shortcut 단축키
Theorem Proving in Lean 4 린 4로 하는 정리 증명
exercise 연습 문제
repository 저장소
Jeremy Avigad 제러미 아비가드
Leonardo de Moura 레오나르두 지 모라
Soonho Kong 공순호
Sebastian Ullrich 제바스티안 울리히
file 파일
Markdown 마크다운
quiz 퀴즈
translation 번역
contribute 기여하다
progress 진도
capacity 이용량
GitHub 깃허브
repo 저장소
game rule 게임 규칙
unlocked 잠금 해제가 된
unlock 잠금 해제를 하다
Markdown 마크다운
+33
View File
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<omegat>
<project version="1.0">
<source_dir>__DEFAULT__</source_dir>
<source_dir_excludes>
<mask>**/.svn/**</mask>
<mask>**/CVS/**</mask>
<mask>**/.cvs/**</mask>
<mask>**/.git/**</mask>
<mask>**/.hg/**</mask>
<mask>**/.repositories/**</mask>
<mask>**/desktop.ini</mask>
<mask>**/Thumbs.db</mask>
<mask>**/.DS_Store</mask>
<mask>**/~$*</mask>
</source_dir_excludes>
<target_dir>__DEFAULT__</target_dir>
<tm_dir>__DEFAULT__</tm_dir>
<glossary_dir>__DEFAULT__</glossary_dir>
<glossary_file>__DEFAULT__</glossary_file>
<dictionary_dir>__DEFAULT__</dictionary_dir>
<export_tm_dir>__DEFAULT__</export_tm_dir>
<export_tm_levels>omegat level1 level2</export_tm_levels>
<source_lang>en</source_lang>
<target_lang>ko</target_lang>
<source_tok>org.omegat.tokenizer.LuceneEnglishTokenizer</source_tok>
<target_tok>org.omegat.tokenizer.HunspellTokenizer</target_tok>
<sentence_seg>true</sentence_seg>
<support_default_translations>true</support_default_translations>
<remove_tags>false</remove_tags>
<external_command></external_command>
</project>
</omegat>
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
../README.md
+1
View File
@@ -0,0 +1 @@
../../en/translation.json
+15
View File
@@ -0,0 +1,15 @@
# 린 4 게임
[English (영어)](./README.md) | 한국어
## 기여하기
`lean4game`의 한국어 번역에 기여하는 활동은 언제든지 환영합니다!
### 한국어 번역
저([차불휘][bc])는 [오메가T(OmegaT)][omt]를 이용해 영어 문서를 한국어로 번역합니다. 오메가T 프로젝트는 이 디렉터리, 다시 말해 `client/public/locales/ko`에 있습니다. 오메가T로 JSON 파일을 구문 분석 하려면 [오메가T를 위한 오카피(Okapi) 필터 플러그인][okapi]을 설치해야 됩니다.
[bc]: https://github.com/chabulhwi
[omt]: https://omegat.org/
[okapi]: https://okapiframework.org/wiki/index.php/Okapi_Filters_Plugin_for_OmegaT
@@ -0,0 +1,99 @@
{
"Tactics": "전략",
"Lean Game Server": "린 게임 서버",
"<p>Game rules determine if it is allowed to skip levels and if the games runs checks to only allow unlocked tactics and theorems in proofs.</p><1>Note: \"Unlocked\" tactics (or theorems) are determined by two things: The set of minimal tactics needed to solve a level, plus any tactics you unlocked in another level. That means if you unlock <1>simp</1> in a level, you can use it henceforth in any level.</1><p>The options are:</p>": "<p>게임 규칙에 따라, 단계들을 건너뛰어도 되는지 그리고 증명을 작성할 때 잠금 해제가 된 전략과 정리만 이용할 수 있는지가 정해집니다.</p><1>\n\n참고: '잠금 해제'가 된 전략이나 정리는 다음 두 부류로 정해집니다. (1) 해당 단계를 푸는 데 필요한 최소한의 전략이나 정리의 모임, (2) 다른 단계에서 잠금 해제를 한 전략이나 정리. 따라서 여러분이 <1>simp</1> 전략을 잠금 해제 하면, 그 뒤로 어느 단계에서든 이 전략을 이용할 수 있습니다.</1><p>선택할 수 있는 게임 규칙은 다음과 같습니다.</p>",
"Game Rules": "게임 규칙",
"levels": "단계",
"tactics": "전략",
"regular": "일반",
"relaxed": "완화됨",
"none": "없음",
"Rules": "규칙",
"Intro": "소개",
"Game Introduction": "게임 소개",
"World selection": "세계 선택",
"Start": "시작하기",
"Inventory": "인벤토리",
"next level": "다음 단계",
"Next": "다음",
"back to world selection": "세계 선택하러 돌아가기",
"Leave World": "세계에서 나가기",
"previous level": "이전 단계",
"Previous": "이전",
"Editor mode is enforced!": "편집기 모드가 강제됐습니다!",
"Editor mode": "편집기 모드",
"Typewriter mode": "타자기 모드",
"information, Impressum, privacy policy": "정보, 관리자 정보, 사생활 정책",
"Preferences": "기본 설정",
"Game Info & Credits": "게임 정보 및 제작자 명단",
"Game Info": "게임 정보",
"Clear Progress": "진도 없애기",
"Erase": "지우기",
"Download Progress": "진도 내려받기",
"Download": "내려받기",
"Load Progress from JSON": "JSON에서 진도 불러오기",
"Upload": "업로드",
"Home": "홈",
"back to games selection": "게임 선택하러 돌아가기",
"close inventory": "인벤토리 닫기",
"show inventory": "인벤토리 보기",
"World": "세계",
"Show more help!": "도움말 더 보기!",
"Goal": "목표",
"Current Goal": "현재 목표",
"Objects": "객체",
"Assumptions": "가정",
"Further Goals": "이후 목표",
"No Goals": "목표 없음",
"Loading goal…": "목표 불러오는 중…",
"Click somewhere in the Lean file to enable the infoview.": "린 파일의 아무 곳이나 눌러 정보창을 여십시오.",
"Waiting for Lean server to start…": "린 서버가 시작하기를 기다리는 중…",
"Level completed! 🎉": "단계 완료! 🎉",
"Level completed with warnings 🎭": "경고 있는 채로 단계 완료 🎭",
"Active Goal": "활성화된 목표",
"Crashed! Go to editor mode and fix your proof! Last server response:": "시스템 다운됨! 편집기 모드에서 증명을 고치십시오! 마지막 서버 응답:",
"Line": "줄",
"Character": "문자",
"Loading messages…": "메시지 불러오는 중…",
"Execute": "실행하기",
"Definitions": "정의",
"Theorems": "정리",
"Not unlocked yet": "아직 잠금 해제가 안 됨",
"Not available in this level": "이 단계에서 쓸 수 없음",
"A repository of learning games for the proof assistant <1>Lean</1> <i>(Lean 4)</i> and its mathematical library <5>mathlib</5>": "<1>린(Lean 4)</1> 증명 보조기와 그 수학 라이브러리 <5>매스리브(mathlib)</5>를 학습하기 위한 게임들의 저장소",
"No Games loaded. Use <1>http://localhost:3000/#/g/local/FOLDER</1> to open a game directly from a local folder.": "불러온 게임 없음. <1>http://localhost:3000/#/g/local/FOLDER</1>를 이용해 지역[로컬] 폴더에서 게임을 직접 여십시오.",
"Prerequisites": "선행 요건",
"Worlds": "세계",
"Levels": "단계",
"Language": "언어",
"Server capacity": "서버 이용량",
"RAM": "램",
"CPU": "CPU",
"Development notes": "개발 노트",
"Adding new games": "새 게임 추가하기",
"Funding": "재정 지원",
"<p>Do you want to delete your saved progress irreversibly?</p><p>(This deletes your proofs and your collected inventory. Saves from other games are not deleted.)</p>": "<p>저장된 진도를 불가역적으로 삭제하시겠습니까?</p><p>(이를 선택하시면 증명과 인벤토리 안의 수집 항목들이 삭제됩니다. 다른 게임에 저장된 정보는 삭제되지 않습니다.)</p>",
"Delete Progress?": "진도를 삭제하시겠습니까?",
"Delete": "삭제하기",
"Download & Delete": "내려받고 삭제하기",
"Cancel": "취소하기",
"Layout": "레이아웃",
"Always visible": "항상 보임",
"Save my settings (in the browser store)": "(브라우저에) 설정 저장하기",
"<p>Select a JSON file with the saved game progress to load your progress.</p><1><0>Warning:</0> This will delete your current game progress! Consider <2>downloading your current progress</2> first!</1>": "<p>저장된 게임 진도가 있는 JSON 파일을 선택해 진도를 불러오십시오.</p><1><0>경고:</0> 이를 실행하면 현재의 게임 진도가 삭제됩니다! <2>현재의 게임 진도</2>를 먼저 내려받을지 판단하십시오!</1>",
"Upload Saved Progress": "저장된 진도 업로드",
"Load selected file": "선택한 파일 열기",
"Mobile": "모바일",
"Auto": "자동",
"Desktop": "데스크톱",
"<0>If you are considering writing your own game, you should use the <1>GameSkeleton Github Repo</1> as a template and read <3>How to Create a Game</3>.</0><1>You can directly load your games into the server and play it using the correct URL. The <1>instructions above</1> also explain the details for how to load your game to the server. We'd like to encourage you to contact us if you have any questions.</1><p>Featured games on this page are added manually. Please get in contact and we'll happily add yours.</p>": "<0>여러분이 직접 게임을 작성할 생각이 있다면, <1>GameSkeleton 깃허브 저장소</1>를 양식[템플릿]으로 이용하고 <3>'게임 만들기(Creating a Game)'</3> 문서를 읽으십시오.</0><1>여러분이 작성한 게임을 직접 서버에서 불러오고, 정확한 URL을 이용해 그 게임을 하실 수 있습니다. 여러분의 게임을 서버에서 불러오는 방법에 관한 세부 사항도 위의 <1>설명서</1>에 나와 있습니다. 궁금한 점이 있으면 저희에게 연락해 주십시오.</1><p>이 페이지에 실린 게임들은 수동으로 추가됐습니다. 저희에게 연락하시면 여러분의 게임을 기꺼이 추가하겠습니다.",
"Level": "단계",
"Introduction": "소개",
"Retry proof from here": "여기부터 증명 다시 시도하기",
"Retry": "다시 시도하기",
"Failed command": "실패한 명령",
"<p>As this server runs lean on our university machines, it has a limited capacity. Our current estimate is about 70 simultaneous games.</p>": "",
"<0>Most aspects of the games and the infrastructure are still in development. Feel free to file a <1>GitHub Issue</1> about any problems you experience!</0>": "",
"This server has been developed as part of the project <1>ADAM: Anticipating the Digital Age of Mathematics</1> at Heinrich Heine University Düsseldorf.": "",
" used": ""
}
+1
View File
@@ -0,0 +1 @@
target/translation.json
+26 -27
View File
@@ -1,7 +1,7 @@
{
"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>",
"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": "策略",
@@ -20,9 +20,10 @@
"Leave World": "离开世界",
"previous level": "上一关",
"Previous": "上一关",
"Editor mode is enforced!": "编辑器模式开启!",
"Editor mode is enforced!": "编辑器模式已启用!",
"Editor mode": "编辑器模式",
"Typewriter mode": "打字机模式",
"information, Impressum, privacy policy": "信息、版权说明、隐私政策",
"Preferences": "偏好设置",
"Game Info & Credits": "游戏信息和荣誉",
"Game Info": "游戏信息",
@@ -42,14 +43,14 @@
"Current Goal": "当前目标",
"Objects": "对象",
"Assumptions": "假设",
"Further Goals": "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! 🎉": "完成关卡!🎉",
"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": "从这里重新试着证明",
"Retry proof from here": "从这里重新尝试证明",
"Active Goal": "当前目标",
"Crashed! Go to editor mode and fix your proof! Last server response:": "程序崩溃!请转到编辑器模式,修复您的证明!最后一次服务器响应:",
"Line": "行",
@@ -58,30 +59,27 @@
"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>的一部分而开发的。",
"A repository of learning games for the proof assistant <1>Lean</1> <i>(Lean 4)</i> and its mathematical library <5>mathlib</5>": "这是一个为证明助手 <1>Lean</1> <i>(Lean 4)</i> 及其数学库 <5>mathlib</5> 设计的学习游戏库",
"No Games loaded. Use <1>http://localhost:3000/#/g/local/FOLDER</1> to open a game directly from a local folder.": "未加载游戏。访问 <1>http://localhost:3000/#/g/local/FOLDER</1> 从本地文件夹打开游戏。",
"<0>If you are considering writing your own game, you should use the <1>GameSkeleton Github Repo</1> as a template and read <3>How to Create a Game</3>.</0><1>You can directly load your games into the server and play it using the correct URL. The <1>instructions above</1> also explain the details for how to load your game to the server. We'd like to encourage you to contact us if you have any questions.</1><p>Featured games on this page are added manually. Please get in contact and we'll happily add yours.</p>": "<0>如果你打算编写自己的游戏,可以使用 <1>GameSkeleton Github Repo</1> 作为模板,并参阅 <3>如何创建游戏</3>。</0><1>你可以直接将游戏上传至服务器,并通过正确的 URL 进行游戏。上面的 <1>说明</1> 已详细介绍了如何将游戏加载到服务器的步骤。如果你有任何疑问,请随时联系我们。</1><p>本页上的精选游戏都是手动添加的。如果你想添加你的游戏,请与我们联系,我们非常欢迎。</p>",
"Prerequisites": "前置条件",
"Worlds": "世界(Worlds)",
"Worlds": "世界",
"Levels": "关卡",
"Language": "语言",
"Development notes": "开发信息",
"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>",
"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>",
"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": "移动端",
@@ -91,10 +89,11 @@
"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>": ""
"<p>As this server runs lean on our university machines, it has a limited capacity. Our current estimate is about 70 simultaneous games.</p>": "",
"<0>Most aspects of the games and the infrastructure are still in development. Feel free to file a <1>GitHub Issue</1> about any problems you experience!</0>": "",
"This server has been developed as part of the project <1>ADAM: Anticipating the Digital Age of Mathematics</1> at Heinrich Heine University Düsseldorf.": "",
"Server capacity": "",
"RAM": "",
" used": "",
"CPU": ""
}
+14 -85
View File
@@ -1,6 +1,5 @@
import * as React from 'react';
import { Outlet, useParams, useSearchParams } from "react-router-dom";
import { Suspense, useEffect, useState } from 'react';
import { Outlet, useParams } from "react-router-dom";
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
@@ -9,101 +8,31 @@ import '@fontsource/roboto/700.css';
import './css/reset.css';
import './css/app.css';
import { GameIdContext, PageContext, PreferencesContext} from './state/context';
import { PreferencesContext} from './components/infoview/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';
import i18n from './i18n';
export const GameIdContext = React.createContext<string>(undefined);
function App() {
let { t, i18n } = useTranslation()
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 gameId = "g/" + params.owner + "/" + params.repo
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])
React.useEffect(() => {
i18n.changeLanguage(language)
}, [language])
return (
<div className="app">
<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 />
<GameIdContext.Provider value={gameId}>
<PreferencesContext.Provider value={{mobile, layout, isSavePreferences, language, setLayout, setIsSavePreferences, setLanguage}}>
<React.Suspense>
<Outlet />
{ popupContent && <Popup /> }
</PageContext.Provider>
</PopupContext.Provider>
</PreferencesContext.Provider>
</React.Suspense>
</PreferencesContext.Provider>
</GameIdContext.Provider>
</div>
)
+311
View File
@@ -0,0 +1,311 @@
/**
* @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, faGear } from '@fortawesome/free-solid-svg-icons'
import { GameIdContext } from "../app"
import { InputModeContext, PreferencesContext, WorldLevelIdContext } from "./infoview/context"
import { GameInfo, useGetGameInfoQuery } from '../state/api'
import { changedOpenedIntro, selectCompleted, selectDifficulty, selectProgress } from '../state/progress'
import { useAppDispatch, useAppSelector } from '../hooks'
import { Button } from './button'
import { downloadProgress } from './popup/erase'
import { useTranslation } from 'react-i18next'
/** navigation buttons for mobile welcome page to switch between intro/tree/inventory. */
function MobileNavButtons({pageNumber, setPageNumber}:
{ pageNumber: number,
setPageNumber: any}) {
const gameId = React.useContext(GameIdContext)
const { t } = useTranslation()
const dispatch = useAppDispatch()
// if `prevText` or `prevIcon` is set, show a button to go back
let prevText = {0: null, 1: t("Intro"), 2: null}[pageNumber]
let prevIcon = {0: null, 1: null, 2: faBookOpen}[pageNumber]
let prevTitle = {0: null, 1: t("Game Introduction"), 2: t("World selection")}[pageNumber]
// if `nextText` or `nextIcon` is set, show a button to go forward
let nextText = {0: t("Start"), 1: null, 2: null}[pageNumber]
let nextIcon = {0: null, 1: faBook, 2: null}[pageNumber]
let nextTitle = {0: t("World selection"), 1: t("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. */
export function MenuButton({navOpen, setNavOpen}) {
return <Button to="" className="btn toggle-width" id="menu-btn" onClick={(ev) => {setNavOpen(!navOpen)}}>
{navOpen ? <FontAwesomeIcon icon={faXmark} /> : <FontAwesomeIcon icon={faBars} />}
</Button>
}
/** 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 { t } = useTranslation()
const gameId = React.useContext(GameIdContext)
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
return (levelId < worldSize ?
<Button inverted="true"
to={`/${gameId}/world/${worldId}/level/${levelId + 1}`} title={t("next level")}
disabled={difficulty >= 2 && !(completed || levelId == 0)}
onClick={() => setNavOpen(false)}>
<FontAwesomeIcon icon={faArrowRight} />&nbsp;{levelId ? t("Next") : t("Start")}
</Button>
:
<Button to={`/${gameId}`} inverted="true" title={t("back to world selection")} id="home-btn">
<FontAwesomeIcon icon={faHome} />&nbsp;{t("Leave World")}
</Button>
)
}
/** button to go one level back.
* only renders if the current level id is > 0.
*/
function PreviousButton({setNavOpen}) {
const { t } = useTranslation()
const gameId = React.useContext(GameIdContext)
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
return (levelId > 0 && <>
<Button disabled={levelId <= 0} inverted="true"
to={`/${gameId}/world/${worldId}/level/${levelId - 1}`}
title={t("previous level")}
onClick={() => setNavOpen(false)}>
<FontAwesomeIcon icon={faArrowLeft} />&nbsp;{t("Previous")}
</Button>
</>)
}
/** button to toggle between editor and typewriter */
function InputModeButton({setNavOpen, isDropdown}) {
const { t } = useTranslation()
const {levelId} = React.useContext(WorldLevelIdContext)
const {typewriterMode, setTypewriterMode, lockEditorMode} = React.useContext(InputModeContext)
/** toggle input mode if allowed */
function toggleInputMode(ev: React.MouseEvent) {
if (!lockEditorMode){
setTypewriterMode(!typewriterMode)
setNavOpen(false)
}
}
return <Button
className={`btn btn-inverted ${isDropdown? '' : 'toggle-width'}`} disabled={levelId <= 0 || lockEditorMode}
inverted="true" to=""
onClick={(ev) => toggleInputMode(ev)}
title={lockEditorMode ? t("Editor mode is enforced!") : typewriterMode ? t("Editor mode") : t("Typewriter mode")}>
<FontAwesomeIcon icon={(typewriterMode && !lockEditorMode) ? faCode : faTerminal} />
{isDropdown && ((typewriterMode && !lockEditorMode) ? <>&nbsp;{t("Editor mode")}</> : <>&nbsp;{t("Typewriter mode")}</>)}
</Button>
}
/** button to toggle iimpressum popup
*
* Note: Do not translate the word "Impressum"! German GDPR needs this.
*/
export function ImpressumButton({setNavOpen, toggleImpressum, isDropdown}) {
const { t } = useTranslation()
return <Button className="btn btn-inverted"
title={t("information, Impressum, privacy policy")} inverted="true" to="" onClick={(ev) => {toggleImpressum(ev); setNavOpen(false)}}>
<FontAwesomeIcon icon={faCircleInfo} />
{isDropdown && <>&nbsp;Impressum</>}
</Button>
}
export function PreferencesButton({setNavOpen, togglePreferencesPopup}) {
const { t } = useTranslation()
return <Button title={t("Preferences")} inverted="true" to="" onClick={() => {togglePreferencesPopup(); setNavOpen(false)}}>
<FontAwesomeIcon icon={faGear} />&nbsp;{t("Preferences")}
</Button>
}
function GameInfoButton({setNavOpen, toggleInfo}) {
const { t } = useTranslation()
return <Button className="btn btn-inverted"
title={t("Game Info & Credits")} inverted="true" to="" onClick={() => {toggleInfo(); setNavOpen(false)}}>
<FontAwesomeIcon icon={faCircleInfo} />&nbsp;{t("Game Info")}
</Button>
}
function EraseButton ({setNavOpen, toggleEraseMenu}) {
const { t } = useTranslation()
return <Button title={t("Clear Progress")} inverted="true" to="" onClick={() => {toggleEraseMenu(); setNavOpen(false)}}>
<FontAwesomeIcon icon={faEraser} />&nbsp;{t("Erase")}
</Button>
}
function DownloadButton ({setNavOpen, gameId, gameProgress}) {
const { t } = useTranslation()
return <Button title={t("Download Progress")} inverted="true" to="" onClick={(ev) => {downloadProgress(gameId, gameProgress, ev); setNavOpen(false)}}>
<FontAwesomeIcon icon={faDownload} />&nbsp;{t("Download")}
</Button>
}
function UploadButton ({setNavOpen, toggleUploadMenu}) {
const { t } = useTranslation()
return <Button title={t("Load Progress from JSON")} inverted="true" to="" onClick={() => {toggleUploadMenu(); setNavOpen(false)}}>
<FontAwesomeIcon icon={faUpload} />&nbsp;{t("Upload")}
</Button>
}
/** button to go back to welcome page */
function HomeButton({isDropdown}) {
const { t } = useTranslation()
const gameId = React.useContext(GameIdContext)
return <Button to={`/${gameId}`} inverted="true" title={t("back to world selection")} id="home-btn">
<FontAwesomeIcon icon={faHome} />
{isDropdown && <>&nbsp;{t("Home")}</>}
</Button>
}
function LandingPageButton() {
const { t } = useTranslation()
return <Button inverted="false" title={t("back to games selection")} to="/">
<FontAwesomeIcon icon={faArrowLeft} />&nbsp;<FontAwesomeIcon icon={faGlobe} />
</Button>
}
/** button in mobile level to toggle inventory.
* only displays a button if `setPageNumber` is set.
*/
function InventoryButton({pageNumber, setPageNumber}) {
const { t } = useTranslation()
return (setPageNumber &&
<Button to="" className="btn btn-inverted toggle-width"
title={pageNumber ? t("close inventory") : t("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, togglePreferencesPopup} : {
pageNumber: number,
setPageNumber: any,
gameInfo: GameInfo,
toggleImpressum: any,
toggleEraseMenu: any,
toggleUploadMenu: any,
toggleInfo: any,
togglePreferencesPopup: () => void;
}) {
const { t } = useTranslation()
const gameId = React.useContext(GameIdContext)
const gameProgress = useAppSelector(selectProgress(gameId))
const {mobile} = React.useContext(PreferencesContext)
const [navOpen, setNavOpen] = React.useState(false)
return <div className="app-bar">
<div className='app-bar-left'>
<LandingPageButton />
<span className="app-bar-title"></span>
</div>
<div>
{!mobile && <span className="app-bar-title">{t(gameInfo?.title, {ns: gameId})}</span>}
</div>
<div className="nav-btns">
{mobile && <MobileNavButtons pageNumber={pageNumber} setPageNumber={setPageNumber} />}
<MenuButton navOpen={navOpen} setNavOpen={setNavOpen} />
</div>
<div className={'menu dropdown' + (navOpen ? '' : ' hidden')}>
<GameInfoButton setNavOpen={setNavOpen} toggleInfo={toggleInfo}/>
<EraseButton setNavOpen={setNavOpen} toggleEraseMenu={toggleEraseMenu}/>
<DownloadButton setNavOpen={setNavOpen} gameId={gameId} gameProgress={gameProgress}/>
<UploadButton setNavOpen={setNavOpen} toggleUploadMenu={toggleUploadMenu}/>
<ImpressumButton setNavOpen={setNavOpen} toggleImpressum={toggleImpressum} isDropdown={true} />
<PreferencesButton setNavOpen={setNavOpen} togglePreferencesPopup={togglePreferencesPopup}/>
</div>
</div>
}
/** the navigation bar in a level */
export function LevelAppBar({isLoading, levelTitle, toggleImpressum, toggleInfo, togglePreferencesPopup, pageNumber=undefined, setPageNumber=undefined} : {
isLoading: boolean,
levelTitle: string,
toggleImpressum: any,
toggleInfo: any,
togglePreferencesPopup: any,
pageNumber?: number,
setPageNumber?: any,
}) {
const { t } = useTranslation()
const gameId = React.useContext(GameIdContext)
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
const {mobile} = React.useContext(PreferencesContext)
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}/>
<GameInfoButton setNavOpen={setNavOpen} toggleInfo={toggleInfo}/>
<ImpressumButton setNavOpen={setNavOpen} toggleImpressum={toggleImpressum} isDropdown={true} />
<PreferencesButton setNavOpen={setNavOpen} togglePreferencesPopup={togglePreferencesPopup}/>
</div>
</> :
<>
{/* DESKTOP VERSION */}
<div className='app-bar-left'>
<HomeButton isDropdown={false} />
<span className="app-bar-title">{worldTitle && `${t("World")}: ${t(worldTitle, {ns: gameId})}`}</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}/>
<MenuButton navOpen={navOpen} setNavOpen={setNavOpen}/>
</div>
<div className={'menu dropdown' + (navOpen ? '' : ' hidden')}>
<GameInfoButton setNavOpen={setNavOpen} toggleInfo={toggleInfo}/>
<ImpressumButton setNavOpen={setNavOpen} toggleImpressum={toggleImpressum} isDropdown={true} />
<PreferencesButton setNavOpen={setNavOpen} togglePreferencesPopup={togglePreferencesPopup}/>
</div>
</>
}
</div>
}
+15
View File
@@ -0,0 +1,15 @@
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>
}
}
-450
View File
@@ -1,450 +0,0 @@
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&nbsp;<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} />&nbsp;{t("Home")}
</Button> :
<Button className="btn"
title=""
to={`/${gameId}/world/${worldId}/level/${levelId + 1}`} >
{t("Next")}&nbsp;<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>
}
-104
View File
@@ -1,104 +0,0 @@
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>
}
@@ -1,37 +0,0 @@
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>
</>
}
-443
View File
@@ -1,443 +0,0 @@
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} />&nbsp;{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")}&nbsp;{diag.range.start.line}, {t("Character")}&nbsp;{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} />&nbsp;{t("Home")}
</Button>
:
<Button to={`/${gameId}/world/${props.world}/level/${props.level + 1}`}>
Next&nbsp;<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>
}
+8 -15
View File
@@ -1,24 +1,17 @@
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">
<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>
<h1>Oops!</h1>
<p>Sorry, an unexpected error has occurred.</p>
<p>
<i>{error.statusText || error.message}</i>
</p>
</div>
)
);
}
-14
View File
@@ -1,14 +0,0 @@
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>
}
-112
View File
@@ -1,112 +0,0 @@
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.')
}
+125
View File
@@ -0,0 +1,125 @@
import { GameHint, InteractiveGoalsWithHints, ProofState } from "./infoview/rpc_api";
import * as React from 'react';
import Markdown from './markdown';
import { DeletedChatContext, ProofContext } from "./infoview/context";
import { lastStepHasErrors } from "./infoview/goals";
import { Button } from "./button";
import { useTranslation } from "react-i18next";
import { GameIdContext } from "../app";
/** Plug-in the variable names in a hint. We do this client-side to prepare
* for i18n in the future. i.e. one should be able translate the `rawText`
* and have the variables substituted just before displaying.
*/
function getHintText(hint: GameHint): string {
const gameId = React.useContext(GameIdContext)
let { t } = useTranslation()
if (hint.rawText) {
// Replace the variable names used in the hint with the ones used by the player
// variable names are marked like `«{g}»` inside the text.
return t(hint.rawText, {ns: gameId}).replaceAll(/«\{(.*?)\}»/g, ((_, v) =>
// `hint.varNames` contains tuples `[oldName, newName]`
(hint.varNames.find(x => x[0] == v))[1]))
} else {
// hints created in the frontend do not have a `rawText`
// TODO: `hint.text` could be removed in theory.
return t(hint.text, {ns: gameId})
}
}
export function Hint({hint, step, selected, toggleSelection, lastLevel} : {hint: GameHint, step: number, selected: number, toggleSelection: any, lastLevel?: boolean}) {
return <div className={`message information step-${step}` + (step == selected ? ' selected' : '') + (lastLevel ? ' recent' : '')} onClick={toggleSelection}>
<Markdown>{getHintText(hint)}</Markdown>
</div>
}
export function HiddenHint({hint, step, selected, toggleSelection, lastLevel} : {hint: GameHint, step: number, selected: number, toggleSelection: any, lastLevel?: boolean}) {
return <div className={`message warning step-${step}` + (step == selected ? ' selected' : '') + (lastLevel ? ' recent' : '')} onClick={toggleSelection}>
<Markdown>{getHintText(hint)}</Markdown>
</div>
}
export function Hints({hints, showHidden, step, selected, toggleSelection, lastLevel} : {hints: GameHint[], showHidden: boolean, step: number, selected: number, toggleSelection: any, lastLevel?: boolean}) {
if (!hints) {return <></>}
const openHints = hints.filter(hint => !hint.hidden)
const hiddenHints = hints.filter(hint => hint.hidden)
// 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>{getHintText(hint)}</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.
* Hidden hints are not filtered.
*/
export function filterHints(hints: GameHint[], prevHints: GameHint[]): GameHint[] {
if (!hints) {
return []}
else if (!prevHints) {
return hints }
else {
return hints.filter((hint) => hint.hidden ||
(prevHints.find(x => (x.text == hint.text && x.hidden == hint.hidden)) === undefined)
)
}
}
function hasHiddenHints(step: InteractiveGoalsWithHints): boolean {
return step?.goals[0]?.hints.some((hint) => hint.hidden)
}
export function MoreHelpButton({selected=null} : {selected?: number}) {
const { t } = useTranslation()
const {proof, setProof} = React.useContext(ProofContext)
const {deletedChat, setDeletedChat, showHelp, setShowHelp} = React.useContext(DeletedChatContext)
let k = proof?.steps.length ?
((selected === null) ? (proof?.steps.length - (lastStepHasErrors(proof) ? 2 : 1)) : selected)
: 0
const activateHiddenHints = (ev) => {
// If the last step (`k`) has errors, we want the hidden hints from the
// second-to-last step to be affected
if (!(proof?.steps.length)) {return}
// state must not be mutated, therefore we need to clone the set
let tmp = new Set(showHelp)
if (tmp.has(k)) {
tmp.delete(k)
} else {
tmp.add(k)
}
setShowHelp(tmp)
console.debug(`help: ${Array.from(tmp.values())}`)
}
if (hasHiddenHints(proof?.steps[k]) && !showHelp.has(k)) {
return <Button to="" onClick={activateHiddenHints}>
{t("Show more help!")}
</Button>
}
}
@@ -5,20 +5,14 @@ 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';
import { GameHint, InteractiveGoal, InteractiveTermGoal,InteractiveGoalsWithHints, ProofState } from './rpc_api';
import { PreferencesState } from '../../state/preferences';
export const MonacoEditorContext = React.createContext<monaco.editor.IStandaloneCodeEditor>(
null as any)
export type InfoStatus = 'updating' | 'error' | 'ready';
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 */
@@ -99,45 +93,36 @@ export const PreferencesContext = React.createContext<IPreferencesContext>({
setLanguage: () => {},
})
export const WorldLevelIdContext = React.createContext<{
worldId : string,
levelId: number
}>({
worldId : null,
levelId: 0,
})
export const ChatContext = React.createContext<{
selectedStep : number
/** Context to keep highlight selected proof step and corresponding chat messages. */
export const SelectionContext = 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: () => {},
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: () => {},
chatRef: null
setShowHelp: () => {}
})
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>>,
+17 -34
View File
@@ -4,15 +4,15 @@
* 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 { PageContext } from '../../state/context';
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, InteractiveGoalsWithHints, InteractiveHypothesisBundle, ProofState } from './rpc_api';
// import { RpcSessionAtPos } from '@leanprover/infoview/*';
// import { DocumentPosition } from '../../../../node_modules/lean4-infoview/src/infoview/util';
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';
@@ -76,7 +76,7 @@ function getFilteredHypotheses(hyps: InteractiveHypothesisBundle[], filter: Goal
interface HypProps {
hyp: InteractiveHypothesisBundle
mvarId?: any // MVarId
mvarId?: MVarId
}
function Hyp({ hyp: h, mvarId }: HypProps) {
@@ -133,14 +133,13 @@ interface GoalProps {
filter: GoalFilterState
showHints?: boolean
typewriter: boolean
unbundle?: boolean /** unbundle `x y : Nat` into `x : Nat` and `y : Nat` */
}
/**
* 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, unbundle } = props
const { goal, filter, showHints, typewriter } = props
let { t } = useTranslation()
// TODO: Apparently `goal` can be `undefined`
@@ -154,8 +153,8 @@ export const Goal = React.memo((props: GoalProps) => {
{ ...locs, subexprTemplate: { mvarId: goal.mvarId, loc: { target: '' }}} :
undefined,
[locs, goal.mvarId])
const goalLi = <div key={'goal'} className="goal">
{/* <div className="goal-title">{t("Goal")}:</div> */}
const goalLi = <div key={'goal'}>
<div className="goal-title">{t("Goal")}:</div>
<LocationsContext.Provider value={goalLocs}>
<InteractiveCode fmt={goal.type} />
</LocationsContext.Provider>
@@ -165,38 +164,22 @@ export const Goal = React.memo((props: GoalProps) => {
// if (props.goal.isInserted) cn += 'b--inserted '
// if (props.goal.isRemoved) cn += 'b--removed '
function unbundleHyps (hyps: InteractiveHypothesisBundle[]) : InteractiveHypothesisBundle[] {
return hyps.flatMap(hyp => (
unbundle ? hyp.names.map(name => {return {...hyp, names: [name]}}) : [hyp]
))
}
// 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))
const objectHyps = hyps.filter(hyp => !hyp.isAssumption)
const assumptionHyps = hyps.filter(hyp => hyp.isAssumption)
return <>
return <div>
{/* {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">{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">{t("Assumptions")}:</div>
{assumptionHyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)}</div> }
</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}
</>}
{!filter.reverse && goalLi}
{/* {showHints && hints} */}
</>
</div>
})
export const MainAssumptions = React.memo((props: GoalProps2) => {
+12 -15
View File
@@ -13,11 +13,10 @@ import { RpcContext, useRpcSessionAtPos } from '../../../../node_modules/lean4-i
import { GoalsLocation, Locations, LocationsContext } from '../../../../node_modules/lean4-infoview/src/infoview/goalLocation'
import { AllMessages, lspDiagToInteractive } from './messages'
// import { goalsToString, Goal, MainAssumptions, OtherGoals } from './goals'
import { goalsToString, Goal, MainAssumptions, OtherGoals } from './goals'
import { InteractiveTermGoal, InteractiveGoalsWithHints, InteractiveGoals, ProofState } from './rpc_api'
import { MonacoEditorContext, ProofStateProps, InfoStatus, ProofContext } from '../../state/context'
import { MonacoEditorContext, ProofStateProps, InfoStatus, ProofContext } from './context'
import { useTranslation } from 'react-i18next'
import { GoalsTabs } from './main'
// TODO: All about pinning could probably be removed
type InfoKind = 'cursor' | 'pin'
@@ -124,39 +123,37 @@ const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => {
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 && <>
<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} /> */}
<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">{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 : []}
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>{t("Loading goal…")}</div></>)} */}
<><CircularProgress /><div>{t("Loading goal…")}</div></>)}
<AllMessages />
{/* <LocationsContext.Provider value={locs}>
{goals && goals.goals.length > 1 && <div className="goals-section other-goals">
<div className="goals-section-title">Weitere Goals</div>
+56 -55
View File
@@ -18,26 +18,27 @@ 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, useGetGameInfoQuery, useLoadLevelQuery } from '../../state/api';
import { LevelInfo, useGetGameInfoQuery } from '../../state/api';
import { changedInventory, levelCompleted, selectCode, selectCompleted, selectInventory } from '../../state/progress';
import Markdown from '../markdown';
import { Infos } from './infos';
import { AllMessages, Errors, WithLspDiagnosticsContext } from './messages';
import { Goal, isLastStepWithErrors, lastStepHasErrors, loadGoals } from './goals';
import { ChatContext, PageContext, PreferencesContext, MonacoEditorContext, ProofContext, GameIdContext } from '../../state/context';
import { DeletedChatContext, InputModeContext, PreferencesContext, MonacoEditorContext, ProofContext, SelectionContext, WorldLevelIdContext } from './context';
import { Typewriter, getInteractiveDiagsAt, hasErrors, hasInteractiveErrors } from './typewriter';
import { InteractiveDiagnostic } from '@leanprover/infoview/*';
import { Button } from '../button';
import { CircularProgress } from '@mui/material';
import { GameHint, InteractiveGoalWithHints, InteractiveGoalsWithHints, ProofState } from './rpc_api';
import { GameHint, InteractiveGoalsWithHints, ProofState } from './rpc_api';
import { store } from '../../state/store';
import { Hints, MoreHelpButton, filterHints } from '../hints';
import { DocumentPosition } from '../../../../node_modules/lean4-infoview/src/infoview/util';
import { DiagnosticSeverity } from 'vscode-languageclient';
import { useTranslation } from 'react-i18next';
import path from 'path';
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
@@ -45,10 +46,10 @@ import { Button, Markdown } from '../utils'
*/
export function DualEditor({ level, codeviewRef, levelId, worldId, worldSize }) {
const ec = React.useContext(EditorContext)
const { typewriterMode, lockEditorMode } = React.useContext(PageContext)
const { typewriterMode, lockEditorMode } = React.useContext(InputModeContext)
return <>
<div className={(typewriterMode && !lockEditorMode) ? 'hidden' : ''}>
{/* <ExerciseStatement showLeanStatement={true} /> */}
<ExerciseStatement data={level} showLeanStatement={true} />
<div ref={codeviewRef} className={'codeview'}></div>
</div>
{ec ?
@@ -62,8 +63,8 @@ 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, lockEditorMode } = React.useContext(PageContext)
const gameId = React.useContext(GameIdContext)
const { typewriterMode, lockEditorMode } = React.useContext(InputModeContext)
const {proof, setProof} = React.useContext(ProofContext)
@@ -76,7 +77,7 @@ function DualEditorMain({ worldId, levelId, level, worldSize }: { worldId: strin
// On completion, add the names of all new items to the local storage
let newTiles = [
...level?.tactics,
...level?.theorems,
...level?.lemmas,
...level?.definitions
].filter((tile) => tile.new).map((tile) => tile.name)
@@ -134,24 +135,20 @@ function DualEditorMain({ worldId, levelId, level, worldSize }: { worldId: strin
*
* If `showLeanStatement` is true, it will additionally display the lean code.
*/
export function ExerciseStatement({ showLeanStatement = false }) {
function ExerciseStatement({ data, showLeanStatement = false }) {
let { t } = useTranslation()
const {gameId, worldId, levelId } = React.useContext(GameIdContext)
const levelInfo = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
const gameId = React.useContext(GameIdContext)
if (!(levelInfo.data?.descrText || levelInfo.data?.descrFormat)) { return <></> }
if (!(data?.descrText || data?.descrFormat)) { return <></> }
return <>
<div className="exercise-statement">
{levelInfo.data?.descrText ?
{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}\``}
{(data?.displayName ? `**Theorem** \`${data?.displayName}\`: ` : '') + t(data?.descrText, {ns: gameId})}
</Markdown>
}
{levelInfo.data?.descrFormat && showLeanStatement &&
<p><code className="lean-code">{levelInfo.data?.descrFormat}</code></p>
{data?.descrFormat && showLeanStatement &&
<p><code className="lean-code">{data?.descrFormat}</code></p>
}
</div>
</>
@@ -162,16 +159,19 @@ export function ExerciseStatement({ showLeanStatement = false }) {
export function Main(props: { world: string, level: number, data: LevelInfo}) {
let { t } = useTranslation()
const ec = React.useContext(EditorContext);
const {gameId, worldId, levelId} = React.useContext(GameIdContext)
const gameId = React.useContext(GameIdContext)
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
const { proof, setProof } = React.useContext(ProofContext)
const {selectedStep, setSelectedStep, setDeletedChat, showHelp, setShowHelp} = React.useContext(ChatContext)
const {selectedStep, setSelectedStep} = React.useContext(SelectionContext)
const { setDeletedChat, showHelp, setShowHelp } = React.useContext(DeletedChatContext)
function toggleSelection(line: number) {
return (ev) => {
console.debug('toggled selection')
if (selectedStep == line) {
setSelectedStep(null)
setSelectedStep(undefined)
} else {
setSelectedStep(line)
}
@@ -246,11 +246,11 @@ export function Main(props: { world: string, level: number, data: LevelInfo}) {
</div>
}
<Infos />
{/* <Hints hints={proof?.steps[curPos?.line]?.goals[0]?.hints}
<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}/> */}
<MoreHelpButton selected={curPos?.line}/>
</div>
}
@@ -266,7 +266,7 @@ const goalFilter = {
}
/** The display of a single entered lean command */
export function Command({ proof, i, deleteProof }: { proof: ProofState, i: number, deleteProof: any }) {
function Command({ proof, i, deleteProof }: { proof: ProofState, i: number, deleteProof: any }) {
let {t} = useTranslation()
// The first step will always have an empty command
@@ -311,7 +311,7 @@ export function Command({ proof, i, deleteProof }: { proof: ProofState, i: numbe
// message = diag.message
// }
// const { typewriterMode } = React.useContext(PageContext)
// const { typewriterMode } = React.useContext(InputModeContext)
// return (
// // <details open>
@@ -339,18 +339,18 @@ export function Command({ proof, i, deleteProof }: { proof: ProofState, i: numbe
// )
// }, fastIsEqual)
/** 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 }) {
/** The tabs of goals that lean ahs after the command of this step has been processed */
function GoalsTabs({ proofStep, last, onClick, onGoalChange=(n)=>{}}: { proofStep: InteractiveGoalsWithHints, last : boolean, onClick? : any, onGoalChange?: (n?: number) => void }) {
let { t } = useTranslation()
const [selectedGoal, setSelectedGoal] = React.useState<number>(0)
if (goals.length == 0) {
if (proofStep.goals.length == 0) {
return <></>
}
return <div className="goal-tabs" onClick={onClick}>
<div className={`tab-bar ${last ? 'current' : ''}`}>
{goals.map((goal, i) => (
{proofStep.goals.map((goal, i) => (
// TODO: Should not use index as key.
<div key={`proof-goal-${i}`} className={`tab ${i == (selectedGoal) ? "active" : ""}`} onClick={(ev) => { onGoalChange(i); setSelectedGoal(i); ev.stopPropagation() }}>
{i ? t("Goal") + ` ${i + 1}` : t("Active Goal")}
@@ -358,7 +358,7 @@ export function GoalsTabs({ goals, last, onClick, onGoalChange=(n)=>{}}: { goals
))}
</div>
<div className="goal-tab vscode-light">
<Goal typewriter={false} filter={goalFilter} goal={goals[selectedGoal]?.goal} unbundle={false} />
<Goal typewriter={false} filter={goalFilter} goal={proofStep.goals[selectedGoal]?.goal} />
</div>
</div>
}
@@ -366,7 +366,7 @@ export function GoalsTabs({ goals, last, onClick, onGoalChange=(n)=>{}}: { goals
// 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',
@@ -400,21 +400,23 @@ export function TypewriterInterfaceWrapper(props: { world: string, level: number
export function TypewriterInterface({props}) {
let { t } = useTranslation()
const ec = React.useContext(EditorContext)
const {gameId,worldId, levelId} = React.useContext(GameIdContext)
const gameId = React.useContext(GameIdContext)
const editor = React.useContext(MonacoEditorContext)
const model = editor.getModel()
const uri = model.uri.toString()
const gameInfo = useGetGameInfoQuery({game: gameId})
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
let image: string = gameInfo.data?.worlds.nodes[worldId].image
const [disableInput, setDisableInput] = React.useState<boolean>(false)
const [loadingProgress, setLoadingProgress] = React.useState<number>(0)
const { selectedStep, setSelectedStep, setDeletedChat, showHelp, setShowHelp } = React.useContext(ChatContext)
const { setDeletedChat, showHelp, setShowHelp } = React.useContext(DeletedChatContext)
const {mobile} = React.useContext(PreferencesContext)
const { proof, setProof, crashed, setCrashed, interimDiags } = React.useContext(ProofContext)
const { setTypewriterInput } = React.useContext(PageContext)
const { setTypewriterInput } = React.useContext(InputModeContext)
const { selectedStep, setSelectedStep } = React.useContext(SelectionContext)
const proofPanelRef = React.useRef<HTMLDivElement>(null)
// const config = useEventResult(ec.events.changedInfoviewConfig) ?? defaultInfoviewConfig;
@@ -447,7 +449,7 @@ export function TypewriterInterface({props}) {
text: '',
forceMoveMarkers: false
}])
setSelectedStep(null)
setSelectedStep(undefined)
setTypewriterInput(proof?.steps[line].command)
// Reload proof on deleting
loadGoals(rpcSess, uri, setProof, setCrashed)
@@ -459,7 +461,7 @@ export function TypewriterInterface({props}) {
return (ev) => {
if (mobile) {return}
if (selectedStep == line) {
setSelectedStep(null)
setSelectedStep(undefined)
console.debug(`unselected step`)
} else {
setSelectedStep(line)
@@ -508,19 +510,19 @@ export function TypewriterInterface({props}) {
return <div className="typewriter-interface">
<RpcContext.Provider value={rpcSess}>
<div className="content">
{/* <div className='world-image-container empty'>
<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 className="tmp-pusher">
{/* <div className="world-image-container empty">
</div>
</div> */}
</div> */}
</div>
<div className='proof' ref={proofPanelRef}>
{/* <ExerciseStatement /> */}
<ExerciseStatement data={props.data} />
{crashed ? <div>
<p className="crashed_message">{t("Crashed! Go to editor mode and fix your proof! Last server response:")}</p>
{interimDiags.map(diag => {
@@ -531,7 +533,7 @@ export function TypewriterInterface({props}) {
[DiagnosticSeverity.Hint]: 'hint',
}[diag.severity] : '';
return <div key={diag.message} >
return <div>
<div className={`${severityClass} ml1 message`}>
<p className="mv2">{t("Line")}&nbsp;{diag.range.start.line}, {t("Character")}&nbsp;{diag.range.start.character}</p>
<pre className="font-code pre-wrap">
@@ -555,10 +557,8 @@ export function TypewriterInterface({props}) {
// </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} />
</>}
<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>
@@ -566,11 +566,12 @@ export function TypewriterInterface({props}) {
}
{mobile &&
<Hints key={`hints-${i}`}
hints={filteredHints.map(hint => ({hint: hint, step: i}))} />
hints={filteredHints} showHidden={showHelp.has(i)} step={i}
selected={selectedStep} toggleSelection={toggleSelectStep(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) => {}}/>
<GoalsTabs proofStep={step} last={i == proof?.steps.length - (lastStepHasErrors(proof) ? 2 : 1)} onClick={toggleSelectStep(i)} onGoalChange={i == proof?.steps.length - (lastStepHasErrors(proof) ? 2 : 1) ? (n) => setDisableInput(n > 0) : (n) => {}}/>
}
{mobile && i == proof?.steps.length - 1 &&
<MoreHelpButton />
@@ -601,7 +602,7 @@ export function TypewriterInterface({props}) {
<div className="button-row mobile">
{props.level >= props.worldSize ?
<Button to={`/${gameId}`}>
<FontAwesomeIcon icon={faHome} />&nbsp;{t("Home")}
<FontAwesomeIcon icon={faHome} />&nbsp;{t("Leave World")}
</Button>
:
<Button to={`/${gameId}/world/${props.world}/level/${props.level + 1}`}>
+2 -2
View File
@@ -10,7 +10,7 @@ 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 { PageContext } from '../../state/context'
import { InputModeContext } from './context'
import { useTranslation } from 'react-i18next'
interface MessageViewProps {
@@ -80,7 +80,7 @@ const MessageView = React.memo(({uri, diag}: MessageViewProps) => {
message = diag.message
}
const { typewriterMode, lockEditorMode } = React.useContext(PageContext)
const { typewriterMode, lockEditorMode } = React.useContext(InputModeContext)
return (
// <details open>
@@ -1,72 +1,67 @@
/**
* @fileOverview Defines the interface for the communication with the server.
*
* This file is based on `vscode-lean4/vscode-lean4/src/rpcApi.ts`
*/
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'
import type { Range } from 'vscode-languageserver-protocol';
import type { ContextInfo, FVarId, CodeWithInfos, MVarId } from '@leanprover/infoview-api';
import { InteractiveDiagnostic, TermInfo } from '@leanprover/infoview/*';
import type { Diagnostic } from 'vscode-languageserver-protocol';
export interface InteractiveHypothesisBundle {
/** The pretty names of the variables in the bundle. Anonymous names are rendered
* as `"[anonymous]"` whereas inaccessible ones have a `✝` appended at the end.
* Use `InteractiveHypothesisBundle_nonAnonymousNames` to filter anonymouse ones out. */
names: string[]
fvarIds?: FVarId[]
type: CodeWithInfos
val?: CodeWithInfos
isInstance?: boolean
isType?: boolean
isInserted?: boolean
isRemoved?: boolean
isAssumption?: boolean
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
hyps: InteractiveHypothesisBundle[];
type: CodeWithInfos;
ctx?: ContextInfo;
}
export interface InteractiveGoal extends InteractiveGoalCore {
userName?: string
goalPrefix?: string
mvarId?: MVarId
isInserted?: boolean
isRemoved?: boolean
userName?: string;
goalPrefix?: string;
mvarId?: MVarId;
isInserted?: boolean;
isRemoved?: boolean;
}
export interface InteractiveGoals extends InteractiveGoalCore {
goals: InteractiveGoals[]
goals: InteractiveGoals[];
}
export interface InteractiveTermGoal extends InteractiveGoalCore {
range?: Range
term?: TermInfo
range?: Range;
term?: TermInfo;
}
export interface GameHint {
text: string
hidden: boolean
rawText: string
varNames?: string[][] // in Lean: `Array (Name × Name)`
text: string;
hidden: boolean;
rawText: string;
varNames: string[][]; // in Lean: `Array (Name × Name)`
}
export interface InteractiveGoalWithHints {
goal: InteractiveGoal
hints: GameHint[]
goal: InteractiveGoal;
hints: GameHint[];
}
export interface InteractiveGoalsWithHints {
goals: InteractiveGoalWithHints[]
command: string
diags: InteractiveDiagnostic[]
goals: InteractiveGoalWithHints[];
command: string;
diags: InteractiveDiagnostic[];
}
/**
@@ -79,11 +74,11 @@ export interface ProofState {
*
* In particular `step[i]` is the proof step at the beginning of line `i` in vscode.
*/
steps: InteractiveGoalsWithHints[]
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
diagnostics : InteractiveDiagnostic[];
completed : Boolean;
completedWithWarnings : Boolean;
}
+21 -32
View File
@@ -3,26 +3,24 @@ 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 { 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 { 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 { ChatContext, PageContext, MonacoEditorContext, ProofContext, GameIdContext } from '../../state/context'
import { DeletedChatContext, InputModeContext, MonacoEditorContext, ProofContext } from './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;
@@ -37,12 +35,6 @@ 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')
@@ -83,23 +75,21 @@ export function Typewriter({disabled}: {disabled?: boolean}) {
/** 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(PageContext)
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)
const {gameId, worldId, levelId} = React.useContext(GameIdContext)
// state to store the last batch of deleted messages
const {setDeletedChat} = React.useContext(ChatContext)
const {setDeletedChat} = React.useContext(DeletedChatContext)
const rpcSess = React.useContext(RpcContext)
@@ -110,13 +100,13 @@ export function Typewriter({disabled}: {disabled?: boolean}) {
// 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
@@ -126,7 +116,7 @@ export function Typewriter({disabled}: {disabled?: boolean}) {
loadGoals(rpcSess, uri, setProof, setCrashed)
}
editor?.setPosition(pos)
editor.setPosition(pos)
}, [typewriterInput, editor])
useEffect(() => {
@@ -137,9 +127,8 @@ export function Typewriter({disabled}: {disabled?: boolean}) {
/* Load proof on start/switching to typewriter */
useEffect(() => {
setProof(null)
loadGoals(rpcSess, uri, setProof, setCrashed)
}, [gameId, worldId, levelId])
}, [])
/** If the last step has an error, add the command to the typewriter. */
useEffect(() => {
@@ -162,7 +151,7 @@ export function Typewriter({disabled}: {disabled?: boolean}) {
// 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}`)
@@ -181,6 +170,7 @@ export function Typewriter({disabled}: {disabled?: boolean}) {
// }, [uri]);
useEffect(() => {
const myEditor = monaco.editor.create(inputRef.current!, {
value: typewriterInput,
@@ -211,7 +201,6 @@ export function Typewriter({disabled}: {disabled?: boolean}) {
},
overviewRulerBorder: false,
theme: 'vs-code-theme-converted',
fontFamily: "JuliaMono",
contextmenu: false
})
@@ -240,7 +229,7 @@ export function Typewriter({disabled}: {disabled?: boolean}) {
if (!oneLineEditor) return
// Run command when pressing enter
const l = oneLineEditor.onKeyUp((ev) => {
if (ev.code === "Enter" || ev.code === "NumpadEnter") {
if (ev.code === "Enter") {
runCommand()
}
})
+117 -219
View File
@@ -1,68 +1,117 @@
import * as React from 'react'
import { useState, useEffect, createContext, useContext } from 'react';
import * as React from 'react';
import { useState, useEffect } from 'react';
import '../css/inventory.css'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faLock, faBan, faCheck, faXmark } from '@fortawesome/free-solid-svg-icons'
import { faLock, faBan, faCheck } from '@fortawesome/free-solid-svg-icons'
import { faClipboard } from '@fortawesome/free-regular-svg-icons'
import { useLoadDocQuery, InventoryTile, useLoadInventoryOverviewQuery, useLoadLevelQuery } from '../state/api';
import { changedInventory, selectDifficulty, selectInventory } from '../state/progress';
import { GameIdContext } from '../app';
import Markdown from './markdown';
import { useLoadDocQuery, InventoryTile, LevelInfo, InventoryOverview, useLoadInventoryOverviewQuery } from '../state/api';
import { selectDifficulty, selectInventory } from '../state/progress';
import { store } from '../state/store';
import { useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { t } from 'i18next';
import { NavButton } from './navigation';
import { LoadingIcon, Markdown } from './utils';
import { GameIdContext } from '../state/context';
import { useAppDispatch } from '../hooks';
export function Inventory({levelInfo, openDoc, lemmaTab, setLemmaTab, enableAll=false} :
{
levelInfo: LevelInfo|InventoryOverview,
openDoc: (props: {name: string, type: string}) => void,
lemmaTab: any,
setLemmaTab: any,
enableAll?: boolean,
}) {
const { t } = useTranslation()
/** 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: () => {}
})
return (
<div className="inventory">
{/* TODO: Click on Tactic: show info
TODO: click on paste icon -> paste into command line */}
<h2>{t("Tactics")}</h2>
{levelInfo?.tactics &&
<InventoryList items={levelInfo?.tactics} docType="Tactic" openDoc={openDoc} enableAll={enableAll}/>
}
<h2>{t("Definitions")}</h2>
{levelInfo?.definitions &&
<InventoryList items={levelInfo?.definitions} docType="Definition" openDoc={openDoc} enableAll={enableAll}/>
}
<h2>{t("Theorems")}</h2>
{levelInfo?.lemmas &&
<InventoryList items={levelInfo?.lemmas} docType="Lemma" openDoc={openDoc} level={levelInfo} enableAll={enableAll} tab={lemmaTab} setTab={setLemmaTab}/>
}
</div>
)
}
function InventoryList({items, docType, openDoc, tab=null, setTab=undefined, level=undefined, enableAll=false} :
{
items: InventoryTile[],
docType: string,
openDoc(props: {name: string, type: string}): void,
tab?: any,
setTab?: any,
level?: LevelInfo|InventoryOverview,
enableAll?: boolean,
}) {
// 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?
/**
*/
function InventoryItem({item, name, displayName, locked, disabled, newly, showDoc } :
{ item: InventoryTile,
name: any,
displayName: any,
locked: any,
disabled: any,
newly: any,
showDoc: any
}) {
const gameId = React.useContext(GameIdContext)
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()
// 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)
return <>
{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>)}
</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)
).filter(item => !item.hidden && ((tab ?? categories[0]) == item.category)).map((item, i) => {
return <InventoryItem key={`${item.category}-${item.name}`}
item={item}
showDoc={() => {openDoc({name: item.name, type: docType})}}
name={item.name} displayName={item.displayName} locked={difficulty > 0 ? item.locked : false}
disabled={item.disabled} newly={item.new} enableAll={enableAll} />
})
}
</div>
</>
}
function InventoryItem({item, name, displayName, locked, disabled, newly, showDoc, enableAll=false}) {
const icon = locked ? <FontAwesomeIcon icon={faLock} /> :
disabled ? <FontAwesomeIcon icon={faBan} /> : <></>
disabled ? <FontAwesomeIcon icon={faBan} /> : item.st
const className = locked ? "locked" : disabled ? "disabled" : newly ? "new" : ""
// Note: This is somewhat a hack as the statement of theorems comes currently in the form
// Note: This is somewhat a hack as the statement of lemmas comes currently in the form
// `Namespace.statement_name (x y : Nat) : some type`
const title = locked ? 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) {
if (enableAll || !locked) {
showDoc()
// }
}
}
const copyItemName = (ev) => {
@@ -74,10 +123,7 @@ function InventoryItem({item, name, displayName, locked, disabled, newly, showDo
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}>
return <div className={`item ${className}${enableAll ? ' enabled' : ''}`} onClick={handleClick} title={title}>
{icon} {displayName}
<div className="copy-button" onClick={copyItemName}>
{copied ? <FontAwesomeIcon icon={faCheck} /> : <FontAwesomeIcon icon={faClipboard} />}
@@ -85,190 +131,42 @@ function InventoryItem({item, name, displayName, locked, disabled, newly, showDo
</div>
}
function InventoryList({ items, tab=null, setTab=()=>{} } :
{
items: InventoryTile[],
tab?: string,
setTab?: React.Dispatch<React.SetStateAction<string>>
}) {
const { gameId, worldId, levelId } = React.useContext(GameIdContext)
const { setDocTile, categoryTab, setCategoryTab } = useContext(InventoryContext)
const difficulty = useSelector(selectDifficulty(gameId))
const inventory: string[] = selectInventory(gameId)(store.getState())
const [categories, setCategories] = useState<Array<string>>([])
const [modifiedItems, setModifiedItems] = useState<Array<InventoryTile>>([])
const [currentWorldItems, setCurrentWorldItems] = useState<Array<InventoryTile>>([])
useEffect(() => {
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 &&
<div className="tab-bar">
{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(
// 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}`}
item={item}
showDoc={() => {setDocTile(item)}}
name={item.name} displayName={item.displayName} locked={difficulty > 0 ? item.locked : false}
disabled={item.disabled}
newly={item.new} />
})
}
</div>
</>
}
/** 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="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>
)
}
/** 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) }
export function Documentation({name, type, handleClose}) {
const gameId = React.useContext(GameIdContext)
const doc = useLoadDocQuery({game: gameId, type: type, name: name})
return <div className="documentation">
<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 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>{t(doc.data?.content, {ns: gameId})}</Markdown>
</div>
}
/** 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})
/** 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)
const [lemmaTab, setLemmaTab] = useState(levelInfo?.lemmaTab)
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 [docTile, setDocTile] = useState<InventoryTile>(null)
const [inventoryDoc, setInventoryDoc] = useState<{name: string, type: string}>(null)
// Set `inventoryDoc` to `null` to close the doc
function closeInventoryDoc() {setInventoryDoc(null)}
useEffect(() => {
// If the level specifies `TheoremTab "Nat"`, we switch to this tab on loading.
useEffect(() => {
// If the level specifies `LemmaTab "Nat"`, we switch to this tab on loading.
// `defaultTab` is `null` or `undefined` otherwise, in which case we don't want to switch.
if (levelInfo?.data?.theoremTab) {
setTheoremTab(levelInfo?.data?.theoremTab)
if (levelInfo?.lemmaTab) {
setLemmaTab(levelInfo?.lemmaTab)
}}, [levelInfo])
return <div className={`column inventory-panel ${visible ? '' : 'hidden'}`}>
<InventoryContext.Provider value={{theoremTab, setTheoremTab, categoryTab, setCategoryTab, docTile, setDocTile }}>
{docTile ?
<Documentation />
{inventoryDoc ?
<Documentation name={inventoryDoc.name} type={inventoryDoc.type} handleClose={closeInventoryDoc}/>
:
<Inventory />
<Inventory levelInfo={levelInfo} openDoc={setInventoryDoc} enableAll={true} lemmaTab={lemmaTab} setLemmaTab={setLemmaTab}/>
}
</InventoryContext.Provider>
</div>
}
// HERE: next up: locked items should not be disabled!
+123 -36
View File
@@ -10,16 +10,27 @@ 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';
import { PreferencesPopup } from './popup/preferences';
import { ImpressumButton, MenuButton, PreferencesButton } from './app_bar';
import ReactCountryFlag from 'react-country-flag';
import lean4gameConfig from '../config.json'
import i18next from 'i18next';
import { useContext } from 'react';
import { PopupContext } from './popup/popup';
import { Flag } from './flag';
import { Markdown } from './utils';
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>
}
function Tile({gameId, data}: {gameId: string, data: GameTile|undefined}) {
let { t } = useTranslation()
@@ -54,13 +65,15 @@ function Tile({gameId, data}: {gameId: string, data: GameTile|undefined}) {
<td>{t("Levels")}</td>
<td>{data.levels}</td>
</tr>
<tr className="languages">
<tr>
<td>{t("Language")}</td>
<td>
{data.languages.map((lang) => (
<Flag key={lang} iso={lang} showTitle={true} />
))}
{data.languages.map((lang) => {
let langOpt = lean4gameConfig.languages.find((e) => e.iso == lang)
return <ReactCountryFlag key={`flag-${lang}`} title={langOpt?.name} countryCode={langOpt?.flag} className="emojiFlag"/>
})}
</td>
</tr>
</tbody>
@@ -72,7 +85,18 @@ function Tile({gameId, data}: {gameId: string, data: GameTile|undefined}) {
function LandingPage() {
const navigate = useNavigate();
const { setPopupContent } = useContext(PopupContext)
const [impressumPopup, setImpressumPopup] = React.useState(false);
const [preferencesPopup, setPreferencesPopup] = React.useState(false);
const [navOpen, setNavOpen] = React.useState(false);
const openImpressum = () => setImpressumPopup(true);
const closeImpressum = () => setImpressumPopup(false);
const toggleImpressum = () => setImpressumPopup(!impressumPopup);
const closePreferencesPopup = () => setPreferencesPopup(false);
const togglePreferencesPopup = () => setPreferencesPopup(!preferencesPopup);
const [usageCPU, setUsageCPU] = React.useState<number>()
const [usageMem, setUsageMem] = React.useState<number>()
const { t, i18n } = useTranslation()
@@ -82,11 +106,38 @@ function LandingPage() {
let allTiles = lean4gameConfig.allGames.map((gameId) => {
let q = useGetGameInfoQuery({game: `g/${gameId}`})
// if (q.isError) {
// if (q.error?.originalStatus === 404) {
// // Handle 404 error
// console.log('File not found');
// } else {
// // Suppress additional console.error messages
// console.error(q.error);
// }
// }
return q.data?.tile
})
/** Parse `games/stats.csv` if present and display server capacity. */
React.useEffect(() => {
const interval = setInterval(() => {
fetch_stats();
}, 2000)
return () => clearInterval(interval)
}, [])
return <div className="landing-page">
<header style={{backgroundImage: `url(${bgImage})`}}>
<nav className="landing-page-nav">
<GithubIcon url="https://github.com/leanprover-community/lean4game"/>
<MenuButton navOpen={navOpen} setNavOpen={setNavOpen}/>
<div className={'menu dropdown' + (navOpen ? '' : ' hidden')}>
<ImpressumButton setNavOpen={setNavOpen} toggleImpressum={toggleImpressum} isDropdown={true} />
<PreferencesButton setNavOpen={setNavOpen} togglePreferencesPopup={togglePreferencesPopup}/>
</div>
</nav>
<div id="main-title">
<h1>{t("Lean Game Server")}</h1>
<p>
@@ -98,35 +149,45 @@ function LandingPage() {
</p>
</div>
</header>
<React.Suspense>
<div className="game-list">
{allTiles.filter(x => x != null).length == 0 ?
<div className="game-list">
{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>
{ // show server capacity from `games/stats.csv` if present
(usageMem >= 0 || usageCPU >= 0 ) &&
<section>
<div className="wrapper">
<h2>{t("Server capacity")}</h2>
<Trans>
<p>
As this server runs lean on our university machines, it has a limited capacity.
Our current estimate is about 70 simultaneous games.
</p>
</Trans>
<p>
<Trans>
No Games loaded. Use <a>http://localhost:3000/#/g/local/FOLDER</a> to open a
game directly from a local folder.
</Trans>
{ usageMem >= 0 && <> {t("RAM")}: <strong>{usageMem.toFixed(2)} %</strong>{t(" used")}.<br/></> }
{ usageCPU >= 0 && <> {t("CPU")}: <strong>{usageCPU.toFixed(2)} %</strong>{t(" used")}. </> }
</p>
: lean4gameConfig.allGames.map((id, i) => (
<Tile
key={id}
gameId={`g/${id}`}
data={allTiles[i]}
/>
))
}
</div>
</React.Suspense>
</div>
</section>
}
<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
@@ -164,19 +225,45 @@ function LandingPage() {
<p>
<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.
project <a target="_blank" href="https://hhu-adam.github.io">ADAM: Anticipating the Digital Age of Mathematics</a> at
Heinrich Heine University Düsseldorf.
</Trans>
</p>
</div>
</section>
<footer>
{/* Do not translate "Impressum", it's needed for German GDPR */}
<a className="link" onClick={() => {setPopupContent("impressum")}}>{t("Impressum")}</a>
<a className="link" onClick={() => {setPopupContent("privacy")}}>{t("Privacy Policy")}</a>
<a className="link" onClick={openImpressum}>Impressum</a>
{impressumPopup? <PrivacyPolicyPopup handleClose={closeImpressum} />: null}
{preferencesPopup ? <PreferencesPopup handleClose={closePreferencesPopup} /> : null}
</footer>
</div>
function fetch_stats() {
fetch(`${window.location.origin}/data/stats`)
.then(response => {
if (response.ok) {
return response.text();
} else { throw ""; }
})
.then(data => {
// Parse the CSV content
const lines = data.split('\n');
const [header, line2] = lines;
if (!(header.replace(' ', '').startsWith("CPU,MEM"))) {
console.info("Not displaying server stats: received unexpected: ", header);
}
if (line2) {
let values = line2.split(',');
setUsageCPU(100 * parseFloat(values[0]));
setUsageMem(100 * parseFloat(values[1]));
}
}).catch(err => {
console.info('server stats unavailable');
console.debug(err);
});
}
}
export default LandingPage
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
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
+1 -1
View File
@@ -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 './utils';
import Markdown from './markdown';
function Message({ isOpen, content, close }) {
-363
View File
@@ -1,363 +0,0 @@
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 && <>&nbsp;{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>
}
+25 -48
View File
@@ -1,18 +1,18 @@
/**
* @fileOverview
*/
import * as React from 'react'
import { useSelector } from 'react-redux'
import { GameIdContext } from '../../app'
import { useAppDispatch } from '../../hooks'
import { deleteLevelProgress, deleteProgress, deleteWorldProgress, selectProgress } from '../../state/progress'
import { deleteProgress, selectProgress } from '../../state/progress'
import { downloadFile } from '../world_tree'
import { Button } from '../utils'
import { Button } from '../button'
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) {
// ev.preventDefault()
export function downloadProgress(gameId: string, gameProgress: any, ev: React.MouseEvent) {
ev.preventDefault()
downloadFile({
data: JSON.stringify(gameProgress, null, 2),
fileName: `lean4game-${gameId}-${new Date().toLocaleDateString()}.json`,
@@ -25,60 +25,37 @@ export function downloadProgress(gameId: string, gameProgress) {
* `handleClose` is the function to close it again because it's open/closed state is
* controlled by the containing element.
*/
export function ErasePopup () {
export function ErasePopup ({handleClose}) {
let { t } = useTranslation()
const { gameId, worldId, levelId } = React.useContext(GameIdContext)
const { setPage } = useContext(PageContext)
const gameId = React.useContext(GameIdContext)
const gameProgress = useSelector(selectProgress(gameId))
const dispatch = useAppDispatch()
const { setPopupContent } = useContext(PopupContext)
const eraseProgress = (ev) => {
const eraseProgress = () => {
dispatch(deleteProgress({game: gameId}))
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()
handleClose()
}
const downloadAndErase = (ev) => {
downloadProgress(gameId, gameProgress)
eraseProgress(ev)
downloadProgress(gameId, gameProgress, ev)
eraseProgress()
}
return <>
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>{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.)
(This deletes your proofs and your collected inventory.
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>
</>
<Button onClick={eraseProgress} to="">{t("Delete")}</Button>
<Button onClick={downloadAndErase} to="">{t("Download & Delete")}</Button>
<Button onClick={handleClose} to="">{t("Cancel")}</Button>
</div>
</div>
}
+28
View File
@@ -0,0 +1,28 @@
/**
* @fileOverview
*/
import * as React from 'react'
import { Typography } from '@mui/material'
import Markdown from '../markdown'
import { useTranslation } from 'react-i18next'
import { GameIdContext } from '../../app'
/** Pop-up that is displaying the Game Info.
*
* `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}) {
let { t } = useTranslation()
const gameId = React.useContext(GameIdContext)
return <div className="modal-wrapper">
<div className="modal-backdrop" onClick={handleClose} />
<div className="modal">
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
<Typography variant="body1" component="div" className="welcome-text">
<Markdown>{t(info, {ns: gameId})}</Markdown>
</Typography>
</div>
</div>
}
-43
View File
@@ -1,43 +0,0 @@
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')}
</>
}
-56
View File
@@ -1,56 +0,0 @@
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>
</>
}
-58
View File
@@ -1,58 +0,0 @@
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>
}
+10 -18
View File
@@ -1,6 +1,6 @@
import * as React from 'react'
import { Input, MenuItem, Select, SelectChangeEvent, Typography } from '@mui/material'
import { Markdown } from '../utils'
import Markdown from '../markdown'
import { Switch, Button, ButtonGroup } from '@mui/material';
import Box from '@mui/material/Box';
import Slider from '@mui/material/Slider';
@@ -8,15 +8,14 @@ import lean4gameConfig from '../../config.json'
import FormControlLabel from '@mui/material/FormControlLabel';
import { IPreferencesContext, PreferencesContext } from "../../state/context"
import { IPreferencesContext, PreferencesContext } from "../infoview/context"
import ReactCountryFlag from 'react-country-flag';
import { useTranslation } from 'react-i18next';
export function PreferencesPopup () {
export function PreferencesPopup({ handleClose }: { handleClose: () => void }) {
let { t } = useTranslation()
const {layout, isSavePreferences, language, setLayout, setIsSavePreferences, setLanguage} = React.useContext(PreferencesContext)
const marks = [
{
value: 0,
@@ -43,7 +42,10 @@ export function PreferencesPopup () {
setLanguage(ev.target.value as IPreferencesContext["language"])
}
return <>
return <div className="modal-wrapper">
<div className="modal-backdrop" onClick={handleClose} />
<div className="modal">
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
<Typography variant="body1" component="div" className="settings">
<div className='preferences-category'>
<div className='category-title'>
@@ -57,23 +59,12 @@ export function PreferencesPopup () {
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}/>}
&nbsp;
{lang.name}
</MenuItem>
})}
{lean4gameConfig.languages.map(lang => {return <MenuItem key={`menu-item-lang-${lang.iso}`} value={lang.iso}><ReactCountryFlag countryCode={lang.flag}/>&nbsp;{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'>
@@ -119,5 +110,6 @@ export function PreferencesPopup () {
</div>
</div>
</Typography>
</>
</div>
</div>
}
-48
View File
@@ -1,48 +0,0 @@
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')}
</>
}
@@ -0,0 +1,61 @@
/**
* @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.
*
* Note: Do not translate the Impressum!
*/
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 &amp; 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 &amp; 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>
}
-88
View File
@@ -1,88 +0,0 @@
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>
</>
}
@@ -0,0 +1,60 @@
/**
* @fileOverview
*/
import * as React from 'react'
import { Trans, useTranslation } from 'react-i18next'
/** Pop-up that is displayed when opening the help explaining the game rules.
*
* `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}) {
const { t } = useTranslation()
return <div className="privacy-policy modal-wrapper">
<div className="modal-backdrop" onClick={handleClose} />
<div className="modal">
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
<h2>{t("Game Rules")}</h2>
<Trans>
<p>
Game rules determine if it is allowed to skip levels and if the games runs checks to only
allow unlocked tactics and theorems in proofs.
</p>
<p>
Note: "Unlocked" tactics (or theorems) are determined by two things: The set of minimal
tactics needed to solve a level, plus any tactics you unlocked in another level. That means
if you unlock <code>simp</code> in a level, you can use it henceforth in any level.
</p>
<p>The options are:</p>
</Trans>
<table>
<thead>
<tr>
<th scope="col"></th>
<th scope="col">{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>
</div>
</div>
}
+11 -11
View File
@@ -3,30 +3,26 @@
*/
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 '../utils'
import { Button } from '../button'
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 () {
export function UploadPopup ({handleClose}) {
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])
@@ -43,7 +39,7 @@ export function UploadPopup () {
console.debug("Json Data", data)
dispatch(loadProgress({game: gameId, data: data}))
}
setPopupContent(null) // close the popup
handleClose()
}
/** Download the current progress (i.e. what's saved in the browser store) */
@@ -57,7 +53,10 @@ export function UploadPopup () {
}
return <>
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>{t("Upload Saved Progress")}</h2>
<Trans>
<p>Select a JSON file with the saved game progress to load your progress.</p>
@@ -71,5 +70,6 @@ export function UploadPopup () {
</p>
<Button to="" onClick={uploadProgress}>{t("Load selected file")}</Button>
</>
</div>
</div>
}
-43
View File
@@ -1,43 +0,0 @@
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" />
);
}
+151
View File
@@ -0,0 +1,151 @@
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 { PreferencesContext } 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 { PreferencesPopup} from "./popup/preferences"
import { WorldTreePanel } from './world_tree'
import '../css/welcome.css'
import { WelcomeAppBar } from './app_bar'
import { Hint } from './hints'
import i18next from 'i18next'
import { useTranslation } from 'react-i18next'
/** the panel showing the game's introduction text */
function IntroductionPanel({introduction, setPageNumber}: {introduction: string, setPageNumber}) {
const {mobile} = React.useContext(PreferencesContext)
const gameId = React.useContext(GameIdContext)
let { t } = useTranslation()
const dispatch = useAppDispatch()
// TODO: I left the setup for splitting up the introduction in place, but if it's not needed
// then this can be simplified.
// let text: Array<string> = introduction.split(/\n(\s*\n)+/)
let text: Array<string> = introduction ? [t(introduction, {ns : gameId})] : []
return <div className="column chat-panel">
<div className="chat">
{text?.map(((t, i) =>
t.trim() ?
<Hint key={`intro-p-${i}`}
hint={{text: t, hidden: false, rawText: t, varNames: []}}
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&nbsp;<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)
// Load the namespace of the game
i18next.loadNamespaces(gameId)
const {mobile} = React.useContext(PreferencesContext)
const {layout, isSavePreferences, language, setLayout, setIsSavePreferences, setLanguage} = React.useContext(PreferencesContext)
const gameInfo = useGetGameInfoQuery({game: gameId})
const inventory = useLoadInventoryOverviewQuery({game: gameId})
// 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)
const [preferencesPopup, setPreferencesPopup] = React.useState(false)
function closeEraseMenu() {setEraseMenu(false)}
function closeImpressum() {setImpressum(false)}
function closeInfo() {setInfo(false)}
function closeRulesHelp() {setRulesHelp(false)}
function closeUploadMenu() {setUploadMenu(false)}
function closePreferencesPopup() {setPreferencesPopup(false)}
function toggleEraseMenu() {setEraseMenu(!eraseMenu)}
function toggleImpressum() {setImpressum(!impressum)}
function toggleInfo() {setInfo(!info)}
function toggleUploadMenu() {setUploadMenu(!uploadMenu)}
function togglePreferencesPopup() {setPreferencesPopup(!preferencesPopup)}
// 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} togglePreferencesPopup={togglePreferencesPopup}/>
<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}
{preferencesPopup ? <PreferencesPopup handleClose={closePreferencesPopup} /> : null}
</>
}
export default Welcome
+70 -26
View File
@@ -3,18 +3,21 @@
*/
import * as React from 'react'
import { Link } from 'react-router-dom'
import { useSelector } from 'react-redux'
import { useStore, useSelector } from 'react-redux'
import { Slider } from '@mui/material'
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 { selectDifficulty, selectCompleted } from '../state/progress'
import { GameIdContext } from '../app'
import { useAppDispatch } from '../hooks'
import { selectDifficulty, changedDifficulty, selectCompleted } from '../state/progress'
import { store } from '../state/store'
import '../css/world_tree.css'
import { GameIdContext } from '../state/context'
import { PreferencesContext } from './infoview/context'
import { useTranslation } from 'react-i18next'
import { useGetGameInfoQuery } from '../state/api'
import { LoadingIcon } from './utils'
// Settings for the world tree
cytoscape.use( klay )
@@ -62,7 +65,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))
@@ -134,7 +137,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}` : ''}
@@ -192,6 +195,47 @@ 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 { t, i18n } = useTranslation()
const gameId = React.useContext(GameIdContext)
const difficulty = useSelector(selectDifficulty(gameId))
const dispatch = useAppDispatch()
const { mobile } = React.useContext(PreferencesContext)
function label(x : number) {
return x == 0 ? t("none") : x == 1 ? t("relaxed") : t("regular")
}
return <nav className={`world-selection-menu${mobile ? '' : ' desktop'}`}>
<div className="slider-wrap">
<span className="difficulty-label">{t("Rules")}
<FontAwesomeIcon icon={rulesHelp ? faXmark : faCircleQuestion} className='helpButton' onClick={() => (setRulesHelp(!rulesHelp))}/>
</span>
<Slider
orientation="vertical"
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}))
}}
></Slider>
</div>
</nav>
}
export function computeWorldLayout(worlds) {
let elements = []
for (let id in worlds.nodes) {
@@ -227,14 +271,15 @@ export function computeWorldLayout(worlds) {
}
export function WorldTreePanel ({visible = true}) {
const {gameId} = React.useContext(GameIdContext)
export function WorldTreePanel({worlds, worldSize, rulesHelp, setRulesHelp}:
{ worlds: any,
worldSize: any,
rulesHelp: boolean,
setRulesHelp: any,
}) {
const gameId = React.useContext(GameIdContext)
const difficulty = useSelector(selectDifficulty(gameId))
const gameInfo = useGetGameInfoQuery({game: gameId})
const {nodes, bounds}: any = gameInfo.data?.worlds ? computeWorldLayout(gameInfo.data?.worlds) : {nodes: []}
const {nodes, bounds}: any = worlds ? computeWorldLayout(worlds) : {nodes: []}
// scroll to playable world
React.useEffect(() => {
@@ -247,7 +292,7 @@ export function WorldTreePanel ({visible = true}) {
console.debug(`scrolling to ${elem.textContent}`)
elem.scrollIntoView({block: "center"})
}
}, [gameInfo])
}, [worlds, worldSize])
let svgElements = []
@@ -256,18 +301,18 @@ export function WorldTreePanel ({visible = true}) {
// Indices `1, …, n` indicate if the corresponding level is completed
var completed = {}
if (gameInfo.data?.worlds && gameInfo.data?.worldSize) {
if (worlds && worldSize) {
// Fill `completed` with the level data.
for (let worldId in nodes) {
completed[worldId] = Array.from({ length: gameInfo.data?.worldSize[worldId] + 1 }, (_, i) => {
completed[worldId] = Array.from({ length: 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 gameInfo.data?.worlds.edges) {
const edge = gameInfo.data?.worlds.edges[i]
for (let i in worlds.edges) {
const edge = 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}
@@ -287,11 +332,11 @@ export function WorldTreePanel ({visible = true}) {
completedLevels={completed[worldId]}
difficulty={difficulty}
key={`${gameId}-${worldId}`}
worldSize={gameInfo.data?.worldSize[worldId]}
worldSize={worldSize[worldId]}
/>
)
for (let i = 1; i <= gameInfo.data?.worldSize[worldId]; i++) {
for (let i = 1; i <= worldSize[worldId]; i++) {
svgElements.push(
<LevelIcon
world={worldId}
@@ -300,7 +345,7 @@ export function WorldTreePanel ({visible = true}) {
completed={completed[worldId][i]}
unlocked={completed[worldId][i-1]}
key={`${gameId}-${worldId}-${i}`}
worldSize={gameInfo.data?.worldSize[worldId]}
worldSize={worldSize[worldId]}
/>
)
}
@@ -314,14 +359,13 @@ export function WorldTreePanel ({visible = true}) {
let dx = bounds ? s*(bounds.x2 - bounds.x1) + 2*padding : null
return <div className={`${visible ? '' : 'hidden'}`}>
{/* <WorldSelectionMenu rulesHelp={rulesHelp} setRulesHelp={setRulesHelp} /> */}
{ gameInfo.data ?
return <div className="column">
<WorldSelectionMenu rulesHelp={rulesHelp} setRulesHelp={setRulesHelp} />
<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> : <LoadingIcon/> }
</svg>
</div>
}
+10 -15
View File
@@ -1,7 +1,7 @@
{
"allGames": [
"leanprover-community/nng4",
"hhu-adam/Robo",
"hhu-adam/robo",
"djvelleman/stg4",
"trequetrum/lean4game-logic"
],
@@ -21,21 +21,16 @@
"iso": "zh",
"flag": "CN",
"name": "中文"
}
],
"newLanguages": {
"en": {
"flag": "GB",
"name": "English"
},
"de": {
"flag": "DE",
"name": "Deutsch"
{
"iso": "es",
"flag": "ES",
"name": "Español"
},
"zh": {
"flag": "CN",
"name": "中文"
{
"iso": "ko",
"flag": "KR",
"name": "한국어"
}
},
"useFlags": false
]
}
+16 -33
View File
@@ -6,25 +6,18 @@
--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: "Roboto", sans-serif;
font-family: var(--ff-primary);
-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);
@@ -82,7 +75,6 @@ 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] {
@@ -96,12 +88,16 @@ em {
/* App Bar */
#root {
height: 100%;
}
.app {
height: 100%;
display: flex;
flex-direction: column;
}
/* TODO: remove */
.app-bar {
flex: 0;
background: var(--clr-primary);
@@ -130,6 +126,14 @@ 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;
}
@@ -138,27 +142,6 @@ em {
margin:0 1.5em 1.5em 1.5em;
}
.modal-wrapper {
.privacy-policy {
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');
}
-99
View File
@@ -1,99 +0,0 @@
.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);
}
-27
View File
@@ -1,27 +0,0 @@
.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;
} */
-60
View File
@@ -1,60 +0,0 @@
#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;
}
-37
View File
@@ -1,37 +0,0 @@
.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;
}
+44 -43
View File
@@ -1,4 +1,28 @@
.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;
@@ -70,6 +94,13 @@
flex-direction: column;
}
.typewriter-interface .proof .MuiCircularProgress-root {
left: 50%;
position: relative;
margin-left: -20px;
margin-bottom: 0.6em;
}
.typewriter .typewriter-input {
flex: 1;
}
@@ -91,8 +122,8 @@
flex-direction: row;
}
.goal-tabs {
width: 100%;
.goals-section div {
flex-grow: 1;
}
#current-proof, #main-assumptions {
@@ -136,12 +167,22 @@
}
/* 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;
@@ -169,8 +210,8 @@
.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;
}
@@ -184,43 +225,3 @@
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;
}
+2 -59
View File
@@ -10,7 +10,6 @@
.documentation h1 {
font-weight: 900;
font-family: "JuliaMono", monospace;
}
.documentation.hidden {
@@ -34,15 +33,9 @@
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 {
@@ -53,21 +46,13 @@
background-color: rgb(255, 242, 190);
}
.inventory .item.current-world {
background-color: rgb(250, 231, 255);
}
.inventory .item.recent {
background-color: rgb(242, 190, 255);
}
.inventory .item, .inventory .item.enabled {
.inventory .item:not(.locked), .inventory .item.enabled {
cursor: pointer;
}
.tab-bar {
border-bottom: 0.1em solid var(--clr-dark-gray);
margin-bottom: 1em;
margin-bottom: 0.5em;
}
.tab {
@@ -82,43 +67,11 @@
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;
@@ -141,13 +94,3 @@
align-items: end;
text-align: end;
}
.documentation .nav-button {
float: right;
font-size: 1.5rem;
}
.documentation .nav-button.lock {
float: left;
font-size: .7rem;
}
+6 -6
View File
@@ -9,6 +9,10 @@ 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;
}
@@ -161,12 +165,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);
@@ -227,7 +231,3 @@ footer .link {
margin-right: calc(50% - 650px);
}
}
.info .languages span + span {
margin-left: .5em;
}
+93 -71
View File
@@ -5,12 +5,31 @@
/* display: flex; */
}
/* .inventory-panel, .exercise-panel, .doc-panel, .introduction-panel {
.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 {
height: 100%;
width: 100%;
overflow: auto;
position: relative;
} */
}
.infoview {
padding-top: 1em;
@@ -44,11 +63,11 @@
padding-top: 0em;
}
/* .exercise {
.exercise {
flex: 1 1 auto;
display: flex;
flex-flow: column;
} */
}
.codeview {
flex: 1 1 auto;
@@ -174,21 +193,70 @@ td code {
.exercise {
height: 100%;
width: 100%;
}
.exercise-content {
.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%;
} */
.button-row.mobile {
margin: .5rem;
padding-top: .2rem;
}
.button-row.mobile .btn {
padding: .5em;
border-radius: .2em;
width: 100%;
margin: 0;
text-align: center;
}
.typewriter-interface {
display: flex;
flex-flow: column;
height: 100%;
z-index: 1;
}
.typewriter {
flex: 0 1 auto;
}
.exercise-content {
.typewriter-interface .content {
flex: 1 1 auto;
overflow-y: scroll;
padding: 0;
@@ -208,7 +276,16 @@ td code {
margin-top: 10px;
}
.exercise .step.selected .goal-tab {
.chat .message {
margin-left: .5em;
margin-right: .5em;
}
.chat .recent {
box-shadow: .0em .0em .4em .1em #8cbbe9;
}
.exercise .step.selected .goal-tabs, .chat .selected {
/* border: 3px solid #5191d1; */
box-shadow: .0em .0em .4em .1em var(--clr-primary);
}
@@ -264,31 +341,22 @@ td code {
justify-content: center;
}
.exercise {
.typewriter-interface .content, .world-image-container.empty {
background-color: #eee;
position: relative;
}
.world-image {
height: 100%;
width: 100%;
overflow: hidden;
.world-image-container {
display: flex;
flex-direction: column;
position: absolute;
z-index: 0;
/* min-height: 0px;*/ /* somehow this has a desired affect, but why? */
min-height: 0px; /* somehow this has a desired affect, but why? */
overflow: hidden;
}
.world-image img.contain {
height: 100%;
width: 100%;
.world-image-container img.contain {
object-fit: contain;
object-position: top center;
pointer-events: none;
}
.world-image.center {
.world-image-container.center {
justify-content: center;
}
@@ -298,43 +366,10 @@ td code {
object-fit: cover;
}
.proof {
z-index: 2;
.typewriter-interface .proof {
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;
@@ -358,16 +393,3 @@ td code {
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;
}
-97
View File
@@ -1,97 +0,0 @@
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; */
}
-30
View File
@@ -1,30 +0,0 @@
/* 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;
}
+17 -1
View File
@@ -14,6 +14,9 @@
width: 100%;
}
.app-content {
height: 100%
}
.welcome .column {
height: 100%;
@@ -156,6 +159,19 @@ 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%;
}
@@ -182,4 +198,4 @@ h5, h6 {
.preferences-item.leave-left-gap{
margin-left: 3em;
}
}
+21 -20
View File
@@ -8,52 +8,53 @@ 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 './i18n';
import Game from './components/game'
// 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")
// }
let landing_page: RouteObject = single_game ? {
path: "/",
loader: () => redirect("/g/local/game")
} : {
path: "/",
element: <LandingPage />,
}
let root_object: RouteObject = single_game ? {
path: "/",
loader: () => redirect("/g/local/game")
} : {
path: "/",
element: <App />,
errorElement: <ErrorPage />,
children: [
{
path: "/",
element: <LandingPage />,
}
]
}
const router = createHashRouter([
// root_object,
root_object,
{
// For backwards compatibility
path: "/game/nng",
loader: () => redirect("/g/hhu-adam/NNG4")
loader: () => redirect("/g/leanprover-community/nng4")
},
{
// For backwards compatibility
path: "/g/hhu-adam/NNG4",
loader: () => redirect("/g/leanprover-community/NNG4")
loader: () => redirect("/g/leanprover-community/nng4")
},
{
path: "/",
path: "/g/:owner/:repo",
element: <App />,
errorElement: <ErrorPage />,
children: [
landing_page,
{
path: "/g/:owner/:repo",
element: <Game />,
element: <Welcome />,
},
{
path: "/g/:owner/:repo/world/:worldId/level/:levelId",
element: <Game />,
element: <Level />,
},
],
},
+7 -16
View File
@@ -36,9 +36,6 @@ export interface InventoryTile {
new: boolean,
hidden: boolean
altTitle: string,
world : string|null,
level : number|null,
proven : boolean
}
export interface LevelInfo {
@@ -47,11 +44,11 @@ export interface LevelInfo {
conclusion: null|string,
index: number,
tactics: InventoryTile[],
theorems: InventoryTile[],
lemmas: InventoryTile[],
definitions: InventoryTile[],
descrText: null|string,
descrFormat: null|string,
theoremTab: null|string,
lemmaTab: null|string,
statementName: null|string,
displayName: null|string,
template: null|string,
@@ -61,9 +58,9 @@ export interface LevelInfo {
/** Used to display the inventory on the welcome page */
export interface InventoryOverview {
tactics: InventoryTile[],
theorems: InventoryTile[],
lemmas: InventoryTile[],
definitions: InventoryTile[],
theoremTab: null,
lemmaTab: null,
}
interface Doc {
@@ -84,19 +81,13 @@ export const apiSlice = createApi({
query: ({game}) => `${game}/game.json`,
}),
loadLevel: builder.query<LevelInfo, {game: string, world: string, level: number}>({
query: ({game, world, level}) => {
if (world && level > 0) {
return `${game}/level__${world}__${level}.json`
} else {
return `${game}/inventory.json`
}
},
query: ({game, world, level}) => `${game}/level__${world}__${level}.json`,
}),
loadInventoryOverview: builder.query<InventoryOverview, {game: string}>({
query: ({game}) => `${game}/inventory.json`,
}),
loadDoc: builder.query<Doc, {game: string, name: string }>({
query: ({game, name}) => `${game}/doc__${name}.json`,
loadDoc: builder.query<Doc, {game: string, name: string, type: "lemma"|"tactic"}>({
query: ({game, type, name}) => `${game}/doc__${type}__${name}.json`,
}),
}),
})
+2 -2
View File
@@ -18,8 +18,8 @@ export const AUTO_SWITCH_THRESHOLD = 800
const initialState: PreferencesState = loadPreferences() ??{
layout: "auto",
isSavePreferences: false,
language: "en",
}
language: import.meta.env.VITE_CLIENT_DEFAULT_LANGUAGE || "en",
};
export const preferencesSlice = createSlice({
name: "preferences",
+29 -46
View File
@@ -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, readIntro: boolean},
[world: string] : {[level: number]: LevelProgressState},
}
export interface GameProgressState {
inventory: string[],
difficulty: number,
readIntro: boolean,
openedIntro: boolean,
data: WorldProgressState,
typewriterMode?: boolean
}
@@ -54,24 +54,19 @@ 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.toLowerCase()]) {
state.games[action.payload.game.toLowerCase()] = {inventory: [], readIntro: false, data: {}, difficulty: DEFAULT_DIFFICULTY}
state.games[action.payload.game.toLowerCase()] = {inventory: [], openedIntro: true, data: {}, difficulty: DEFAULT_DIFFICULTY}
}
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)
addWorldProgress(state, action)
if (!state.games[action.payload.game.toLowerCase()].data[action.payload.world]) {
state.games[action.payload.game.toLowerCase()].data[action.payload.world] = {}
}
if (!state.games[action.payload.game.toLowerCase()].data[action.payload.world][action.payload.level]) {
state.games[action.payload.game.toLowerCase()].data[action.payload.world][action.payload.level] = {...initalLevelProgressState}
}
@@ -85,7 +80,7 @@ export const progressSlice = createSlice({
codeEdited(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number, code: string}>) {
addLevelProgress(state, action)
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
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[]}>) {
@@ -105,16 +100,11 @@ export const progressSlice = createSlice({
},
/** delete all progress for this game */
deleteProgress(state: ProgressState, action: PayloadAction<{game: string}>) {
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}
state.games[action.payload.game.toLowerCase()] = {inventory: [], data: {}, openedIntro: true, difficulty: DEFAULT_DIFFICULTY}
},
/** delete progress for this level */
deleteLevelProgress(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number}>) {
// addLevelProgress(state, action)
addLevelProgress(state, action)
state.games[action.payload.game.toLowerCase()].data[action.payload.world][action.payload.level] = initalLevelProgressState
},
/** load progress, e.g. from external import */
@@ -133,13 +123,9 @@ export const progressSlice = createSlice({
state.games[action.payload.game.toLowerCase()].difficulty = action.payload.difficulty
},
/** set the difficulty */
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
}
changedOpenedIntro(state: ProgressState, action: PayloadAction<{game: string, openedIntro: boolean}>) {
addGameProgress(state, action)
state.games[action.payload.game.toLowerCase()].openedIntro = action.payload.openedIntro
},
/** set the typewriter mode */
changeTypewriterMode(state: ProgressState, action: PayloadAction<{game: string, typewriterMode: boolean}>) {
@@ -151,82 +137,79 @@ export const progressSlice = createSlice({
/** if the level does not exist, return default values */
export function selectLevel(game: string, world: string, level: number) {
return (state) => {
if (!state.progress.games[game?.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 (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?.toLowerCase(), 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?.toLowerCase()]) { return [] }
return state.progress.games[game?.toLowerCase()]?.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?.toLowerCase(), 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?.toLowerCase(), 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?.toLowerCase(), 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?.toLowerCase()] ?? 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?.toLowerCase()]?.difficulty ?? DEFAULT_DIFFICULTY
return state.progress.games[game.toLowerCase()]?.difficulty ?? DEFAULT_DIFFICULTY
}
}
/** return whether the intro has been read */
export function selectReadIntro(game: string, worldId: string) {
export function selectOpenedIntro(game: string) {
return (state) => {
if (worldId) {
return state.progress.games[game?.toLowerCase()]?.data[worldId]?.readIntro
}
return state.progress.games[game?.toLowerCase()]?.readIntro
return state.progress.games[game.toLowerCase()]?.openedIntro
}
}
/** return typewriter mode for the current game if it exists */
export function selectTypewriterMode(game: string) {
return (state) => {
return state.progress.games[game?.toLowerCase()]?.typewriterMode ?? true
return state.progress.games[game.toLowerCase()]?.typewriterMode ?? true
}
}
/** Export actions to modify the progress */
export const { changedSelection, codeEdited, levelCompleted, deleteProgress,
deleteLevelProgress, deleteWorldProgress, loadProgress, helpEdited, changedInventory, changedReadIntro,
deleteLevelProgress, loadProgress, helpEdited, changedInventory, changedOpenedIntro,
changedDifficulty, changeTypewriterMode} = progressSlice.actions
-9
View File
@@ -1,9 +0,0 @@
# 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.
+18 -26
View File
@@ -6,11 +6,11 @@ This tutorial walks you through creating a new game for lean4. It covers from wr
1. Use the [GameSkeleton template](https://github.com/hhu-adam/GameSkeleton) to create a new github repo for your game: On github, click on "Use this template" > "Create a new repository".
2. Clone the game repo.
3. Call `lake update && lake exe cache get && lake build` to build the Lean project.
3. Call `lake update -R && lake build` to build the Lean project.
Note that you need to host your game's code on github to publish it online later on. If you only
want to play it locally, you can simply clone the NNG repo and start modifying that one.
### Running the game
Now you can open the game in VSCode (`cd YourGame/` and `code .`), and start modifying it like a regular Lean project. To run the game consult the section "**5. Testing the Game Locally**" below.
## 2. Game.lean
@@ -46,22 +46,6 @@ 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
@@ -210,7 +194,7 @@ You can but a `Statement` inside namespaces like you would with `theorem`.
#### Doc String / Exercise statement
Add a docstring that contains the exercise statement in natural language. If you do this, it will appear at the top of the exercise. It supports Latex.
Add a docstring that contains the exercise statement in natural language. If you do this, it will appear at the top of the exercise. See [LaTeX in Games](latex.md) for more details on formatting.
```lean
/-- The exercise statement in natural language using latex: $\iff$. -/
@@ -281,13 +265,21 @@ CoverImage "images/cover.png"
* `Prerequisites` a list of other games you should play before this one, e.g. `Prerequisites "NNG" "STG"`. The game names are free-text.
* `CoverImage`: You can create a folder `images/` and put images there for the game to use. The maximal ratio is ca. 500x200 (W x H) but it might be cropped horizontally on narrow screens.
## Further Notes
## 10. Advanced Topics
Here are some random further things you should consider designing a new game:
### Escaping
* Inside strings, you need to escape backslashes, but not inside doc-strings, therefore you
Inside strings, you need to escape backslashes, but not inside doc-strings, therefore you
would write `Introduction "some latex here: $\\iff$."` but
`/-- some latex here: $\iff$. -/ Statement ...`
* A world with more than 16 levels will be displayed with the levels spiraling outwards,
it might be desirable to stay below that bound. Above 22 levels the spiral starts getting out
of control.
### LaTeX support
LaTeX is rendered using the [KaTeX library](https://katex.org/),
see [Using LaTeX in the Game](latex.md) for details.
### Number Of Levels Limit
A world with more than 16 levels will be displayed with the levels spiraling outwards,
it might be desirable to stay below that bound. Above 22 levels the spiral starts getting out
of control.
+6 -36
View File
@@ -33,7 +33,7 @@ You can use `Branch` to place hints
in dead ends or alternative proof strands.
A proof inside a `Branch`-block is normally evaluated by lean, but it's discarded at the end
so that no progress has been made on proofing the goal.
so that no progress has been made on proving the goal.
```
Statement .... := by
@@ -49,6 +49,9 @@ Statement .... := by
Put variables in the hint text inside brackets like this: `{h}`! This way the server can replace
the variable's name with the one the user actually used.
*Note*: This means you need to escape any other uses of **opening** curly brackets (i.e. `\{`). See also [LaTeX in Games](latex.md) for
examples of this.
For example, if the sample proof contains
```
@@ -84,41 +87,8 @@ create new assumptions.
## 6. Formatting
You can add use markdown to format your hints, for example you can use KaTex: `$\\iff$`
**Escaping**: Generally, if you add text inside quotes `" "` (e.g. in `Hint`) you need to escape
backslashes, but if you provide text inside a doc comment
`/-- -/` (e.g. in the `Statement` description) you do not!
TODO: Write a doc about latex/markdown options available.
### Commutative diagrams
Here is an example of how to write a commutative diagram in KaTeX:
$$
\begin{CD}
A @>{f}>> B @<{g}<< C \\
@V{h}VV @V{i}VV @V{j}VV \\
D @<{k}<< E @>{l}>> F \\
@A{m}AA @A{n}AA @V{p}VV \\
G @<{q}<< H @>{r}>> I
\end{CD}
$$
```
$$
\\begin{CD}
A @>{f}>> B @<{g}<< C \\\\
@V{h}VV @V{i}VV @V{j}VV \\\\
D @<{k}<< E @>{l}>> F \\\\
@A{m}AA @A{n}AA @V{p}VV \\\\
G @<{q}<< H @>{r}>> I
\\end{CD}
$$
```
See https://www.jmilne.org/not/Mamscd.pdf
You can use Markdown to format your hints and you can
use LaTeX. See [LaTeX in Games](latex.md) for more details.
### Images
+78
View File
@@ -0,0 +1,78 @@
There are multiple ways how to format the text content of your game. Notably Markdown with KaTeX.
# Escaping
Generally, if you add text inside quotes `" "` (e.g. in `Hint`) you need to escape
backslashes, but if you provide text inside a doc comment
`/-- -/` (e.g. in the `Statement` description) you do not!
This means for example you'd write `/-- $\iff$ -/` but `"$\\iff$"`.
Furthermore, inside `Hint` you need to escape all opening curly brackets as `\{` since `{h}` is syntax for inserting a variable name `h`.
# KaTeX
LaTeX syntax is provided trough the [KaTeX library](https://katex.org). KateX supports most but not all of latex and its packages.
See [supported](https://katex.org/docs/supported.html).
## Examples
### Commutative diagrams
Here is an example of how to write a commutative diagram in KaTeX:
$$
\begin{CD}
A @>{f}>> B @<{g}<< C \\
@V{h}VV @V{i}VV @V{j}VV \\
D @<{k}<< E @>{l}>> F \\
@A{m}AA @A{n}AA @V{p}VV \\
G @<{q}<< H @>{r}>> I
\end{CD}
$$
```
$$
\begin{CD}
A @>{f}>> B @<{g}<< C \\
@V{h}VV @V{i}VV @V{j}VV \\
D @<{k}<< E @>{l}>> F \\
@A{m}AA @A{n}AA @V{p}VV \\
G @<{q}<< H @>{r}>> I
\end{CD}
$$
```
Again, note that inside a string like `Hint`/`Introduction`/`Conclusion`/etc. you need to escape `\` and potentially `{`.
E.g. `\begin` as `\\begin`, `\\` as `\\\\` and inside a
`Hint`, `@>{f}>>` as `@>\{f}>>`.
See https://www.jmilne.org/not/Mamscd.pdf
### Truth Tables
KaTeX does not support the tabular environment. You can use the array environment instead.
$$
\begin{array}{|c|c|}
\hline
P & ¬P \\
\hline
T & F \\
F & T \\
\hline
\end{array}
$$
```
$$
\begin{array}{|c|c|}
\hline
P & ¬P \\
\hline
T & F \\
F & T \\
\hline
\end{array}
$$
```
+10
View File
@@ -7,3 +7,13 @@ Internally, websocket requests to `ws://localhost:3000/websockets` will be forwa
On the server side, the command will set up a docker image containing the Lean server. The two parts can be built separately using `npm run build_client` and `npm run build_server`.
* `npm run production`: Start the project in production mode. This requires that the build script has been run. It will start a server on the port specified in the `PORT` environment variable or by default on `8080`. You can run on a specific port by running `PORT=80 npm run production`. The server will serve the files in `client/dist` via http and give access to the bubblewrapped Lean server via the web socket protocol.
### Environment Variables
The client and server ports, as well as the default language, can be configured using environment variables:
* `PORT`: Sets the port for the backend server (default: `8080`).
* `CLIENT_PORT`: Sets the port for the client server (default: `3000`).
* `VITE_CLIENT_DEFAULT_LANGUAGE`: Sets the default language for the application (default: `en`).
Ensure these environment variables are set appropriately in your environment to configure the project as needed.
+19
View File
@@ -26,3 +26,22 @@ where you replace:
Everything downloaded remains in the folder `lean4game/games`.
The subfolder `tmp` contains downloaded artifacts and can be deleted without loss.
The other folders should only contain the built lean-games, sorted by owner and repo.
## Server capacity
If you would like to display the server capacity on the landing page,
you can create a file `lean4game/games/stats.csv` of the following form:
```
CPU,MEM
0.1,0.8
```
These numbers will be displayed on the landing page ("CPU: 10 % used" and "RAM: 80 % used").
If you only want one of the numbers, replace the number you don't want with `nan` (or anything
else which does not parse as number).
If you don't want to show either, simply do not create `stats.csv`
Use your own script or cronjob to update the CSV file as desired.
+2 -2
View File
@@ -48,9 +48,9 @@ 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:
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_theorem ...
Statement my_simp_lemma ...
```
+9 -10
View File
@@ -5,6 +5,11 @@
<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>
@@ -18,23 +23,17 @@
</p>
<p>
<strong>Impressum:</strong><br />
Marcus Zibrowius, Jon Eugster<br />
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>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 />
+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>
</div>
</noscript>
<script type="module" src="/client/src/index.tsx"></script>
<script type="module" src="client/src/index.tsx"></script>
</body>
</html>
+8382 -4931
View File
File diff suppressed because it is too large Load Diff
+5 -7
View File
@@ -14,6 +14,7 @@
"@fortawesome/free-regular-svg-icons": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"@leanprover/infoview": "^0.4.3",
"@mui/icons-material": "^5.11.0",
"@mui/material": "^5.11.1",
"@reduxjs/toolkit": "^1.9.1",
@@ -29,28 +30,25 @@
"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",
"lean4monaco": "^1.0.29",
"memfs": "^4.11.1",
"lean4web": "github:hhu-adam/lean4web#414d9e62638a392fca278761b4c61a1d2e138bc7",
"octokit": "^3.1.2",
"path-browserify": "^1.0.1",
"react-contenteditable": "^3.3.7",
"react": "^18.2.0",
"react-country-flag": "^3.1.0",
"react-dom": "^18.2.0",
"react-i18next": "^14.1.0",
"react-markdown": "^8.0.4",
"react-native": "^0.72.3",
"react-redux": "^8.0.5",
"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",
@@ -58,7 +56,6 @@
"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",
@@ -71,6 +68,7 @@
"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": {
+7 -16
View File
@@ -1,21 +1,17 @@
#/bin/bash
# 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)"
ELAN_HOME=$(lake env printenv ELAN_HOME)
# $1 : the game directory
# $2 : the lean4game folder
# $3 : the gameserver executable
# # print commands as they are executed
# set -x
(exec bwrap\
--ro-bind $2 /lean4game \
--ro-bind $1 /game \
--ro-bind "$LEAN_ROOT" /lean \
--ro-bind /usr /usr \
--bind $2 /lean4game \
--bind $1 /game \
--bind $ELAN_HOME /elan \
--bind /usr /usr \
--dev /dev \
--proc /proc \
--symlink usr/lib /lib\
@@ -23,9 +19,8 @@ LEAN_PATH="$(cd $1 && lake env printenv LEAN_PATH)"
--symlink usr/bin /bin\
--symlink usr/sbin /sbin\
--clearenv \
--setenv PATH "/lean/bin" \
--setenv LAKE "/no" `# tries to invoke git otherwise` \
--setenv LEAN_PATH "$LEAN_PATH" \
--setenv PATH "/elan/bin:/bin" \
--setenv ELAN_HOME "/elan" \
--unshare-user \
--unshare-pid \
--unshare-net \
@@ -35,7 +30,3 @@ LEAN_PATH="$(cd $1 && lake env printenv LEAN_PATH)"
--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
+43
View File
@@ -0,0 +1,43 @@
import time
def measure_proc_stat() -> dict[str, int]:
proc_stat_header = open("/proc/stat", "r").readline()
proc_stat = proc_stat_header.split(' ')[2:]
proc_stat[-1] = proc_stat[-1].removesuffix('\n')
proc_stat = list(map(int, proc_stat))
proc_stat_dict= {'user': proc_stat[0],
'nice': proc_stat[1],
'system': proc_stat[2],
'idle': proc_stat[3],
'iowait': proc_stat[4],
'irq': proc_stat[5],
'softirq': proc_stat[6],
'steal': proc_stat[7],
'guest': proc_stat[8],
'guest_nice': proc_stat[9]}
return proc_stat_dict
if __name__ == "__main__":
"""
Script emulates htop calculation of CPU at the
moment of calling.
"""
prev = measure_proc_stat()
prev_idle = prev.get('idle') + prev.get('iowait')
prev_non_idle = prev.get('user') + prev.get('nice') + prev.get('system') + prev.get('irq') + prev.get('softirq') + prev.get('steal')
prev_total = prev_idle + prev_non_idle
time.sleep(0.1)
curr = measure_proc_stat()
curr_idle = curr.get('idle') + curr.get('iowait')
curr_non_idle = curr.get('user') + curr.get('nice') + curr.get('system') + curr.get('irq') + curr.get('softirq') + curr.get('steal')
curr_total = curr_idle + curr_non_idle
d_total = curr_total - prev_total
d_idle = curr_idle - prev_idle
cpu_usage = ((d_total - d_idle)/d_total)
print(cpu_usage)
+135 -138
View File
@@ -1,18 +1,17 @@
import { WebSocketServer } from 'ws'
import { WebSocketServer } from 'ws';
import express from 'express'
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 path from 'path'
import * as cp from 'child_process';
import * as url from 'url';
import * as rpc from 'vscode-ws-jsonrpc';
import * as jsonrpcserver from 'vscode-ws-jsonrpc/server';
import os from 'os';
import fs from 'fs';
import anonymize from 'ip-anonymize';
import { importTrigger, importStatus } from './import.mjs'
import process from 'process';
import { spawn } from 'child_process'
// import fs from 'fs'
/**
* Add a game here if the server should keep a queue of pre-loaded games ready at all times.
@@ -21,76 +20,85 @@ import { importTrigger, importStatus } from './import.mjs'
*/
const queueLength = {
"g/hhu-adam/robo": 2,
"g/hhu-adam/nng4": 5,
"g/djvelleman/stg4": 0,
"g/trequetrum/lean4game-logic": 0,
"g/leanprover-community/nng4": 5,
"g/djvelleman/stg4": 2,
"g/trequetrum/lean4game-logic": 2,
}
let socketCounter = 0
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 __filename = url.fileURLToPath(import.meta.url)
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
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 __filename = url.fileURLToPath(import.meta.url);
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
const app = express()
var router = express.Router()
const PORT = process.env.PORT || 8080;
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 server = app
.use(express.static(path.join(__dirname, '..', 'client', 'dist'))) // TODO: add a dist folder from inside the game
.use('/i18n/g/:owner/:repo/:lang/*', (req, res, next) => {
const owner = req.params.owner;
const repo = req.params.repo
const lang = req.params.lang
const filename = req.params[0]
req.url = filename
express.static(path.join(getGameDir(owner,repo),".i18n",lang))(req, res, next)
const ip = anonymize(req.headers['x-forwarded-for'] || req.socket.remoteAddress)
const log = `${process.cwd()}/logs/game-access.log`
const header = "date;anon-ip;game;lang\n"
const data = `${new Date()};${ip};${owner}/${repo};${lang}\n`
fs.writeFile(log, header.concat(data), { flag: 'ax' }, (file_exists) => {
if (file_exists) {
fs.appendFile(log, data, (err) => {
if (err) console.log("Failed to append to log!")
});
}
});
console.log(`[${new Date()}] ${ip} requested translation for ${owner}/${repo} in ${lang}`)
const filename = req.params[0];
req.url = filename;
express.static(path.join(getGameDir(owner,repo),".i18n",lang))(req, res, next);
})
app.use('/data/g/:owner/:repo/*', (req, res, next) => {
const owner = req.params.owner
.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)
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)
.use('/data/stats', (req, res, next) => {
const statsProcess = spawn('/bin/bash', [path.join(__dirname, "stats.sh"), process.pid])
let outputData = ''
let errorData = ''
statsProcess.stdout.on('data', (data) => {
outputData += data.toString();
})
statsProcess.stderr.on('data', (data) => {
errorData += data.toString();
})
statsProcess.on('close', (code) => {
if (code === 0) {
res.send(outputData);
} else {
res.status(500).send(`Error executing script: ${errorData}`)
console.error(`stats.sh exited with code ${code}. Error: ${errorData}`)
}
})
})
} else {
const PORT = process.env.PORT ?? 8080
server = app.listen(PORT,
() => console.log(`HTTP on port ${PORT}`))
}
.use('/', router)
.listen(PORT, () => console.log(`Listening on ${PORT}`));
const wss = new WebSocketServer({ server })
var socketCounter = 0
const environment = process.env.NODE_ENV
const isDevelopment = environment === 'development'
/** We keep queues of started Lean Server processes to be ready when a user arrives */
const queue = {}
@@ -114,8 +122,7 @@ function getGameDir(owner, repo) {
}
let game_dir = (owner == 'local') ?
// note: in the local case we need `repo` to be case sensitive
path.join(__dirname, '..', '..', repo) :
path.join(__dirname, '..', '..', repo) : // note: here we need `repo` to be case sensitive
path.join(__dirname, '..', 'games', `${owner}`, `${repo.toLowerCase()}`)
if(!fs.existsSync(game_dir)) {
@@ -123,20 +130,16 @@ 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
@@ -149,24 +152,19 @@ function startServerProcess(owner, repo) {
{ cwd: path.join(__dirname, "..", "server", ".lake", "build", "bin") })
}
} else {
console.info("Running with Bubblewrap container.")
serverProcess = cp.spawn(
"./bubblewrap.sh",
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}`)
)
serverProcess.on('close', (code, _signal) => {
console.log(`Lean server exited with code ${code}`)
})
if (serverProcess.stderr !== null) {
serverProcess.stderr.on('data', data =>
console.error(`Lean Server: ${data}`)
)
}
return serverProcess
}
@@ -191,64 +189,63 @@ function fillQueue(tag) {
// }
// }
const urlRegEx = /^\/websocket\/g\/([\w.-]+)\/([\w.-]+)$/
wss.addListener("connection", function(ws, req) {
// 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 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 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
}
const tag = getTag(owner, repo)
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)}`)
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)
}
return message
})
serverConnection.forward(socketConnection, message => {
if (isDevelopment) {
console.log(`SERVER: ${JSON.stringify(message)}`)
if (ps == null) {
console.error('server process is undefined/null')
return
}
return message
})
ws.on('close', () => {
console.log(`[${new Date()}] Socket closed - ${ip}`)
socketCounter -= 1
})
socketCounter += 1;
const ip = anonymize(req.headers['x-forwarded-for'] || req.socket.remoteAddress)
socketConnection.onClose(() => serverConnection.dispose())
serverConnection.onClose(() => socketConnection.dispose())
// TODO (Matvey): extract further information from `req`, for example browser language.
console.log(`[${new Date()}] Socket opened - ${ip} - ${owner}/${repo}`)
console.log(`[${new Date()}] Socket opened - ${ip}`)
socketCounter += 1
logStats()
const socket = {
onMessage: (cb) => { ws.on("message", cb) },
onError: (cb) => { ws.on("error", cb) },
onClose: (cb) => { ws.on("close", cb) },
send: (data, cb) => { ws.send(data,cb) }
}
const reader = new rpc.WebSocketMessageReader(socket)
const writer = new rpc.WebSocketMessageWriter(socket)
const socketConnection = jsonrpcserver.createConnection(reader, writer, () => ws.close())
const serverConnection = jsonrpcserver.createProcessStreamConnection(ps)
socketConnection.forward(serverConnection, message => {
if (isDevelopment) {console.log(`CLIENT: ${JSON.stringify(message)}`)}
return message;
})
serverConnection.forward(socketConnection, message => {
if (isDevelopment) {console.log(`SERVER: ${JSON.stringify(message)}`)}
return message;
});
console.log(`[${new Date()}] Number of open sockets - ${socketCounter}`)
console.log(`[${new Date()}] Free RAM - ${Math.round(os.freemem() / 1024 / 1024)} / ${Math.round(os.totalmem() / 1024 / 1024)} MB`)
ws.on('close', () => {
console.log(`[${new Date()}] Socket closed - ${ip}`)
socketCounter -= 1
})
socketConnection.onClose(() => serverConnection.dispose())
serverConnection.onClose(() => socketConnection.dispose())
})
Executable
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Load python interpreter
python=/usr/bin/python3
# Load python script
cpu_usage=relay/cpu_usage.py
# Execute python script
cpu=$($python $cpu_usage)
# Calculate memory usage by computing 1 - %free_memory
mem=$(free | sed '2q;d' | awk '{print 1 - ($4/$2)}')
printf "CPU, MEM\n%f, %f\n" $cpu $mem
+1 -1
View File
@@ -21,7 +21,7 @@ def abstractCtx (goal : MVarId) : MetaM AbstractCtxResult := do
def openAbstractCtxResult (res : AbstractCtxResult) (k : Array Expr → Expr → MetaM α) : MetaM α := do
let (_mvars, _binderInfo, expr) ← openAbstractMVarsResult res.abstractMVarsResult
lambdaLetTelescope (← instantiateMVars expr) k
-- TODO: Unfornately, lambdaLetTelescope does not allow us to provide the number of arguments.
-- TODO: Unfortunately, lambdaLetTelescope does not allow us to provide the number of arguments.
-- If the goal is a function, this will not work.
end AbstractCtx
+106 -75
View File
@@ -4,7 +4,7 @@ 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 GameServer.RpcHandlers -- only needed to collect the translations of "level completed" msgs
import I18n
open Lean Meta Elab Command
@@ -119,7 +119,7 @@ elab "Languages" t:str* : command => do
modifyCurGame fun game => pure {game with
tile := {game.tile with languages := t.map (·.getString) |>.toList}}
/-- The Image of the game (optional). TODO: Not impementeds -/
/-- The Image of the game (optional). TODO: Not implemented -/
elab "CoverImage" t:str : command => do
let file := t.getString
if not <| ← System.FilePath.pathExists file then
@@ -176,7 +176,7 @@ elab doc:docComment ? "TheoremDoc" name:ident "as" displayName:str "in" category
let doc ← parseDocCommentLegacy doc content
let doc ← doc.translate
modifyEnv (inventoryTemplateExt.addEntry · {
type := .Theorem
type := .Lemma
name := name.getId
category := category.getString
displayName := displayName.getString
@@ -236,13 +236,13 @@ elab "NewHiddenTactic" args:ident* : command => do
/-- Declare theorems that are introduced by this level. -/
elab "NewTheorem" args:ident* : command => do
checkCommandNotDuplicated ((←getCurLevel).theorems.new) "NewTheorem"
checkCommandNotDuplicated ((←getCurLevel).lemmas.new) "NewTheorem"
for name in ↑args do
try let _decl ← getConstInfo name.getId catch
| _ => logErrorAt name m!"unknown identifier '{name}'."
checkInventoryDoc .Theorem name -- TODO: Add (template := "[mathlib]")
checkInventoryDoc .Lemma name -- TODO: Add (template := "[mathlib]")
modifyCurLevel fun level => pure {level with
theorems := {level.theorems with new := args.map (·.getId)}}
lemmas := {level.lemmas with new := args.map (·.getId)}}
/-- Declare definitions that are introduced by this level. -/
elab "NewDefinition" args:ident* : command => do
@@ -262,10 +262,10 @@ elab "DisabledTactic" args:ident* : command => do
/-- 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
checkCommandNotDuplicated ((←getCurLevel).lemmas.disabled) "DisabledTheorem"
for name in ↑args do checkInventoryDoc .Lemma name
modifyCurLevel fun level => pure {level with
theorems := {level.theorems with disabled := args.map (·.getId)}}
lemmas := {level.lemmas with disabled := args.map (·.getId)}}
/-- Declare definitions that are temporarily disabled in this level -/
elab "DisabledDefinition" args:ident* : command => do
@@ -283,10 +283,10 @@ elab "OnlyTactic" args:ident* : command => do
/-- 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
checkCommandNotDuplicated ((←getCurLevel).lemmas.only) "OnlyTheorem"
for name in ↑args do checkInventoryDoc .Lemma name
modifyCurLevel fun level => pure {level with
theorems := {level.theorems with only := args.map (·.getId)}}
lemmas := {level.lemmas with only := args.map (·.getId)}}
/-- Temporarily disable all definitions except the ones declared here.
This is ignored if `OnlyDefinition` is set. -/
@@ -296,10 +296,52 @@ elab "OnlyDefinition" args:ident* : command => do
modifyCurLevel fun level => pure {level with
definitions := {level.definitions with only := args.map (·.getId)}}
/-- Define which tab of Theorems is opened by default. Usage: `TheoremTab "Nat"`.
/-- Define which tab of Lemmas is opened by default. Usage: `TheoremTab "Nat"`.
If omitted, the current tab will remain open. -/
elab "TheoremTab" category:str : command =>
modifyCurLevel fun level => pure {level with theoremTab := category.getString}
modifyCurLevel fun level => pure {level with lemmaTab := category.getString}
/-! DEPRECATED -/
elab doc:docComment ? "LemmaDoc" name:ident "as" displayName:str "in" category:str content:str ? :
command => do
logWarning "Deprecated. Has been renamed to `TheoremDoc`"
let doc ← parseDocCommentLegacy doc content
modifyEnv (inventoryTemplateExt.addEntry · {
type := .Lemma
name := name.getId
category := category.getString
displayName := displayName.getString
content := doc })
elab "NewLemma" args:ident* : command => do
logWarning "Deprecated. Has been renamed to `NewTheorem`"
checkCommandNotDuplicated ((←getCurLevel).lemmas.new) "NewLemma"
for name in ↑args do
try let _decl ← getConstInfo name.getId catch
| _ => logErrorAt name m!"unknown identifier '{name}'."
checkInventoryDoc .Lemma name -- TODO: Add (template := "[mathlib]")
modifyCurLevel fun level => pure {level with
lemmas := {level.lemmas with new := args.map (·.getId)}}
elab "DisabledLemma" args:ident* : command => do
logWarning "Deprecated. Has been renamed to `DisabledTheorem`"
checkCommandNotDuplicated ((←getCurLevel).lemmas.disabled) "DisabledLemma"
for name in ↑args do checkInventoryDoc .Lemma name
modifyCurLevel fun level => pure {level with
lemmas := {level.lemmas with disabled := args.map (·.getId)}}
elab "OnlyLemma" args:ident* : command => do
logWarning "Deprecated. Has been renamed to `OnlyTheorem`"
checkCommandNotDuplicated ((←getCurLevel).lemmas.only) "OnlyLemma"
for name in ↑args do checkInventoryDoc .Lemma name
modifyCurLevel fun level => pure {level with
lemmas := {level.lemmas with only := args.map (·.getId)}}
elab "LemmaTab" category:str : command => do
logWarning "Deprecated. Has been renamed to `TheoremTab`"
modifyCurLevel fun level => pure {level with lemmaTab := category.getString}
/-! # Exercise Statement -/
@@ -326,15 +368,15 @@ elab doc:docComment ? attrs:Parser.Term.attributes ?
"Statement" statementName:ident ? preamble:preambleArg ? sig:declSig val:declVal : command => do
let lvlIdx ← getCurLevelIdx
-- Add an optional tactic sequence that the engine executes before the game starts
-- 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
| none => `(Parser.Tactic.tacticSeq|skip)
| some x => match x with
| `(preambleArg| (preamble := $tac)) => pure tac
| _ => `(Parser.Tactic.tacticSeq| skip)
| none => `(Parser.Tactic.tacticSeq| skip)
| _ => `(Parser.Tactic.tacticSeq|skip)
-- Translate the docstring of the `Statement`
let docComment : Option String ← match (← parseDocComment doc) with
let docContent ← parseDocComment doc
let docContent ← match docContent with
| none => pure none
| some d => d.translate
@@ -346,17 +388,18 @@ elab doc:docComment ? attrs:Parser.Term.attributes ?
let defaultDeclName : Ident := mkIdent <| (← getCurGame).name ++ (← getCurWorld).name ++
("level" ++ toString lvlIdx : String)
-- Collect all used tactics/theorems in the sample proof:
let usedInventory : UsedInventory ← match val with
-- Collect all used tactics/lemmas in the sample proof:
let 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`!"
-- extract the `tacticSeq` from `val` in order to add `let_intros` in front.
-- TODO: don't understand meta-programming enough to avoid having `let_intros`
-- duplicated three times below…
let tacticStx : TSyntax `Lean.Parser.Tactic.tacticSeq := match val with
| `(Parser.Command.declVal| := by $proof) => proof
| _ => panic "expected `:= by`"
-- Add theorem to context.
match statementName with
@@ -371,17 +414,17 @@ elab doc:docComment ? attrs:Parser.Term.attributes ?
-- 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 $modifiedVal)
let thmStatement ← `(command| $[$doc]? $[$attrs:attributes]? theorem $defaultDeclName $sig := by {let_intros; $(⟨preambleSeq⟩); $(⟨tacticStx⟩)})
elabCommand thmStatement
-- Check that statement has a docs entry.
checkInventoryDoc .Theorem name (name := fullName) (template := docComment)
checkInventoryDoc .Lemma name (name := fullName) (template := docContent)
else
let thmStatement ← `(command| $[$doc]? $[$attrs:attributes]? theorem $name $sig $modifiedVal)
let thmStatement ← `(command| $[$doc]? $[$attrs:attributes]? theorem $name $sig := by {let_intros; $(⟨preambleSeq⟩); $(⟨tacticStx⟩)})
elabCommand thmStatement
-- Check that statement has a docs entry.
checkInventoryDoc .Theorem name (name := fullName) (template := docComment)
checkInventoryDoc .Lemma name (name := fullName) (template := docContent)
| none =>
let thmStatement ← `(command| $[$doc]? $[$attrs:attributes]? theorem $defaultDeclName $sig $modifiedVal)
let thmStatement ← `(command| $[$doc]? $[$attrs:attributes]? theorem $defaultDeclName $sig := by {let_intros; $(⟨preambleSeq⟩); $(⟨tacticStx⟩)})
elabCommand thmStatement
let msgs := (← get).messages
@@ -394,7 +437,6 @@ 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
@@ -425,7 +467,6 @@ elab doc:docComment ? attrs:Parser.Term.attributes ?
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
@@ -460,7 +501,7 @@ elab doc:docComment ? attrs:Parser.Term.attributes ?
goal := sig,
preamble := preambleSeq
scope := scope,
descrText := docComment
descrText := docContent
statementName := match statementName with
| none => default
| some name => currNamespace ++ name.getId
@@ -468,7 +509,7 @@ 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}
theorems := {level.theorems with used := usedInventory.theorems.toArray}
lemmas := {level.lemmas with used := usedInventory.lemmas.toArray}
}
/-! # Hints -/
@@ -481,7 +522,6 @@ 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
@@ -503,8 +543,6 @@ 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
@@ -528,7 +566,6 @@ 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
@@ -573,7 +610,7 @@ where filterArgs (args : List Syntax) : List Syntax :=
| Syntax.node _ `GameServer.Tactic.Hint _ :: _ :: r
| Syntax.node _ `GameServer.Tactic.Branch _ :: _ :: r =>
filterArgs r
-- delete `Hint` and `Branch` occurence at the end of the tactic sequence.
-- delete `Hint` and `Branch` occurrence at the end of the tactic sequence.
| Syntax.node _ `GameServer.Tactic.Hint _ :: []
| Syntax.node _ `GameServer.Tactic.Branch _ :: [] =>
[]
@@ -601,7 +638,7 @@ elab "Template" tacs:tacticSeq : tactic => do
-- open Lean Meta Elab Command Tactic Simp
-- def Lean.Meta.SimpTheorems.hasAttribute (d : SimpTheorems) (decl : Name) :=
-- d.isTheorem (.decl decl) || d.isDeclToUnfold decl
-- d.isLemma (.decl decl) || d.isDeclToUnfold decl
-- def isInSimpset (simpAttr decl: Name) : CoreM Bool := do
-- let .some simpDecl ←getSimpExtension? simpAttr | return false
@@ -619,8 +656,8 @@ def Parser.dependency := Parser.sepBy1Indent Parser.ident "→"
/-- Manually add a dependency between two worlds.
Normally, the dependencies are computed automatically by the
tactics & theorems used in the example
proof and the ones introduced by `NewTheorem`/`NewTactic`.
tactics & lemmas used in the example
proof and the ones introduced by `NewLemma`/`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. -/
@@ -665,10 +702,10 @@ elab "MakeGame" : command => do
s!"[mathlib doc](https://leanprover-community.github.io/mathlib4_docs/find/?pattern={name}#doc)"
match item.type with
| .Theorem =>
| .Lemma =>
modifyEnv (inventoryExt.addEntry · { item with
content := content
-- Add the theorem statement to the doc
-- Add the lemma statement to the doc
statement := (← getStatementString name)
})
| _ =>
@@ -693,14 +730,14 @@ 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, .Theorem] do
for inventoryType in #[.Tactic, .Definition, .Lemma] 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 theorem
if inventoryType == .Theorem then
-- if the previous level was named, we need to add it as a new lemma
if inventoryType == .Lemma then
match levelId with
| 0 => pure ()
| 1 => pure () -- level ids start with 1, so we need to skip 1, too
@@ -713,9 +750,9 @@ elab "MakeGame" : command => do
let name := Name.str pre s
newItems := newItems.insert name
if inventoryType == .Theorem then
if inventoryType == .Lemma then
-- if the last level was named, we need to add it as a new theorem
-- if the last level was named, we need to add it as a new lemma
let i₀ := world.levels.size
let some idx := world.levels.find? (i₀) | throwError s!"Level {i₀} not found for world {worldId}!"
@@ -820,27 +857,27 @@ 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, .Theorem] do
for inventoryType in #[.Tactic, .Definition, .Lemma] do
-- Which items are introduced in which world?
let mut theoremStatements : HashMap (Name × Nat) Name := {}
let mut lemmaStatements : 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 `Theorem`s as well, but so far that seems to cause no problems
-- e.g. we iterate over `ring` for `Lemma`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 newTheorems := (level.getInventory inventoryType).new
newItems := newItems.insertMany newTheorems
allItems := allItems.insertMany newTheorems
if inventoryType == .Theorem then
let newLemmas := (level.getInventory inventoryType).new
newItems := newItems.insertMany newLemmas
allItems := allItems.insertMany newLemmas
if inventoryType == .Lemma then
-- For levels `2, 3, …` we check if the previous level was named
-- in which case we add it as available theorem.
-- in which case we add it as available lemma.
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 theorems.
-- add named statement from previous level to the available lemmas.
let some idx := world.levels.find? (i₀) | throwError s!"Level {i₀ + 1} not found for world {worldId}!"
match (idx).statementName with
| .anonymous => pure ()
@@ -849,9 +886,9 @@ elab "MakeGame" : command => do
let name := Name.str pre s
newItems := newItems.insert name
allItems := allItems.insert name
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
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
let i₀ := world.levels.size
match i₀ with
| 0 => logWarning m!"World `{worldId}` contains no levels."
@@ -873,7 +910,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 theorem statement and doc entry
-- Note: we did have a panic here before because lemma statement and doc entry
-- had mismatching namespaces
logError m!"There is no inventory item ({inventoryType}) for: {item}."
panic s!"Inventory item {item} not found!"
@@ -927,8 +964,8 @@ elab "MakeGame" : command => do
-- add the exercise statement from the previous level
-- if it was named
if inventoryType == .Theorem then
match theoremStatements.find? (worldId, levelId) with
if inventoryType == .Lemma then
match lemmaStatements.find? (worldId, levelId) with
| none => pure ()
| some name =>
let data := (← getInventoryItem? name inventoryType).get!
@@ -936,17 +973,11 @@ 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` theorems here, so that they only apply to
-- add marks for `disabled` and `new` lemmas 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)
@@ -968,10 +999,10 @@ elab "MakeGame" : command => do
| 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})
lemmas := (← getTiles .Lemma).map (fun tile => {tile with hidden := hiddenItems.contains tile.name})
tactics := (← getTiles .Tactic).map (fun tile => {tile with hidden := hiddenItems.contains tile.name})
definitions := (← getTiles .Definition).map (fun tile => {tile with hidden := hiddenItems.contains tile.name})
theoremTab := none
lemmaTab := none
}
saveGameData allItemsByType inventory
+30 -49
View File
@@ -5,14 +5,9 @@ import GameServer.Hints
open GameServer
-- TODO: Is there a better place?
/-- Keywords that the server should not consider as tactics.
Note: Added `clear` tactic because currently it is very useful in combination with
`Branch` and `Hint` (i.e. using `clear` before a `Hint` in order to remove any irrelevant
hypotheses).
-/
/-- Keywords that the server should not consider as tactics. -/
def GameServer.ALLOWED_KEYWORDS : List String :=
["with", "fun", "at", "only", "by", "generalizing", "if", "then", "else", "clear", "using"]
["with", "fun", "at", "only", "by", "generalizing"]
/-- The default game name if `Game "MyGame"` is not used. -/
def defaultGameName: String := "MyGame"
@@ -32,40 +27,40 @@ open Lean
/-! ## Inventory (documentation)
The inventory contains documentation that the user can access.
There are three inventory types: Theorem, Tactic, Definition. They vary about in the information
There are three inventory types: Lemma, Tactic, Definition. They vary about in the information
they carry.
The commands `TheoremDoc`, `TacticDoc`, and `DefinitionDoc` add keys and templates to an
env. extension called `InventoryTemplateExt`. Commands like `NewTheorem`, etc. as well as
env. extension called `InventoryTemplateExt`. Commands like `NewLemma`, etc. as well as
`Statement` check if there is a key registered in this extension and might add a default or
print a warning if not.
Then, `MakeGame` takes the templates from `InventoryTemplateExt` and creates the documentation entries
that are sent to the client. This allows us to modify them like adding information from
mathlib or from parsing the theorem in question.
mathlib or from parsing the lemma in question.
-/
/-- The game knows three different inventory types that contain slightly different information -/
inductive InventoryType := | Tactic | Theorem | Definition
inductive InventoryType := | Tactic | Lemma | Definition
deriving ToJson, FromJson, Repr, BEq, Hashable, Inhabited
-- TODO: golf this?
instance : ToString InventoryType := ⟨fun t => match t with
| .Tactic => "Tactic"
| .Theorem => "Theorem"
| .Lemma => "Lemma"
| .Definition => "Definition"⟩
/-- The keys/templates of the inventory items, stored in `InventoryTemplateExt`. -/
structure InventoryTemplate where
/-- Theorem, Tactic, or Definition -/
/-- Lemma, Tactic, or Definition -/
type: InventoryType
/-- Depends on the type:
* Tactic: the tactic's name
* Theorem: fully qualified theorem name
* Lemma: fully qualified lemma name
* Definition: no restrictions (preferably the definitions fully qualified name)
-/
name: Name
/-- Only for Theorems. To sort them into tabs -/
/-- Only for Lemmas. To sort them into tabs -/
category: String := default
/-- Free-text short name -/
displayName: String := name.toString
@@ -85,20 +80,14 @@ structure InventoryTile where
The name of the item. The restrictions are:
* for Tactics: The name of the tactic.
* for Theorems: *Fully qualified* theorem name.
* for Lemmas: *Fully qualified* lemma name.
* for Definitions: no restrictions.
-/
name : Name
/-- The display name shown in the inventory. This can be free-text. -/
displayName : String
/-- Category to group inventory items by (currently only used for theorems). -/
/-- Category to group inventory items by (currently only used for lemmas). -/
category : String
/-- The world which introduced this item. -/
world : Option Name := none
/-- The level which introduced this item. -/
level : Option Nat := none
/-- Set to `true` if there exists an exercise in the game proving this statement. -/
proven := false
/-- If `true` then the item only gets unlocked in a later level. -/
locked := true
/-- If `true` then the item is blocked for this level. -/
@@ -109,7 +98,7 @@ structure InventoryTile where
hidden := false
/-- hover text -/
altTitle : String := default
deriving ToJson, FromJson, Repr, Inhabited, BEq
deriving ToJson, FromJson, Repr, Inhabited
def InventoryItem.toTile (item : InventoryItem) : InventoryTile := {
name := item.name,
@@ -146,16 +135,16 @@ def getInventoryItem? [Monad m] [MonadEnv m] (n : Name) (type : InventoryType) :
structure InventoryOverview where
tactics : Array InventoryTile
theorems : Array InventoryTile
lemmas : Array InventoryTile
definitions : Array InventoryTile
theoremTab : Option String
lemmaTab : Option String
deriving ToJson, FromJson
-- TODO: Reuse the following code for checking available tactics in user code:
structure UsedInventory where
(tactics : HashSet Name := {})
(definitions : HashSet Name := {})
(theorems : HashSet Name := {})
(lemmas : HashSet Name := {})
/-! ## Environment extensions for game specification -/
@@ -255,7 +244,7 @@ structure GameLevel where
/-- Introduction text shown all the time. (markdown) -/
introduction: String := default
conclusion: String := default
/-- The name of the exercise proven. If provided this theorem will be available in
/-- The name of the exercise proven. If provided this lemma will be available in
future levels. -/
statementName: Name := default
hints: Array GoalHintEntry := default
@@ -265,13 +254,13 @@ structure GameLevel where
/-- The mathematical statement in mathematician-readable form. (markdown) -/
descrText: Option String := none
descrFormat : String := default
/-- The `category` of theorems to be open by default -/
theoremTab: Option String := none
/-- The `category` of lemmas to be open by default -/
lemmaTab: Option String := none
/-- The module to be imported when playing this level -/
module : Name := default
tactics: InventoryInfo := default
definitions: InventoryInfo := default
theorems: InventoryInfo := default
lemmas: InventoryInfo := default
/-- A proof template that is printed in an empty editor. -/
template: Option String := none
/-- The image for this level. -/
@@ -282,20 +271,20 @@ deriving Inhabited, Repr
/-- Json-encodable version of `GameLevel`
Fields:
- description: Theorem in mathematical language.
- descriptionGoal: Theorem printed as Lean-Code.
- description: Lemma in mathematical language.
- descriptionGoal: Lemma printed as Lean-Code.
-/
structure LevelInfo where
index : Nat
title : String
tactics : Array InventoryTile
theorems : Array InventoryTile
lemmas : Array InventoryTile
definitions : Array InventoryTile
introduction : String
conclusion : String
descrText : Option String := none
descrFormat : String := ""
theoremTab : Option String
lemmaTab : Option String
module : Name
displayName : Option String
statementName : Option String
@@ -307,17 +296,17 @@ def GameLevel.toInfo (lvl : GameLevel) (env : Environment) : LevelInfo :=
{ index := lvl.index,
title := lvl.title,
tactics := lvl.tactics.tiles,
theorems := lvl.theorems.tiles,
lemmas := lvl.lemmas.tiles,
definitions := lvl.definitions.tiles,
descrText := lvl.descrText,
descrFormat := lvl.descrFormat --toExpr <| format (lvl.goal.raw) --toString <| Syntax.formatStx (lvl.goal.raw) --Syntax.formatStx (lvl.goal.raw) , -- TODO
introduction := lvl.introduction
conclusion := lvl.conclusion
theoremTab := match lvl.theoremTab with
lemmaTab := match lvl.lemmaTab with
| some tab => tab
| none =>
-- Try to set the theorem tab to the category of the first added theorem
match lvl.theorems.tiles.find? (·.new) with
-- Try to set the lemma tab to the category of the first added lemma
match lvl.lemmas.tiles.find? (·.new) with
| some tile => tile.category
| none => none
statementName := lvl.statementName.toString
@@ -325,11 +314,11 @@ def GameLevel.toInfo (lvl : GameLevel) (env : Environment) : LevelInfo :=
displayName := match lvl.statementName with
| .anonymous => none
| name => match (inventoryExt.getState env).find?
(fun x => x.name == name && x.type == .Theorem) with
(fun x => x.name == name && x.type == .Lemma) with
| some n => n.displayName
| none => name.toString
-- Note: we could call `.find!` because we check in `Statement` that the
-- theorem doc must exist.
-- lemma doc must exist.
template := lvl.template
image := lvl.image
}
@@ -532,11 +521,3 @@ def modifyLevel (levelId : LevelId) (fn : GameLevel → m GameLevel) [MonadError
let world' := {world with levels := world.levels.insert levelId.level level'}
let game' := {game with worlds := game.worlds.insertNode levelId.world world'}
insertGame levelId.game game'
-- def getIntroducedInventory (game : Game) [MonadError m] : m (Array Name) := do
-- let allItems : Array Name := game.worlds.nodes.fold (fun L _ world => L ++
-- world.levels.fold (fun LL _ level =>
-- LL ++ level.tactics.new ++ level.theorems.new
-- ) #[]) #[]
-- pure allItems
+8 -17
View File
@@ -166,7 +166,7 @@ partial def findForbiddenTactics (inputCtx : Parser.InputContext) (workerState :
-- Forbid the theorem we are proving currently
addMessage info inputCtx (severity := .error)
s!"Structural recursion: you can't use '{n}' to proof itself!"
let theoremsAndDefs := levelInfo.theorems ++ levelInfo.definitions
let theoremsAndDefs := levelInfo.lemmas ++ levelInfo.definitions
match theoremsAndDefs.find? (·.name == n) with
| none =>
-- Theorem will never be introduced in this game
@@ -187,7 +187,7 @@ partial def findForbiddenTactics (inputCtx : Parser.InputContext) (workerState :
where addMessageByDifficulty (info : SourceInfo) (s : MessageData) :=
-- See `GameServer.FileWorker.WorkerState.difficulty`. Send nothing/warnings/errors
-- deppending on difficulty.
-- depending on difficulty.
let difficulty := workerState.difficulty
if difficulty > 0 then
addMessage info inputCtx (if difficulty > 1 then .error else .warning) s
@@ -257,13 +257,13 @@ def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets
-- Insert final `done` command to display unsolved goal error in the end
let done := Syntax.node (.synthetic cmdParserState.pos cmdParserState.pos) ``Lean.Parser.Tactic.done #[]
let tacticStx := (#[skip] ++ tacticStx.getArgs ++ #[done]).map (⟨.⟩)
let tacticStx' := ← `(Lean.Parser.Tactic.tacticSeq| $[$(tacticStx)]*)
let tacticStx := ← `(Lean.Parser.Tactic.tacticSeq| $[$(tacticStx)]*)
-- Always call `let_intros` to get rid `let` statements in the goal.
-- This makes the experience for the user much nicer and allows for local
-- definitions in the exercise.
let cmdStx ← `(command|
theorem the_theorem $(level.goal) := by {let_intros; $(⟨level.preamble⟩); $(⟨tacticStx'⟩)} )
theorem the_theorem $(level.goal) := by {let_intros; $(⟨level.preamble⟩); $(⟨tacticStx⟩)} )
Elab.Command.elabCommandTopLevel cmdStx)
cmdCtx cmdStateRef
let postNew := (← tacticCacheNew.get).post
@@ -383,7 +383,7 @@ def publishProofState (m : DocumentMeta) (snap : Snapshot) (initParams : Lsp.Ini
-- -- Should we get the diags from there?
-- let diag : Array Widget.InteractiveDiagnostic := snap.interactiveDiags.toArray
-- -- Level is completed if there are no errrors or warnings
-- -- Level is completed if there are no errors or warnings
-- let completed : Bool := ¬ diag.any (fun d =>
-- d.severity? == some .error ∨ d.severity? == some .warning)
@@ -742,6 +742,7 @@ def initAndRunWorker (i o e : FS.Stream) (opts : Options) (gameDir : String) : I
let _ ← IO.setStderr e
try
-- BIG MODIFICATION
let game ← loadGameData gameDir
-- TODO: We misuse the `rootUri` field to the gameName
@@ -751,18 +752,8 @@ def initAndRunWorker (i o e : FS.Stream) (opts : Options) (gameDir : String) : I
initParams meta.mkInputContext.fileName
| throwServerError s!"Could not determine level ID: {meta.mkInputContext.fileName}"
let levelInfo ← loadLevelData gameDir levelId.world levelId.level
-- TODO: I don't know how to pass these options to the Lean server
-- with the new lean4monaco setup!!
-- let some initializationOptions := initRequest.param.initializationOptions?
-- | throwServerError "no initialization options found"
let initializationOptions : Game.InitializationOptions := {
difficulty := 1
inventory := #[]
editDelay? := none
hasWidgets? := none
}
let some initializationOptions := initRequest.param.initializationOptions?
| throwServerError "no initialization options found"
let gameWorkerState : WorkerState := {
inventory := initializationOptions.inventory
difficulty := initializationOptions.difficulty

Some files were not shown because too many files have changed in this diff Show More