level one (applications prototype finished!)

This commit is contained in:
Elizabeth Hunt 2024-03-07 20:45:47 -07:00
parent 823620b2a6
commit e6e2944056
Signed by: simponic
GPG Key ID: 52B3774857EB24B1
44 changed files with 575 additions and 66 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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/

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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>

View File

@ -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={width}
height={height}
width={loading ? 50 : width}
height={loading ? 50 : height}
></canvas>
{loading && <span className="loading">Loading...</span>}
</div>
);
}
return (
<div className="centered-game">
<Title setReady={setReady} />
</div>
);
};

17
src/components/Title.tsx Normal file
View File

@ -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>
);
};

View File

@ -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 {

View File

@ -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() {

View File

@ -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;
}
}

View File

@ -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())),
]);

View File

@ -1,3 +1,4 @@
export * from "./constants";
export * from "./assets";
export * from "./sprites";
export * from "./sounds";

View File

@ -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,
];

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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 interaction = () => {
if (modalOpen) {
const doModalClose = () => {
SOUNDS.get(ModalClose.name)!.play();
modalOpen = false;
closeModal();
};
const interaction = () => {
if (modalOpen) {
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();
});
};

View File

@ -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) {

View File

@ -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();
}
}

View File

@ -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

View File

@ -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());

View File

@ -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 {

View File

@ -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,

View File

@ -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));

View File

@ -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();
}
}
}

View File

@ -6,4 +6,5 @@ export namespace SystemNames {
export const GridSpawner = "GridSpawner";
export const Collision = "Collision";
export const Life = "Life";
export const Music = "Music";
}

View File

@ -7,3 +7,4 @@ export * from "./Grid";
export * from "./GridSpawner";
export * from "./Collision";
export * from "./Life";
export * from "./Music";

View File

@ -36,6 +36,6 @@ export const closeModal = (
modal.style.display = "none";
modalOpen = false;
}, 250);
}, 200);
}
};

View File

@ -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 ]/;

View File

@ -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;
};

View File

@ -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);
};