checkpoint

This commit is contained in:
Elizabeth Hunt 2025-03-06 08:44:43 -07:00
parent 78797aa175
commit 958134419d
Signed by: simponic
GPG Key ID: 2909B9A7FF6213EE
9 changed files with 191 additions and 55 deletions

View File

@ -38,7 +38,9 @@ export class TheAbstractionEngine {
[
new RadialObserve(),
new Modal(),
new Level(isDev ? LevelNames.CarCadr : LevelNames.LevelSelection),
new Level(
isDev ? LevelNames.ChurchNumeralsOne : LevelNames.LevelSelection,
),
inputSystem,
facingDirectionSystem,
new Grid(

View File

@ -24,8 +24,9 @@ import { Game } from "..";
import { Grid as GridSystem, SystemNames } from "../systems";
import { colors, tryWrap } from "../utils";
import {
InvalidLambdaTermError,
DebrujinIndex,
SymbolTable,
Visitors,
emitNamed,
interpret,
} from "../../interpreter";
@ -41,16 +42,9 @@ const APPLICATION_RESULTS: Record<
export class FunctionApplication extends Entity {
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();
Object.keys(APPLICATION_RESULTS).forEach((key) => {
this.symbolTable.add(key);
});
const dimension = {
width: FunctionApplication.spriteSpec.width,
height: FunctionApplication.spriteSpec.height,
@ -151,7 +145,10 @@ export class FunctionApplication extends Entity {
);
const newCode = applicationTerm.code.replace("_INPUT", functionTerm.code);
const result = tryWrap(() => interpret(newCode, this.symbolTable, true));
const { symbolTable, visitors } = this.getVisitors(game);
const result = tryWrap(() =>
interpret(newCode, symbolTable, true, visitors),
);
applicationTerm.last = result;
if (result.error || !result.data) {
console.error(result.error);
@ -166,13 +163,6 @@ export class FunctionApplication extends Entity {
let applicationResultingEntity: Entity | null = null; // this should be its own function
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);
@ -187,20 +177,100 @@ export class FunctionApplication extends Entity {
}
game.removeEntity(entity.id);
if (applicationResultingEntity) {
const grid = applicationResultingEntity.getComponent<Grid>(
ComponentNames.Grid,
);
grid.movingDirection = entityGrid.previousDirection;
applicationResultingEntity.addComponent(grid);
if (!applicationResultingEntity) {
return;
}
applicationResultingEntity.getComponent<Grid>(
ComponentNames.Grid,
).movingDirection = entityGrid.previousDirection;
game.addEntity(applicationResultingEntity);
}
SOUNDS.get(LambdaTransformSound.name)!.play();
private getVisitors(game: Game): {
visitors: Visitors;
symbolTable: SymbolTable;
} {
const directionKeywords = {
_LEFT: Direction.LEFT,
_RIGHT: Direction.RIGHT,
_DOWN: Direction.DOWN,
_UP: Direction.UP,
};
const entityKeywords = {
_KEY: (pos: Coord2D) => new Key(pos),
};
const visitors: Visitors = new Map();
visitors.set("_SPAWN", (_term) => {
const position = this.getComponent<Grid>(
ComponentNames.Grid,
).gridPosition;
return {
abstraction: {
param: "_DIRECTION",
body: (direction) => {
const destinationDirection =
directionKeywords[
(direction as DebrujinIndex)
.name! as keyof typeof directionKeywords
];
const destination = game
.getSystem<GridSystem>(SystemNames.Grid)
.getNewGridPosition(position, destinationDirection);
return {
abstraction: {
param: "_ENTITY",
body: (entityType) => {
const entityFactory =
entityKeywords[
(entityType as DebrujinIndex)
.name! as keyof typeof entityKeywords
];
const newEntity = entityFactory(destination);
game.addEntity(newEntity);
return {
abstraction: {
param: "_x",
body: (_t) => {
return {
application: {
left: {
index: 1,
name: "_SPAWN",
},
args: [direction, entityType],
},
};
},
},
};
},
},
};
},
},
};
});
return {
visitors,
symbolTable: SymbolTable.from(
Array.from(visitors.keys())
.concat(Object.keys(APPLICATION_RESULTS))
.concat(Object.keys(directionKeywords))
.concat(Object.keys(entityKeywords))
.concat(["_x"]),
),
};
}
private addParticles(game: Game, position: Coord2D) {
const gridSystem = game.getSystem<GridSystem>(SystemNames.Grid);
const { dimension } = gridSystem;
SOUNDS.get(LambdaTransformSound.name)!.play();
const particles = new Particles({
center: gridSystem.gridToScreenPosition(nextPosition),
center: gridSystem.gridToScreenPosition(position),
spawnerDimensions: {
width: dimension.width / 2,
height: dimension.height / 2,

View File

@ -9,8 +9,6 @@ import {
Player,
Wall,
} from "../entities";
import { Piston } from "../entities/Piston";
import { Direction } from "../interfaces";
import { Grid, SystemNames } from "../systems";
import { normalRandom } from "../utils";

View File

@ -0,0 +1,45 @@
import { FunctionApplication, Grass, LambdaFactory, Player } from "../entities";
import { Game } from "../Game";
import { Grid, SystemNames } from "../systems";
import { normalRandom } from "../utils";
import { Level } from "./Level";
import { LevelNames } from "./LevelNames";
export class ChurchNumeralsOne extends Level {
constructor() {
super(LevelNames.ChurchNumeralsOne);
}
public init(game: Game) {
const grid = game.getSystem<Grid>(SystemNames.Grid);
const dimensions = grid.getGridDimensions();
const grasses = Array.from({ length: dimensions.width })
.fill(0)
.map(() => {
// random grass
return new Grass({
x: Math.floor(
normalRandom(dimensions.width / 2, dimensions.width / 4, 1.5),
),
y: Math.floor(
normalRandom(dimensions.height / 2, dimensions.height / 4, 1.5),
),
});
});
[
...grasses,
new LambdaFactory({ x: 1, y: 1 }, "(\\ (f) . (\\ (x) . (f f x)))", 1),
new FunctionApplication(
{ x: 2, y: 2 },
"(_INPUT ((_SPAWN _RIGHT) _KEY))",
),
new FunctionApplication(
{ x: 3, y: 3 },
"(_INPUT _EMPTY)",
),
new Player({ x: 0, y: 0 }),
].forEach((e) => game.addEntity(e));
}
}

View File

@ -1,5 +1,6 @@
export namespace LevelNames {
export const Tutorial = "0";
export const CarCadr = "1";
export const ChurchNumeralsOne = "2";
export const LevelSelection = "LevelSelection";
}

View File

@ -36,6 +36,7 @@ export class Tutorial extends Level {
});
});
// TODO: new level which adds introductory syntax
const entities = [
...grasses,
new Sign(
@ -51,7 +52,7 @@ export class Tutorial extends Level {
new Wall({ x: 11, y: 10 }),
new Curry({ x: 10, y: 10 }),
new LockedDoor({ x: 9, y: 10 }),
new LambdaFactory({ x: 6, y: 3 }, "// TODO: Remove line\n(λ (x) . x)", 3),
new LambdaFactory({ x: 6, y: 3 }, "// TODO: Remove this comment\n(λ (x) . x)", 3),
new FunctionApplication({ x: 6, y: 6 }, "(_INPUT _KEY)"),
new Player({ x: 2, y: 2 }),
];

View File

@ -6,13 +6,16 @@ export * from "./CarCadr";
import { LevelNames } from ".";
import { CarCadr, LevelSelection, Tutorial, Level } from ".";
import { ChurchNumeralsOne } from "./ChurchNumeralsOne";
export const LEVELS: Level[] = [
new LevelSelection(),
new Tutorial(),
new CarCadr(),
new ChurchNumeralsOne(),
];
export const LEVEL_PROGRESSION: Record<string, string[]> = {
[LevelNames.LevelSelection]: [LevelNames.Tutorial],
[LevelNames.Tutorial]: [LevelNames.CarCadr],
[LevelNames.CarCadr]: [LevelNames.ChurchNumeralsOne],
};

View File

@ -46,4 +46,10 @@ export class SymbolTable {
public createChild(): SymbolTable {
return new SymbolTable(this);
}
public static from(collection: Array<string> | Set<string>): SymbolTable {
const table = new SymbolTable();
collection.forEach((symbol) => table.add(symbol));
return table;
}
}

View File

@ -11,6 +11,11 @@ export class InvalidLambdaTermError extends Error {}
export class MaxRecursionDepthError extends Error {}
export type Visitors = Map<
string,
(term: DebrujinifiedLambdaTerm) => DebrujinifiedLambdaTerm
>;
export type DebrujinAbstraction = {
abstraction: {
param: string;
@ -28,6 +33,7 @@ export type DebrujinApplication = {
export type DebrujinIndex = { name: string; index: number };
export type DebrujinifiedLambdaTerm =
| ((t: DebrujinifiedLambdaTerm) => DebrujinifiedLambdaTerm)
| DebrujinAbstraction
| DebrujinApplication
| DebrujinIndex;
@ -76,6 +82,10 @@ export const substitute = (
index: number,
withTerm: DebrujinifiedLambdaTerm,
): DebrujinifiedLambdaTerm => {
if (typeof inTerm === "function") {
return inTerm(withTerm);
}
if ("index" in inTerm) {
if (inTerm.index > index) {
return adjustIndices(inTerm, -1);
@ -154,51 +164,45 @@ export const adjustIndices = (
export const betaReduce = (
term: DebrujinifiedLambdaTerm,
visitors: Visitors,
maxDepth: number,
): DebrujinifiedLambdaTerm => {
if (maxDepth === 0) {
throw new MaxRecursionDepthError("max recursion depth identified");
}
if ("index" in term) {
if (typeof term === "function") {
return term;
}
if ("index" in term) {
const replacement = visitors.get(term.name)?.apply(null, [term]);
return replacement ?? term;
}
if ("abstraction" in term) {
const { body, param } = term.abstraction;
return {
abstraction: {
body: betaReduce(body, maxDepth - 1),
body: betaReduce(body, visitors, maxDepth - 1),
param,
},
};
}
if ("application" in term) {
const { left } = term.application;
const args = term.application.args.map((term) =>
betaReduce(term, maxDepth - 1),
const { left, args } = term.application;
const [reducedLeft, ...reducedArgs] = [left, ...args].map((term) =>
betaReduce(term, visitors, maxDepth - 1),
);
return args.reduce((acc: DebrujinifiedLambdaTerm, x) => {
return reducedArgs.reduce((acc: DebrujinifiedLambdaTerm, x) => {
if ("abstraction" in acc) {
const { body } = acc.abstraction;
const newBody = substitute(body, 1, x);
return newBody;
const substituted = substitute(body, 1, x);
return substituted;
}
if ("application" in acc) {
const {
application: { left, args },
} = acc;
return {
application: {
left,
args: [...args, x],
},
};
}
return { application: { left: acc, args: [x] } };
}, left);
return acc;
}, reducedLeft);
}
throw new InvalidLambdaTermError(
@ -207,6 +211,10 @@ export const betaReduce = (
};
export const emitDebrujin = (term: DebrujinifiedLambdaTerm): string => {
if (typeof term === "function") {
return term.toString();
}
if ("index" in term) {
return term.index.toString();
}
@ -250,18 +258,20 @@ export const interpret = (
term: string,
symbolTable = new SymbolTable(),
allowUnderscores = false, // in our world, underscores should be internal to the game.
maxDepth = 15,
visitors: Visitors = new Map(),
maxDepth = 20,
): DebrujinifiedLambdaTerm => {
const ast = parse(term, allowUnderscores);
const debrujined = debrujinify(ast, symbolTable);
let prev = debrujined;
let next = betaReduce(prev, maxDepth);
let next = betaReduce(prev, visitors, maxDepth);
while (emitDebrujin(prev) !== emitDebrujin(next)) {
// alpha equivalence
prev = next;
next = betaReduce(prev, maxDepth);
next = betaReduce(prev, visitors, maxDepth);
}
console.log(next);
return next;
};