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{" "} | inspired by{" "}
<a href="https://hempuli.com/baba/" target="_blank"> <a href="https://hempuli.com/baba/" target="_blank">
baba is you baba is you
</a>{" "}
| music by{" "}
<a href="https://www.scottbuckley.com.au" target="_blank">
scott buckley
</a> </a>
</span> </span>
</div> </div>

View File

@ -1,6 +1,7 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { TheAbstractionEngine, Game } from "../engine"; import { TheAbstractionEngine, Game } from "../engine";
import { Miscellaneous } from "../engine/config"; import { Miscellaneous } from "../engine/config";
import { Title } from "./Title";
export interface GameCanvasProps { export interface GameCanvasProps {
width: number; width: number;
@ -10,6 +11,8 @@ export interface GameCanvasProps {
export const GameCanvas = ({ width, height }: GameCanvasProps) => { export const GameCanvas = ({ width, height }: GameCanvasProps) => {
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const [game, setGame] = useState<TheAbstractionEngine>(); const [game, setGame] = useState<TheAbstractionEngine>();
const [ready, setReady] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
if (canvasRef.current && !game) { if (canvasRef.current && !game) {
@ -21,25 +24,35 @@ export const GameCanvas = ({ width, height }: GameCanvasProps) => {
theAbstractionEngine.init().then(() => { theAbstractionEngine.init().then(() => {
theAbstractionEngine.play(); theAbstractionEngine.play();
setGame(theAbstractionEngine);
canvas.focus(); canvas.focus();
setGame(theAbstractionEngine);
setLoading(false);
}); });
return () => theAbstractionEngine.stop(); 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 ( return (
<div className="centered-game"> <div className="centered-game">
<canvas <Title setReady={setReady} />
id={Miscellaneous.CANVAS_ID}
tabIndex={1}
ref={canvasRef}
width={width}
height={height}
></canvas>
</div> </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; padding: 0;
margin: 0; margin: 0;
font-family: "scientifica", monospace; font-family: "scientifica", monospace;
transition: background 0.2s ease-in-out;
font-smooth: never; font-smooth: never;
} }
@ -21,10 +20,13 @@ body {
margin: 0; margin: 0;
width: 100%; width: 100%;
height: 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 { body {
background-color: var(--bg);
color: var(--text); color: var(--text);
} }
@ -51,17 +53,20 @@ a:visited {
justify-content: space-around; justify-content: space-around;
padding-top: 1rem; padding-top: 1rem;
padding-bottom: 1rem; padding-bottom: 1rem;
border-bottom: 2px solid var(--yellow);
border-radius: 0.5rem;
} }
.content { .content {
border-top: 1px solid var(--yellow);
border-bottom: 1px solid var(--yellow);
max-height: 90vh; max-height: 90vh;
} }
.footer { .footer {
border-top: 2px solid var(--yellow);
border-radius: 0.5rem;
padding-top: 1rem; padding-top: 1rem;
padding-bottom: 1rem; padding-bottom: 1rem;
text-align: center;
} }
.nav { .nav {
@ -88,12 +93,13 @@ a:visited {
.centered-game canvas { .centered-game canvas {
display: block; display: block;
max-height: 90%; max-height: 100%;
width: auto; width: auto;
max-width: 100%; max-width: 100%;
border: 1px solid var(--yellow); border: 1px solid var(--yellow);
border-radius: 0.5rem; border-radius: 0.5rem;
margin: 0; margin-bottom: 2rem;
background-color: var(--bg);
} }
button { button {

View File

@ -7,6 +7,7 @@ import {
Key, Key,
LockedDoor, LockedDoor,
Curry, Curry,
FunctionApplication,
} from "./entities"; } from "./entities";
import { import {
Grid, Grid,
@ -16,6 +17,7 @@ import {
Collision, Collision,
GridSpawner, GridSpawner,
Life, Life,
Music,
} from "./systems"; } from "./systems";
export class TheAbstractionEngine { export class TheAbstractionEngine {
@ -49,8 +51,9 @@ export class TheAbstractionEngine {
), ),
new GridSpawner(), new GridSpawner(),
new Collision(), new Collision(),
new Render(this.ctx),
new Life(), new Life(),
new Music(),
new Render(this.ctx),
].forEach((system) => this.game.addSystem(system)); ].forEach((system) => this.game.addSystem(system));
const player = new Player(); const player = new Player();
@ -70,6 +73,9 @@ export class TheAbstractionEngine {
const curry = new Curry({ x: 9, y: 8 }); const curry = new Curry({ x: 9, y: 8 });
this.game.addEntity(curry); this.game.addEntity(curry);
const application = new FunctionApplication({ x: 5, y: 5 }, "(_INPUT key)");
this.game.addEntity(application);
} }
public play() { public play() {

View File

@ -6,6 +6,7 @@ export class Grid extends Component {
public gridPosition: Coord2D; public gridPosition: Coord2D;
public movingDirection: Direction; public movingDirection: Direction;
public previousDirection: Direction;
constructor(position: Coord2D = { x: 0, y: 0 }) { constructor(position: Coord2D = { x: 0, y: 0 }) {
super(ComponentNames.Grid); super(ComponentNames.Grid);
@ -13,5 +14,6 @@ export class Grid extends Component {
this.initialized = false; this.initialized = false;
this.gridPosition = position; this.gridPosition = position;
this.movingDirection = Direction.NONE; this.movingDirection = Direction.NONE;
this.previousDirection = this.movingDirection;
} }
} }

View File

@ -1,4 +1,5 @@
import { type SpriteSpec, SPRITE_SPECS } from "."; import { type SpriteSpec, SPRITE_SPECS } from ".";
import { SOUND_SPECS, SoundSpec } from "./sounds";
export const FONT = new FontFace("scientifica", "url(/fonts/scientifica.ttf)"); export const FONT = new FontFace("scientifica", "url(/fonts/scientifica.ttf)");
FONT.load().then((font) => { FONT.load().then((font) => {
@ -6,6 +7,7 @@ FONT.load().then((font) => {
}); });
export const IMAGES = new Map<string, HTMLImageElement>(); export const IMAGES = new Map<string, HTMLImageElement>();
export const SOUNDS = new Map<string, HTMLAudioElement>();
export const loadSpritesIntoImageElements = ( export const loadSpritesIntoImageElements = (
spriteSpecs: Partial<SpriteSpec>[], spriteSpecs: Partial<SpriteSpec>[],
@ -35,6 +37,45 @@ export const loadSpritesIntoImageElements = (
return spritePromises; 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 = () => export const loadAssets = () =>
Promise.all([ Promise.all([
...loadSpritesIntoImageElements( ...loadSpritesIntoImageElements(
@ -43,5 +84,5 @@ export const loadAssets = () =>
), ),
), ),
FONT.load(), FONT.load(),
// TODO: Sound ...loadSoundsIntoAudioElements(Array.from(SOUND_SPECS.values())),
]); ]);

View File

@ -1,3 +1,4 @@
export * from "./constants"; export * from "./constants";
export * from "./assets"; export * from "./assets";
export * from "./sprites"; 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, KEY,
LOCKED_DOOR, LOCKED_DOOR,
CURRY, CURRY,
BUBBLE,
} }
export interface SpriteSpec { export interface SpriteSpec {
@ -96,3 +97,12 @@ const currySpriteSpec = {
sheet: "/assets/curry.png", sheet: "/assets/curry.png",
}; };
SPRITE_SPECS.set(Sprites.CURRY, currySpriteSpec); 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 { 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); 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 { import {
IMAGES, IMAGES,
Miscellaneous, Miscellaneous,
ModalClose,
ModalOpen,
SOUNDS,
SPRITE_SPECS, SPRITE_SPECS,
SpriteSpec, SpriteSpec,
Sprites, Sprites,
@ -72,10 +75,15 @@ export const makeLambdaTermHighlightComponent = (entity: Entity) => {
const onHighlight = () => { const onHighlight = () => {
let modalOpen = false; let modalOpen = false;
const doModalClose = () => {
SOUNDS.get(ModalClose.name)!.play();
modalOpen = false;
closeModal();
};
const interaction = () => { const interaction = () => {
if (modalOpen) { if (modalOpen) {
modalOpen = false; doModalClose();
closeModal();
return; 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>`, `<div style="text-align:center"><p>${code}</p> <br> <button id="close">Close</button></div>`,
); );
modalOpen = true; modalOpen = true;
SOUNDS.get(ModalOpen.name)!.play();
document.getElementById("close")!.addEventListener("click", () => { document.getElementById("close")!.addEventListener("click", () => {
closeModal(); doModalClose();
document.getElementById(Miscellaneous.CANVAS_ID)!.focus(); document.getElementById(Miscellaneous.CANVAS_ID)!.focus();
}); });
}; };

View File

@ -1,6 +1,11 @@
import { import {
Failure,
IMAGES, IMAGES,
LambdaSave,
LambdaTransformSound,
Miscellaneous, Miscellaneous,
ModalOpen,
SOUNDS,
SPRITE_SPECS, SPRITE_SPECS,
SpriteSpec, SpriteSpec,
Sprites, Sprites,
@ -144,6 +149,8 @@ export class LambdaFactory extends Entity {
const text = this.getComponent<Text>(ComponentNames.Text); const text = this.getComponent<Text>(ComponentNames.Text);
text.text = spawner.spawnsLeft.toString(); text.text = spawner.spawnsLeft.toString();
this.addComponent(text); this.addComponent(text);
SOUNDS.get(LambdaTransformSound.name)!.play();
} }
private openCodeEditor() { private openCodeEditor() {
@ -185,6 +192,8 @@ export class LambdaFactory extends Entity {
canvas, canvas,
closeButton, closeButton,
}; };
SOUNDS.get(ModalOpen.name)!.play();
} }
private refreshCodeEditorText(text: string) { private refreshCodeEditorText(text: string) {
@ -239,6 +248,7 @@ export class LambdaFactory extends Entity {
}); });
syntaxError.innerText = e.message; syntaxError.innerText = e.message;
SOUNDS.get(Failure.name)!.play();
return; return;
} }
@ -250,6 +260,7 @@ export class LambdaFactory extends Entity {
closeModal(); closeModal();
canvas.focus(); canvas.focus();
SOUNDS.get(LambdaSave.name)!.play();
} }
private onHighlight(direction: Direction) { private onHighlight(direction: Direction) {

View File

@ -7,7 +7,14 @@ import {
Sprite, Sprite,
ComponentNames, ComponentNames,
} from "../components"; } 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 { Coord2D } from "../interfaces";
import { Grid as GridSystem, SystemNames } from "../systems"; import { Grid as GridSystem, SystemNames } from "../systems";
import { colors } from "../utils"; import { colors } from "../utils";
@ -82,5 +89,12 @@ export class LockedDoor extends Entity {
}); });
game.addEntity(particles); 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) Math.floor(Math.random() * options.particleColors.length)
]; ];
const position = { const position = {
x: options.center.x + Math.cos(angle) * options.spawnerDimensions.width, x:
y: options.center.y + Math.sin(angle) * options.spawnerDimensions.height, 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") { if (options.spawnerShape === "rectangle") {
// determine a random position on the edge of the spawner based on the angle // determine a random position on the edge of the spawner based on the angle

View File

@ -6,6 +6,7 @@ import {
Grid, Grid,
BoundingBox, BoundingBox,
Control, Control,
Pushable,
} from "../components"; } from "../components";
import { Direction } from "../interfaces/"; import { Direction } from "../interfaces/";
@ -28,6 +29,8 @@ export class Player extends Entity {
), ),
); );
this.addComponent(new Pushable());
this.addComponent(new Control()); this.addComponent(new Control());
this.addComponent(new Grid()); this.addComponent(new Grid());

View File

@ -6,6 +6,7 @@ import { BoundingBox, Colliding, ComponentNames, Grid } from "../components";
const collisionMap: Record<string, Set<string>> = { const collisionMap: Record<string, Set<string>> = {
[EntityNames.Key]: new Set([EntityNames.LockedDoor]), [EntityNames.Key]: new Set([EntityNames.LockedDoor]),
[EntityNames.Curry]: new Set([EntityNames.Player]), [EntityNames.Curry]: new Set([EntityNames.Player]),
[EntityNames.FunctionApplication]: new Set([EntityNames.FunctionBox]),
}; };
export class Collision extends System { export class Collision extends System {

View File

@ -33,8 +33,8 @@ export class Grid extends System {
this.rebuildGrid(game); this.rebuildGrid(game);
this.highlightEntitiesLookedAt(game); this.highlightEntitiesLookedAt(game);
this.propogateEntityMovements(game);
this.propogateEntityMovements(game);
this.updateMovingEntities(dt, game); this.updateMovingEntities(dt, game);
} }
@ -209,9 +209,11 @@ export class Grid extends System {
) { ) {
game.forEachEntityWithComponent(ComponentNames.Grid, (entity) => { game.forEachEntityWithComponent(ComponentNames.Grid, (entity) => {
const grid = entity.getComponent<GridComponent>(ComponentNames.Grid)!; const grid = entity.getComponent<GridComponent>(ComponentNames.Grid)!;
if (grid.movingDirection === Direction.NONE) { if (grid.movingDirection === Direction.NONE) {
return; return;
} }
grid.previousDirection = grid.movingDirection;
const boundingBox = entity.getComponent<BoundingBox>( const boundingBox = entity.getComponent<BoundingBox>(
ComponentNames.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; let { x: newX, y: newY } = prev;
switch (direction) { switch (direction) {
case Direction.LEFT: case Direction.LEFT:
@ -290,6 +292,25 @@ export class Grid extends System {
return { x: newX, y: newY }; 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( private isEntityPastCenterWhenMoving(
direction: Direction, direction: Direction,
gridPosition: Coord2D, gridPosition: Coord2D,

View File

@ -2,7 +2,7 @@ import { Grid as GridSystem, SystemNames, System } from ".";
import { Game } from ".."; import { Game } from "..";
import { ComponentNames, Grid, Interactable } from "../components"; import { ComponentNames, Grid, Interactable } from "../components";
import { Control } from "../components/Control"; import { Control } from "../components/Control";
import { Action, KeyConstants } from "../config"; import { Action, KeyConstants, MovingSound, SOUNDS } from "../config";
import { Entity, Particles } from "../entities"; import { Entity, Particles } from "../entities";
import { Coord2D, Direction } from "../interfaces"; import { Coord2D, Direction } from "../interfaces";
import { colors } from "../utils"; import { colors } from "../utils";
@ -105,27 +105,44 @@ export class Input extends System {
} }
if (moveUp || moveLeft || moveRight || moveDown) { if (moveUp || moveLeft || moveRight || moveDown) {
const gridPosition = gridComponent.gridPosition; this.spawnParticlesAround(entity, game);
const gridSystem = game.getSystem<GridSystem>(SystemNames.Grid); this.playMoveSound();
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);
} }
entity.addComponent(gridComponent); 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 { private hasSomeKey(keys?: string[]): boolean {
if (keys) { if (keys) {
return keys.some((key) => this.keys.has(key)); 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 GridSpawner = "GridSpawner";
export const Collision = "Collision"; export const Collision = "Collision";
export const Life = "Life"; export const Life = "Life";
export const Music = "Music";
} }

View File

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

View File

@ -36,6 +36,6 @@ export const closeModal = (
modal.style.display = "none"; modal.style.display = "none";
modalOpen = false; 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 : {}; options = options !== undefined ? options : {};
var peg$FAILED = {}; var peg$FAILED = {};
@ -215,7 +215,7 @@ export default (function () {
var peg$c2 = "."; var peg$c2 = ".";
var peg$c3 = "\r\n"; 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$r1 = /^[\\\u03BB]/;
var peg$r2 = /^[\t-\n ]/; 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 => { export const emitDebrujin = (term: DebrujinifiedLambdaTerm): string => {
if ("index" in term) { if ("index" in term) {
return term.index.toString(); return term.index.toString();
@ -253,3 +237,22 @@ export const emitNamed = (term: DebrujinifiedLambdaTerm): string => {
throw new InvalidLambdaTermError(`Invalid lambda term: ${term}`); 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"; return typeof term === "string";
}; };
export const parse = (term: string, library = false) => { export const parse = (
return peggyParser.parse(term, { peg$library: library }); term: string,
allowUnderscores = false,
library = false,
) => {
return peggyParser.parse(term, { peg$library: library }, allowUnderscores);
}; };