about summary refs log tree commit diff stats
path: root/html/text-world/js
diff options
context:
space:
mode:
Diffstat (limited to 'html/text-world/js')
-rw-r--r--html/text-world/js/b.js126
-rw-r--r--html/text-world/js/ecs.js93
-rw-r--r--html/text-world/js/game.js239
-rw-r--r--html/text-world/js/where.js82
-rw-r--r--html/text-world/js/worldgen.js130
5 files changed, 670 insertions, 0 deletions
diff --git a/html/text-world/js/b.js b/html/text-world/js/b.js
new file mode 100644
index 0000000..3869f62
--- /dev/null
+++ b/html/text-world/js/b.js
@@ -0,0 +1,126 @@
+/*
+//
+//  # The Witch in the Glass
+//  
+//  My mother says I must not pass
+//  Too near that glass;
+//  She is afraid that I will see
+//  A little witch that looks like me,
+//  With a red, red mouth to whisper low
+//  The very thing I should not know!
+//  
+//  Alack for all your mother's care!
+//  A bird of the air,
+//  A wistful wind, or (I suppose
+//  Sent by some hapless boy) a rose,
+//  With breath too sweet, will whisper low
+//  The very thing you should not know!
+//  
+//  - Sarah Morgan Bryan Piatt
+//
+*/
+
+// b is for useful stuff
+
+// curry: Converts a function to its curried form, allowing for partial application.
+// pipe: Runs functions from left to right, passing the result of one to the next.
+// compose: Runs functions from right to left, passing the result of one to the next.
+// identity: Returns whatever you give it as is. Useful for composition...sometimes.
+// match: Creates a curried function to match a regular expression in a string.
+// replace: Creates a curried function to replace parts of a string, based on a regex or substring.
+// filter: Curried array filtering by a predicate function.
+// map: Curried array mapping.
+// deepMap: Curried recursive mapping for nested arrays or matrices, of any shape.
+
+'use strict'
+
+const b = {
+  /**
+   * Converts a function into a curried function, allowing for partial application.
+   * A curried function can be partially applied and will return a new function until all arguments are provided.
+   * @param {Function} fn - The function to curry.
+   * @returns {Function} - The curried function.
+   */
+  curry: function (fn) {
+    const curried = (...args) => {
+      if (args.length >= fn.length)
+        return fn(...args)
+      else
+        return (...rest) => curried(...args, ...rest)
+    }
+    return curried
+  },
+
+  /**
+   * Composes functions from left to right.
+   * Takes any number of functions as arguments and returns a function that applies them in sequence to a supplied value.
+   * @param {...Function} fns - The functions to be composed.
+   * @returns {Function} - A function that takes an initial value and applies the composed functions from left to right.
+   */
+  pipe: (...fns) => (value) => fns.reduce((acc, fn) => fn(acc), value),
+
+  /**
+   * Composes functions from right to left.
+   * Takes any number of functions as arguments and returns a function that applies them in sequence from right to left.
+   * @param {...Function} fns - The functions to be composed.
+   * @returns {Function} - A function that applies the composed functions from right to left.
+   */
+  compose: (...fns) => (...args) => fns.reduceRight((res, fn) => fn(...[].concat(res)), args),
+
+  /**
+   * A function that returns its input totally and completely unchanged.
+   * @param {*} x - The input value.
+   * @returns {*} - The same input value.
+   */
+  identity: x => x,
+
+  /**
+   * Curried function that matches a regular expression against a string.
+   * @returns {Function} - A curried function that takes a regex and a string and returns the match result.
+   */
+  match: function () {
+    return this.curry((what, s) => s.match(what));
+  },
+
+  /**
+   * Curried function that replaces parts of a string based on a regex or substring.
+   * @returns {Function} - A curried function that takes a regex or substring, a replacement, and a string, and returns the replaced string.
+   */
+  replace: function () {
+    return this.curry((what, replacement, s) => s.replace(what, replacement));
+  },
+
+  /**
+   * Curried function that filters an array based on a predicate function.
+   * @returns {Function} - A curried function that takes a predicate function and an array, and returns the filtered array.
+   */
+  filter: function () {
+    return this.curry((f, xs) => xs.filter(f));
+  },
+
+  /**
+   * Curried function that maps a function over an array.
+   * @returns {Function} - A curried function that takes a mapping function and an array, and returns the mapped array.
+   */
+  map: function () {
+    return this.curry((f, xs) => xs.map(f));
+  },
+
+  /**
+   * Curried function that recursively maps a function over an array or matrix of any shape.
+   * @returns {Function} - A curried function that takes a mapping function and a nested array, and returns the deeply mapped array.
+   */
+  deepMap: function () {
+    return this.curry(function deepMap(f, xs) {
+      return Array.isArray(xs)
+        ? xs.map((x) => deepMap(f, x))
+        : f(xs);
+    });
+  },
+};
+
+
+
+if (typeof module !== 'undefined' && module.exports) {
+  module.exports = b;
+}
diff --git a/html/text-world/js/ecs.js b/html/text-world/js/ecs.js
new file mode 100644
index 0000000..c2c5add
--- /dev/null
+++ b/html/text-world/js/ecs.js
@@ -0,0 +1,93 @@
+'use strict';
+
+// Components remain as simple data structures
+class Component {
+    constructor(data = {}) {
+        Object.assign(this, data);
+    }
+}
+
+class Position extends Component {
+    constructor(x = 0, y = 0) {
+        super({ x, y });
+    }
+}
+
+class Description extends Component {
+    constructor(short = "", long = "", explorable = {}) {
+        super({ short, long, explorable });
+    }
+}
+
+class Inventory extends Component {
+    constructor(items = []) {
+        super({ items });
+    }
+}
+
+class Messages extends Component {
+    constructor(messages = []) {
+        super({ messages });
+    }
+}
+
+class Item extends Component {
+    constructor(name, description, collectable = true) {
+        super({ name, description, collectable });
+    }
+}
+
+// World becomes a pure functional interface
+const World = {
+    create() {
+        return {
+            entities: new Map(),
+            nextEntityId: 1
+        };
+    },
+
+    createEntity(world) {
+        const id = world.nextEntityId;
+        const newEntities = new Map(world.entities);
+        newEntities.set(id, new Map());
+        
+        return {
+            ...world,
+            entities: newEntities,
+            nextEntityId: id + 1
+        };
+    },
+
+    addComponent(world, entityId, component) {
+        if (!world.entities.has(entityId)) return world;
+        
+        const newEntities = new Map(world.entities);
+        const entityComponents = new Map(world.entities.get(entityId));
+        entityComponents.set(component.constructor.name, component);
+        newEntities.set(entityId, entityComponents);
+        
+        return {
+            ...world,
+            entities: newEntities
+        };
+    },
+
+    getComponent(world, entityId, componentType) {
+        if (!world.entities.has(entityId)) return null;
+        return world.entities.get(entityId).get(componentType);
+    },
+
+    removeComponent(world, entityId, componentType) {
+        if (!world.entities.has(entityId)) return world;
+        
+        const newEntities = new Map(world.entities);
+        const entityComponents = new Map(world.entities.get(entityId));
+        entityComponents.delete(componentType);
+        newEntities.set(entityId, entityComponents);
+        
+        return {
+            ...world,
+            entities: newEntities
+        };
+    }
+};
diff --git a/html/text-world/js/game.js b/html/text-world/js/game.js
new file mode 100644
index 0000000..8f2d4d6
--- /dev/null
+++ b/html/text-world/js/game.js
@@ -0,0 +1,239 @@
+class Game {
+    constructor() {
+        const generated = WorldGenerator.generate();
+        this.state = {
+            world: generated.world,
+            playerId: generated.playerId
+        };
+        
+        this.setupUI();
+        this.print("Welcome to the Text Adventure!");
+        this.describeCurrentLocation();
+    }
+
+    setState(newState) {
+        this.state = newState;
+    }
+
+    setupUI() {
+        this.output = document.getElementById('output');
+        this.input = document.getElementById('input');
+        
+        this.input.addEventListener('keypress', (e) => {
+            if (e.key === 'Enter') {
+                const command = this.input.value.toLowerCase();
+                this.input.value = '';
+                this.handleCommand(command);
+            }
+        });
+    }
+
+    print(text) {
+        const line = document.createElement('div');
+        line.textContent = text;
+        this.output.appendChild(line);
+        this.output.scrollTop = this.output.scrollHeight;
+    }
+
+    handleCommand(command) {
+        this.print(`> ${command}`);
+
+        if (command.match(/^(where am i|look around)$/)) {
+            this.describeCurrentLocation();
+        }
+        else if (command.startsWith('move ')) {
+            const newState = this.handleMove(command.split(' ')[1]);
+            if (newState) {
+                this.setState(newState);
+                this.describeCurrentLocation();
+            }
+        }
+        else if (command.startsWith('explore ') || command.startsWith('look at ')) {
+            const target = command.replace(/^(explore|look at) /, '');
+            this.exploreTarget(target);
+        }
+        else if (command === 'phone') {
+            this.checkMessages();
+        }
+        else if (command === 'inventory') {
+            this.checkInventory();
+        }
+        else if (command.startsWith('get ') || command.startsWith('take ')) {
+            const itemName = command.replace(/^(get|take) /, '');
+            const newState = this.collectItem(itemName);
+            if (newState) {
+                this.setState(newState);
+            }
+        }
+        else {
+            this.print("I don't understand that command.");
+        }
+    }
+
+    getCurrentLocation() {
+        const playerPos = World.getComponent(this.state.world, this.state.playerId, 'Position');
+        
+        if (!playerPos) {
+            console.error('Player position not found');
+            return null;
+        }
+        
+        // Find the environment at the player's position
+        const locationEntity = Array.from(this.state.world.entities.entries()).find(([entityId, _]) => {
+            if (entityId === this.state.playerId) return false;
+            
+            const pos = World.getComponent(this.state.world, entityId, 'Position');
+            return pos && pos.x === playerPos.x && pos.y === playerPos.y;
+        });
+
+        if (!locationEntity) return null;
+
+        const [entityId, _] = locationEntity;
+        const desc = World.getComponent(this.state.world, entityId, 'Description');
+        
+        // Add items to the location description
+        const items = this.getItemsAtLocation(playerPos);
+        return { entityId, desc, items };
+    }
+
+    getItemsAtLocation(pos) {
+        return Array.from(this.state.world.entities.entries())
+            .filter(([entityId, _]) => {
+                const itemPos = World.getComponent(this.state.world, entityId, 'Position');
+                const item = World.getComponent(this.state.world, entityId, 'Item');
+                return itemPos && item && 
+                       itemPos.x === pos.x && 
+                       itemPos.y === pos.y;
+            })
+            .map(([entityId, _]) => ({
+                entityId,
+                item: World.getComponent(this.state.world, entityId, 'Item')
+            }));
+    }
+
+    describeCurrentLocation() {
+        const location = this.getCurrentLocation();
+        if (location) {
+            this.print(`You are in: ${location.desc.short}`);
+            this.print(location.desc.long);
+            
+            if (location.items.length > 0) {
+                this.print("\nYou can see:");
+                location.items.forEach(({ item }) => {
+                    this.print(`- ${item.name}`);
+                });
+            }
+        }
+    }
+
+    handleMove(direction) {
+        const playerPos = World.getComponent(this.state.world, this.state.playerId, 'Position');
+        
+        if (!playerPos) {
+            console.error('Player position not found');
+            return null;
+        }
+
+        const moves = {
+            'north': { x: 0, y: -1 },
+            'south': { x: 0, y: 1 },
+            'east': { x: 1, y: 0 },
+            'west': { x: -1, y: 0 }
+        };
+
+        if (!moves[direction]) {
+            this.print("Invalid direction. Use north, south, east, or west.");
+            return null;
+        }
+
+        const newX = playerPos.x + moves[direction].x;
+        const newY = playerPos.y + moves[direction].y;
+
+        if (newX < 0 || newX >= 10 || newY < 0 || newY >= 10) {
+            this.print("You can't go that way - you've reached the edge of the world.");
+            return null;
+        }
+
+        const newWorld = World.addComponent(
+            this.state.world,
+            this.state.playerId,
+            new Position(newX, newY)
+        );
+
+        this.print(`You move ${direction}.`);
+        
+        return {
+            ...this.state,
+            world: newWorld
+        };
+    }
+
+    exploreTarget(target) {
+        const location = this.getCurrentLocation();
+        if (location && location.desc.explorable[target]) {
+            this.print(location.desc.explorable[target]);
+        } else {
+            this.print("There's nothing special about that.");
+        }
+    }
+
+    checkMessages() {
+        const messages = World.getComponent(this.state.world, this.state.playerId, 'Messages');
+        if (messages.messages.length === 0) {
+            this.print("You have no messages.");
+        } else {
+            this.print("Your messages:");
+            messages.messages.forEach(msg => this.print(`- ${msg}`));
+        }
+    }
+
+    collectItem(itemName) {
+        const playerPos = World.getComponent(this.state.world, this.state.playerId, 'Position');
+        const items = this.getItemsAtLocation(playerPos);
+        
+        const itemEntry = items.find(({ item }) => 
+            item.name.toLowerCase() === itemName.toLowerCase()
+        );
+
+        if (!itemEntry) {
+            this.print(`There is no ${itemName} here.`);
+            return null;
+        }
+
+        if (!itemEntry.item.collectable) {
+            this.print(`You can't take the ${itemName}.`);
+            return null;
+        }
+
+        // Add to inventory
+        const inventory = World.getComponent(this.state.world, this.state.playerId, 'Inventory');
+        const updatedInventory = new Inventory([...inventory.items, itemEntry.item]);
+        
+        // Remove from world and update inventory
+        let newWorld = World.removeComponent(this.state.world, itemEntry.entityId, 'Position');
+        newWorld = World.addComponent(newWorld, this.state.playerId, updatedInventory);
+
+        this.print(`You take the ${itemName}.`);
+        return {
+            ...this.state,
+            world: newWorld
+        };
+    }
+
+    checkInventory() {
+        const inventory = World.getComponent(this.state.world, this.state.playerId, 'Inventory');
+        if (inventory.items.length === 0) {
+            this.print("Your inventory is empty.");
+        } else {
+            this.print("Your inventory contains:");
+            inventory.items.forEach(item => {
+                this.print(`- ${item.name}`);
+            });
+        }
+    }
+}
+
+// Start the game when the page loads
+window.addEventListener('load', () => {
+    window.game = new Game();
+});
diff --git a/html/text-world/js/where.js b/html/text-world/js/where.js
new file mode 100644
index 0000000..60e3797
--- /dev/null
+++ b/html/text-world/js/where.js
@@ -0,0 +1,82 @@
+/**
+ * A mostly functional query filter similar to SQL's WHERE
+ * @param {Array} collection - Array of objects to filter
+ * @param {Object} properties - Object containing properties to match against
+ * @returns {Array} - Filtered array of objects
+ */
+const where = (collection, properties) => {
+
+    if (!collection || !properties) {
+        return [];
+    }
+
+    const propertyPaths = Object.entries(properties).map(([key, value]) => ({
+        path: key,
+        parts: key.split('.'),
+        value
+    }));
+
+    const getNestedValue = (() => {
+        const cache = new WeakMap();
+        
+        return (obj, parts) => {
+            if (!obj) return undefined;
+            
+            let cached = cache.get(obj);
+            if (!cached) {
+                cached = new Map();
+                cache.set(obj, cached);
+            }
+            
+            const pathKey = parts.join('.');
+            if (cached.has(pathKey)) {
+                return cached.get(pathKey);
+            }
+
+            const value = parts.reduce((current, key) => 
+                current && current[key] !== undefined ? current[key] : undefined, 
+                obj
+            );
+            
+            cached.set(pathKey, value);
+            return value;
+        };
+    })();
+
+    const isEqual = (value1, value2) => {
+
+        if (value2 === undefined) return true;
+        if (value1 === value2) return true;
+        if (value1 === null || value2 === null) return false;
+
+        if (Array.isArray(value1) && Array.isArray(value2)) {
+            return value1.length === value2.length &&
+                   value1.every((val, idx) => isEqual(val, value2[idx]));
+        }
+
+        if (typeof value2 === 'object') {
+            return Object.entries(value2).every(([key, val]) => 
+                value1 && isEqual(value1[key], val)
+            );
+        }
+
+        return false;
+    };
+
+    const matchesProperties = item => 
+        propertyPaths.every(({ parts, value }) => 
+            isEqual(getNestedValue(item, parts), value)
+        );
+
+    if (!Array.isArray(collection) || typeof properties !== 'object') {
+        return [];
+    }
+
+    return collection.filter(matchesProperties);
+};
+
+
+
+if (typeof module !== 'undefined' && module.exports) {
+    module.exports = where;
+}
diff --git a/html/text-world/js/worldgen.js b/html/text-world/js/worldgen.js
new file mode 100644
index 0000000..c8f5778
--- /dev/null
+++ b/html/text-world/js/worldgen.js
@@ -0,0 +1,130 @@
+'use strict';
+
+const WorldGenerator = {
+    generate() {
+        let world = World.create();
+        
+        // Create player
+        let result = WorldGenerator.createPlayer(world);
+        world = result.world;
+        const playerId = result.entityId;
+
+        // Generate environments
+        world = WorldGenerator.generateEnvironments(world);
+
+        return { world, playerId };
+    },
+
+    createPlayer(world) {
+        const newWorld = World.createEntity(world);
+        const entityId = newWorld.nextEntityId - 1;
+        
+        return {
+            world: World.addComponent(
+                World.addComponent(
+                    World.addComponent(
+                        newWorld,
+                        entityId,
+                        new Position(0, 0)
+                    ),
+                    entityId,
+                    new Inventory([])
+                ),
+                entityId,
+                new Messages([])
+            ),
+            entityId
+        };
+    },
+
+    generateEnvironments(world) {
+        let newWorld = world;
+        
+        for (let x = 0; x < 10; x++) {
+            for (let y = 0; y < 10; y++) {
+                newWorld = WorldGenerator.createEnvironment(newWorld, x, y);
+            }
+        }
+        
+        return newWorld;
+    },
+
+    createEnvironment(world, x, y) {
+        const newWorld = World.createEntity(world);
+        const entityId = newWorld.nextEntityId - 1;
+        
+        // Add basic components
+        let updatedWorld = World.addComponent(
+            World.addComponent(
+                newWorld,
+                entityId,
+                new Position(x, y)
+            ),
+            entityId,
+            this.generateEnvironmentDescription(x, y)
+        );
+
+        // Add items based on environment type
+        const items = this.generateItems(x, y);
+        items.forEach(item => {
+            const itemEntity = World.createEntity(updatedWorld);
+            updatedWorld = World.addComponent(
+                World.addComponent(
+                    updatedWorld,
+                    itemEntity,
+                    new Position(x, y)
+                ),
+                itemEntity,
+                item
+            );
+        });
+
+        return updatedWorld;
+    },
+
+    generateEnvironmentDescription(x, y) {
+        const environments = [
+            {
+                short: "Forest Clearing",
+                long: "A peaceful clearing surrounded by tall trees. Sunlight filters through the canopy above.",
+                explorable: {
+                    "trees": "The trees here are ancient oaks, their bark rough and weathered.",
+                    "ground": "The forest floor is covered in soft moss and fallen leaves."
+                }
+            },
+            {
+                short: "Rocky Outcrop",
+                long: "A rocky area with large boulders scattered about. Some seem to hide small caves.",
+                explorable: {
+                    "boulders": "The boulders are covered in colorful lichens.",
+                    "caves": "Small openings that could shelter wildlife."
+                }
+            }
+        ];
+
+        const index = (x * 7 + y * 13) % environments.length;
+        return new Description(
+            environments[index].short,
+            environments[index].long,
+            environments[index].explorable
+        );
+    },
+
+    generateItems(x, y) {
+        const items = [
+            new Item("mushroom", "A small, colorful mushroom that might be edible."),
+            new Item("stone", "A smooth, flat stone that catches your eye."),
+            new Item("stick", "A sturdy wooden stick."),
+            new Item("flower", "A beautiful wildflower with vibrant petals."),
+            new Item("feather", "A delicate feather from some unknown bird.")
+        ];
+
+        // Use coordinates to deterministically generate items
+        const numItems = ((x * 3 + y * 5) % 3) + 1; // 1-3 items per location
+        const startIndex = (x * 7 + y * 11) % items.length;
+        
+        return Array.from({ length: numItems }, (_, i) => 
+            items[(startIndex + i) % items.length]
+        );
+    }
+};