Merge branch 'main' of github.com:leanprover-community/lean4game
This commit is contained in:
@@ -31,6 +31,7 @@ function Level() {
|
||||
|
||||
const params = useParams();
|
||||
const levelId = parseInt(params.levelId)
|
||||
const worldId = params.worldId
|
||||
|
||||
const [tacticDocs, setTacticDocs] = useState([])
|
||||
const [lemmaDocs, setLemmaDocs] = useState([])
|
||||
@@ -105,7 +106,7 @@ function Level() {
|
||||
return () => { editor.dispose() }
|
||||
}, [])
|
||||
|
||||
const uri = `file:///level${levelId}`
|
||||
const uri = `file:///${worldId}/${levelId}`
|
||||
|
||||
// The next function will be called when the level changes
|
||||
useEffect(() => {
|
||||
@@ -122,7 +123,7 @@ function Level() {
|
||||
new AbbreviationRewriter(new AbbreviationProvider(), model, editor)
|
||||
|
||||
|
||||
leanClient.sendRequest("loadLevel", {world: "TestWorld", level: levelId}).then((res) => {
|
||||
leanClient.sendRequest("loadLevel", {world: worldId, level: levelId}).then((res) => {
|
||||
// setLevelTitle("Level " + res["index"] + ": " + res["title"])
|
||||
// setIndex(parseInt(res["index"]))
|
||||
setTacticDocs(res["tactics"])
|
||||
@@ -160,8 +161,8 @@ function Level() {
|
||||
</Grid>
|
||||
<Grid xs={4} className="info-panel">
|
||||
|
||||
<Button disabled={levelId <= 1} component={RouterLink} to={`/level/${levelId - 1}`} sx={{ ml: 3, mt: 2, mb: 2 }} disableFocusRipple>Previous Level</Button>
|
||||
<Button disabled={false} component={RouterLink} to={`/level/${levelId + 1}`} sx={{ ml: 3, mt: 2, mb: 2 }} disableFocusRipple>Next Level</Button>
|
||||
<Button disabled={levelId <= 1} component={RouterLink} to={`/world/${worldId}/level/${levelId - 1}`} sx={{ ml: 3, mt: 2, mb: 2 }} disableFocusRipple>Previous Level</Button>
|
||||
<Button disabled={false} component={RouterLink} to={`/world/${worldId}/level/${levelId + 1}`} sx={{ ml: 3, mt: 2, mb: 2 }} disableFocusRipple>Next Level</Button>
|
||||
|
||||
<div style={{display: expertInfoview ? 'block' : 'none' }} ref={infoviewRef} className="infoview vscode-light"></div>
|
||||
<div style={{display: expertInfoview ? 'none' : 'block' }}>
|
||||
|
||||
@@ -12,6 +12,8 @@ import { Paper, Box, Typography, Alert } from '@mui/material';
|
||||
|
||||
const errorRegex = /<stdin>:1:(?<col>[^:]*): (?<msg>.*)/;
|
||||
|
||||
// TODO: Dead variables (x✝) are not displayed correctly.
|
||||
|
||||
function Goal({ goal }) {
|
||||
const hasObject = typeof goal.objects === "object" && goal.objects.length > 0
|
||||
const hasAssumption = typeof goal.assumptions === "object" && goal.assumptions.length > 0
|
||||
|
||||
@@ -11,7 +11,7 @@ import cytoscape from 'cytoscape'
|
||||
import klay from 'cytoscape-klay';
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { fetchGame } from '../game/gameSlice'
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { Link as RouterLink, useNavigate } from 'react-router-dom';
|
||||
|
||||
|
||||
cytoscape.use( klay );
|
||||
@@ -24,6 +24,7 @@ import { useAppDispatch, useAppSelector } from '../hooks';
|
||||
|
||||
function Welcome() {
|
||||
const dispatch = useAppDispatch()
|
||||
const navigate = useNavigate();
|
||||
|
||||
const worldsRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -42,7 +43,7 @@ function Welcome() {
|
||||
})
|
||||
}
|
||||
const layout : any = {name: "klay", klay: {direction: "DOWN"}}
|
||||
cytoscape({ container: worldsRef.current!, elements, layout,
|
||||
const cy = cytoscape({ container: worldsRef.current!, elements, layout,
|
||||
style: [ // the stylesheet for the graph
|
||||
{
|
||||
selector: 'node',
|
||||
@@ -68,6 +69,10 @@ function Welcome() {
|
||||
autoungrabify: true,
|
||||
autounselectify: true,
|
||||
})
|
||||
|
||||
cy.on('click', 'node', function(evt){
|
||||
navigate(`/world/${this.id()}/level/1`);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => { dispatch(fetchGame); }, [])
|
||||
@@ -91,7 +96,7 @@ function Welcome() {
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box textAlign='center' sx={{ m: 5 }}>
|
||||
<Button component={RouterLink} to="/level/1" variant="contained">Start rescue mission</Button>
|
||||
<Button component={RouterLink} to="/world/TestWorld/level/1" variant="contained">Start rescue mission</Button>
|
||||
</Box>
|
||||
<div ref={worldsRef} style={{"width": "100%","height": "50em"}} />
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@ const router = createHashRouter([
|
||||
element: <Welcome />,
|
||||
},
|
||||
{
|
||||
path: "/level/:levelId",
|
||||
path: "/world/:worldId/level/:levelId",
|
||||
element: <Level />,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Lean
|
||||
|
||||
import GameServer.Utils
|
||||
import GameServer.EnvExtensions
|
||||
|
||||
open Lean Meta
|
||||
|
||||
@@ -58,18 +58,20 @@ structure PlainGoal where
|
||||
goals : Array GameGoal
|
||||
deriving FromJson, ToJson
|
||||
|
||||
#check String.split
|
||||
|
||||
-- TODO: Find a better way to pass on the file name?
|
||||
def levelIdFromFileName [Monad m] [MonadError m] [MonadEnv m] (fileName : String) : m Nat := do
|
||||
if fileName.startsWith "/level" then
|
||||
if let some id := (fileName.drop "/level".length).toNat? then
|
||||
return id
|
||||
def levelIdFromFileName [Monad m] [MonadError m] [MonadEnv m] (fileName : String) : m LevelId := do
|
||||
let fileParts := fileName.splitOn "/"
|
||||
if fileParts.length == 3 then
|
||||
if let some level := fileParts[2]!.toNat? then
|
||||
return {game := `TestGame, world := fileParts[1]!, level := level}
|
||||
throwError s!"Could not find level ID in file name: {fileName}"
|
||||
return 1
|
||||
|
||||
def getLevelByFileName [Monad m] [MonadError m] [MonadEnv m] (fileName : String) : m GameLevel := do
|
||||
let levelId ← levelIdFromFileName fileName
|
||||
-- TODO: make world and game configurable
|
||||
let some level ← getLevel? {game := `TestGame, world := `TestWorld, level := levelId}
|
||||
let some level ← getLevel? levelId
|
||||
| throwError "Level not found"
|
||||
return level
|
||||
|
||||
|
||||
@@ -1,289 +0,0 @@
|
||||
/-
|
||||
This is the Lean 4 game server. It offers a way to interact with Lean 4 which
|
||||
is completely distinct from running the command-line lean or the language server protocol.
|
||||
|
||||
It is based on lean-gym by Daniel Selsam.
|
||||
-/
|
||||
import Lean.Data.Json.Basic
|
||||
|
||||
import GameServer.Utils
|
||||
import GameServer.EnvExtensions
|
||||
import GameServer.Communication
|
||||
|
||||
open Lean Meta Elab Tactic Std
|
||||
|
||||
/-- Convert format to string without line breaks -/
|
||||
def Std.Format.oneline : Format → String
|
||||
| .nil => ""
|
||||
| .line => ""
|
||||
| .text s => s
|
||||
| .nest _ f => f.oneline
|
||||
| .append f g => f.oneline ++ g.oneline
|
||||
| .group f _ => f.oneline
|
||||
| .tag _ f => f.oneline
|
||||
|
||||
/-- Convert JSON to string without line breaks -/
|
||||
instance instToStringJsonOneLine : ToString Json := ⟨fun o => o.render.oneline⟩
|
||||
attribute [-instance] Lean.Json.instToStringJson
|
||||
|
||||
/-! ## GameGoal -/
|
||||
structure GameGoal where
|
||||
objects : List LocalDecl
|
||||
assumptions : List LocalDecl
|
||||
goal : String
|
||||
mvarid : MVarId
|
||||
|
||||
def Lean.MVarId.toGameGoal (goal : MVarId) : MetaM GameGoal := do
|
||||
match (← getMCtx).findDecl? goal with
|
||||
| none => throwError "unknown goal"
|
||||
| some mvarDecl => do
|
||||
-- toGameGoalAux below will sort local declarations from the context of goal into data and assumptions,
|
||||
-- discarding auxilliary declarations
|
||||
let rec toGameGoalAux : List (Option LocalDecl) → MetaM (List LocalDecl × List LocalDecl)
|
||||
| (some decl)::t => withLCtx mvarDecl.lctx mvarDecl.localInstances do
|
||||
let (o, a) ← toGameGoalAux t
|
||||
if decl.isAuxDecl then
|
||||
return (o, a)
|
||||
if (← inferType decl.type).isProp then
|
||||
return (o, decl::a)
|
||||
else
|
||||
return (decl::o, a)
|
||||
| none:: t => toGameGoalAux t
|
||||
| [] => return ([], [])
|
||||
withLCtx mvarDecl.lctx mvarDecl.localInstances do
|
||||
let (objects, assumptions) ← toGameGoalAux mvarDecl.lctx.decls.toList
|
||||
return {objects := objects, assumptions := assumptions, goal := toString (← Meta.ppExpr mvarDecl.type),
|
||||
mvarid := goal }
|
||||
|
||||
def GameGoal.toJson (gameGoal : GameGoal) : MetaM Json :=
|
||||
gameGoal.mvarid.withContext do
|
||||
return Json.mkObj [("objects", Lean.ToJson.toJson (← gameGoal.objects.mapM Lean.LocalDecl.toJson)),
|
||||
("assumptions", Lean.ToJson.toJson (← gameGoal.assumptions.mapM Lean.LocalDecl.toJson)),
|
||||
("goal", gameGoal.goal)]
|
||||
|
||||
|
||||
/-! ## Action -/
|
||||
|
||||
inductive Action where
|
||||
| info : Action
|
||||
| loadLevel : Nat → Action
|
||||
| runTactic : String → Action
|
||||
| undo : Action
|
||||
| restartGame : Action
|
||||
| restartLevel : Action
|
||||
| quit : Action
|
||||
| next : Action
|
||||
| prev : Action
|
||||
| invalid : String → Action -- Used for broken Json parsing
|
||||
deriving ToJson, FromJson, Repr
|
||||
|
||||
def Action.parse (s : String) : Action :=
|
||||
let e : Except String Action :=
|
||||
try
|
||||
return ← fromJson? (← Json.parse s)
|
||||
catch _ => return Action.invalid s
|
||||
match e with
|
||||
| Except.ok a => a
|
||||
| _ => Action.info
|
||||
|
||||
def Action.get : IO Action :=
|
||||
return Action.parse (← (← IO.getStdin).getLine)
|
||||
|
||||
|
||||
/-! ## LevelInfo -/
|
||||
|
||||
structure LevelInfo extends GameLevel where
|
||||
goals : List GameGoal := []
|
||||
errors : Array String := #[]
|
||||
message : String := ""
|
||||
|
||||
def LevelInfo.toJson (info : LevelInfo) : MetaM Json :=
|
||||
return Json.mkObj [("index", Lean.ToJson.toJson info.index),
|
||||
("title", Lean.ToJson.toJson info.title),
|
||||
("tactics", Lean.ToJson.toJson info.tactics),
|
||||
("lemmas", Lean.ToJson.toJson info.lemmas),
|
||||
("errors", Lean.ToJson.toJson info.errors),
|
||||
("goals", Lean.ToJson.toJson (← info.goals.mapM (·.toJson))),
|
||||
("message", info.message)]
|
||||
|
||||
namespace Server
|
||||
|
||||
/-! ## LevelM and Response -/
|
||||
|
||||
structure SavedState where
|
||||
tacticState : Tactic.SavedState
|
||||
message : String := ""
|
||||
|
||||
abbrev LevelM := ReaderT GameLevel StateRefT (Array SavedState) TermElabM
|
||||
|
||||
/-- Returns the most recent SavedState. -/
|
||||
def getState : LevelM SavedState := do
|
||||
let some state := (← get).back? | throwError "Couldn't find tactic state"
|
||||
return state
|
||||
|
||||
|
||||
structure Response : Type where
|
||||
goals : List GameGoal := []
|
||||
errors : Array String := #[]
|
||||
message : String := ""
|
||||
|
||||
def Response.toJson (resp : Response) : MetaM Json :=
|
||||
return Json.mkObj [("errors", Lean.ToJson.toJson resp.errors),
|
||||
("goals", Lean.ToJson.toJson (← resp.goals.mapM (·.toJson))),
|
||||
("message", resp.message)]
|
||||
|
||||
/-! ## Main running functions -/
|
||||
|
||||
/-- Dummy `Core.Context` value to be fed to `Lean.Core.CoreM.toIO` -/
|
||||
def coreCtx : Core.Context := {
|
||||
currNamespace := Name.anonymous,
|
||||
openDecls := [],
|
||||
fileName := "<Game>",
|
||||
fileMap := { source := "", positions := #[0], lines := #[1] } }
|
||||
|
||||
|
||||
structure RunTacticParams where
|
||||
tactic : String
|
||||
deriving FromJson
|
||||
|
||||
structure LoadLevelParams where
|
||||
number : Nat
|
||||
deriving FromJson
|
||||
|
||||
partial def runLevel (requestId : JsonRpc.RequestID) (env : Environment) (GameName : Name) (levels : HashMap Nat GameLevel) (idx : Nat) : IO Unit := do
|
||||
let levelName : Name := s!"Level{toString idx}"
|
||||
let termElabM : TermElabM Unit := do
|
||||
let some lvl := levels.find? idx | throwError s!"Cannot find level {idx}"
|
||||
let mvar ← mkFreshExprMVar (some lvl.goal) (kind := MetavarKind.synthetic)
|
||||
let (_, mvar) ← mvar.mvarId!.introNP lvl.intro_nb
|
||||
mvar.withContext do
|
||||
let state := #[{tacticState := { term := ← Term.saveState, tactic := { goals := [mvar] }}}]
|
||||
let levelM : LevelM Unit := do
|
||||
let resp := {← mkResponse with message := lvl.introduction}
|
||||
let levelInfo : LevelInfo :=
|
||||
{ index := lvl.index,
|
||||
title := lvl.title,
|
||||
tactics := lvl.tactics,
|
||||
lemmas := lvl.lemmas,
|
||||
errors := resp.errors,
|
||||
goals := resp.goals,
|
||||
message := resp.message }
|
||||
let hOut ← IO.getStdout
|
||||
hOut.writeGspResponse ⟨requestId, ← levelInfo.toJson⟩
|
||||
mainLoop
|
||||
levelM.run lvl |>.run' state
|
||||
let metaM : MetaM Unit := termElabM.run' (ctx := {})
|
||||
try
|
||||
discard <| metaM.run'.toIO coreCtx { env := env }
|
||||
catch
|
||||
| .userError s => output s!"Could not run level {idx}: {s}"
|
||||
| _ => output s!"Could not run level {idx}"
|
||||
|
||||
where
|
||||
|
||||
mkResponse (errors : Array String := #[]) : LevelM Response := do
|
||||
let savedState ← getState
|
||||
let goals := savedState.tacticState.tactic.goals
|
||||
return { goals := (← liftM $ goals.mapM Lean.MVarId.toGameGoal), message := savedState.message, errors := errors }
|
||||
|
||||
/-- Try to parse the given String as a single line tactic invocation. Update the LevelM state only
|
||||
if the tactic succeeds. Always return a Response object which contains information about the current
|
||||
state with a message and errors if any. -/
|
||||
runTactic (tacticString : String) : LevelM Response := do
|
||||
let lvl ← read
|
||||
let tacticNames := (lvl.tactics.map (·.name.toString)).toList
|
||||
let tacName := (tacticString.trim.splitOn " ").headD ""
|
||||
if not (tacticNames.contains tacName) then mkResponse #["Unrecognized tactic"] else
|
||||
let savedState ← getState
|
||||
match Parser.runParserCategory (← getEnv) `tactic tacticString "<stdin>" with
|
||||
| Except.error err => mkResponse #[err]
|
||||
| Except.ok stx => do
|
||||
savedState.tacticState.term.restore
|
||||
let mvarId : MVarId := savedState.tacticState.tactic.goals.head!
|
||||
try
|
||||
let unsolvedGoals ← Tactic.run mvarId do set savedState.tacticState.tactic
|
||||
evalTactic stx
|
||||
if (← getThe Core.State).messages.hasErrors then
|
||||
let messages := (← getThe Core.State).messages.getErrorMessages.toList.toArray
|
||||
mkResponse (← (messages.map Message.data).mapM fun md => md.toString)
|
||||
else
|
||||
let savedState : Tactic.SavedState := { term := (← Term.saveState), tactic := { goals := unsolvedGoals}}
|
||||
let mut message := ""
|
||||
match unsolvedGoals with
|
||||
| goal::_ => do
|
||||
let normalized_tgt ← normalizedRevertExpr goal
|
||||
for msg in lvl.messages do
|
||||
if ← isDefEq normalized_tgt msg.normalized_goal then
|
||||
message := msg.message
|
||||
break
|
||||
| [] => pure ()
|
||||
if unsolvedGoals matches [] then
|
||||
message := (← read).conclusion
|
||||
modify fun s => s.push {tacticState := savedState, message := message}
|
||||
mkResponse
|
||||
catch ex => mkResponse #[← ex.toMessageData.toString]
|
||||
|
||||
mainLoop : LevelM Unit := do
|
||||
let hIn ← IO.getStdin
|
||||
let hOut ← IO.getStdout
|
||||
let hLog ← IO.getStderr
|
||||
let m ← hIn.readGspMessage
|
||||
match m with
|
||||
| .request id "runTactic" params => do
|
||||
match fromJson? (toJson (params)) with
|
||||
| Except.ok (v : RunTacticParams) =>
|
||||
let resp ← runTactic v.tactic
|
||||
hOut.writeGspResponse ⟨id, ← resp.toJson⟩
|
||||
| Except.error inner =>
|
||||
hLog.putStr s!"Invalid params: {inner}"
|
||||
hLog.flush
|
||||
| .request id "loadLevel" (some params) =>
|
||||
match fromJson? (toJson (params)) with
|
||||
| Except.ok (v : LoadLevelParams) =>
|
||||
runLevel id env GameName levels v.number
|
||||
| Except.error inner =>
|
||||
hLog.putStr s!"Invalid params: {inner}"
|
||||
hLog.flush
|
||||
| .request id "undo" _ => do
|
||||
modify fun s => s.pop
|
||||
hOut.writeGspResponse ⟨id, ← (← mkResponse).toJson⟩
|
||||
| _ =>
|
||||
hLog.putStr s!"Invalid action: {toJson m}"
|
||||
hLog.flush
|
||||
mainLoop
|
||||
|
||||
open System Lean Std in
|
||||
partial def runGame (GameName : Name) : IO Unit := do
|
||||
let env ← importModules [{ module := `Init : Import }, { module := GameName : Import }] {} 0
|
||||
let termElabM : TermElabM Unit := do
|
||||
let levels := levelsExt.getState env
|
||||
let game := {← gameExt.get with nb_levels := levels.size }
|
||||
mainLoop env game levels
|
||||
let metaM : MetaM Unit := termElabM.run' (ctx := {})
|
||||
discard <| metaM.run'.toIO coreCtx { env := env }
|
||||
where
|
||||
mainLoop (env : Environment) (game : Game) (levels : HashMap Nat GameLevel): IO Unit := do
|
||||
let hIn ← IO.getStdin
|
||||
let hOut ← IO.getStdout
|
||||
let hLog ← IO.getStderr
|
||||
let m ← hIn.readGspMessage
|
||||
match m with
|
||||
| .request id "info" params =>
|
||||
hOut.writeGspResponse ⟨id, game⟩
|
||||
| .request id "loadLevel" (some params) =>
|
||||
match fromJson? (toJson (params)) with
|
||||
| Except.ok (v : LoadLevelParams) =>
|
||||
runLevel id env GameName levels v.number
|
||||
| Except.error inner =>
|
||||
hLog.putStr s!"Invalid params: {inner}"
|
||||
hLog.flush
|
||||
| .request id m _ =>
|
||||
hOut.writeGspResponseError
|
||||
⟨id, JsonRpc.ErrorCode.methodNotFound, s!"Method not found: {m}", none⟩
|
||||
| _ =>
|
||||
hLog.putStr s!"Invalid action: {toJson m}"
|
||||
hLog.flush
|
||||
|
||||
mainLoop env game levels
|
||||
|
||||
end Server
|
||||
@@ -1,14 +0,0 @@
|
||||
import Lean
|
||||
|
||||
open Lean
|
||||
|
||||
def Lean.MessageLog.getErrorMessages (log : MessageLog) : MessageLog :=
|
||||
{ msgs := log.msgs.filter (·.severity matches .error) }
|
||||
|
||||
/-- A version of `println!` that actually does its job by flushing stdout. -/
|
||||
def output {α : Type} [ToString α] (s : α) : IO Unit := do
|
||||
println! s
|
||||
IO.FS.Stream.flush (← IO.getStdout)
|
||||
|
||||
def Lean.LocalDecl.toJson (decl : LocalDecl) : MetaM Json :=
|
||||
return Lean.ToJson.toJson [toString decl.userName, toString (← Meta.ppExpr decl.type)]
|
||||
@@ -1,15 +0,0 @@
|
||||
import Lean.Data.JsonRpc
|
||||
|
||||
-- The worker implementation roughly follows `Lean/Server/FileWorker.lean`.
|
||||
|
||||
|
||||
#check IO.bindTask
|
||||
|
||||
#check Task
|
||||
#check Task.CancelToken
|
||||
|
||||
#check EIO.mapTask
|
||||
|
||||
#check maybeTee
|
||||
|
||||
#check IO.FS.Stream.writeMessage
|
||||
@@ -17,17 +17,3 @@ unsafe def main : List String → IO UInt32 := fun args => do
|
||||
|
||||
|
||||
-- TODO: Potentially it could be useful to pass in the `gameName` via the websocket connection
|
||||
|
||||
-- unsafe def main (args : List String) : IO UInt32 := do
|
||||
|
||||
|
||||
-- -- Check if required arguments are given by the user
|
||||
-- if args.length != 2 then
|
||||
-- throw (IO.userError $ "Expected two arguments:" ++
|
||||
-- "The name of the game module and the path to the game project.")
|
||||
-- let gameName := args[0]!
|
||||
-- let gameDir := args[1]!
|
||||
|
||||
|
||||
-- -- Run the game
|
||||
-- Server.runGame gameName
|
||||
|
||||
Reference in New Issue
Block a user