initalize repo

Co-authored-by: Patrick Massot PatrickMassot@users.noreply.github.com
This commit is contained in:
Alexander Bentkamp
2022-10-17 15:12:26 +02:00
co-authored by Patrick Massot PatrickMassot@users.noreply.github.com
commit 7563730292
47 changed files with 25052 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
import React from 'react';
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import { Box, Typography, Grid } from '@mui/material';
function GoodBye({ message }) {
return (<Grid container
direction="row"
justifyContent="center"
alignItems="center">
<Grid item xs={12} sm={6}>
<Box sx={{ m: 3 }}>
<Typography variant="body1" component="div">{message}</Typography>
</Box>
</Grid>
</Grid>)
}
export default GoodBye
+62
View File
@@ -0,0 +1,62 @@
import React, { useEffect, useState } from 'react';
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import { Typography, Button, Paper, TextField, List, ListItem } from '@mui/material';
function InputZone({ index, history, messageOpen, setMessageOpen, completed, sendTactic, nbLevels, loadNextLevel, errors, lastTactic, undo, finishGame }) {
const [curInput, setCurInput] = useState("")
const inputRef = React.createRef()
const nextRef = React.createRef()
function handleCurInputChange(evt) { setCurInput(evt.target.value) }
function showPrevMessage() { setMessageOpen(true) }
useEffect(() => {
if (!messageOpen && !completed) inputRef.current.focus()
if (!messageOpen && completed && index < nbLevels) nextRef.current.focus()
if (!messageOpen && completed && index === nbLevels) finishGame()
}, [messageOpen, inputRef, nextRef, completed, finishGame, index, nbLevels])
async function submitForm(evt) {
evt.preventDefault(); // prevent app reloading on form submission
await sendTactic(curInput)
setCurInput("");
}
return (
<Paper sx={{
height: "100%", pt: 1, pl: 3, pr: 3, pb: 1,
justifyContent: "space-between", alignItems: "center", textAlign: "center"
}}
elevation={5}>
<Typography variant="h5">Invocation zone</Typography>
<List dense={true}>
{history.map((item, idx) => <ListItem key={idx}>{item}</ListItem>)}
</List>
<form onSubmit={submitForm}>
<TextField
id="outlined-input"
label="Invocation"
autoFocus={true}
inputRef={inputRef}
value={curInput}
onChange={handleCurInputChange}
/>
<br />
<Button type="submit" variant="contained" sx={{ mt: 2 }} disabled={errors.length > 0 || completed || curInput.trim() === ""} disableFocusRipple>Cast spell</Button>
<br />
<Button variant="text" onClick={undo} sx={{ ml: 3, mt: 2, mb: 2 }} disabled={lastTactic === "" ? true : false || completed}>Undo</Button>
{!messageOpen && <Button variant="text" onClick={showPrevMessage} sx={{ ml: 3, mt: 2, mb: 2 }}>See previous message</Button>}
<br />
{completed && index < nbLevels && <Button variant="contained" ref={nextRef} onClick={loadNextLevel} sx={{ ml: 3, mt: 2, mb: 2 }} disableFocusRipple>Go to Next Level</Button>}
</form>
</Paper>)
}
export default InputZone
+82
View File
@@ -0,0 +1,82 @@
import React, { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import { MathJax } from "better-react-mathjax";
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import { Paper, Box, Typography, Accordion, AccordionSummary, AccordionDetails, Tabs, Tab } from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
function TacticDoc(props) {
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>{props.tactic.name}</Typography>
</AccordionSummary>
<AccordionDetails>
<MathJax>
<ReactMarkdown>{props.tactic.content}</ReactMarkdown>
</MathJax>
</AccordionDetails>
</Accordion>)
}
function LemmaDoc({ lemma }) {
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>{lemma.userName}</Typography>
</AccordionSummary>
<AccordionDetails>
<MathJax>
<ReactMarkdown>{lemma.content}</ReactMarkdown>
</MathJax>
</AccordionDetails>
</Accordion>)
}
function LemmaDocs({ lemmas }) {
const [categories, setCategories] = useState(new Map())
const [curCategory, setCurCategory] = useState("")
useEffect(() => {
const cats = new Map()
lemmas.forEach(function (item) {
const category = item.category
cats.set(category, (cats.get(category) || []).concat([item]))
});
setCategories(cats)
setCurCategory(cats.keys().next().value)
}, [lemmas]);
return (
<Paper sx={{ px: 2, py: 1, mt: 2 }}>
<Typography variant="h5">Inventory</Typography>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs
value={curCategory}
aria-label="Categories" variant="scrollable" scrollButtons="auto">
{(Array.from(categories)).map(([category, _]) => <Tab value={category} label={category} key={category} wrapped />)}
</Tabs>
</Box>
{curCategory && categories.get(curCategory).map((lemma) => <LemmaDoc lemma={lemma} key={lemma.name} />)}
</Paper>
)
}
function LeftPanel({ spells, inventory }) {
return (
<Box>
{spells.length > 0 &&
<Paper sx={{ px: 2, py: 1 }}>
<Typography variant="h5" sx={{ mb: 2 }}>Spell book</Typography>
{spells.map((spell) => <TacticDoc key={spell.name} tactic={spell} />)}
</Paper>}
{inventory.length > 0 && <LemmaDocs lemmas={inventory} />}
</Box>)
}
export default LeftPanel;
+114
View File
@@ -0,0 +1,114 @@
import React, { useEffect, useState } from 'react';
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import Grid from '@mui/material/Unstable_Grid2';
import LeftPanel from './LeftPanel';
import InputZone from './InputZone';
import Message from './Message';
import TacticState from './TacticState';
function Level({ sendJsonMessage, lastMessage, lastJsonMessage, nbLevels, level, setCurLevel, setLevelTitle, setFinished }) {
const [index, setIndex] = useState(level) // Level number
const [tacticDocs, setTacticDocs] = useState([])
const [lemmaDocs, setLemmaDocs] = useState([])
const [leanData, setLeanData] = useState({})
const [history, setHistory] = useState([])
const [lastTactic, setLastTactic] = useState("")
const [errors, setErrors] = useState([])
const [message, setMessage] = useState("")
const [messageOpen, setMessageOpen] = useState(false)
const [completed, setCompleted] = useState(false)
// The next function will be called when the level changes
useEffect(() => {
sendJsonMessage({ "loadLevel": level });
}, [level, sendJsonMessage])
// The next function will be called when a message arrives or the level title changes
useEffect(() => {
console.log(lastMessage)
console.log(lastJsonMessage)
if ("nb_levels" in lastJsonMessage) { return } // this is an old message from starting the game
const data = lastJsonMessage;
if ("title" in data) { // This is the level metadata coming in
setLevelTitle("Level " + data["index"] + ": " + data["title"])
setIndex(parseInt(data["index"]))
setTacticDocs(data["tactics"])
setLemmaDocs(data["lemmas"])
}
if (data["message"] !== "" && data.errors.length === 0) {
setMessage(data["message"])
setMessageOpen(true)
}
setLeanData(data);
setErrors(data.errors);
if (data.goals.length === 0 && data.errors.length === 0) {
setCompleted(true)
}
}, [lastJsonMessage, lastMessage, setLevelTitle])
function sendTactic(input) {
sendJsonMessage({ "runTactic": input });
setLastTactic(input);
setHistory(history.concat([input]));
}
function undo() {
if (errors.length === 0) {
sendJsonMessage('undo');
}
if (history.length > 1) {
setLastTactic(history[history.length - 1]);
} else {
setLastTactic("");
};
setErrors([]);
setHistory(history.slice(0, -1));
}
function loadNextLevel() {
setCompleted(false)
setHistory([])
setCurLevel(index + 1)
}
function closeMessage() {
setMessageOpen(false)
}
function finishGame() {
setLevelTitle("")
setFinished(true)
}
return (
<Grid container sx={{ mt: 3, ml: 1, mr: 1 }} columnSpacing={{ xs: 1, sm: 2, md: 3 }}>
<Grid xs={4}>
<LeftPanel spells={tacticDocs} inventory={lemmaDocs} />
</Grid>
<Grid xs={4}>
<InputZone index={index} history={history} messageOpen={messageOpen} setMessageOpen={setMessageOpen} completed={completed} sendTactic={sendTactic} nbLevels={nbLevels} loadNextLevel={loadNextLevel}
errors={errors} lastTactic={lastTactic} undo={undo} finishGame={finishGame} />
<Message isOpen={messageOpen} content={message} close={closeMessage} />
</Grid>
<Grid xs={4}>
<TacticState goals={leanData.goals} errors={errors} lastTactic={lastTactic} completed={completed} />
</Grid>
</Grid>
)
}
export default Level
+29
View File
@@ -0,0 +1,29 @@
import React from 'react';
import ReactMarkdown from 'react-markdown';
import { MathJax } from "better-react-mathjax";
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import { Button, Dialog, DialogContent, DialogContentText, DialogActions } from '@mui/material';
function Message({ isOpen, content, close }) {
return (
<Dialog open={isOpen} onClose={close}>
<DialogContent>
<DialogContentText id="alert-dialog-description">
<MathJax><ReactMarkdown>{content}</ReactMarkdown></MathJax>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={close} autoFocus={true} disableFocusRipple>
Ok
</Button>
</DialogActions>
</Dialog>
)
}
export default Message
+69
View File
@@ -0,0 +1,69 @@
import React from 'react';
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import { Paper, Box, Typography } from '@mui/material';
const errorRegex = /<stdin>:1:(?<col>[^:]*): (?<msg>.*)/;
function Goal({ goal }) {
const hasObject = typeof goal.objects === "object" && goal.objects.length > 0
const hasAssumption = typeof goal.assumptions === "object" && goal.assumptions.length > 0
return (
<Box sx={{ pl: 2 }}>
{hasObject && <Box><Typography variant="h7">Objects</Typography>
<List>
{goal.objects.map((item) =>
<ListItem key={item[0]}>
<Typography color="primary" sx={{ mr: 1 }}>{item[0]}</Typography> :
<Typography color="secondary" sx={{ ml: 1 }}>{item[1]}</Typography>
</ListItem>)}
</List></Box>}
{hasAssumption && <Box><Typography variant="h7">Assumptions</Typography>
<List>
{goal.assumptions.map((item) => <ListItem key={item}><Typography color="primary" sx={{ mr: 1 }}>{item[0]}</Typography> :
<Typography color="secondary" sx={{ ml: 1 }}>{item[1]}</Typography></ListItem>)}
</List></Box>}
<Typography variant="h7">Prove:</Typography>
<Typography color="primary" sx={{ ml: 2 }}>{goal.goal}</Typography>
</Box>)
}
function TacticState({ goals, errors, lastTactic, completed }) {
const hasError = typeof errors === "object" && errors.length > 0
const hasGoal = typeof goals === "object" && goals.length > 0
const hasManyGoal = hasGoal && goals.length > 1
var col = ""
var msg = ""
if (hasError) {
const m = errors[0].match(errorRegex)
if (m) {
col = `Column ${m.groups.col}: `
msg = m.groups.msg
} else {
msg = errors[0]
if (msg === "Unrecognized tactic") { msg = "Unknown spell!" }
}
}
return (
<Box sx={{ height: "100%" }}>
{hasGoal && <Paper sx={{ pt: 1, pl: 2, pr: 3, pb: 1, height: "100%" }}><Typography variant="h5">Current goal</Typography> <Goal goal={goals[0]} /></Paper>}
{completed && <Typography variant="h6">Level completed ! 🎉</Typography>}
{hasError && <Paper sx={{ pt: 1, pl: 2, pr: 3, pb: 1, height: "100%" }}><Typography variant="h5" color="error">Spell invocation failed</Typography>
<Typography sx={{ my: 1 }}>{lastTactic}</Typography>
<Typography component="pre" sx={{ my: 1 }}>{col}{msg}</Typography>
<Typography>Use the undo button to go back to a sane state.</Typography>
</Paper>}
{hasManyGoal && <Paper sx={{ pt: 1, pl: 2, pr: 3, pb: 1, mt: 1 }}>
<Typography variant="h6" sx={{ mb: 2 }}>Other goals</Typography>
{goals.slice(1).map((goal, index) => <Paper><Goal key={index} goal={goal} /></Paper>)}
</Paper>}
</Box>
)
}
export default TacticState
+52
View File
@@ -0,0 +1,52 @@
import React, { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import { MathJax } from "better-react-mathjax";
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import { Box, Typography, Button, CircularProgress, Grid } from '@mui/material';
function Welcome({ sendJsonMessage, lastJsonMessage, setNbLevels, setTitle, startGame, setConclusion }) {
const [leanData, setLeanData] = useState({})
// Will run at the very beginning
useEffect(() => {
sendJsonMessage('info')
}, [sendJsonMessage])
// Will run when a Json message arrives
useEffect(() => {
if (lastJsonMessage != null && lastJsonMessage.hasOwnProperty("nb_levels")) {
setLeanData(lastJsonMessage)
setNbLevels(lastJsonMessage.nb_levels)
setTitle(lastJsonMessage.title)
document.title = lastJsonMessage.title
setConclusion(lastJsonMessage.conclusion)
}
}, [lastJsonMessage, setNbLevels, setTitle, setConclusion])
let content
if ("introduction" in leanData) {
content = (<Box sx={{ m: 3 }}>
<Typography variant="body1" component="div">
<MathJax>
<ReactMarkdown>{leanData["introduction"]}</ReactMarkdown>
</MathJax>
</Typography>
<Box textAlign='center' sx={{ m: 5 }}>
<Button onClick={startGame} variant="contained">Start rescue mission</Button>
</Box>
</Box>)
} else {
content = <Box display="flex" alignItems="center" justifyContent="center" sx={{ height: "calc(100vh - 64px)" }}><CircularProgress /></Box>
}
return <Grid container direction="row" justifyContent="center" alignItems="center">
<Grid item xs={12} sm={6}>{content}</Grid>
</Grid>
}
export default Welcome