1
0
Fork 0
tsgames/src/games/binario/world.ts

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