Merge branch 'dev'

cleanup_stuff
Jon Eugster 3 years ago
commit 8c84d3fae7

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 KiB

@ -86,7 +86,7 @@ function InventoryList({items, docType, openDoc, defaultTab=null, level=undefine
{[...modifiedItems].sort( {[...modifiedItems].sort(
// For lemas, sort entries `available > disabled > locked` // For lemas, sort entries `available > disabled > locked`
// otherwise alphabetically // otherwise alphabetically
(x, y) => +(docType == "Lemma") * (+x.locked - +y.locked || +x.disabled - +y.disabled) (x, y) => +(docType == "Lemma") * (+x.locked - +y.locked || +x.disabled - +y.disabled) || x.displayName.localeCompare(y.displayName)
).filter(item => !item.hidden && ((tab ?? categories[0]) == item.category)).map((item, i) => { ).filter(item => !item.hidden && ((tab ?? categories[0]) == item.category)).map((item, i) => {
return <InventoryItem key={`${item.category}-${item.name}`} return <InventoryItem key={`${item.category}-${item.name}`}
showDoc={() => {openDoc({name: item.name, type: docType})}} showDoc={() => {openDoc({name: item.name, type: docType})}}

@ -7,12 +7,12 @@ import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css'; import '@fontsource/roboto/700.css';
import '../css/landing_page.css' import '../css/landing_page.css'
import coverRobo from '../assets/covers/formaloversum.png'
import coverNNG from '../assets/covers/nng.png'
import bgImage from '../assets/bg.jpg' import bgImage from '../assets/bg.jpg'
import Markdown from './markdown'; import Markdown from './markdown';
import {PrivacyPolicyPopup} from './popup/privacy_policy' import {PrivacyPolicyPopup} from './popup/privacy_policy'
import { GameTile, useGetGameInfoQuery } from '../state/api'
import path from 'path';
const flag = { const flag = {
'Dutch': '🇳🇱', 'Dutch': '🇳🇱',
@ -33,47 +33,42 @@ function GithubIcon({url='https://github.com'}) {
</div> </div>
} }
function GameTile({ function Tile({gameId, data}: {gameId: string, data: GameTile|undefined}) {
title,
gameId,
intro, // Catchy intro phrase.
image=null,
worlds='?',
levels='?',
prereq='&ndash;', // Optional list of games that this game builds on. Use markdown.
description, // Longer description. Supports Markdown.
language}) {
let navigate = useNavigate(); let navigate = useNavigate();
const routeChange = () =>{ const routeChange = () =>{
navigate(gameId); navigate(gameId);
} }
if (typeof data === 'undefined') {
return <></>
}
return <div className="game" onClick={routeChange}> return <div className="game" onClick={routeChange}>
<div className="wrapper"> <div className="wrapper">
<div className="title">{title}</div> <div className="title">{data.title}</div>
<div className="short-description">{intro} <div className="short-description">{data.short}
</div> </div>
{ image ? <img className="image" src={image} alt="" /> : <div className="image"/> } { data.image ? <img className="image" src={path.join("data", gameId, data.image)} alt="" /> : <div className="image"/> }
<div className="long description"><Markdown>{description}</Markdown></div> <div className="long description"><Markdown>{data.long}</Markdown></div>
</div> </div>
<table className="info"> <table className="info">
<tbody> <tbody>
<tr> <tr>
<td title="consider playing these games first.">Prerequisites</td> <td title="consider playing these games first.">Prerequisites</td>
<td><Markdown>{prereq}</Markdown></td> <td><Markdown>{data.prerequisites.join(', ')}</Markdown></td>
</tr> </tr>
<tr> <tr>
<td>Worlds</td> <td>Worlds</td>
<td>{worlds}</td> <td>{data.worlds}</td>
</tr> </tr>
<tr> <tr>
<td>Levels</td> <td>Levels</td>
<td>{levels}</td> <td>{data.levels}</td>
</tr> </tr>
<tr> <tr>
<td>Language</td> <td>Language</td>
<td title={`in ${language}`}>{flag[language]}</td> <td title={`in ${data.languages.join(', ')}`}>{data.languages.map((lan) => flag[lan]).join(', ')}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -89,6 +84,56 @@ function LandingPage() {
const openImpressum = () => setImpressum(true); const openImpressum = () => setImpressum(true);
const closeImpressum = () => setImpressum(false); const closeImpressum = () => setImpressum(false);
// const [allGames, setAllGames] = React.useState([])
// const [allTiles, setAllTiles] = React.useState([])
// const getTiles=()=>{
// fetch('featured_games.json', {
// headers : {
// 'Content-Type': 'application/json',
// 'Accept': 'application/json'
// }
// }
// ).then(function(response){
// return response.json()
// }).then(function(data) {
// setAllGames(data.featured_games)
// })
// }
// React.useEffect(()=>{
// getTiles()
// },[])
// React.useEffect(()=>{
// Promise.allSettled(
// allGames.map((gameId) => (
// fetch(`data/g/${gameId}/game.json`).catch(err => {return undefined})))
// ).then(responses =>
// responses.forEach((result) => console.log(result)))
// // Promise.all(responses.map(res => {
// // if (res.status == "fulfilled") {
// // console.log(res.value.json())
// // return res.value.json()
// // } else {
// // return undefined
// // }
// // }))
// // ).then(allData => {
// // setAllTiles(allData.map(data => data?.tile))
// // })
// },[allGames])
// TODO: I would like to read the supported games list form a JSON,
// Then load all these games in
//
let allGames = [
"leanprover-community/nng4",
"djvelleman/stg4",
"hhu-adam/robo"]
let allTiles = allGames.map((gameId) => (useGetGameInfoQuery({game: `g/${gameId}`}).data?.tile))
return <div className="landing-page"> return <div className="landing-page">
<header style={{backgroundImage: `url(${bgImage})`}}> <header style={{backgroundImage: `url(${bgImage})`}}>
@ -105,8 +150,19 @@ function LandingPage() {
</div> </div>
</header> </header>
<div className="game-list"> <div className="game-list">
{allTiles.length == 0 ?
<GameTile <p>No Games loaded. Use <a>http://localhost:3000/#/g/local/FOLDER</a> to open a
game directly from a local folder.
</p>
: allGames.map((id, i) => (
<Tile
key={id}
gameId={`g/${id}`}
data={allTiles[i]}
/>
))
}
{/* <GameTile
title="Natural Number Game" title="Natural Number Game"
gameId="g/hhu-adam/NNG4" gameId="g/hhu-adam/NNG4"
intro="The classical introduction game for Lean." intro="The classical introduction game for Lean."
@ -129,18 +185,7 @@ This is a good first introduction to Lean!"
levels="30" levels="30"
language="English" language="English"
/> />
*/}
<GameTile
title="Formaloversum"
gameId="g/hhu-adam/Robo"
intro="Erkunde das Leansche Universum mit deinem Robo, welcher dir bei der Verständigung mit den Formalosophen zur Seite steht."
description="
Dieses Spiel führt die Grundlagen zur Beweisführung in Lean ein und schneidet danach verschiedene Bereiche des Bachelorstudiums an.
(Das Spiel befindet sich noch in der Entstehungsphase.)"
image={coverRobo}
language="German"
/>
</div> </div>
<section> <section>
<div className="wrapper"> <div className="wrapper">

@ -34,6 +34,7 @@ import { DualEditor } from './infoview/main'
import { GameHint } from './infoview/rpc_api' import { GameHint } from './infoview/rpc_api'
import { DeletedHints, Hint, Hints } from './hints' import { DeletedHints, Hint, Hints } from './hints'
import { PrivacyPolicyPopup } from './popup/privacy_policy' import { PrivacyPolicyPopup } from './popup/privacy_policy'
import path from 'path';
import '@fontsource/roboto/300.css' import '@fontsource/roboto/300.css'
import '@fontsource/roboto/400.css' import '@fontsource/roboto/400.css'
@ -467,6 +468,11 @@ function Introduction({impressum, setImpressum}) {
const gameInfo = useGetGameInfoQuery({game: gameId}) const gameInfo = useGetGameInfoQuery({game: gameId})
const {worldId} = useContext(WorldLevelIdContext)
let image: string = gameInfo.data?.worlds.nodes[worldId].image
const toggleImpressum = () => { const toggleImpressum = () => {
setImpressum(!impressum) setImpressum(!impressum)
} }
@ -480,7 +486,12 @@ function Introduction({impressum, setImpressum}) {
: :
<Split minSize={0} snapOffset={200} sizes={[25, 50, 25]} className={`app-content level`}> <Split minSize={0} snapOffset={200} sizes={[25, 50, 25]} className={`app-content level`}>
<IntroductionPanel gameInfo={gameInfo} /> <IntroductionPanel gameInfo={gameInfo} />
<div className="world-image-container empty"></div> <div className="world-image-container empty">
{image &&
<img src={path.join("data", gameId, image)} alt="" />
}
</div>
<InventoryPanel levelInfo={inventory?.data} /> <InventoryPanel levelInfo={inventory?.data} />
</Split> </Split>
} }

@ -336,6 +336,16 @@ td code {
background-color: #eee; background-color: #eee;
} }
.world-image-container {
display: flex;
flex-direction: column;
justify-content: center;
}
.world-image-container img {
object-fit: contain;
}
.typewriter-interface .proof { .typewriter-interface .proof {
background-color: #fff; background-color: #fff;
} }

@ -3,14 +3,28 @@
*/ */
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export interface GameTile {
title: string
short: string
long: string
languages: Array<string>
prerequisites: Array<string>
worlds: number
levels: number
image: string
}
export interface GameInfo { export interface GameInfo {
title: null|string, title: null|string,
introduction: null|string, introduction: null|string,
info: null|string, info: null|string,
worlds: null|{nodes: {[id:string]: {id: string, title: string, introduction: string}}, edges: string[][]}, worlds: null|{nodes: {[id:string]: {id: string, title: string, introduction: string, image: string}}, edges: string[][]},
worldSize: null|{[key: string]: number}, worldSize: null|{[key: string]: number},
authors: null|string[], authors: null|string[],
conclusion: null|string, conclusion: null|string,
tile: null|GameTile,
image: null|string
} }
export interface InventoryTile { export interface InventoryTile {
@ -36,7 +50,8 @@ export interface LevelInfo {
lemmaTab: null|string, lemmaTab: null|string,
statementName: null|string, statementName: null|string,
displayName: null|string, displayName: null|string,
template: null|string template: null|string,
image: null|string
} }
/** Used to display the inventory on the welcome page */ /** Used to display the inventory on the welcome page */
@ -59,19 +74,19 @@ interface Doc {
// Define a service using a base URL and expected endpoints // Define a service using a base URL and expected endpoints
export const apiSlice = createApi({ export const apiSlice = createApi({
reducerPath: 'gameApi', reducerPath: 'gameApi',
baseQuery: fetchBaseQuery({ baseUrl: window.location.origin + "/api" }), baseQuery: fetchBaseQuery({ baseUrl: window.location.origin + "/data" }),
endpoints: (builder) => ({ endpoints: (builder) => ({
getGameInfo: builder.query<GameInfo, {game: string}>({ getGameInfo: builder.query<GameInfo, {game: string}>({
query: ({game}) => `${game}/game`, query: ({game}) => `${game}/game.json`,
}), }),
loadLevel: builder.query<LevelInfo, {game: string, world: string, level: number}>({ loadLevel: builder.query<LevelInfo, {game: string, world: string, level: number}>({
query: ({game, world, level}) => `${game}/level/${world}/${level}`, query: ({game, world, level}) => `${game}/level__${world}__${level}.json`,
}), }),
loadInventoryOverview: builder.query<InventoryOverview, {game: string}>({ loadInventoryOverview: builder.query<InventoryOverview, {game: string}>({
query: ({game}) => `${game}/inventory`, query: ({game}) => `${game}/inventory.json`,
}), }),
loadDoc: builder.query<Doc, {game: string, name: string, type: "lemma"|"tactic"}>({ loadDoc: builder.query<Doc, {game: string, name: string, type: "lemma"|"tactic"}>({
query: ({game, type, name}) => `${game}/doc/${type}/${name}`, query: ({game, type, name}) => `${game}/doc__${type}__${name}.json`,
}), }),
}), }),
}) })

@ -46,7 +46,9 @@ elab "Title" t:str : command => do
match ← getCurLayer with match ← getCurLayer with
| .Level => modifyCurLevel fun level => pure {level with title := t.getString} | .Level => modifyCurLevel fun level => pure {level with title := t.getString}
| .World => modifyCurWorld fun world => pure {world with title := t.getString} | .World => modifyCurWorld fun world => pure {world with title := t.getString}
| .Game => modifyCurGame fun game => pure {game with title := t.getString} | .Game => modifyCurGame fun game => pure {game with
title := t.getString
tile := {game.tile with title := t.getString}}
/-- Define the introduction of the current game/world/level. -/ /-- Define the introduction of the current game/world/level. -/
elab "Introduction" t:str : command => do elab "Introduction" t:str : command => do
@ -58,10 +60,32 @@ elab "Introduction" t:str : command => do
/-- Define the info of the current game. Used for e.g. credits -/ /-- Define the info of the current game. Used for e.g. credits -/
elab "Info" t:str : command => do elab "Info" t:str : command => do
match ← getCurLayer with match ← getCurLayer with
| .Level => pure () | .Level =>
| .World => pure () logError "Can't use `Info` in a level!"
pure ()
| .World =>
logError "Can't use `Info` in a world"
pure ()
| .Game => modifyCurGame fun game => pure {game with info := t.getString} | .Game => modifyCurGame fun game => pure {game with info := t.getString}
/-- Provide the location of the image for the current game/world/level.
Paths are relative to the lean project's root. -/
elab "Image" t:str : command => do
let file := t.getString
if not <| ← System.FilePath.pathExists file then
logWarningAt t s!"Make sure the cover image '{file}' exists."
if not <| file.startsWith "images/" then
logWarningAt t s!"The file name should start with `images/`. Make sure all images are in that folder."
match ← getCurLayer with
| .Level =>
logWarning "Level-images not implemented yet" -- TODO
modifyCurLevel fun level => pure {level with image := file}
| .World =>
modifyCurWorld fun world => pure {world with image := file}
| .Game =>
logWarning "Main image of the game not implemented yet" -- TODO
modifyCurGame fun game => pure {game with image := file}
/-- Define the conclusion of the current game or current level if some /-- Define the conclusion of the current game or current level if some
building a level. -/ building a level. -/
@ -71,6 +95,38 @@ elab "Conclusion" t:str : command => do
| .World => modifyCurWorld fun world => pure {world with conclusion := t.getString} | .World => modifyCurWorld fun world => pure {world with conclusion := t.getString}
| .Game => modifyCurGame fun game => pure {game with conclusion := t.getString} | .Game => modifyCurGame fun game => pure {game with conclusion := t.getString}
/-- A list of games that should be played before this one. Example `Prerequisites "NNG" "STG"`. -/
elab "Prerequisites" t:str* : command => do
modifyCurGame fun game => pure {game with
tile := {game.tile with prerequisites := t.map (·.getString) |>.toList}}
/-- Short caption for the game (1 sentence) -/
elab "CaptionShort" t:str : command => do
modifyCurGame fun game => pure {game with
tile := {game.tile with short := t.getString}}
/-- More detailed description what the game is about (2-4 sentences). -/
elab "CaptionLong" t:str : command => do
modifyCurGame fun game => pure {game with
tile := {game.tile with long := t.getString}}
/-- A list of Languages the game is translated to. For example `Languages "German" "English"`.
NOTE: For the time being, only a single language is supported.
-/
elab "Languages" t:str* : command => do
modifyCurGame fun game => pure {game with
tile := {game.tile with languages := t.map (·.getString) |>.toList}}
/-- The Image of the game (optional). TODO: Not impementeds -/
elab "CoverImage" t:str : command => do
let file := t.getString
if not <| ← System.FilePath.pathExists file then
logWarningAt t s!"Make sure the cover image '{file}' exists."
if not <| file.startsWith "images/" then
logWarningAt t s!"The file name should start with `images/`. Make sure all images are in that folder."
modifyCurGame fun game => pure {game with
tile := {game.tile with image := file}}
/-! # Inventory /-! # Inventory
@ -626,6 +682,27 @@ elab "Template" tacs:tacticSeq : tactic => do
modifyLevel (←getCurLevelId) fun level => do modifyLevel (←getCurLevelId) fun level => do
return {level with template := s!"{template}"} return {level with template := s!"{template}"}
open IO.FS System FilePath in
/-- Copies the folder `images/` to `.lake/gamedata/images/` -/
def copyImages : IO Unit := do
let target : FilePath := ".lake" / "gamedata"
if ← FilePath.pathExists "images" then
for file in ← walkDir "images" do
let outFile := target.join file
-- create the directories
if ← file.isDir then
createDirAll outFile
else
if let some parent := outFile.parent then
createDirAll parent
-- copy file
let content ← readBinFile file
writeBinFile outFile content
-- TODO: Notes for testing if a declaration has the simp attribute -- TODO: Notes for testing if a declaration has the simp attribute
-- -- Test: From zulip -- -- Test: From zulip
@ -647,7 +724,7 @@ elab "Template" tacs:tacticSeq : tactic => do
#eval IO.FS.createDirAll ".lake/gamedata/" #eval IO.FS.createDirAll ".lake/gamedata/"
-- TODO: register all of this as ToJson instance? -- TODO: register all of this as ToJson instance?
def saveGameData (allItemsByType : HashMap InventoryType (HashSet Name)) : CommandElabM Unit:= do def saveGameData (allItemsByType : HashMap InventoryType (HashSet Name)) : CommandElabM Unit := do
let game ← getCurGame let game ← getCurGame
let env ← getEnv let env ← getEnv
let path : System.FilePath := s!"{← IO.currentDir}" / ".lake" / "gamedata" let path : System.FilePath := s!"{← IO.currentDir}" / ".lake" / "gamedata"
@ -656,6 +733,9 @@ def saveGameData (allItemsByType : HashMap InventoryType (HashSet Name)) : Comma
IO.FS.removeDirAll path IO.FS.removeDirAll path
IO.FS.createDirAll path IO.FS.createDirAll path
-- copy the images folder
copyImages
for (worldId, world) in game.worlds.nodes.toArray do for (worldId, world) in game.worlds.nodes.toArray do
for (levelId, level) in world.levels.toArray do for (levelId, level) in world.levels.toArray do
IO.FS.writeFile (path / s!"level__{worldId}__{levelId}.json") (toString (toJson (level.toInfo env))) IO.FS.writeFile (path / s!"level__{worldId}__{levelId}.json") (toString (toJson (level.toInfo env)))
@ -848,8 +928,12 @@ elab "MakeGame" : command => do
-- Items that should not be displayed in inventory -- Items that should not be displayed in inventory
let mut hiddenItems : HashSet Name := {} let mut hiddenItems : HashSet Name := {}
let allWorlds := game.worlds.nodes.toArray
let nrWorlds := allWorlds.size
let mut nrLevels := 0
-- Calculate which "items" are used/new in which world -- Calculate which "items" are used/new in which world
for (worldId, world) in game.worlds.nodes.toArray do for (worldId, world) in allWorlds do
let mut usedItems : HashSet Name := {} let mut usedItems : HashSet Name := {}
let mut newItems : HashSet Name := {} let mut newItems : HashSet Name := {}
for inventoryType in #[.Tactic, .Definition, .Lemma] do for inventoryType in #[.Tactic, .Definition, .Lemma] do
@ -888,9 +972,12 @@ elab "MakeGame" : command => do
-- logInfo m!"{worldId} uses: {usedItems.toList}" -- logInfo m!"{worldId} uses: {usedItems.toList}"
-- logInfo m!"{worldId} introduces: {newItems.toList}" -- logInfo m!"{worldId} introduces: {newItems.toList}"
-- Moreover, count the number of levels in the game
nrLevels := nrLevels + world.levels.toArray.size
/- for each "item" this is a HashSet of `worldId`s that introduce this item -/ /- for each "item" this is a HashSet of `worldId`s that introduce this item -/
let mut worldsWithNewItem : HashMap Name (HashSet Name) := {} let mut worldsWithNewItem : HashMap Name (HashSet Name) := {}
for (worldId, _world) in game.worlds.nodes.toArray do for (worldId, _world) in allWorlds do
for newItem in newItemsInWorld.findD worldId {} do for newItem in newItemsInWorld.findD worldId {} do
worldsWithNewItem := worldsWithNewItem.insert newItem $ worldsWithNewItem := worldsWithNewItem.insert newItem $
(worldsWithNewItem.findD newItem {}).insert worldId (worldsWithNewItem.findD newItem {}).insert worldId
@ -902,7 +989,7 @@ elab "MakeGame" : command => do
let mut dependencyReasons : HashMap (Name × Name) (HashSet Name) := {} let mut dependencyReasons : HashMap (Name × Name) (HashSet Name) := {}
-- Calculate world dependency graph `game.worlds` -- Calculate world dependency graph `game.worlds`
for (dependentWorldId, _dependentWorld) in game.worlds.nodes.toArray do for (dependentWorldId, _dependentWorld) in allWorlds do
let mut dependsOnWorlds : HashSet Name := {} let mut dependsOnWorlds : HashSet Name := {}
-- Adding manual dependencies that were specified via the `Dependency` command. -- Adding manual dependencies that were specified via the `Dependency` command.
for (sourceId, targetId) in game.worlds.edges do for (sourceId, targetId) in game.worlds.edges do
@ -953,11 +1040,21 @@ elab "MakeGame" : command => do
logError m!"{w1} depends on {w2} because of {items.toList}." logError m!"{w1} depends on {w2} because of {items.toList}."
else else
worldDependsOnWorlds ← removeTransitive worldDependsOnWorlds worldDependsOnWorlds ← removeTransitive worldDependsOnWorlds
-- need to delete all existing edges as they are already present in `worldDependsOnWorlds`.
modifyCurGame fun game =>
pure {game with worlds := {game.worlds with edges := Array.empty}}
for (dependentWorldId, worldIds) in worldDependsOnWorlds.toArray do for (dependentWorldId, worldIds) in worldDependsOnWorlds.toArray do
modifyCurGame fun game => modifyCurGame fun game =>
pure {game with worlds := {game.worlds with pure {game with worlds := {game.worlds with
edges := game.worlds.edges.append (worldIds.toArray.map fun wid => (wid, dependentWorldId))}} edges := game.worlds.edges.append (worldIds.toArray.map fun wid => (wid, dependentWorldId))}}
-- Add the number of levels and worlds to the tile for the landing page
modifyCurGame fun game => pure {game with tile := {game.tile with
levels := nrLevels
worlds := nrWorlds }}
-- Apparently we need to reload `game` to get the changes to `game.worlds` we just made -- Apparently we need to reload `game` to get the changes to `game.worlds` we just made
let game ← getCurGame let game ← getCurGame

@ -265,7 +265,9 @@ structure GameLevel where
lemmas: InventoryInfo := default lemmas: InventoryInfo := default
/-- A proof template that is printed in an empty editor. -/ /-- A proof template that is printed in an empty editor. -/
template: Option String := none template: Option String := none
deriving Inhabited, Repr /-- The image for this level. -/
image : String := default
deriving Inhabited, Repr
/-- Json-encodable version of `GameLevel` /-- Json-encodable version of `GameLevel`
Fields: Fields:
@ -286,6 +288,7 @@ structure LevelInfo where
displayName : Option String displayName : Option String
statementName : Option String statementName : Option String
template : Option String template : Option String
image: Option String
deriving ToJson, FromJson deriving ToJson, FromJson
def GameLevel.toInfo (lvl : GameLevel) (env : Environment) : LevelInfo := def GameLevel.toInfo (lvl : GameLevel) (env : Environment) : LevelInfo :=
@ -315,6 +318,7 @@ def GameLevel.toInfo (lvl : GameLevel) (env : Environment) : LevelInfo :=
-- Note: we could call `.find!` because we check in `Statement` that the -- Note: we could call `.find!` because we check in `Statement` that the
-- lemma doc must exist. -- lemma doc must exist.
template := lvl.template template := lvl.template
image := lvl.image
} }
/-! ## World -/ /-! ## World -/
@ -331,17 +335,46 @@ structure World where
conclusion : String := default conclusion : String := default
/-- The levels of the world. -/ /-- The levels of the world. -/
levels: HashMap Nat GameLevel := default levels: HashMap Nat GameLevel := default
/-- The introduction image of the world. -/
image: String := default
deriving Inhabited deriving Inhabited
instance : ToJson World := ⟨ instance : ToJson World := ⟨
fun world => Json.mkObj [ fun world => Json.mkObj [
("name", toJson world.name), ("name", toJson world.name),
("title", world.title), ("title", world.title),
("introduction", world.introduction)] ("introduction", world.introduction),
("image", world.image)]
/-! ## Game -/ /-! ## Game -/
/-- A tile as they are displayed on the servers landing page. -/
structure GameTile where
/-- The title of the game -/
title: String
/-- One catch phrase about the game -/
short: String := default
/-- One paragraph description what the game is about -/
long: String := default
/-- List of languages the game supports
TODO: What's the expectected format
TODO: Must be a list with a single language currently
-/
languages: List String := default
/-- A list of games which this one builds upon -/
prerequisites: List String := default
/-- Number of worlds in the game -/
worlds: Nat := default
/-- Number of levels in the game -/
levels: Nat := default
/-- A cover image of the game
TODO: What's the format? -/
image: String := default
deriving Inhabited, ToJson
structure Game where structure Game where
/-- Internal name of the game. -/ /-- Internal name of the game. -/
name : Name name : Name
@ -356,6 +389,10 @@ structure Game where
/-- TODO: currently unused. -/ /-- TODO: currently unused. -/
authors : List String := default authors : List String := default
worlds : Graph Name World := default worlds : Graph Name World := default
/-- The tile displayed on the server's landing page. -/
tile : GameTile := default
/-- The path to the background image of the world. -/
image : String := default
deriving Inhabited, ToJson deriving Inhabited, ToJson
def getGameJson (game : «Game») : Json := Id.run do def getGameJson (game : «Game») : Json := Id.run do

@ -126,7 +126,7 @@ def findHints (goal : MVarId) (doc : FileWorker.EditableDocument) (initParams :
then then
let userFVars := hintFVars.map fun v => bij.forward.findD v.fvarId! v.fvarId! let userFVars := hintFVars.map fun v => bij.forward.findD v.fvarId! v.fvarId!
let text := (← evalHintMessage hint.text) (userFVars.map Expr.fvar) let text := (← evalHintMessage hint.text) (userFVars.map Expr.fvar)
let ctx := {env := ← getEnv, mctx := ← getMCtx, lctx := ← getLCtx, opts := {}} let ctx := {env := ← getEnv, mctx := ← getMCtx, lctx := lctx, opts := {}}
let text ← (MessageData.withContext ctx text).toString let text ← (MessageData.withContext ctx text).toString
return some { text := text, hidden := hint.hidden } return some { text := text, hidden := hint.hidden }
else return none else return none

@ -108,12 +108,11 @@ async function doImport (owner, repo, id) {
} catch (e) { } catch (e) {
progress[id].output += `Error: ${e.toString()}\n${e.stack}` progress[id].output += `Error: ${e.toString()}\n${e.stack}`
} finally { } finally {
// if (artifactId) { // clean-up temp. files
// // fs.rmSync(`tmp/artifact_${artifactId}.zip`, {force: true, recursive: true}); if (artifactId) {
// // fs.rmSync(`tmp/artifact_${artifactId}`, {force: true, recursive: true}); fs.rmSync(`${__dirname}/../games/tmp/${owner}_${repo}_${artifactId}.zip`, {force: true, recursive: false});
// // fs.rmSync(`tmp/artifact_${artifactId}_inner`, {force: true, recursive: true}); fs.rmSync(`${__dirname}/../games/tmp/${owner}_${repo}_${artifactId}`, {force: true, recursive: true});
// // fs.rmSync(`tmp/archive_${artifactId}.tar`, {force: true, recursive: true}); }
// }
progress[id].done = true progress[id].done = true
} }
await new Promise(resolve => setTimeout(resolve, 10000)) await new Promise(resolve => setTimeout(resolve, 10000))

@ -34,34 +34,15 @@ var router = express.Router();
router.get('/import/status/:owner/:repo', importStatus) router.get('/import/status/:owner/:repo', importStatus)
router.get('/import/trigger/:owner/:repo', importTrigger) router.get('/import/trigger/:owner/:repo', importTrigger)
function loadJson(req, filename) {
const owner = req.params.owner;
const repo = req.params.repo
return JSON.parse(fs.readFileSync(path.join(getGameDir(owner,repo),".lake","gamedata",filename)))
}
router.get("/api/g/:owner/:repo/game", (req, res) => {
res.send(loadJson(req, `game.json`));
});
router.get("/api/g/:owner/:repo/inventory", (req, res) => {
res.send(loadJson(req, `inventory.json`));
});
router.get("/api/g/:owner/:repo/level/:world/:level", (req, res) => {
const world = req.params.world;
const level = req.params.level;
res.send(loadJson(req, `level__${world}__${level}.json`));
});
router.get("/api/g/:owner/:repo/doc/:type/:name", (req, res) => {
const type = req.params.type;
const name = req.params.name;
res.send(loadJson(req, `doc__${type}__${name}.json`));
});
const server = app const server = app
.use(express.static(path.join(__dirname, '../client/dist/'))) .use(express.static(path.join(__dirname, '../client/dist/'))) // TODO: add a dist folder from inside the game
.use('/data/g/:owner/:repo/*', (req, res, next) => {
const owner = req.params.owner;
const repo = req.params.repo
const filename = req.params[0];
req.url = filename;
express.static(path.join(getGameDir(owner,repo),".lake","gamedata"))(req, res, next);
})
.use('/', router) .use('/', router)
.listen(PORT, () => console.log(`Listening on ${PORT}`)); .listen(PORT, () => console.log(`Listening on ${PORT}`));
@ -84,13 +65,13 @@ function getGameDir(owner, repo) {
if (owner == 'local') { if (owner == 'local') {
if(!isDevelopment) { if(!isDevelopment) {
console.error(`No local games in production mode.`) console.error(`No local games in production mode.`)
return return ""
} }
} else { } else {
if(!fs.existsSync(path.join(__dirname, '..', 'games'))) { if(!fs.existsSync(path.join(__dirname, '..', 'games'))) {
console.error(`Did not find the following folder: ${path.join(__dirname, '..', 'games')}`) console.error(`Did not find the following folder: ${path.join(__dirname, '..', 'games')}`)
console.error('Did you already import any games?') console.error('Did you already import any games?')
return return ""
} }
} }
@ -100,7 +81,7 @@ function getGameDir(owner, repo) {
if(!fs.existsSync(game_dir)) { if(!fs.existsSync(game_dir)) {
console.error(`Game '${game_dir}' does not exist!`) console.error(`Game '${game_dir}' does not exist!`)
return return ""
} }
return game_dir; return game_dir;
@ -114,7 +95,7 @@ function startServerProcess(owner, repo) {
let serverProcess let serverProcess
if (isDevelopment) { if (isDevelopment) {
let args = ["--server", game_dir] let args = ["--server", game_dir]
serverProcess = cp.spawn("./gameserver", args, serverProcess = cp.spawn("./gameserver", args, // TODO: find gameserver inside the games
{ cwd: path.join(__dirname, "./.lake/build/bin/") }) { cwd: path.join(__dirname, "./.lake/build/bin/") })
} else { } else {
serverProcess = cp.spawn("./bubblewrap.sh", serverProcess = cp.spawn("./bubblewrap.sh",

@ -4,10 +4,10 @@
[{"url": "https://github.com/leanprover/std4.git", [{"url": "https://github.com/leanprover/std4.git",
"type": "git", "type": "git",
"subDir": null, "subDir": null,
"rev": "a652e09bd81bcb43ea132d64ecc16580b0c7fa50", "rev": "2e4a3586a8f16713f16b2d2b3af3d8e65f3af087",
"name": "std", "name": "std",
"manifestFile": "lake-manifest.json", "manifestFile": "lake-manifest.json",
"inputRev": "v4.3.0-rc2", "inputRev": "v4.3.0",
"inherited": false, "inherited": false,
"configFile": "lakefile.lean"}], "configFile": "lakefile.lean"}],
"name": "GameServer", "name": "GameServer",

@ -1 +1 @@
leanprover/lean4:v4.3.0-rc2 leanprover/lean4:v4.3.0

@ -41,7 +41,7 @@ export default defineConfig({
'/import': { '/import': {
target: 'http://localhost:8080', target: 'http://localhost:8080',
}, },
'/api': { '/data': {
target: 'http://localhost:8080', target: 'http://localhost:8080',
}, },
} }

Loading…
Cancel
Save