452 lines
17 KiB
TypeScript
452 lines
17 KiB
TypeScript
import { ALL_DIRECTIONS, Direction, NEXT_DIRECTION, cyrb32, exp, getDirection, getOppositeDirection, isDirection, movePoint, pointsEquals, trunc } from "./utils";
|
|
|
|
export enum TileType {
|
|
DESTINATION,
|
|
SOURCE,
|
|
EXTRACTOR,
|
|
CONVEYOR,
|
|
NOT,
|
|
AND,
|
|
OR,
|
|
}
|
|
|
|
export enum PortDirection {
|
|
INPUT,
|
|
OUTPUT
|
|
}
|
|
|
|
export enum ResourceType {
|
|
NUMBER,
|
|
ITEM,
|
|
}
|
|
|
|
export enum ResourceItemType {
|
|
|
|
}
|
|
|
|
interface TileLimit {
|
|
cooldown: number;
|
|
resourcesRequired: number;
|
|
capacity: number;
|
|
maxOutputs: number;
|
|
}
|
|
|
|
export const LIMITS: Record<TileType, TileLimit> = {
|
|
[TileType.CONVEYOR]: { cooldown: 300, resourcesRequired: 1, capacity: 1, maxOutputs: 4 },
|
|
|
|
[TileType.DESTINATION]: { cooldown: 0, resourcesRequired: 1, capacity: 4, maxOutputs: 0 },
|
|
[TileType.SOURCE]: { cooldown: 10000, resourcesRequired: 0, capacity: 0, maxOutputs: 0 },
|
|
[TileType.EXTRACTOR]: { cooldown: 300, resourcesRequired: 0, capacity: 0, maxOutputs: 4 },
|
|
[TileType.NOT]: { cooldown: 3000, resourcesRequired: 1, capacity: 1, maxOutputs: 1 },
|
|
[TileType.AND]: { cooldown: 6000, resourcesRequired: 2, capacity: 2, maxOutputs: 1 },
|
|
[TileType.OR]: { cooldown: 3000, resourcesRequired: 2, capacity: 2, maxOutputs: 1 },
|
|
};
|
|
|
|
interface BaseResource {
|
|
}
|
|
|
|
interface ResourceNumber extends BaseResource {
|
|
type: ResourceType.NUMBER;
|
|
value: number;
|
|
}
|
|
|
|
interface ResourceItem extends BaseResource {
|
|
type: ResourceType.ITEM;
|
|
itemType: ResourceItemType;
|
|
}
|
|
|
|
export type Resource = ResourceNumber | ResourceItem;
|
|
|
|
interface Port {
|
|
direction: PortDirection;
|
|
}
|
|
|
|
type Ports = Partial<Record<Direction, Port>>;
|
|
type Inventory = Partial<Record<Direction, Resource>>;
|
|
type AnimationTimers = Map<Direction, [number, number]>;
|
|
|
|
interface BaseTile {
|
|
ports: Ports;
|
|
inv: Inventory;
|
|
|
|
acceptedResources?: ResourceType[];
|
|
|
|
inputAnimations?: AnimationTimers;
|
|
|
|
bufferedDirections?: Direction[];
|
|
nextInput?: Direction;
|
|
nextOutput?: Direction;
|
|
timer?: number;
|
|
}
|
|
|
|
interface TileDestination extends BaseTile {
|
|
type: TileType.DESTINATION;
|
|
center: boolean;
|
|
}
|
|
|
|
interface TileSource extends BaseTile {
|
|
type: TileType.SOURCE;
|
|
resource: Resource;
|
|
}
|
|
|
|
interface TileExtractor extends BaseTile {
|
|
type: TileType.EXTRACTOR;
|
|
source: TileSource;
|
|
}
|
|
|
|
interface TileConveyor extends BaseTile {
|
|
type: TileType.CONVEYOR;
|
|
}
|
|
|
|
interface TileUnaryLogic extends BaseTile {
|
|
type: TileType.NOT;
|
|
}
|
|
|
|
interface TileBinaryLogic extends BaseTile {
|
|
type: TileType.AND | TileType.OR;
|
|
// TODO internal buffers
|
|
}
|
|
|
|
export type Tile = TileDestination | TileSource | TileExtractor | TileConveyor | TileUnaryLogic | TileBinaryLogic;
|
|
|
|
const id = (point: Point) => ((Math.floor(point[0]) & 0xFFFF) << 16) | Math.floor(point[1]) & 0xFFFF;
|
|
const deid = (pid: number): Point => [(pid >> 16) & 0xFFFF, pid & 0xFFFF];
|
|
|
|
const rid = (resource: Resource): number => {
|
|
const idPart = (resource.type & 0xFFFF) << 16;
|
|
if (resource.type === ResourceType.NUMBER) {
|
|
return idPart | (resource.value) & 0xFFFF;
|
|
} else if (resource.type === ResourceType.ITEM) {
|
|
return idPart | (resource.itemType) & 0xFFFF;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
export const getPortDirections = (ports: Ports) => Object.keys(ports).map(k => +k as Direction);
|
|
|
|
export const getTileInputs = (tile: Tile) => ALL_DIRECTIONS.map(d => tile.inv[d]).filter(r => r != null);
|
|
export const getTileOutput = (tile: Tile): [Resource | undefined, Direction[]] => {
|
|
let bufferedResource = tile.inv[Direction.NONE];
|
|
let inputDirections: Direction[] = tile.bufferedDirections ?? [];
|
|
const availableResources = getTileInputs(tile);
|
|
const limits = LIMITS[tile.type];
|
|
if (availableResources.length < limits.resourcesRequired) {
|
|
return [undefined, []];
|
|
}
|
|
|
|
if (bufferedResource == null) {
|
|
if (tile.type === TileType.EXTRACTOR) {
|
|
bufferedResource = tile.source.resource;
|
|
} else if (limits.resourcesRequired === 1) {
|
|
const inputDirection = ALL_DIRECTIONS.find(d => typeof tile.inv[d] !== 'undefined');
|
|
if (inputDirection) {
|
|
let resource = tile.inv[inputDirection];
|
|
if (tile.type === TileType.NOT && resource?.type === ResourceType.NUMBER) {
|
|
resource = { ...resource, value: ~(resource.value) & 0xF };
|
|
}
|
|
inputDirections = [inputDirection];
|
|
bufferedResource = resource;
|
|
}
|
|
} else if (limits.resourcesRequired === 2) {
|
|
inputDirections = ALL_DIRECTIONS.filter(d => typeof tile.inv[d] !== 'undefined').slice(0, 2);
|
|
const [x, y] = inputDirections.map(d => tile.inv[d]);
|
|
if (x?.type === ResourceType.NUMBER && y?.type === ResourceType.NUMBER) {
|
|
switch (tile.type) {
|
|
case TileType.AND:
|
|
bufferedResource = { type: ResourceType.NUMBER, value: (x.value & y.value) & 0xF };
|
|
break;
|
|
case TileType.OR:
|
|
bufferedResource = { type: ResourceType.NUMBER, value: (x.value | y.value) & 0xF };
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
tile.inv[Direction.NONE] = bufferedResource;
|
|
tile.bufferedDirections = inputDirections;
|
|
}
|
|
return [bufferedResource, [Direction.NONE, ...inputDirections]];
|
|
}
|
|
|
|
const findNextPort = (ports: Ports, portDirection: PortDirection, prevDirection: Direction | undefined): Direction => {
|
|
const outputs = getPortDirections(ports).filter(d => d && ports[d]?.direction === portDirection);
|
|
if (outputs.length === 0) return Direction.NONE;
|
|
|
|
if (prevDirection) {
|
|
let nextDirection = NEXT_DIRECTION[prevDirection];
|
|
while (nextDirection !== prevDirection) {
|
|
if (outputs.includes(nextDirection)) break;
|
|
nextDirection = NEXT_DIRECTION[nextDirection]
|
|
}
|
|
return nextDirection;
|
|
} else {
|
|
return outputs[0];
|
|
}
|
|
}
|
|
|
|
export default class World {
|
|
private world = new Map<number, Tile>();
|
|
private deliveredResources = new Map<number, number>();
|
|
constructor(private seed: number = Math.random() * 2e9) {
|
|
for (let x = 0; x < 5; x++) {
|
|
for (let y = 0; y < 5; y++) {
|
|
const ports: Ports = {};
|
|
if (y == 0) ports[Direction.NORTH] = { direction: PortDirection.INPUT };
|
|
if (y == 4) ports[Direction.SOUTH] = { direction: PortDirection.INPUT };
|
|
if (x == 0) ports[Direction.WEST] = { direction: PortDirection.INPUT };
|
|
if (x == 4) ports[Direction.EAST] = { direction: PortDirection.INPUT };
|
|
|
|
this.setTile([x - 2, y - 2], {
|
|
type: TileType.DESTINATION,
|
|
ports,
|
|
center: x == 2 && y == 2,
|
|
inv: {},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
placeTile(position: Point, type: TileType, prevPosition: Point | null = null) {
|
|
position = trunc(position);
|
|
if (prevPosition) {
|
|
prevPosition = trunc(prevPosition);
|
|
}
|
|
let tile: Tile | undefined;
|
|
const existingTile = this.getTile(position);
|
|
switch (type) {
|
|
case TileType.CONVEYOR:
|
|
case TileType.NOT:
|
|
case TileType.AND:
|
|
case TileType.OR:
|
|
if (prevPosition) {
|
|
const prevPositionTile = this.getTile(prevPosition);
|
|
const ports: Ports = {};
|
|
const direction = getDirection(exp`${position} - ${prevPosition}`);
|
|
const oppositeDirection = getOppositeDirection(direction);
|
|
|
|
if (prevPositionTile) {
|
|
if (!prevPositionTile.ports[direction]) {
|
|
prevPositionTile.ports[direction] = { direction: PortDirection.OUTPUT };
|
|
}
|
|
ports[oppositeDirection] = { direction: PortDirection.INPUT };
|
|
}
|
|
if (existingTile) {
|
|
if (!existingTile.ports[oppositeDirection]) {
|
|
existingTile.ports[oppositeDirection] = { direction: PortDirection.INPUT };
|
|
}
|
|
} else {
|
|
tile = {
|
|
type,
|
|
ports,
|
|
inv: {},
|
|
}
|
|
}
|
|
} else if (!existingTile) {
|
|
const ports = this.connectPorts(position);
|
|
tile = {
|
|
type,
|
|
ports,
|
|
inv: {},
|
|
};
|
|
}
|
|
break;
|
|
case TileType.EXTRACTOR:
|
|
if (existingTile?.type === TileType.SOURCE) {
|
|
const ports = this.connectPorts(position, PortDirection.OUTPUT);
|
|
|
|
tile = {
|
|
type: TileType.EXTRACTOR,
|
|
ports,
|
|
source: existingTile,
|
|
inv: {},
|
|
};
|
|
}
|
|
break;
|
|
case TileType.DESTINATION:
|
|
case TileType.SOURCE:
|
|
// Naturally generated only
|
|
break;
|
|
default:
|
|
if (!existingTile) {
|
|
tile = {
|
|
type,
|
|
ports: {},
|
|
inv: {},
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (tile) {
|
|
this.setTile(position, tile);
|
|
}
|
|
}
|
|
|
|
private setTile(position: Point, tile: Tile) {
|
|
this.world.set(id(position), tile);
|
|
}
|
|
|
|
private connectPorts(position: Point, portDirection = PortDirection.INPUT): Ports {
|
|
const ports: Ports = {};
|
|
for (const direction of ALL_DIRECTIONS) {
|
|
const [neighbour, oppositeDirection] = this.getNeighbour(position, direction);
|
|
if (neighbour && neighbour.type !== TileType.SOURCE && neighbour.type !== TileType.DESTINATION) {
|
|
if (!neighbour.ports[oppositeDirection]) {
|
|
neighbour.ports[oppositeDirection] = {
|
|
direction: portDirection === PortDirection.OUTPUT
|
|
? PortDirection.INPUT
|
|
: PortDirection.OUTPUT
|
|
};
|
|
}
|
|
ports[direction] = { direction: portDirection };
|
|
}
|
|
}
|
|
return ports;
|
|
}
|
|
|
|
removeTile(position: Point) {
|
|
position = trunc(position);
|
|
const pid = id(position);
|
|
const existingTile = this.world.get(pid);
|
|
const type = existingTile?.type;
|
|
if (type === TileType.DESTINATION) {
|
|
return;
|
|
}
|
|
if (existingTile) {
|
|
for (const direction of getPortDirections(existingTile.ports)) {
|
|
const [neighbour, oppositeDirection] = this.getNeighbour(position, direction);
|
|
if (neighbour) {
|
|
delete neighbour.ports[oppositeDirection];
|
|
delete neighbour.inv[oppositeDirection];
|
|
if (neighbour.nextInput === oppositeDirection) {
|
|
neighbour.nextInput = findNextPort(neighbour.ports, PortDirection.INPUT, neighbour.nextInput);
|
|
}
|
|
if (neighbour.nextOutput === oppositeDirection) {
|
|
neighbour.nextOutput = findNextPort(neighbour.ports, PortDirection.OUTPUT, neighbour.nextOutput);
|
|
}
|
|
}
|
|
}
|
|
this.world.delete(pid);
|
|
}
|
|
}
|
|
|
|
getTile(position: Point): Tile | null {
|
|
const [x, y] = position;
|
|
const pid = id(position);
|
|
const tile = this.world.get(pid);
|
|
if (tile) return tile;
|
|
|
|
if (Math.abs(x) >= 3 && Math.abs(y) >= 3) {
|
|
return this.genTile(trunc(position));
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private genTile(position: Point): Tile | null {
|
|
const hash = cyrb32(this.seed, ...position);
|
|
|
|
if ([42, 69, 0x42, 0x69].includes(hash & 0xFF)) {
|
|
let mask = 1;
|
|
const dist = Math.log10(Math.hypot(...position));
|
|
for (let i = 1; i < dist; i++) {
|
|
mask = (mask << 1) | 1;
|
|
}
|
|
const value = (hash >> 12) & mask;
|
|
const newTile: Tile = { type: TileType.SOURCE, resource: { type: ResourceType.NUMBER, value }, ports: {}, inv: {} };
|
|
return newTile;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private get tiles() {
|
|
return this.world.entries();
|
|
}
|
|
|
|
private getNeighbour(position: Point, direction: Direction | undefined): [Tile | null, Direction, Point] {
|
|
if (!direction) {
|
|
return [this.genTile(position), Direction.NONE, position];
|
|
}
|
|
const neighbourPosition = movePoint(position, direction);
|
|
const neighbour = this.getTile(neighbourPosition);
|
|
const oppositeDirection = getOppositeDirection(direction);
|
|
|
|
return [neighbour, oppositeDirection, neighbourPosition];
|
|
}
|
|
|
|
update(dt: number) {
|
|
for (const [pid, tile] of this.tiles) {
|
|
if (!tile.nextInput) {
|
|
tile.nextInput = findNextPort(tile.ports, PortDirection.INPUT, tile.nextInput);
|
|
}
|
|
if (!tile.nextOutput) {
|
|
tile.nextOutput = findNextPort(tile.ports, PortDirection.OUTPUT, tile.nextOutput);
|
|
}
|
|
const position = deid(pid);
|
|
tile.timer = Math.max(0, (tile.timer ?? 0) - dt);
|
|
|
|
const animationTimers: AnimationTimers = tile.inputAnimations ?? new Map<Direction, [number, number]>();
|
|
for (const [direction, [timer, timerMax]] of animationTimers) {
|
|
animationTimers.set(direction, [Math.max(0, timer - dt), timerMax]);
|
|
}
|
|
tile.inputAnimations = animationTimers;
|
|
|
|
if (!tile.timer || tile.timer <= 0) {
|
|
switch (tile.type) {
|
|
case TileType.DESTINATION:
|
|
const isInputting = Array.from(tile.inputAnimations?.values() ?? []).some(a => a[0] > 0);
|
|
if (!isInputting) {
|
|
for (const resource of Object.values(tile.inv)) {
|
|
const resourceId = rid(resource);
|
|
const gatheredAmount = this.deliveredResources.get(resourceId) ?? 0;
|
|
this.deliveredResources.set(resourceId, gatheredAmount + 1);
|
|
}
|
|
tile.inv = {};
|
|
}
|
|
break;
|
|
case TileType.SOURCE:
|
|
break; // source itself does nothing
|
|
default:
|
|
const [resource, inputDirections] = getTileOutput(tile);
|
|
if (resource != null) {
|
|
tile.nextOutput = findNextPort(tile.ports, PortDirection.OUTPUT, tile.nextOutput);
|
|
if (tile.nextOutput) {
|
|
const [neighbour, inputDirection, neighbourPosition] = this.getNeighbour(position, tile.nextOutput);
|
|
if (neighbour) {
|
|
const [priorityPusher,] = this.getNeighbour(neighbourPosition, neighbour.nextInput);
|
|
const neighbourLimits = LIMITS[neighbour.type];
|
|
const limits = LIMITS[tile.type];
|
|
|
|
if (
|
|
neighbour.ports[inputDirection]?.direction === PortDirection.INPUT
|
|
&& neighbour.inv[inputDirection] == null
|
|
&& getTileInputs(neighbour).length < neighbourLimits.capacity
|
|
&& (
|
|
neighbour.nextInput == null
|
|
|| neighbour.nextInput === inputDirection
|
|
|| !priorityPusher
|
|
|| getTileOutput(priorityPusher)[0] == null
|
|
|| neighbourLimits.resourcesRequired > 1
|
|
)
|
|
) {
|
|
neighbour.inv[inputDirection] = resource;
|
|
neighbour.inputAnimations =
|
|
(neighbour.inputAnimations ?? new Map<Direction, [number, number]>())
|
|
.set(inputDirection, [LIMITS[TileType.CONVEYOR].cooldown, LIMITS[TileType.CONVEYOR].cooldown]);
|
|
if (getTileInputs(neighbour).length >= neighbourLimits.capacity) {
|
|
neighbour.timer = neighbourLimits.cooldown;
|
|
neighbour.nextInput = findNextPort(neighbour.ports, PortDirection.INPUT, neighbour.nextInput);
|
|
}
|
|
|
|
inputDirections.forEach(inputDirection => delete tile.inv[inputDirection]);
|
|
tile.timer = limits.cooldown;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |