summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElizabeth Hunt <elizabeth.hunt@simponic.xyz>2024-03-07 20:45:47 -0700
committerElizabeth Hunt <elizabeth.hunt@simponic.xyz>2024-03-07 20:45:47 -0700
commite6e29440563e33bb67e0ad51f9fb6c5c2c3fe809 (patch)
tree5deaee322ff1a039dc44a3cb52ecc48a671fda2a
parent823620b2a6ebb7ece619991e47a37ad46542b69f (diff)
downloadthe-abstraction-engine-e6e29440563e33bb67e0ad51f9fb6c5c2c3fe809.tar.gz
the-abstraction-engine-e6e29440563e33bb67e0ad51f9fb6c5c2c3fe809.zip
level one (applications prototype finished!)
-rw-r--r--public/assets/bubble.pngbin1467 -> 2499 bytes
-rw-r--r--public/assets/key.pngbin1086 -> 1624 bytes
-rw-r--r--public/assets/locked_door.pngbin1845 -> 2139 bytes
-rw-r--r--public/assets/sound/failure.wavbin0 -> 28064 bytes
-rw-r--r--public/assets/sound/keyopen.wavbin0 -> 39610 bytes
-rw-r--r--public/assets/sound/lambda_save.wavbin0 -> 12066 bytes
-rw-r--r--public/assets/sound/lambda_transform.wavbin0 -> 30836 bytes
-rw-r--r--public/assets/sound/modal_close.wavbin0 -> 22162 bytes
-rw-r--r--public/assets/sound/modal_open.wavbin0 -> 17092 bytes
-rw-r--r--public/assets/sound/move_1.wavbin0 -> 3656 bytes
-rw-r--r--public/assets/sound/move_2.wavbin0 -> 12524 bytes
-rw-r--r--public/assets/sound/move_3.wavbin0 -> 4136 bytes
-rw-r--r--public/assets/sound/move_4.wavbin0 -> 4312 bytes
-rw-r--r--public/assets/sound/music/credits.txt20
-rw-r--r--public/assets/sound/music/hope.mp3bin0 -> 13708261 bytes
-rw-r--r--public/assets/sound/music/jul.mp3bin0 -> 8308188 bytes
-rw-r--r--public/assets/sound/music/moonlight.mp3bin0 -> 10177651 bytes
-rw-r--r--public/assets/sound/music/reverie.mp3bin0 -> 8950768 bytes
-rw-r--r--src/App.tsx4
-rw-r--r--src/components/GameCanvas.tsx33
-rw-r--r--src/components/Title.tsx17
-rw-r--r--src/css/style.css18
-rw-r--r--src/engine/TheAbstractionEngine.ts8
-rw-r--r--src/engine/components/Grid.ts2
-rw-r--r--src/engine/config/assets.ts43
-rw-r--r--src/engine/config/index.ts1
-rw-r--r--src/engine/config/sounds.ts84
-rw-r--r--src/engine/config/sprites.ts10
-rw-r--r--src/engine/entities/FunctionApplication.ts180
-rw-r--r--src/engine/entities/FunctionBox.ts15
-rw-r--r--src/engine/entities/LambdaFactory.ts11
-rw-r--r--src/engine/entities/LockedDoor.ts16
-rw-r--r--src/engine/entities/Particles.ts8
-rw-r--r--src/engine/entities/Player.ts3
-rw-r--r--src/engine/systems/Collision.ts1
-rw-r--r--src/engine/systems/Grid.ts25
-rw-r--r--src/engine/systems/Input.ts51
-rw-r--r--src/engine/systems/Music.ts40
-rw-r--r--src/engine/systems/SystemNames.ts1
-rw-r--r--src/engine/systems/index.ts1
-rw-r--r--src/engine/utils/modal.ts2
-rw-r--r--src/interpreter/PeggyParser.js4
-rw-r--r--src/interpreter/interpreter.ts35
-rw-r--r--src/interpreter/parser.ts8
44 files changed, 575 insertions, 66 deletions
diff --git a/public/assets/bubble.png b/public/assets/bubble.png
index 62744b3..9c8b758 100644
--- a/public/assets/bubble.png
+++ b/public/assets/bubble.png
Binary files differ
diff --git a/public/assets/key.png b/public/assets/key.png
index c6d07a4..23bada6 100644
--- a/public/assets/key.png
+++ b/public/assets/key.png
Binary files differ
diff --git a/public/assets/locked_door.png b/public/assets/locked_door.png
index 4a90d84..7175ae4 100644
--- a/public/assets/locked_door.png
+++ b/public/assets/locked_door.png
Binary files differ
diff --git a/public/assets/sound/failure.wav b/public/assets/sound/failure.wav
new file mode 100644
index 0000000..c7dbddc
--- /dev/null
+++ b/public/assets/sound/failure.wav
Binary files differ
diff --git a/public/assets/sound/keyopen.wav b/public/assets/sound/keyopen.wav
new file mode 100644
index 0000000..bdc2d18
--- /dev/null
+++ b/public/assets/sound/keyopen.wav
Binary files differ
diff --git a/public/assets/sound/lambda_save.wav b/public/assets/sound/lambda_save.wav
new file mode 100644
index 0000000..b08c015
--- /dev/null
+++ b/public/assets/sound/lambda_save.wav
Binary files differ
diff --git a/public/assets/sound/lambda_transform.wav b/public/assets/sound/lambda_transform.wav
new file mode 100644
index 0000000..03a180a
--- /dev/null
+++ b/public/assets/sound/lambda_transform.wav
Binary files differ
diff --git a/public/assets/sound/modal_close.wav b/public/assets/sound/modal_close.wav
new file mode 100644
index 0000000..fe978d2
--- /dev/null
+++ b/public/assets/sound/modal_close.wav
Binary files differ
diff --git a/public/assets/sound/modal_open.wav b/public/assets/sound/modal_open.wav
new file mode 100644
index 0000000..f28b1c7
--- /dev/null
+++ b/public/assets/sound/modal_open.wav
Binary files differ
diff --git a/public/assets/sound/move_1.wav b/public/assets/sound/move_1.wav
new file mode 100644
index 0000000..76949dd
--- /dev/null
+++ b/public/assets/sound/move_1.wav
Binary files differ
diff --git a/public/assets/sound/move_2.wav b/public/assets/sound/move_2.wav
new file mode 100644
index 0000000..d69e5e5
--- /dev/null
+++ b/public/assets/sound/move_2.wav
Binary files differ
diff --git a/public/assets/sound/move_3.wav b/public/assets/sound/move_3.wav
new file mode 100644
index 0000000..3ade3bc
--- /dev/null
+++ b/public/assets/sound/move_3.wav
Binary files differ
diff --git a/public/assets/sound/move_4.wav b/public/assets/sound/move_4.wav
new file mode 100644
index 0000000..0407d8a
--- /dev/null
+++ b/public/assets/sound/move_4.wav
Binary files differ
diff --git a/public/assets/sound/music/credits.txt b/public/assets/sound/music/credits.txt
new file mode 100644
index 0000000..0ac1440
--- /dev/null
+++ b/public/assets/sound/music/credits.txt
@@ -0,0 +1,20 @@
+Reverie by
+Music promoted by https://www.chosic.com/free-music/all/
+Creative Commons CC BY 4.0
+https://creativecommons.org/licenses/by/4.0/
+
+Moonlight by Scott Buckley | www.scottbuckley.com.au
+Music promoted by https://www.chosic.com/free-music/all/
+Creative Commons CC BY 4.0
+https://creativecommons.org/licenses/by/4.0/
+
+Jul by Scott Buckley | www.scottbuckley.com.au
+Music promoted by https://www.chosic.com/free-music/all/
+Creative Commons Attribution 4.0 International (CC BY 4.0)
+https://creativecommons.org/licenses/by/4.0/
+
+A Kind Of Hope by Scott Buckley | www.scottbuckley.com.au
+Music promoted by https://www.chosic.com/free-music/all/
+Creative Commons CC BY 4.0
+https://creativecommons.org/licenses/by/4.0/
+
diff --git a/public/assets/sound/music/hope.mp3 b/public/assets/sound/music/hope.mp3
new file mode 100644
index 0000000..2a3501a
--- /dev/null
+++ b/public/assets/sound/music/hope.mp3
Binary files differ
diff --git a/public/assets/sound/music/jul.mp3 b/public/assets/sound/music/jul.mp3
new file mode 100644
index 0000000..c87218c
--- /dev/null
+++ b/public/assets/sound/music/jul.mp3
Binary files differ
diff --git a/public/assets/sound/music/moonlight.mp3 b/public/assets/sound/music/moonlight.mp3
new file mode 100644
index 0000000..f1c0294
--- /dev/null
+++ b/public/assets/sound/music/moonlight.mp3
Binary files differ
diff --git a/public/assets/sound/music/reverie.mp3 b/public/assets/sound/music/reverie.mp3
new file mode 100644
index 0000000..e77af11
--- /dev/null
+++ b/public/assets/sound/music/reverie.mp3
Binary files differ
diff --git a/src/App.tsx b/src/App.tsx
index 3f3f67d..295b01b 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -32,6 +32,10 @@ export const App = () => {
| inspired by{" "}
<a href="https://hempuli.com/baba/" target="_blank">
baba is you
+ </a>{" "}
+ | music by{" "}
+ <a href="https://www.scottbuckley.com.au" target="_blank">
+ scott buckley
</a>
</span>
</div>
diff --git a/src/components/GameCanvas.tsx b/src/components/GameCanvas.tsx
index 09351e3..b6c585d 100644
--- a/src/components/GameCanvas.tsx
+++ b/src/components/GameCanvas.tsx
@@ -1,6 +1,7 @@
import { useState, useEffect, useRef } from "react";
import { TheAbstractionEngine, Game } from "../engine";
import { Miscellaneous } from "../engine/config";
+import { Title } from "./Title";
export interface GameCanvasProps {
width: number;
@@ -10,6 +11,8 @@ export interface GameCanvasProps {
export const GameCanvas = ({ width, height }: GameCanvasProps) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [game, setGame] = useState<TheAbstractionEngine>();
+ const [ready, setReady] = useState(false);
+ const [loading, setLoading] = useState(true);
useEffect(() => {
if (canvasRef.current && !game) {
@@ -21,25 +24,35 @@ export const GameCanvas = ({ width, height }: GameCanvasProps) => {
theAbstractionEngine.init().then(() => {
theAbstractionEngine.play();
- setGame(theAbstractionEngine);
-
canvas.focus();
+
+ setGame(theAbstractionEngine);
+ setLoading(false);
});
return () => theAbstractionEngine.stop();
}
}
- }, [canvasRef]);
+ }, [canvasRef, ready]);
+
+ if (ready) {
+ return (
+ <div className="centered-game">
+ <canvas
+ id={Miscellaneous.CANVAS_ID}
+ tabIndex={1}
+ ref={canvasRef}
+ width={loading ? 50 : width}
+ height={loading ? 50 : height}
+ ></canvas>
+ {loading && <span className="loading">Loading...</span>}
+ </div>
+ );
+ }
return (
<div className="centered-game">
- <canvas
- id={Miscellaneous.CANVAS_ID}
- tabIndex={1}
- ref={canvasRef}
- width={width}
- height={height}
- ></canvas>
+ <Title setReady={setReady} />
</div>
);
};
diff --git a/src/components/Title.tsx b/src/components/Title.tsx
new file mode 100644
index 0000000..99c7584
--- /dev/null
+++ b/src/components/Title.tsx
@@ -0,0 +1,17 @@
+export interface TitleProps {
+ setReady: (ready: boolean) => void;
+}
+
+export const Title = ({ setReady }: TitleProps) => {
+ return (
+ <div style={{ textAlign: "center" }}>
+ <h1>the abstraction engine</h1>
+ <p>a game based on the lambda calculus</p>
+
+ <br />
+ <hr />
+ <br />
+ <button onClick={() => setReady(true)}>ready</button>
+ </div>
+ );
+};
diff --git a/src/css/style.css b/src/css/style.css
index 35fdc31..1b98555 100644
--- a/src/css/style.css
+++ b/src/css/style.css
@@ -12,7 +12,6 @@
padding: 0;
margin: 0;
font-family: "scientifica", monospace;
- transition: background 0.2s ease-in-out;
font-smooth: never;
}
@@ -21,10 +20,13 @@ body {
margin: 0;
width: 100%;
height: 100%;
+ background: radial-gradient(ellipse at top, var(--bg), transparent),
+ radial-gradient(ellipse at left, var(--blue), transparent),
+ radial-gradient(ellipse at right, var(--purple), transparent),
+ radial-gradient(ellipse at bottom, var(--bg), transparent);
}
body {
- background-color: var(--bg);
color: var(--text);
}
@@ -51,17 +53,20 @@ a:visited {
justify-content: space-around;
padding-top: 1rem;
padding-bottom: 1rem;
+ border-bottom: 2px solid var(--yellow);
+ border-radius: 0.5rem;
}
.content {
- border-top: 1px solid var(--yellow);
- border-bottom: 1px solid var(--yellow);
max-height: 90vh;
}
.footer {
+ border-top: 2px solid var(--yellow);
+ border-radius: 0.5rem;
padding-top: 1rem;
padding-bottom: 1rem;
+ text-align: center;
}
.nav {
@@ -88,12 +93,13 @@ a:visited {
.centered-game canvas {
display: block;
- max-height: 90%;
+ max-height: 100%;
width: auto;
max-width: 100%;
border: 1px solid var(--yellow);
border-radius: 0.5rem;
- margin: 0;
+ margin-bottom: 2rem;
+ background-color: var(--bg);
}
button {
diff --git a/src/engine/TheAbstractionEngine.ts b/src/engine/TheAbstractionEngine.ts
index 20ff6cc..93684ef 100644
--- a/src/engine/TheAbstractionEngine.ts
+++ b/src/engine/TheAbstractionEngine.ts
@@ -7,6 +7,7 @@ import {
Key,
LockedDoor,
Curry,
+ FunctionApplication,
} from "./entities";
import {
Grid,
@@ -16,6 +17,7 @@ import {
Collision,
GridSpawner,
Life,
+ Music,
} from "./systems";
export class TheAbstractionEngine {
@@ -49,8 +51,9 @@ export class TheAbstractionEngine {
),
new GridSpawner(),
new Collision(),
- new Render(this.ctx),
new Life(),
+ new Music(),
+ new Render(this.ctx),
].forEach((system) => this.game.addSystem(system));
const player = new Player();
@@ -70,6 +73,9 @@ export class TheAbstractionEngine {
const curry = new Curry({ x: 9, y: 8 });
this.game.addEntity(curry);
+
+ const application = new FunctionApplication({ x: 5, y: 5 }, "(_INPUT key)");
+ this.game.addEntity(application);
}
public play() {
diff --git a/src/engine/components/Grid.ts b/src/engine/components/Grid.ts
index a62cc7b..9fad5ce 100644
--- a/src/engine/components/Grid.ts
+++ b/src/engine/components/Grid.ts
@@ -6,6 +6,7 @@ export class Grid extends Component {
public gridPosition: Coord2D;
public movingDirection: Direction;
+ public previousDirection: Direction;
constructor(position: Coord2D = { x: 0, y: 0 }) {
super(ComponentNames.Grid);
@@ -13,5 +14,6 @@ export class Grid extends Component {
this.initialized = false;
this.gridPosition = position;
this.movingDirection = Direction.NONE;
+ this.previousDirection = this.movingDirection;
}
}
diff --git a/src/engine/config/assets.ts b/src/engine/config/assets.ts
index 5ce13e8..fbfab2f 100644
--- a/src/engine/config/assets.ts
+++ b/src/engine/config/assets.ts
@@ -1,4 +1,5 @@
import { type SpriteSpec, SPRITE_SPECS } from ".";
+import { SOUND_SPECS, SoundSpec } from "./sounds";
export const FONT = new FontFace("scientifica", "url(/fonts/scientifica.ttf)");
FONT.load().then((font) => {
@@ -6,6 +7,7 @@ FONT.load().then((font) => {
});
export const IMAGES = new Map<string, HTMLImageElement>();
+export const SOUNDS = new Map<string, HTMLAudioElement>();
export const loadSpritesIntoImageElements = (
spriteSpecs: Partial<SpriteSpec>[],
@@ -35,6 +37,45 @@ export const loadSpritesIntoImageElements = (
return spritePromises;
};
+export const loadSoundsIntoAudioElements = (
+ soundSpecs: SoundSpec[],
+): Promise<void>[] => {
+ const soundPromises: Promise<void>[] = [];
+
+ for (const soundSpec of soundSpecs) {
+ if (soundSpec.url) {
+ const promise = fetch(soundSpec.url)
+ .then((response) => response.blob())
+ .then((blob) => {
+ const audio = new Audio();
+ audio.src = URL.createObjectURL(blob);
+ audio.volume = soundSpec.volume ?? 1;
+
+ SOUNDS.set(soundSpec.name, audio);
+ return new Promise<void>((resolve, rej) => {
+ audio.oncanplaythrough = () => {
+ resolve();
+ };
+
+ audio.onerror = (e) => {
+ console.error(soundSpec);
+ rej(e);
+ };
+ });
+ });
+ soundPromises.push(promise);
+ }
+
+ if (soundSpec.states) {
+ soundPromises.push(
+ ...loadSoundsIntoAudioElements(Array.from(soundSpec.states.values())),
+ );
+ }
+ }
+
+ return soundPromises;
+};
+
export const loadAssets = () =>
Promise.all([
...loadSpritesIntoImageElements(
@@ -43,5 +84,5 @@ export const loadAssets = () =>
),
),
FONT.load(),
- // TODO: Sound
+ ...loadSoundsIntoAudioElements(Array.from(SOUND_SPECS.values())),
]);
diff --git a/src/engine/config/index.ts b/src/engine/config/index.ts
index a574965..9b69740 100644
--- a/src/engine/config/index.ts
+++ b/src/engine/config/index.ts
@@ -1,3 +1,4 @@
export * from "./constants";
export * from "./assets";
export * from "./sprites";
+export * from "./sounds";
diff --git a/src/engine/config/sounds.ts b/src/engine/config/sounds.ts
new file mode 100644
index 0000000..b182c86
--- /dev/null
+++ b/src/engine/config/sounds.ts
@@ -0,0 +1,84 @@
+export type SoundSpec = {
+ name: string;
+ url?: string;
+ volume?: number;
+ states?: Map<string | number, SoundSpec>;
+};
+
+export const MovingSound: SoundSpec = {
+ name: "moving",
+ states: new Map([
+ [1, { name: "moving_1", url: "/assets/sound/move_1.wav" }],
+ // [2, { name: "moving_2", url: "/assets/sound/move_2.wav" }],
+ // [3, { name: "moving_3", url: "/assets/sound/move_3.wav" }],
+ [4, { name: "moving_4", url: "/assets/sound/move_4.wav" }],
+ ]),
+};
+
+export const LambdaTransformSound: SoundSpec = {
+ name: "lambdaTransform",
+ url: "/assets/sound/lambda_transform.wav",
+ volume: 0.3,
+};
+
+export const LambdaSave: SoundSpec = {
+ name: "lambdaSave",
+ url: "/assets/sound/lambda_save.wav",
+};
+
+export const Failure: SoundSpec = {
+ name: "failure",
+ url: "/assets/sound/failure.wav",
+ volume: 0.5,
+};
+
+export const ModalOpen: SoundSpec = {
+ name: "modalOpen",
+ url: "/assets/sound/modal_open.wav",
+ volume: 0.5,
+};
+
+export const ModalClose: SoundSpec = {
+ name: "modalClose",
+ url: "/assets/sound/modal_close.wav",
+ volume: 0.5,
+};
+
+export const KeyOpen: SoundSpec = {
+ name: "keyOpen",
+ url: "/assets/sound/keyopen.wav",
+};
+
+export const Music: SoundSpec = {
+ name: "music",
+ states: new Map([
+ [
+ "hope",
+ { name: "hope", url: "/assets/sound/music/hope.mp3", volume: 0.5 },
+ ],
+ ["jul", { name: "jul", url: "/assets/sound/music/jul.mp3", volume: 0.5 }],
+ [
+ "reverie",
+ { name: "reverie", url: "/assets/sound/music/reverie.mp3", volume: 0.5 },
+ ],
+ [
+ "moonlight",
+ {
+ name: "moonlight",
+ url: "/assets/sound/music/moonlight.mp3",
+ volume: 0.5,
+ },
+ ],
+ ]),
+};
+
+export const SOUND_SPECS: SoundSpec[] = [
+ MovingSound,
+ LambdaTransformSound,
+ LambdaSave,
+ Failure,
+ ModalOpen,
+ ModalClose,
+ KeyOpen,
+ Music,
+];
diff --git a/src/engine/config/sprites.ts b/src/engine/config/sprites.ts
index cca5961..83bf0a0 100644
--- a/src/engine/config/sprites.ts
+++ b/src/engine/config/sprites.ts
@@ -8,6 +8,7 @@ export enum Sprites {
KEY,
LOCKED_DOOR,
CURRY,
+ BUBBLE,
}
export interface SpriteSpec {
@@ -96,3 +97,12 @@ const currySpriteSpec = {
sheet: "/assets/curry.png",
};
SPRITE_SPECS.set(Sprites.CURRY, currySpriteSpec);
+
+const bubbleSpriteSpec = {
+ msPerFrame: 200,
+ width: 64,
+ height: 64,
+ frames: 3,
+ sheet: "/assets/bubble.png",
+};
+SPRITE_SPECS.set(Sprites.BUBBLE, bubbleSpriteSpec);
diff --git a/src/engine/entities/FunctionApplication.ts b/src/engine/entities/FunctionApplication.ts
index 31e3490..24e4eec 100644
--- a/src/engine/entities/FunctionApplication.ts
+++ b/src/engine/entities/FunctionApplication.ts
@@ -1,7 +1,183 @@
-import { Entity, EntityNames } from ".";
+import {
+ Entity,
+ EntityNames,
+ FunctionBox,
+ Key,
+ Particles,
+ makeLambdaTermHighlightComponent,
+} from ".";
+import {
+ BoundingBox,
+ Colliding,
+ ComponentNames,
+ Grid,
+ LambdaTerm,
+ Sprite,
+} from "../components";
+import {
+ Failure,
+ IMAGES,
+ LambdaTransformSound,
+ SOUNDS,
+ SPRITE_SPECS,
+ SpriteSpec,
+ Sprites,
+} from "../config";
+import { Coord2D, Direction } from "../interfaces";
+import { Game } from "..";
+import { Grid as GridSystem, SystemNames } from "../systems";
+import { colors } from "../utils";
+import {
+ DebrujinifiedLambdaTerm,
+ SymbolTable,
+ emitNamed,
+ interpret,
+} from "../../interpreter";
export class FunctionApplication extends Entity {
- constructor() {
+ private static spriteSpec = SPRITE_SPECS.get(Sprites.BUBBLE) as SpriteSpec;
+
+ private symbolTable: SymbolTable;
+
+ constructor(gridPosition: Coord2D, lambdaTerm: string) {
super(EntityNames.FunctionApplication);
+
+ this.symbolTable = new SymbolTable();
+ this.symbolTable.add("key");
+
+ const dimension = {
+ width: FunctionApplication.spriteSpec.width,
+ height: FunctionApplication.spriteSpec.height,
+ };
+ this.addComponent(
+ new BoundingBox(
+ {
+ x: 0,
+ y: 0,
+ },
+ dimension,
+ 0,
+ ),
+ );
+
+ this.addComponent(new Grid(gridPosition));
+
+ this.addComponent(new LambdaTerm(lambdaTerm));
+
+ this.addComponent(
+ new Sprite(
+ IMAGES.get(FunctionApplication.spriteSpec.sheet)!,
+ { x: 0, y: 0 },
+ dimension,
+ FunctionApplication.spriteSpec.msPerFrame,
+ FunctionApplication.spriteSpec.frames,
+ ),
+ );
+
+ this.addComponent(new Colliding(this.handleCollision.bind(this)));
+
+ this.addComponent(makeLambdaTermHighlightComponent(this));
+ }
+
+ public handleCollision(game: Game, entity: Entity) {
+ if (entity.name !== EntityNames.FunctionBox) {
+ return;
+ }
+
+ const entityGrid = entity.getComponent<Grid>(ComponentNames.Grid);
+ if (entityGrid.movingDirection !== Direction.NONE) {
+ // prevent recursive functionBox -> application creation
+ return;
+ }
+
+ const grid = this.getComponent<Grid>(ComponentNames.Grid);
+ const gridSystem = game.getSystem<GridSystem>(SystemNames.Grid);
+ const fail = () => {
+ entityGrid.movingDirection = gridSystem.oppositeDirection(
+ entityGrid.previousDirection,
+ );
+ entity.addComponent(entityGrid);
+
+ const failureSound = SOUNDS.get(Failure.name)!;
+ failureSound.play();
+ };
+
+ const applicationTerm = this.getComponent<LambdaTerm>(
+ ComponentNames.LambdaTerm,
+ );
+ const functionTerm = entity.getComponent<LambdaTerm>(
+ ComponentNames.LambdaTerm,
+ );
+ const newCode = applicationTerm.code.replace("_INPUT", functionTerm.code);
+ let result: DebrujinifiedLambdaTerm | null = null;
+ try {
+ result = interpret(newCode, this.symbolTable);
+ } catch (e) {
+ console.error(e);
+ fail();
+ return;
+ }
+
+ const { dimension } = gridSystem;
+ const nextPosition = gridSystem.getNewGridPosition(
+ grid.gridPosition,
+ entityGrid.previousDirection,
+ );
+
+ let applicationResultingEntity: Entity | null = null; // this should be its own function
+ if ("abstraction" in result) {
+ const code = emitNamed(result);
+
+ applicationResultingEntity = new FunctionBox(grid.gridPosition, code);
+ } else if ("name" in result) {
+ const { name } = result;
+ if (name === "key") {
+ applicationResultingEntity = new Key(grid.gridPosition);
+ }
+ } else {
+ fail();
+ return;
+ }
+
+ game.removeEntity(entity.id);
+ if (applicationResultingEntity) {
+ const grid = applicationResultingEntity.getComponent<Grid>(
+ ComponentNames.Grid,
+ );
+ grid.movingDirection = entityGrid.previousDirection;
+ applicationResultingEntity.addComponent(grid);
+
+ game.addEntity(applicationResultingEntity);
+ }
+
+ this.playTransformSound();
+ const particles = new Particles({
+ center: gridSystem.gridToScreenPosition(nextPosition),
+ spawnerDimensions: {
+ width: dimension.width / 2,
+ height: dimension.height / 2,
+ },
+ particleCount: 10,
+ spawnerShape: "circle",
+ particleShape: "circle",
+ particleMeanSpeed: 0.25,
+ particleSpeedVariance: 0.15,
+ particleMeanLife: 150,
+ particleMeanSize: 2,
+ particleSizeVariance: 1,
+ particleLifeVariance: 20,
+ particleColors: [
+ colors.lightAqua,
+ colors.blue,
+ colors.green,
+ colors.lightGreen,
+ ],
+ });
+ 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 92f1908..0c9123e 100644
--- a/src/engine/entities/FunctionBox.ts
+++ b/src/engine/entities/FunctionBox.ts
@@ -1,6 +1,9 @@
import {
IMAGES,
Miscellaneous,
+ ModalClose,
+ ModalOpen,
+ SOUNDS,
SPRITE_SPECS,
SpriteSpec,
Sprites,
@@ -72,10 +75,15 @@ export const makeLambdaTermHighlightComponent = (entity: Entity) => {
const onHighlight = () => {
let modalOpen = false;
+ const doModalClose = () => {
+ SOUNDS.get(ModalClose.name)!.play();
+ modalOpen = false;
+ closeModal();
+ };
+
const interaction = () => {
if (modalOpen) {
- modalOpen = false;
- closeModal();
+ doModalClose();
return;
}
@@ -86,9 +94,10 @@ export const makeLambdaTermHighlightComponent = (entity: Entity) => {
`<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", () => {
- closeModal();
+ doModalClose();
document.getElementById(Miscellaneous.CANVAS_ID)!.focus();
});
};
diff --git a/src/engine/entities/LambdaFactory.ts b/src/engine/entities/LambdaFactory.ts
index a0f5749..9ad1398 100644
--- a/src/engine/entities/LambdaFactory.ts
+++ b/src/engine/entities/LambdaFactory.ts
@@ -1,6 +1,11 @@
import {
+ Failure,
IMAGES,
+ LambdaSave,
+ LambdaTransformSound,
Miscellaneous,
+ ModalOpen,
+ SOUNDS,
SPRITE_SPECS,
SpriteSpec,
Sprites,
@@ -144,6 +149,8 @@ export class LambdaFactory extends Entity {
const text = this.getComponent<Text>(ComponentNames.Text);
text.text = spawner.spawnsLeft.toString();
this.addComponent(text);
+
+ SOUNDS.get(LambdaTransformSound.name)!.play();
}
private openCodeEditor() {
@@ -185,6 +192,8 @@ export class LambdaFactory extends Entity {
canvas,
closeButton,
};
+
+ SOUNDS.get(ModalOpen.name)!.play();
}
private refreshCodeEditorText(text: string) {
@@ -239,6 +248,7 @@ export class LambdaFactory extends Entity {
});
syntaxError.innerText = e.message;
+ SOUNDS.get(Failure.name)!.play();
return;
}
@@ -250,6 +260,7 @@ export class LambdaFactory extends Entity {
closeModal();
canvas.focus();
+ SOUNDS.get(LambdaSave.name)!.play();
}
private onHighlight(direction: Direction) {
diff --git a/src/engine/entities/LockedDoor.ts b/src/engine/entities/LockedDoor.ts
index b4887d6..aa1f328 100644
--- a/src/engine/entities/LockedDoor.ts
+++ b/src/engine/entities/LockedDoor.ts
@@ -7,7 +7,14 @@ import {
Sprite,
ComponentNames,
} from "../components";
-import { IMAGES, SPRITE_SPECS, SpriteSpec, Sprites } from "../config";
+import {
+ IMAGES,
+ KeyOpen,
+ SOUNDS,
+ SPRITE_SPECS,
+ SpriteSpec,
+ Sprites,
+} from "../config";
import { Coord2D } from "../interfaces";
import { Grid as GridSystem, SystemNames } from "../systems";
import { colors } from "../utils";
@@ -82,5 +89,12 @@ export class LockedDoor extends Entity {
});
game.addEntity(particles);
+
+ this.playKeySound();
+ }
+
+ private playKeySound() {
+ const audio = SOUNDS.get(KeyOpen.name)!;
+ audio.play();
}
}
diff --git a/src/engine/entities/Particles.ts b/src/engine/entities/Particles.ts
index 34b475c..5381b23 100644
--- a/src/engine/entities/Particles.ts
+++ b/src/engine/entities/Particles.ts
@@ -156,8 +156,12 @@ export class Particles extends Entity {
Math.floor(Math.random() * options.particleColors.length)
];
const position = {
- x: options.center.x + Math.cos(angle) * options.spawnerDimensions.width,
- y: options.center.y + Math.sin(angle) * options.spawnerDimensions.height,
+ x:
+ options.center.x +
+ (Math.cos(angle) * options.spawnerDimensions.width) / 2,
+ y:
+ options.center.y +
+ (Math.sin(angle) * options.spawnerDimensions.height) / 2,
};
if (options.spawnerShape === "rectangle") {
// determine a random position on the edge of the spawner based on the angle
diff --git a/src/engine/entities/Player.ts b/src/engine/entities/Player.ts
index cb9161b..1b98383 100644
--- a/src/engine/entities/Player.ts
+++ b/src/engine/entities/Player.ts
@@ -6,6 +6,7 @@ import {
Grid,
BoundingBox,
Control,
+ Pushable,
} from "../components";
import { Direction } from "../interfaces/";
@@ -28,6 +29,8 @@ export class Player extends Entity {
),
);
+ this.addComponent(new Pushable());
+
this.addComponent(new Control());
this.addComponent(new Grid());
diff --git a/src/engine/systems/Collision.ts b/src/engine/systems/Collision.ts
index 8ef8215..7d843cc 100644
--- a/src/engine/systems/Collision.ts
+++ b/src/engine/systems/Collision.ts
@@ -6,6 +6,7 @@ import { BoundingBox, Colliding, ComponentNames, Grid } from "../components";
const collisionMap: Record<string, Set<string>> = {
[EntityNames.Key]: new Set([EntityNames.LockedDoor]),
[EntityNames.Curry]: new Set([EntityNames.Player]),
+ [EntityNames.FunctionApplication]: new Set([EntityNames.FunctionBox]),
};
export class Collision extends System {
diff --git a/src/engine/systems/Grid.ts b/src/engine/systems/Grid.ts
index 1d4a623..9ab28e3 100644
--- a/src/engine/systems/Grid.ts
+++ b/src/engine/systems/Grid.ts
@@ -33,8 +33,8 @@ export class Grid extends System {
this.rebuildGrid(game);
this.highlightEntitiesLookedAt(game);
- this.propogateEntityMovements(game);
+ this.propogateEntityMovements(game);
this.updateMovingEntities(dt, game);
}
@@ -209,9 +209,11 @@ export class Grid extends System {
) {
game.forEachEntityWithComponent(ComponentNames.Grid, (entity) => {
const grid = entity.getComponent<GridComponent>(ComponentNames.Grid)!;
+
if (grid.movingDirection === Direction.NONE) {
return;
}
+ grid.previousDirection = grid.movingDirection;
const boundingBox = entity.getComponent<BoundingBox>(
ComponentNames.BoundingBox,
@@ -270,7 +272,7 @@ export class Grid extends System {
});
}
- private getNewGridPosition(prev: Coord2D, direction: Direction) {
+ public getNewGridPosition(prev: Coord2D, direction: Direction) {
let { x: newX, y: newY } = prev;
switch (direction) {
case Direction.LEFT:
@@ -290,6 +292,25 @@ export class Grid extends System {
return { x: newX, y: newY };
}
+ public oppositeDirection(direction: Direction) {
+ let opposite = Direction.NONE;
+ switch (direction) {
+ case Direction.LEFT:
+ opposite = Direction.RIGHT;
+ break;
+ case Direction.RIGHT:
+ opposite = Direction.LEFT;
+ break;
+ case Direction.UP:
+ opposite = Direction.DOWN;
+ break;
+ case Direction.DOWN:
+ opposite = Direction.UP;
+ break;
+ }
+ return opposite;
+ }
+
private isEntityPastCenterWhenMoving(
direction: Direction,
gridPosition: Coord2D,
diff --git a/src/engine/systems/Input.ts b/src/engine/systems/Input.ts
index 8900f4e..c527f29 100644
--- a/src/engine/systems/Input.ts
+++ b/src/engine/systems/Input.ts
@@ -2,7 +2,7 @@ import { Grid as GridSystem, SystemNames, System } from ".";
import { Game } from "..";
import { ComponentNames, Grid, Interactable } from "../components";
import { Control } from "../components/Control";
-import { Action, KeyConstants } from "../config";
+import { Action, KeyConstants, MovingSound, SOUNDS } from "../config";
import { Entity, Particles } from "../entities";
import { Coord2D, Direction } from "../interfaces";
import { colors } from "../utils";
@@ -105,27 +105,44 @@ export class Input extends System {
}
if (moveUp || moveLeft || moveRight || moveDown) {
- const gridPosition = gridComponent.gridPosition;
- const gridSystem = game.getSystem<GridSystem>(SystemNames.Grid);
- const particles = new Particles({
- center: gridSystem.gridToScreenPosition(gridPosition),
- particleCount: 5,
- particleShape: "circle",
- particleMeanSpeed: 0.05,
- particleSpeedVariance: 0.005,
- particleMeanLife: 120,
- particleMeanSize: 5,
- particleSizeVariance: 2,
- particleLifeVariance: 30,
- particleColors: [colors.gray, colors.darkGray],
- });
-
- game.addEntity(particles);
+ this.spawnParticlesAround(entity, game);
+ this.playMoveSound();
}
entity.addComponent(gridComponent);
}
+ private playMoveSound() {
+ const movingSounds = Array.from(MovingSound.states!.values());
+ const soundName =
+ movingSounds[Math.floor(Math.random() * movingSounds.length)].name;
+ SOUNDS.get(soundName)!.play();
+ }
+
+ private spawnParticlesAround(entity: Entity, game: Game) {
+ const gridSystem = game.getSystem<GridSystem>(SystemNames.Grid);
+ const gridComponent = entity.getComponent<Grid>(ComponentNames.Grid)!;
+ const particles = new Particles({
+ center: gridSystem.gridToScreenPosition(gridComponent.gridPosition),
+ particleCount: 4,
+ spawnerShape: "circle",
+ spawnerDimensions: {
+ width: 10,
+ height: 10,
+ },
+ particleShape: "rectangle",
+ particleMeanSpeed: 0.05,
+ particleSpeedVariance: 0.005,
+ particleMeanLife: 120,
+ particleMeanSize: 3,
+ particleSizeVariance: 1,
+ particleLifeVariance: 30,
+ particleColors: [colors.gray, colors.darkGray, colors.lightPurple],
+ });
+
+ game.addEntity(particles);
+ }
+
private hasSomeKey(keys?: string[]): boolean {
if (keys) {
return keys.some((key) => this.keys.has(key));
diff --git a/src/engine/systems/Music.ts b/src/engine/systems/Music.ts
new file mode 100644
index 0000000..6e2004d
--- /dev/null
+++ b/src/engine/systems/Music.ts
@@ -0,0 +1,40 @@
+import { System, SystemNames } from ".";
+import { Music as GameMusic, SOUNDS } from "../config";
+
+export class Music extends System {
+ private songs: string[] = [];
+ private currentSong?: string;
+
+ constructor() {
+ super(SystemNames.Music);
+
+ this.songs = Array.from(GameMusic.states!.values()).map(
+ (state) => state.name,
+ );
+ }
+
+ private chooseRandomSong() {
+ return this.songs[Math.floor(Math.random() * this.songs.length)];
+ }
+
+ public playNext() {
+ let nextSong = this.chooseRandomSong();
+ while (nextSong === this.currentSong) {
+ nextSong = this.chooseRandomSong();
+ }
+
+ this.currentSong = nextSong;
+ SOUNDS.get(this.currentSong)?.play();
+
+ // when done, play next song
+ SOUNDS.get(this.currentSong)?.addEventListener("ended", () => {
+ this.playNext();
+ });
+ }
+
+ public update(_dt: number) {
+ if (!this.currentSong) {
+ this.playNext();
+ }
+ }
+}
diff --git a/src/engine/systems/SystemNames.ts b/src/engine/systems/SystemNames.ts
index 738dfba..363c72c 100644
--- a/src/engine/systems/SystemNames.ts
+++ b/src/engine/systems/SystemNames.ts
@@ -6,4 +6,5 @@ export namespace SystemNames {
export const GridSpawner = "GridSpawner";
export const Collision = "Collision";
export const Life = "Life";
+ export const Music = "Music";
}
diff --git a/src/engine/systems/index.ts b/src/engine/systems/index.ts
index a420216..46c534d 100644
--- a/src/engine/systems/index.ts
+++ b/src/engine/systems/index.ts
@@ -7,3 +7,4 @@ export * from "./Grid";
export * from "./GridSpawner";
export * from "./Collision";
export * from "./Life";
+export * from "./Music";
diff --git a/src/engine/utils/modal.ts b/src/engine/utils/modal.ts
index 48afae8..a378821 100644
--- a/src/engine/utils/modal.ts
+++ b/src/engine/utils/modal.ts
@@ -36,6 +36,6 @@ export const closeModal = (
modal.style.display = "none";
modalOpen = false;
- }, 250);
+ }, 200);
}
};
diff --git a/src/interpreter/PeggyParser.js b/src/interpreter/PeggyParser.js
index 5671d91..632f1b7 100644
--- a/src/interpreter/PeggyParser.js
+++ b/src/interpreter/PeggyParser.js
@@ -201,7 +201,7 @@ export default (function () {
);
};
- function peg$parse(input, options) {
+ function peg$parse(input, options, allowUnderscores = false) {
options = options !== undefined ? options : {};
var peg$FAILED = {};
@@ -215,7 +215,7 @@ export default (function () {
var peg$c2 = ".";
var peg$c3 = "\r\n";
- var peg$r0 = /^[a-zA-Z0-9]/;
+ var peg$r0 = allowUnderscores ? /^[a-zA-Z0-9]_/ : /^[a-zA-Z0-9]/;
var peg$r1 = /^[\\\u03BB]/;
var peg$r2 = /^[\t-\n ]/;
diff --git a/src/interpreter/interpreter.ts b/src/interpreter/interpreter.ts
index 0b3ae34..4599c46 100644
--- a/src/interpreter/interpreter.ts
+++ b/src/interpreter/interpreter.ts
@@ -198,22 +198,6 @@ export const betaReduce = (
);
};
-export const interpret = (term: string): DebrujinifiedLambdaTerm => {
- const ast = parse(term);
- const symbolTable = new SymbolTable();
- const debrujined = debrujinify(ast, symbolTable);
-
- let prev = debrujined;
- let next = betaReduce(prev);
-
- while (emitDebrujin(prev) !== emitDebrujin(next)) {
- // alpha equivalence
- prev = next;
- next = betaReduce(prev);
- }
- return next;
-};
-
export const emitDebrujin = (term: DebrujinifiedLambdaTerm): string => {
if ("index" in term) {
return term.index.toString();
@@ -253,3 +237,22 @@ export const emitNamed = (term: DebrujinifiedLambdaTerm): string => {
throw new InvalidLambdaTermError(`Invalid lambda term: ${term}`);
};
+
+export const interpret = (
+ term: string,
+ symbolTable = new SymbolTable(),
+ allowUnderscores = false,
+): DebrujinifiedLambdaTerm => {
+ const ast = parse(term, allowUnderscores);
+ const debrujined = debrujinify(ast, symbolTable);
+
+ let prev = debrujined;
+ let next = betaReduce(prev);
+
+ while (emitDebrujin(prev) !== emitDebrujin(next)) {
+ // alpha equivalence
+ prev = next;
+ next = betaReduce(prev);
+ }
+ return next;
+};
diff --git a/src/interpreter/parser.ts b/src/interpreter/parser.ts
index 5e3be0f..d288815 100644
--- a/src/interpreter/parser.ts
+++ b/src/interpreter/parser.ts
@@ -30,6 +30,10 @@ export const isVariable = (term: LambdaTerm): term is Variable => {
return typeof term === "string";
};
-export const parse = (term: string, library = false) => {
- return peggyParser.parse(term, { peg$library: library });
+export const parse = (
+ term: string,
+ allowUnderscores = false,
+ library = false,
+) => {
+ return peggyParser.parse(term, { peg$library: library }, allowUnderscores);
};