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.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>; type Inventory = Partial>; type AnimationTimers = Map; 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(); private deliveredResources = new Map(); 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(); 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()) .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; } } } } }