summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElizabeth Hunt <elizabeth@simponic.xyz>2025-03-01 12:36:47 -0700
committerElizabeth Hunt <elizabeth@simponic.xyz>2025-03-01 12:36:47 -0700
commit8dacee8f73633131fd68935c1e2493dc4beec837 (patch)
treefc9adf76fce4761b01208ba2f44e72a6838244aa
parentd903bd9a13e790cf42c84c3dc59bf89ffeae1d80 (diff)
downloadthe-abstraction-engine-8dacee8f73633131fd68935c1e2493dc4beec837.tar.gz
the-abstraction-engine-8dacee8f73633131fd68935c1e2493dc4beec837.zip
updates
-rw-r--r--src/App.tsx3
-rw-r--r--src/engine/TheAbstractionEngine.ts2
-rw-r--r--src/engine/components/ComponentNames.ts1
-rw-r--r--src/engine/components/Control.ts6
-rw-r--r--src/engine/components/LambdaTerm.ts2
-rw-r--r--src/engine/components/Modal.ts10
-rw-r--r--src/engine/components/index.ts1
-rw-r--r--src/engine/config/sounds.ts4
-rw-r--r--src/engine/entities/FunctionApplication.ts108
-rw-r--r--src/engine/entities/FunctionBox.ts99
-rw-r--r--src/engine/entities/LambdaFactory.ts239
-rw-r--r--src/engine/entities/Sign.ts59
-rw-r--r--src/engine/systems/Modal.ts93
-rw-r--r--src/engine/systems/SystemNames.ts1
-rw-r--r--src/engine/systems/index.ts1
-rw-r--r--src/engine/utils/CodeEditor.ts200
-rw-r--r--src/engine/utils/Modal.ts45
-rw-r--r--src/engine/utils/index.ts4
-rw-r--r--src/engine/utils/modal.ts41
-rw-r--r--src/engine/utils/tryWrap.ts7
-rw-r--r--src/interpreter/interpreter.ts2
21 files changed, 572 insertions, 356 deletions
diff --git a/src/App.tsx b/src/App.tsx
index 6bd26b9..b8582de 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -6,8 +6,7 @@ export const App = () => {
<div className="main">
<div id={Miscellaneous.MODAL_ID} className="modal">
<div id={Miscellaneous.MODAL_CONTENT_ID} className="modal-content">
- <span className="close">&times;</span>
- <p>Some text in the Modal..</p>
+ <hr></hr>
</div>
</div>
diff --git a/src/engine/TheAbstractionEngine.ts b/src/engine/TheAbstractionEngine.ts
index 29bc553..96d078f 100644
--- a/src/engine/TheAbstractionEngine.ts
+++ b/src/engine/TheAbstractionEngine.ts
@@ -11,6 +11,7 @@ import {
Life,
Music,
Level,
+ Modal,
} from "./systems";
export class TheAbstractionEngine {
@@ -33,6 +34,7 @@ export class TheAbstractionEngine {
const facingDirectionSystem = new FacingDirection(inputSystem);
[
+ new Modal(),
new Level(LevelNames.LevelSelection),
inputSystem,
facingDirectionSystem,
diff --git a/src/engine/components/ComponentNames.ts b/src/engine/components/ComponentNames.ts
index dd50fb3..241fe3f 100644
--- a/src/engine/components/ComponentNames.ts
+++ b/src/engine/components/ComponentNames.ts
@@ -12,4 +12,5 @@ export namespace ComponentNames {
export const Text = "Text";
export const LambdaTerm = "LambdaTerm";
export const Life = "Life";
+ export const Modal = "Modal";
}
diff --git a/src/engine/components/Control.ts b/src/engine/components/Control.ts
index 48983b2..9196e3b 100644
--- a/src/engine/components/Control.ts
+++ b/src/engine/components/Control.ts
@@ -1,11 +1,7 @@
import { Component, ComponentNames } from ".";
export class Control extends Component {
- public isControllable: boolean = true;
-
- constructor(isControllable = true) {
+ constructor(public isControllable = true) {
super(ComponentNames.Control);
-
- this.isControllable = isControllable;
}
}
diff --git a/src/engine/components/LambdaTerm.ts b/src/engine/components/LambdaTerm.ts
index 63e9889..5ac6215 100644
--- a/src/engine/components/LambdaTerm.ts
+++ b/src/engine/components/LambdaTerm.ts
@@ -1,7 +1,9 @@
import { Component, ComponentNames } from ".";
+import { DebrujinifiedLambdaTerm } from "../../interpreter";
export class LambdaTerm extends Component {
public code: string;
+ public last: null | { data?: DebrujinifiedLambdaTerm; error?: any } = null;
constructor(code: string) {
super(ComponentNames.LambdaTerm);
diff --git a/src/engine/components/Modal.ts b/src/engine/components/Modal.ts
new file mode 100644
index 0000000..9b4031f
--- /dev/null
+++ b/src/engine/components/Modal.ts
@@ -0,0 +1,10 @@
+import { Component, ComponentNames } from ".";
+import { ModalInitState } from "../systems";
+
+export class Modal extends Component {
+ constructor(
+ public initState: ModalInitState
+ ) {
+ super(ComponentNames.Modal);
+ }
+}
diff --git a/src/engine/components/index.ts b/src/engine/components/index.ts
index 023e73d..d3b2fd0 100644
--- a/src/engine/components/index.ts
+++ b/src/engine/components/index.ts
@@ -13,3 +13,4 @@ export * from "./GridSpawn";
export * from "./Text";
export * from "./LambdaTerm";
export * from "./Life";
+export * from "./Modal";
diff --git a/src/engine/config/sounds.ts b/src/engine/config/sounds.ts
index 5900ef0..46870db 100644
--- a/src/engine/config/sounds.ts
+++ b/src/engine/config/sounds.ts
@@ -19,7 +19,7 @@ export const LambdaTransformSound: SoundSpec = {
volume: 0.3,
};
-export const LambdaSave: SoundSpec = {
+export const EditorSave: SoundSpec = {
name: "lambdaSave",
url: "/assets/sound/lambda_save.wav",
volume: 0.3,
@@ -83,7 +83,7 @@ export const Music: SoundSpec = {
export const SOUND_SPECS: SoundSpec[] = [
MovingSound,
LambdaTransformSound,
- LambdaSave,
+ EditorSave,
Failure,
ModalOpen,
ModalClose,
diff --git a/src/engine/entities/FunctionApplication.ts b/src/engine/entities/FunctionApplication.ts
index a266941..f15fcb9 100644
--- a/src/engine/entities/FunctionApplication.ts
+++ b/src/engine/entities/FunctionApplication.ts
@@ -1,17 +1,13 @@
-import {
- Entity,
- EntityNames,
- FunctionBox,
- Key,
- Particles,
- makeLambdaTermHighlightComponent,
-} from ".";
+import { Entity, EntityNames, FunctionBox, Key, Particles } from ".";
import {
BoundingBox,
Colliding,
ComponentNames,
Grid,
+ Highlight,
+ Interactable,
LambdaTerm,
+ Modal,
Sprite,
} from "../components";
import {
@@ -26,9 +22,9 @@ import {
import { Coord2D, Direction } from "../interfaces";
import { Game } from "..";
import { Grid as GridSystem, SystemNames } from "../systems";
-import { colors } from "../utils";
+import { colors, tryWrap } from "../utils";
import {
- DebrujinifiedLambdaTerm,
+ InvalidLambdaTermError,
SymbolTable,
emitNamed,
interpret,
@@ -62,8 +58,8 @@ export class FunctionApplication extends Entity {
y: 0,
},
dimension,
- 0,
- ),
+ 0
+ )
);
this.addComponent(new Grid(gridPosition));
@@ -76,13 +72,48 @@ export class FunctionApplication extends Entity {
{ x: 0, y: 0 },
dimension,
FunctionApplication.spriteSpec.msPerFrame,
- FunctionApplication.spriteSpec.frames,
- ),
+ FunctionApplication.spriteSpec.frames
+ )
);
this.addComponent(new Colliding(this.handleCollision.bind(this)));
- this.addComponent(makeLambdaTermHighlightComponent(this));
+ this.addComponent(
+ new Highlight(this.onHighlight.bind(this), this.onUnhighlight.bind(this))
+ );
+ }
+
+ private onHighlight(_direction: Direction) {
+ this.addComponent(new Interactable(this.interaction.bind(this)));
+ }
+
+ private interaction() {
+ const codeConsumer = (_code: string) => {
+ this.removeComponent(ComponentNames.Modal);
+ return { consumed: true };
+ };
+ const { last, code } = this.getComponent<LambdaTerm>(
+ ComponentNames.LambdaTerm
+ );
+ this.addComponent(
+ new Modal({
+ type: "CODE_EDITOR",
+ codeInit: {
+ code,
+ codeConsumer,
+ readonly: true,
+ result: {
+ error: last?.error && `Error: ${last.error.message}`,
+ data: last?.data && `Last Result: ${emitNamed(last.data)}`,
+ },
+ },
+ })
+ );
+ }
+
+ private onUnhighlight() {
+ this.removeComponent(ComponentNames.Modal);
+ this.removeComponent(ComponentNames.Interactable);
}
public handleCollision(game: Game, entity: Entity) {
@@ -100,7 +131,7 @@ export class FunctionApplication extends Entity {
const gridSystem = game.getSystem<GridSystem>(SystemNames.Grid);
const fail = () => {
entityGrid.movingDirection = gridSystem.oppositeDirection(
- entityGrid.previousDirection,
+ entityGrid.previousDirection
);
entity.addComponent(entityGrid);
@@ -109,18 +140,17 @@ export class FunctionApplication extends Entity {
};
const applicationTerm = this.getComponent<LambdaTerm>(
- ComponentNames.LambdaTerm,
+ ComponentNames.LambdaTerm
);
const functionTerm = entity.getComponent<LambdaTerm>(
- ComponentNames.LambdaTerm,
+ ComponentNames.LambdaTerm
);
const newCode = applicationTerm.code.replace("_INPUT", functionTerm.code);
- let result: DebrujinifiedLambdaTerm | null = null;
- try {
- result = interpret(newCode, this.symbolTable, true);
- } catch (e) {
- console.error(e);
+ const result = tryWrap(() => interpret(newCode, this.symbolTable, true));
+ applicationTerm.last = result;
+ if (result.error || !result.data) {
+ console.error(result.error);
fail();
return;
}
@@ -128,29 +158,34 @@ export class FunctionApplication extends Entity {
const { dimension } = gridSystem;
const nextPosition = gridSystem.getNewGridPosition(
grid.gridPosition,
- entityGrid.previousDirection,
+ entityGrid.previousDirection
);
let applicationResultingEntity: Entity | null = null; // this should be its own function
- if ("abstraction" in result) {
- const code = emitNamed(result);
-
+ const { data } = result;
+ if ("application" in data) {
+ // if we get an application that means we didn't interpret correctly.
+ // this should "not" happen and should be fatal.
+ throw new InvalidLambdaTermError(
+ "produced term should not be an application"
+ );
+ }
+ if ("abstraction" in data) {
+ const code = emitNamed(data);
applicationResultingEntity = new FunctionBox(grid.gridPosition, code);
- } else if ("name" in result) {
- const { name } = result;
+ }
+ if ("name" in data) {
+ const { name } = data;
const entityFactory = APPLICATION_RESULTS[name];
if (entityFactory) {
game.addEntity(entityFactory(nextPosition));
}
- } else {
- fail();
- return;
}
game.removeEntity(entity.id);
if (applicationResultingEntity) {
const grid = applicationResultingEntity.getComponent<Grid>(
- ComponentNames.Grid,
+ ComponentNames.Grid
);
grid.movingDirection = entityGrid.previousDirection;
applicationResultingEntity.addComponent(grid);
@@ -158,7 +193,7 @@ export class FunctionApplication extends Entity {
game.addEntity(applicationResultingEntity);
}
- this.playTransformSound();
+ SOUNDS.get(LambdaTransformSound.name)!.play();
const particles = new Particles({
center: gridSystem.gridToScreenPosition(nextPosition),
spawnerDimensions: {
@@ -183,9 +218,4 @@ export class FunctionApplication extends Entity {
});
game.addEntity(particles);
}
-
- private playTransformSound() {
- const audio = SOUNDS.get(LambdaTransformSound.name)!;
- audio.play();
- }
}
diff --git a/src/engine/entities/FunctionBox.ts b/src/engine/entities/FunctionBox.ts
index 4878c98..9cba029 100644
--- a/src/engine/entities/FunctionBox.ts
+++ b/src/engine/entities/FunctionBox.ts
@@ -1,13 +1,4 @@
-import {
- IMAGES,
- Miscellaneous,
- ModalClose,
- ModalOpen,
- SOUNDS,
- SPRITE_SPECS,
- SpriteSpec,
- Sprites,
-} from "../config";
+import { IMAGES, SPRITE_SPECS, SpriteSpec, Sprites } from "../config";
import { Entity, EntityNames } from ".";
import {
BoundingBox,
@@ -16,18 +7,21 @@ import {
Highlight,
Interactable,
LambdaTerm,
+ Modal,
Pushable,
Sprite,
} from "../components";
import { Coord2D } from "../interfaces";
-import { openModal, closeModal } from "../utils";
export class FunctionBox extends Entity {
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
- Sprites.FUNCTION_BOX,
+ Sprites.FUNCTION_BOX
) as SpriteSpec;
- constructor(gridPosition: Coord2D, code: string) {
+ constructor(
+ gridPosition: Coord2D,
+ private readonly code: string
+ ) {
super(EntityNames.FunctionBox);
this.addComponent(
@@ -40,8 +34,8 @@ export class FunctionBox extends Entity {
width: FunctionBox.spriteSpec.width,
height: FunctionBox.spriteSpec.height,
},
- 0,
- ),
+ 0
+ )
);
this.addComponent(new Pushable());
@@ -57,56 +51,43 @@ export class FunctionBox extends Entity {
height: FunctionBox.spriteSpec.height,
},
FunctionBox.spriteSpec.msPerFrame,
- FunctionBox.spriteSpec.frames,
- ),
+ FunctionBox.spriteSpec.frames
+ )
);
this.addComponent(new LambdaTerm(code));
- this.addComponent(makeLambdaTermHighlightComponent(this));
+ this.addComponent(
+ new Highlight(this.onHighlight.bind(this), this.onUnhighlight.bind(this))
+ );
}
-}
-
-export const makeLambdaTermHighlightComponent = (
- entity: Entity,
- text?: string,
-) => {
- const onUnhighlight = () => {
- closeModal();
- entity.removeComponent(ComponentNames.Interactable);
- };
-
- const onHighlight = () => {
- let modalOpen = false;
- const doModalClose = () => {
- SOUNDS.get(ModalClose.name)!.play();
- modalOpen = false;
- closeModal();
- };
-
- const interaction = () => {
- if (modalOpen) {
- doModalClose();
- return;
- }
- const code =
- text ??
- entity.getComponent<LambdaTerm>(ComponentNames.LambdaTerm)!.code;
- openModal(
- `<div style="text-align:center"><p>${code}</p> <br> <button id="close">Close</button></div>`,
- );
- modalOpen = true;
- SOUNDS.get(ModalOpen.name)!.play();
-
- document.getElementById("close")!.addEventListener("click", () => {
- doModalClose();
- document.getElementById(Miscellaneous.CANVAS_ID)!.focus();
- });
+ private interaction() {
+ const codeConsumer = (_code: string) => {
+ this.removeComponent(ComponentNames.Modal);
+ return { consumed: true };
};
+ const { last } = this.getComponent<LambdaTerm>(ComponentNames.LambdaTerm);
+ this.addComponent(
+ new Modal({
+ type: "CODE_EDITOR",
+ codeInit: {
+ code: this.code,
+ codeConsumer,
+ readonly: true,
+ result: {
+ error: last?.error && `Error: ${last.error.message}`,
+ },
+ },
+ })
+ );
+ }
- entity.addComponent(new Interactable(interaction));
- };
+ public onHighlight() {
+ this.addComponent(new Interactable(this.interaction.bind(this)));
+ }
- return new Highlight(onHighlight, onUnhighlight);
-};
+ public onUnhighlight() {
+ this.removeComponent(ComponentNames.Interactable);
+ }
+}
diff --git a/src/engine/entities/LambdaFactory.ts b/src/engine/entities/LambdaFactory.ts
index 770c096..61a3b0a 100644
--- a/src/engine/entities/LambdaFactory.ts
+++ b/src/engine/entities/LambdaFactory.ts
@@ -1,10 +1,7 @@
import {
Failure,
IMAGES,
- LambdaSave,
LambdaTransformSound,
- Miscellaneous,
- ModalOpen,
SOUNDS,
SPRITE_SPECS,
SpriteSpec,
@@ -19,66 +16,19 @@ import {
GridSpawn,
Highlight,
Interactable,
+ Modal,
Sprite,
Text,
} from "../components";
import { Coord2D, Direction } from "../interfaces";
-import { openModal, closeModal } from "../utils";
-import {
- EditorState,
- StateField,
- StateEffect,
- Range,
- Extension,
-} from "@codemirror/state";
-import { Decoration, EditorView, keymap } from "@codemirror/view";
-import { defaultKeymap } from "@codemirror/commands";
-import rainbowBrackets from "rainbowbrackets";
-import { basicSetup } from "codemirror";
+import { tryWrap } from "../utils";
import { parse } from "../../interpreter";
-interface CodeEditorState {
- view: EditorView;
- editorElement: HTMLElement;
- syntaxError: HTMLElement;
- canvas: HTMLCanvasElement;
- closeButton: HTMLButtonElement;
-}
-
-const highlightEffect = StateEffect.define<Range<Decoration>[]>();
-const highlightExtension = StateField.define({
- create() {
- return Decoration.none;
- },
- update(value, transaction) {
- value = value.map(transaction.changes);
-
- for (let effect of transaction.effects) {
- if (effect.is(highlightEffect))
- value = value.update({ add: effect.value, sort: true });
- }
-
- return value;
- },
- provide: (f) => EditorView.decorations.from(f),
-});
-
-const FontSizeTheme = EditorView.theme({
- $: {
- fontSize: "16pt",
- },
-});
-const FontSizeThemeExtension: Extension = [FontSizeTheme];
-const syntaxErrorDecoration = Decoration.mark({
- class: "syntax-error",
-});
-
export class LambdaFactory extends Entity {
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
- Sprites.LAMBDA_FACTORY,
+ Sprites.LAMBDA_FACTORY
) as SpriteSpec;
- private codeEditorState: CodeEditorState | null;
private spawns: number;
private code: string;
@@ -87,7 +37,6 @@ export class LambdaFactory extends Entity {
this.spawns = spawns;
this.code = code;
- this.codeEditorState = null;
this.addComponent(
new BoundingBox(
@@ -99,8 +48,8 @@ export class LambdaFactory extends Entity {
width: LambdaFactory.spriteSpec.width,
height: LambdaFactory.spriteSpec.height,
},
- 0,
- ),
+ 0
+ )
);
this.addComponent(new Text(spawns.toString()));
@@ -110,8 +59,8 @@ export class LambdaFactory extends Entity {
this.addComponent(
new GridSpawn(
this.spawns,
- () => new FunctionBox({ x: 0, y: 0 }, this.code),
- ),
+ () => new FunctionBox({ x: 0, y: 0 }, this.code)
+ )
);
this.addComponent(new Grid(gridPosition));
@@ -125,166 +74,68 @@ export class LambdaFactory extends Entity {
height: LambdaFactory.spriteSpec.height,
},
LambdaFactory.spriteSpec.msPerFrame,
- LambdaFactory.spriteSpec.frames,
- ),
+ LambdaFactory.spriteSpec.frames
+ )
);
this.addComponent(
- new Highlight(
- (direction) => this.onHighlight(direction),
- () => this.onUnhighlight(),
- ),
+ new Highlight(this.onHighlight.bind(this), this.onUnhighlight.bind(this))
);
}
- private onUnhighlight() {
- closeModal();
- this.removeComponent(ComponentNames.Interactable);
- }
-
- private spawnNewLambda(direction: Direction) {
- try {
- parse(this.code);
- } catch (e: any) {
- SOUNDS.get(Failure.name)!.play();
- return;
+ private codeConsumer(code: string) {
+ const parsed = tryWrap(() => parse(code));
+ if (parsed.error) {
+ return { error: parsed.error };
}
-
- const spawner = this.getComponent<GridSpawn>(ComponentNames.GridSpawn);
- spawner.spawnEntity(direction);
-
- const textComponent = this.getComponent<Text>(ComponentNames.Text);
- textComponent.text = spawner.spawnsLeft.toString();
- this.addComponent(textComponent);
-
- SOUNDS.get(LambdaTransformSound.name)!.play();
- }
-
- private openCodeEditor() {
- const modalContent =
- "<div class='code'><div id='code'></div><br><p id='syntax-error' class='error'></p><button id='close-modal'>Save</button></div>";
- openModal(modalContent);
-
- const startState = EditorState.create({
- doc: this.code,
- extensions: [
- basicSetup,
- keymap.of(defaultKeymap),
- rainbowBrackets(),
- highlightExtension,
- FontSizeThemeExtension,
- ],
- });
-
- const codeBox = document.getElementById("code")!;
- const syntaxError = document.getElementById("syntax-error")!;
- const canvas = document.getElementById(
- Miscellaneous.CANVAS_ID,
- ) as HTMLCanvasElement;
- const closeButton = document.getElementById(
- "close-modal",
- ) as HTMLButtonElement;
- closeButton.addEventListener("click", () => this.saveAndCloseCodeEditor());
-
- const editorView = new EditorView({
- state: startState,
- parent: codeBox,
- });
- editorView.focus();
-
- this.codeEditorState = {
- view: editorView,
- editorElement: codeBox,
- syntaxError,
- canvas,
- closeButton,
- };
-
- SOUNDS.get(ModalOpen.name)!.play();
+ this.code = code;
+ this.removeComponent(ComponentNames.Modal);
+ return { consumed: true };
}
- private refreshCodeEditorText(text: string) {
- if (!this.codeEditorState) {
+ private onHighlight(direction: Direction) {
+ if (direction === Direction.LEFT || direction === Direction.RIGHT) {
+ this.addComponent(new Interactable(() => this.spawnNewLambda(direction)));
return;
}
- const { view } = this.codeEditorState;
- view.dispatch({
- changes: {
- from: 0,
- to: text.length,
- insert: "",
- },
- });
- view.dispatch({
- changes: {
- from: 0,
- to: 0,
- insert: text,
- },
- });
+ this.addComponent(new Interactable(this.interaction.bind(this)));
}
- private saveAndCloseCodeEditor() {
- if (!this.codeEditorState) {
+ private interaction() {
+ if (this.hasComponent(ComponentNames.Modal)) {
return;
}
-
- const { canvas, view, editorElement, syntaxError } = this.codeEditorState;
- const text = view.state.doc.toString();
- this.refreshCodeEditorText(text);
- syntaxError.innerText = "";
-
- try {
- parse(text);
- } catch (e: any) {
- if (!e.location) {
- return;
- }
- const {
- location: {
- start: { offset: start },
- end: { offset: end },
+ this.addComponent(
+ new Modal({
+ type: "CODE_EDITOR",
+ codeInit: {
+ code: this.code,
+ codeConsumer: this.codeConsumer.bind(this),
},
- } = e;
-
- view.dispatch({
- effects: highlightEffect.of([
- syntaxErrorDecoration.range(start === end ? start - 1 : start, end),
- ]),
- });
-
- syntaxError.innerText = e.message;
- SOUNDS.get(Failure.name)!.play();
- return;
- }
-
- this.code = text;
-
- view.destroy();
- editorElement.innerHTML = "";
- this.codeEditorState = null;
- closeModal();
+ })
+ );
+ }
- canvas.focus();
- SOUNDS.get(LambdaSave.name)!.play();
+ private onUnhighlight() {
+ this.removeComponent(ComponentNames.Modal);
+ this.removeComponent(ComponentNames.Interactable);
}
- private onHighlight(direction: Direction) {
- if (direction === Direction.LEFT || direction === Direction.RIGHT) {
- this.addComponent(new Interactable(() => this.spawnNewLambda(direction)));
+ private spawnNewLambda(direction: Direction) {
+ const parsed = tryWrap(() => parse(this.code));
+ if (parsed.error) {
+ SOUNDS.get(Failure.name)!.play();
return;
}
- const interaction = () => {
- if (this.codeEditorState) {
- this.saveAndCloseCodeEditor();
- return;
- }
+ const spawner = this.getComponent<GridSpawn>(ComponentNames.GridSpawn);
+ spawner.spawnEntity(direction);
- this.openCodeEditor();
- };
+ const textComponent = this.getComponent<Text>(ComponentNames.Text);
+ textComponent.text = spawner.spawnsLeft.toString();
+ this.addComponent(textComponent);
- this.addComponent(new Interactable(interaction));
+ SOUNDS.get(LambdaTransformSound.name)!.play();
}
}
diff --git a/src/engine/entities/Sign.ts b/src/engine/entities/Sign.ts
index 45f2986..c85ad40 100644
--- a/src/engine/entities/Sign.ts
+++ b/src/engine/entities/Sign.ts
@@ -1,18 +1,27 @@
-import { Entity, EntityNames, makeLambdaTermHighlightComponent } from ".";
-import { BoundingBox, Colliding, Grid, Sprite } from "../components";
+import { Entity, EntityNames } from ".";
+import {
+ BoundingBox,
+ Colliding,
+ ComponentNames,
+ Grid,
+ Highlight,
+ Interactable,
+ Modal,
+ Sprite,
+} from "../components";
import { IMAGES, SPRITE_SPECS, SpriteSpec, Sprites } from "../config";
import { Coord2D } from "../interfaces";
export class Sign extends Entity {
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
- Sprites.SIGN,
+ Sprites.SIGN
) as SpriteSpec;
- private text: string;
-
- constructor(text: string, gridPosition: Coord2D) {
+ constructor(
+ private readonly text: string,
+ gridPosition: Coord2D
+ ) {
super(EntityNames.Sign);
- this.text = text;
const dimension = {
width: Sign.spriteSpec.width,
@@ -25,8 +34,8 @@ export class Sign extends Entity {
{ x: 0, y: 0 },
dimension,
Sign.spriteSpec.msPerFrame,
- Sign.spriteSpec.frames,
- ),
+ Sign.spriteSpec.frames
+ )
);
this.addComponent(
@@ -36,14 +45,40 @@ export class Sign extends Entity {
y: 0,
},
dimension,
- 0,
- ),
+ 0
+ )
);
this.addComponent(new Grid(gridPosition));
this.addComponent(new Colliding());
- this.addComponent(makeLambdaTermHighlightComponent(this, this.text));
+ this.addComponent(
+ new Highlight(this.onHighlight.bind(this), this.onUnhighlight.bind(this))
+ );
+ }
+
+ private onHighlight() {
+ this.addComponent(new Interactable(this.interaction.bind(this)));
+ }
+
+ private onUnhighlight() {
+ this.removeComponent(ComponentNames.Modal);
+ this.removeComponent(ComponentNames.Interactable);
+ }
+
+ private interaction() {
+ if (this.hasComponent(ComponentNames.Modal)) {
+ this.removeComponent(ComponentNames.Modal);
+ return;
+ }
+ this.addComponent(
+ new Modal({
+ type: "CONTENT",
+ contentInit: {
+ content: `<p>${this.text}</p>`,
+ },
+ })
+ );
}
}
diff --git a/src/engine/systems/Modal.ts b/src/engine/systems/Modal.ts
new file mode 100644
index 0000000..7643a73
--- /dev/null
+++ b/src/engine/systems/Modal.ts
@@ -0,0 +1,93 @@
+import { Game } from "..";
+import { System, SystemNames } from ".";
+import { Miscellaneous, ModalClose, ModalOpen, SOUNDS } from "../config";
+import { ComponentNames, Modal as ModalComponent } from "../components";
+import { Entity } from "../entities";
+import {
+ CodeConsumer,
+ CodeEditorInstance,
+ CodeEditorSingleton,
+ ModalInstance,
+ ModalSingleton,
+} from "../utils";
+
+export interface ModalInitState {
+ type: "CONTENT" | "CODE_EDITOR";
+ contentInit?: {
+ content: string;
+ };
+ codeInit?: {
+ code: string;
+ codeConsumer: CodeConsumer;
+ readonly?: boolean;
+ result?: { data?: string; error?: string };
+ };
+}
+
+export class Modal extends System {
+ private openingEntity: null | Entity = null;
+ private modalInstance: ModalInstance = ModalSingleton;
+ private codeEditorInstance: CodeEditorInstance = CodeEditorSingleton;
+
+ constructor() {
+ super(SystemNames.Modal);
+ }
+
+ public update(_dt: number, game: Game) {
+ if (this.openingEntity) {
+ if (this.openingEntity.hasComponent(ComponentNames.Modal)) {
+ return;
+ }
+ this.closeCallback();
+ }
+
+ game.forEachEntityWithComponent(ComponentNames.Modal, (entity) => {
+ const modalComponent = entity.getComponent<ModalComponent>(
+ ComponentNames.Modal
+ );
+ if (this.openingEntity) {
+ return;
+ }
+
+ this.openingEntity = entity;
+ SOUNDS.get(ModalOpen.name)!.play();
+
+ if (modalComponent.initState.type === "CONTENT") {
+ const content = `
+ <div style="text-align:center">
+ <div>${modalComponent.initState.contentInit!.content}</div>
+ <br>
+ <button id="close">Close</button>
+ </div>`;
+ this.modalInstance.open(content);
+ document
+ .getElementById("close")
+ ?.addEventListener("click", this.closeCallback.bind(this));
+ }
+
+ if (modalComponent.initState.type === "CODE_EDITOR") {
+ this.codeEditorInstance.open(
+ modalComponent.initState.codeInit!.code,
+ (code) => {
+ const result =
+ modalComponent.initState.codeInit!.codeConsumer(code);
+ if (result.consumed) {
+ this.closeCallback();
+ }
+ return result;
+ },
+ !!modalComponent.initState.codeInit!.readonly,
+ modalComponent.initState.codeInit!.result,
+ );
+ }
+ });
+ }
+
+ private closeCallback() {
+ this.openingEntity = null;
+ this.modalInstance.vanish();
+ this.codeEditorInstance.close();
+ document.getElementById(Miscellaneous.CANVAS_ID)!.focus();
+ SOUNDS.get(ModalClose.name)!.play();
+ }
+}
diff --git a/src/engine/systems/SystemNames.ts b/src/engine/systems/SystemNames.ts
index 63f83a4..430ee48 100644
--- a/src/engine/systems/SystemNames.ts
+++ b/src/engine/systems/SystemNames.ts
@@ -8,4 +8,5 @@ export namespace SystemNames {
export const Life = "Life";
export const Music = "Music";
export const Level = "Level";
+ export const Modal = "Modal";
}
diff --git a/src/engine/systems/index.ts b/src/engine/systems/index.ts
index 29dbc7e..75b548d 100644
--- a/src/engine/systems/index.ts
+++ b/src/engine/systems/index.ts
@@ -9,3 +9,4 @@ export * from "./Collision";
export * from "./Life";
export * from "./Music";
export * from "./Level";
+export * from "./Modal"; \ No newline at end of file
diff --git a/src/engine/utils/CodeEditor.ts b/src/engine/utils/CodeEditor.ts
new file mode 100644
index 0000000..e13d1d4
--- /dev/null
+++ b/src/engine/utils/CodeEditor.ts
@@ -0,0 +1,200 @@
+import {
+ EditorState,
+ StateField,
+ StateEffect,
+ Range,
+ Extension,
+} from "@codemirror/state";
+import { Decoration, EditorView, keymap } from "@codemirror/view";
+import { defaultKeymap } from "@codemirror/commands";
+import rainbowBrackets from "rainbowbrackets";
+import { basicSetup } from "codemirror";
+import { ModalInstance, ModalSingleton } from ".";
+import { EditorSave, Failure, ModalOpen, SOUNDS } from "../config";
+
+export interface CodeEditorError extends Error {
+ location: {
+ start: { offset: number };
+ end: { offset: number };
+ };
+}
+
+export type CodeConsumer = (code: string) => {
+ consumed?: boolean;
+ error?: CodeEditorError;
+};
+
+interface CodeEditorState {
+ view: EditorView;
+ editorElement: HTMLElement;
+ errorElement: HTMLElement;
+ resultElement: HTMLElement;
+ closeButton: HTMLButtonElement;
+ codeConsumer: CodeConsumer;
+}
+
+export class CodeEditorInstance {
+ constructor(
+ private modalInstance: ModalInstance = ModalSingleton,
+ private codeEditorState: CodeEditorState | null = null
+ ) {}
+
+ public close() {
+ if (!this.codeEditorState) {
+ return;
+ }
+
+ this.codeEditorState.view.destroy();
+ this.codeEditorState = null;
+
+ SOUNDS.get(EditorSave.name)!.play();
+ }
+
+ public open(
+ initCode: string,
+ codeConsumer: CodeEditorState["codeConsumer"],
+ readonly: boolean = false,
+ initResult: { data?: string; error?: string } = {},
+ ) {
+ if (this.codeEditorState) {
+ throw new Error("code editor instance is already owned.");
+ }
+ const modalContent = `
+ <div class='code'>
+ <div id='code'></div>
+ <br>
+ <p id='error' class='error'></p>
+ <p id='result' class='result'></p>
+ <button id='close-modal'>Done</button>
+ </div>
+ `;
+ this.modalInstance.open(modalContent);
+
+ const startState = EditorState.create({
+ doc: initCode,
+ extensions: [
+ basicSetup,
+ keymap.of(defaultKeymap),
+ rainbowBrackets(),
+ highlightExtension,
+ FontSizeThemeExtension,
+ EditorState.readOnly.of(readonly),
+ ],
+ });
+
+ const codeBox = document.getElementById("code")!;
+ const errorElement = document.getElementById("error")!;
+ const resultElement = document.getElementById("result")!;
+
+ const closeButton = document.getElementById(
+ "close-modal"
+ ) as HTMLButtonElement;
+ closeButton.addEventListener("click", () => this.onSave());
+
+ const editorView = new EditorView({
+ state: startState,
+ parent: codeBox,
+ });
+ editorView.focus();
+
+ this.codeEditorState = {
+ view: editorView,
+ editorElement: codeBox,
+ errorElement,
+ resultElement,
+ closeButton,
+ codeConsumer,
+ };
+ this.setResult(initResult);
+ }
+
+ private refreshCodeEditorText(text: string) {
+ if (!this.codeEditorState) {
+ return;
+ }
+ const { view } = this.codeEditorState;
+
+ view.dispatch({
+ changes: {
+ from: 0,
+ to: text.length,
+ insert: "",
+ },
+ });
+ view.dispatch({
+ changes: {
+ from: 0,
+ to: 0,
+ insert: text,
+ },
+ });
+ }
+
+ private onSave() {
+ if (!this.codeEditorState) {
+ throw new Error();
+ }
+
+ const { view } = this.codeEditorState;
+ const code = view.state.doc.toString();
+ this.refreshCodeEditorText(code);
+
+ const valid = this.codeEditorState.codeConsumer(code);
+ if (!valid.error) {
+ return;
+ }
+
+ const {
+ location: {
+ start: { offset: start },
+ end: { offset: end },
+ },
+ message,
+ } = valid.error;
+
+ view.dispatch({
+ effects: highlightEffect.of([
+ syntaxErrorDecoration.range(start === end ? start - 1 : start, end),
+ ]),
+ });
+
+ this.setResult({ error: message });
+ SOUNDS.get(Failure.name)!.play();
+ }
+
+ private setResult(result: { data?: string; error?: string }) {
+ if (!this.codeEditorState) return;
+ this.codeEditorState.resultElement.innerText = result.data ?? "";
+ this.codeEditorState.errorElement.innerText = result.error ?? "";
+ }
+}
+
+const highlightEffect = StateEffect.define<Range<Decoration>[]>();
+const highlightExtension = StateField.define({
+ create() {
+ return Decoration.none;
+ },
+ update(value, transaction) {
+ value = value.map(transaction.changes);
+
+ for (let effect of transaction.effects) {
+ if (effect.is(highlightEffect))
+ value = value.update({ add: effect.value, sort: true });
+ }
+
+ return value;
+ },
+ provide: (f) => EditorView.decorations.from(f),
+});
+
+const FontSizeTheme = EditorView.theme({
+ $: {
+ fontSize: "14pt",
+ },
+});
+const FontSizeThemeExtension: Extension = [FontSizeTheme];
+const syntaxErrorDecoration = Decoration.mark({
+ class: "syntax-error",
+});
+
+export const CodeEditorSingleton = new CodeEditorInstance();
diff --git a/src/engine/utils/Modal.ts b/src/engine/utils/Modal.ts
new file mode 100644
index 0000000..d46cade
--- /dev/null
+++ b/src/engine/utils/Modal.ts
@@ -0,0 +1,45 @@
+import { Miscellaneous } from "../config";
+
+export class ModalInstance {
+ private modalOpen: boolean = false;
+ private elementId: string = Miscellaneous.MODAL_ID;
+ private contentId: string = Miscellaneous.MODAL_CONTENT_ID;
+
+ public open(content: string) {
+ const modal = document.getElementById(this.elementId);
+ const modalContent = document.getElementById(this.contentId);
+ if (!modal || this.modalOpen) {
+ return;
+ }
+
+ this.modalOpen = true;
+ modal.style.display = "flex";
+ modal.style.animation = "fadeIn 0.25s";
+
+ modalContent!.innerHTML = content;
+ modalContent!.style.animation = "scaleUp 0.25s";
+ }
+
+ public vanish(): Promise<void> {
+ const modal = document.getElementById(this.elementId);
+ const modalContent = document.getElementById(this.contentId);
+ return new Promise((res, _rej) => {
+ if (!(modal && this.modalOpen && modalContent)) {
+ res();
+ return;
+ }
+ modal.style.animation = "fadeOut 0.25s";
+ modalContent.style.animation = "scaleDown 0.25s";
+
+ setTimeout(() => {
+ modalContent.innerHTML = "";
+ modal.style.display = "none";
+
+ this.modalOpen = false;
+ res();
+ }, 200);
+ });
+ }
+}
+
+export const ModalSingleton = new ModalInstance(); \ No newline at end of file
diff --git a/src/engine/utils/index.ts b/src/engine/utils/index.ts
index 48b94b8..ffeef7a 100644
--- a/src/engine/utils/index.ts
+++ b/src/engine/utils/index.ts
@@ -1,6 +1,8 @@
export * from "./clamp";
export * from "./dotProduct";
export * from "./rotateVector";
-export * from "./modal";
export * from "./colors";
export * from "./random";
+export * from './tryWrap';
+export * from "./Modal";
+export * from "./CodeEditor"; \ No newline at end of file
diff --git a/src/engine/utils/modal.ts b/src/engine/utils/modal.ts
deleted file mode 100644
index a378821..0000000
--- a/src/engine/utils/modal.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Miscellaneous } from "../config";
-
-let modalOpen = false;
-
-export const openModal = (
- content: string,
- id = Miscellaneous.MODAL_ID,
- contentId = Miscellaneous.MODAL_CONTENT_ID,
-) => {
- const modal = document.getElementById(id);
- const modalContent = document.getElementById(contentId);
- if (modal && !modalOpen && modalContent) {
- modal.style.display = "flex";
- modal.style.animation = "fadeIn 0.25s";
-
- modalContent.innerHTML = content;
- modalContent.style.animation = "scaleUp 0.25s";
-
- modalOpen = true;
- }
-};
-
-export const closeModal = (
- id = Miscellaneous.MODAL_ID,
- contentId = Miscellaneous.MODAL_CONTENT_ID,
-) => {
- const modal = document.getElementById(id);
- const modalContent = document.getElementById(contentId);
-
- if (modal && modalOpen && modalContent) {
- modal.style.animation = "fadeOut 0.25s";
- modalContent.style.animation = "scaleDown 0.25s";
-
- setTimeout(() => {
- modalContent.innerHTML = "";
- modal.style.display = "none";
-
- modalOpen = false;
- }, 200);
- }
-};
diff --git a/src/engine/utils/tryWrap.ts b/src/engine/utils/tryWrap.ts
new file mode 100644
index 0000000..12e23ea
--- /dev/null
+++ b/src/engine/utils/tryWrap.ts
@@ -0,0 +1,7 @@
+export const tryWrap = <T>(supplier: () => T): { data?: T; error?: any } => {
+ try {
+ return { data: supplier() };
+ } catch (error) {
+ return { error: error as any };
+ }
+};
diff --git a/src/interpreter/interpreter.ts b/src/interpreter/interpreter.ts
index e90b8ce..65a848f 100644
--- a/src/interpreter/interpreter.ts
+++ b/src/interpreter/interpreter.ts
@@ -249,8 +249,8 @@ export const emitNamed = (term: DebrujinifiedLambdaTerm): string => {
export const interpret = (
term: string,
symbolTable = new SymbolTable(),
+ allowUnderscores = false, // in our world, underscores should be internal to the game.
maxDepth = 15,
- allowUnderscores = false
): DebrujinifiedLambdaTerm => {
const ast = parse(term, allowUnderscores);
const debrujined = debrujinify(ast, symbolTable);