Herd Game: Code Structure

Why a herding game?

I was reading the book, AI in Games by Ian Millington (2019). I didn't finish the book, getting through the first three chapters, which is not much if you check, just about 7%, but it is in my immediate queue for reading, apart from Grokking Functional Programming by Michał Płachta, Nature of Code by Daniel Shiffman, and the Systems Approach series.

In his book, Millington mentions a herding game as an example of AI that its players deemed as stupid, namely because the creatures would get stuck in places that should have been easy for a creature to navigate out of. For example, if the creature went into a space surrounded by three rocks, the creature would not be able to get itself out.

This section of the book was about how players perceive AI, when the AI is rather simple. It isn't an actual matter of the AI being complex or being smart with anticipation. Players were likely to consider an AI as smart for its actions at random but crucial moments in the game, such as a randomly-implemented sidestep when a player fires at an enemy, causing the player to miss. At the time that it came out, the same could be said of the ghosts in Pacman.

Having read the prologue and first two chapters of Nature of Code, before having to reconsider my personal priorities (sadly), I was interested in the algorithms for the creatures in a herding game, thus it was added to my projects list, and that remains my only interest here.

Structures

Initially, my task the last week was to develop the algorithms for basic targeting behaviors. I was deriving some options from the Pacman ghosts. Red targets the position of the player, Pink targets an offset of 4 paces from the player, Blue targets an offset of its and the player's positions, and Orange targets the nearest corner. Since the creatures need to evade (or follow) the player, I sought to modify these behaviors accordingly. But I found myself annoyed by the project's code structure.

The initial setup used classes.

Creature Class and Red Creature derivative

class Creature {
    constructor({ x, y, size, speed, color, last_position, proximity }) {
        this.x = x
        this.y = y
        this.size = size
        this.speed = speed
        this.color = color
        this.proximity = proximity
        this.last_position = last_position
    }

    move({ x, y }) {
        this.x = x
        this.y = y
    }

    render(p) {
        p.fill(...this.color)
        p.noStroke()
        p.circle(this.x, this.y, this.size)
    }
}

class RedCreature extends Creature {
    constructor({ x, y }, { x: px, y: py }) {
        super({ 
            x, y, size: 10, speed: 5, color: [255, 0, 0],
            proximity: 50, last_position: { x: px, y: py }
        })
    }

    updatePosition({ x, y }, { width, height }) {}
}

Creature Prototype and Red Creature derivative

function Creature({ x, y, size, speed, color, last_position, proximity }) {
    this.x = x
    this.y = y
    this.size = size
    this.speed = speed
    this.color = color
    this.last_position = last_position
    this.proximity = proximity
}

Creature.prototype.move = function({ x, y }) {
    this.x = x
    this.y = y
}

Creature.prototype.render = function(p) {
    p.fill(...this.color)
    p.noStroke()
    p.circle(this.x, this,y, this.size)
}

function RedCreature({ x, y }, { x: px, y: py }) {
    Creature.call(this, { x, y, size: 10, speed: 5, 
        color: [255, 0, 0],
        last_position: { x: px, y: py },
        proximity: 50
    })
}

RedCreature.prototype.updatePosition = function({ x, y }, { width, height }) {}

What bothered me about this was the redundancy. Notice that when a red creature is instantiated, whether using new Class() or new Function() , it forwards the args, as well as its own presets, to the creature prototype.

Not to mention, when you use a class or constructor function to instantiate an object, even if the prototype chain is not instantiated, an object carries all the data, meaning fields and methods, of every prototype in its chain, nested in its __proto__ field. This means that when a red creature is instantiated, it has all of its data, the data of the Creature prototype, and the data of the Object prototype., the prototype is a function instance. For every prototype in the prototype chain, there is the reference to an anonymous constructor function (javascripttutorial.net).

In the eventual case of rendering and managing a herd of creatures, instantiating 30 red creature objects means the equivalent of 3x data.

Object Literals

const creature = ({ x, y, size, speed, 
    color, 
    last_position, 
    proximity 
}) => ({
    x, y, size, speed, color, last_position, proximity,
    move({ x, y }) {
        this.x = x
        this.y = y
    },
    render(p) {
        p.fill(...this.color)
        p.noStroke()
        p.circle(this.x, this.y, this.size)
    }
})

const RedCreature = ({ x, y }, player) => ({
    ...creature({ 
        x, y, size: 10, speed: 5, color: [255, 0, 0],
        last_position: { x: player.x, y: player.y },
        proximity: 50
    }),
    updatePosition({ x, y }, { width, height }) {}
})

Object literals with the use of destructuring may have solved the issue with a growing prototype chain, but since every object inherits from the Object prototype, it made 3x into 2x. I had yet to realize my error in thinking, namely that I was still thinking in an object-oriented manner. Though I had the notion to try composition, I wasn't thinking functionally.

Here is composition.

Object.create

const actions = {
    move({ x, y }) {
        this.x = x
        this.y = y
    },
    render(p) {
        p.fill(...this.color)
        p.noStroke()
        p.circle(this.x, this.y, this.size)
    }
}

const RedCreature = ({ x, y }, { x: px, y: py }) => {
    const props = {
        x, y, size: 10, speed: 5, color: [255, 0, 0],
        last_position: { x: px, y: py }, proximity: 50
    }
    const red_actions = { ...actions }
    red_actions.updatePosition = function({ x, y }, width, height) {}
    return Object.create(red_actions, props)
}

Alternative: Object.assign

const actions = {
    move({ x, y }) {
        this.x = x
        this.y = y
    },
    render(p) {
        p.fill(...this.color)
        p.noStroke()
        p.circle(this.x, this.y, this.size)
    }
}

const RedCreature = { 
    size: 10, 
    speed: 5, 
    color: [255, 0, 0],
    proximity: 50,
    updatePosition({ x, y }, { width, height }) {}
}

const types = {
    red: RedCreature
}

const createCreature(dims, player, type, position = {
    x: Math.random() * dims.width,
    y: Math.random() * dims.height
}) => {
    if (!types.hasOwnProperty(type)) return false;
    const creature = { 
        ...position, 
        last_position: { x: player.x, y: player.y },
        ...actions
    }
    return Object.assign(creature, types[type])
}

Separating out the actions and properties was on the right track, but this implementation still faced the same problem as using classes and constructor functions, just to a lesser extent, namely that every creature object still contained its own instance of its method, updatePosition.

Zooming out a little, I tried to think in the way that Grokking Simplicity entailed. The book is about how professionals implement the practical aspects of functional programming, first and foremost, the separation of data, calculations and actions. From the perspective of data, I realized the generic creature was unnecessary. It was just a params object to pass in the initial positions of a creature and the player.

const createCreature = ({ position, dims, player }) => {
    if (position === undefined)
        position = {
            x: Math.random() * dims.width,
            y: Math.random() * dims.height
        }

    const creature = {
        ...position,
        last_position: {
            x: player.x,
            y: player.y
        }
    }

    return red_creature(creature)
}

My creature objects were also just data. I could simply use a switch or mapping to enact their behaviors.

const targeting = { ahead: target_ahead }

const update_position = ({ player, dims, creature }) => {
    if (!targeting.hasOwnProperty(creature.target)) return false;
    if (!is_movable(player, creature)) return false;
    return targeting[creature.target]({ player, dims, creature })
}

const red_creature = (creature) => ({
    ...creature,
    size: 10, 
    speed: 5, 
    color: [255, 0, 0], 
    proximity: 50,
    target: 'ahead'
})

As you can see above, update_position and red_creature are pure functions, or rather calculations. When given a certain input, they will always return the same output. There are no side effects, such as rendering and use of the console. Also, update_position, which does not differ from creature to creature, is removed from the creature object. It simply checks if the targeting algorithm is available through the map and if the creature meets the criteria to move, and if so, it has the targeting algorithm determine a new position for a creature.

In terms of actions, render can only be an action. There is no calculation to extract from it.

// params: creature, p5
const render = ({ x, y, color, size }, p) => {
    p.fill(...color)
    p.noStroke()
    p.circle(x, y, size)
}

Parameters

You may have noticed my functions destructuring arguments, but it's not consistent. For example, compare these two declarations:

const update_position = ({ player, dims, creature }) => {}
const render = ({ x, y, color, size }, p) => {}

Last year, I read this article: Always Pass One Argument to Your JavaScript Function. It was an interesting idea. Consider the following:

Standard Practice

const Player = (x, y) => ({ x, y, size: 10, speed: 5 })

const position = { x: dims.width / 2, y: dims.height / 2 }
const player = Player(position.y, position.x)

update_position(dims, player)

Do you see the bug? It would not throw an error but would cause miscalculations and mess up the player's orientation.

Concept

const Player = ({ x, y }) => ({ x, y, size: 10, speed: 5 })

const position = { x: dims.width / 2, y: dims.height / 2 }
const player = Player({ ...position })

update_position({ player, dims })

Between these two code snippets, there are two main advantages of using the concept over the standard practice:

  1. Order of arguments does not matter.

  2. Variable names correspond with parameters.

What this does not allow is destructuring of arguments. Look at my render function: const render = ({ x, y, color, size }, p) => {}. While it destructures an argument, it has two parameters not received in a single object, meaning their order should be maintained and a comment or documentation is required to detail what the first parameter is, such as // params: player, p.

Why this matters, in particular, is because the functions render and update_position are imported from other modules. The player's render comes from the player module, while the creature's render comes from the creature module. This means knowing, or checking, parameters for a module's exports. Using the single argument, when we look at another person's code, we can see when they call a function and in the function declaration what entities are being passed and received. Whether using standard practice or the concept, if a property is not provided in the arguments, it will be undefined.

Defaults are still doable.

const move = ({ x, y, speed = 5 } = {}) => {}
const move = (params = { x: 4, y: 4, speed: 5 }) => {}
const move = ({ speed = 5, x, y } = { x: 4, y: 4 }) => {}

Defaults are a little crazy when it comes to passing objects. Looking at the above example, there are three possibilities.

  1. Extract properties from params, and if speed is undefined, set to 5.

  2. Default object with values for params. None of the values are kept if an object is provided.

  3. Combination of default object and addressing undefined property for incoming params.

The problem with this that I can see happening is someone setting a default object and providing default properties for every function to avoid errors. There would never be an error. Whatever calculations or actions depend on the output of such functions could cause a cascading effect of bugs in an app. Not only would a debugger have to isolate the function where the first miscalculation happens, they would either have to decipher the parameters correctly or analyze the data passed in and sent out, and determine when it's using the defaults as opposed to actual data that happens to coincide with the defaults.

This may seem illogical or inconceivable, but I've known someone who believed that solving an issue or resolving a bug was the same as clearing, or hiding, the error messages. Even if this person wouldn't make the mistake above, an individual who doesn't know better, not thinking about maintainability, readability, or code structure, may make this mistake to avoid the errors.

const move = ({ speed = 5, x, y } = { x: -1, y: -1 }) => {}

function move({ speed = 5, x, y } = { x: -1, y: -1 }) {}

If you saw this in a codebase, would you understand it?

When it comes to arguments, I'm thinking of two situations:

  1. Property is provided

  2. Property is undefined

How will I handle undefined? I'm thinking that defaults should only be provided in exceptional cases. This means no use of speed = 5 or { x, y } = { x: -1, y: -1 }.

Function: Arrow vs Regular

As far as I know, there are only two main advantages for using the regular function declaration:

  1. Hoisting

  2. Object scope

Hoisting loses its advantage in using modules, and in any script, it can actually pose a potential detriment, that being a greater lack for organization. Organizing code becomes more of a team requirement or desirable state, rather than an enforced standard. Without hoisting, a function cannot be called before it has been initialized. In the very least, an individual would have to organize their code by data, primary calculations, then subsequent calculations and actions. Of course, an individual can still have poor organization in some form or another, one organizing their code by locality, another by purpose or categories, and so on.

What I don't know is how the regular form compares with arrow functions when unnamed:

const func = function() {}
const func = () => {}

Object Scope

This, you may not be so familiar with. Basically, if you use a regular function declaration in an object, you can access its other properties, but with an arrow function, you can't.

const Dog = class {
    constructor(food) {
        this.preferred = food
    }
    get preference() { return this.preferred }
    set preference(food) { this.preferred
}

const Dog = function(food) { this.preferred = food }
Dog.prototype.getPreference = function() { return this.preferred }
Dog.prototype.setPreference = function(food) { this.preferred = food }

const Dog = (food) => ({ 
    preferred: food,
    get preference() { return this.preferred },
    set preference(food) { this.preferred = food } 
})

const Dog = (food) => ({
    preferred: food,
    getPreference: function() { return this.preferred },
    setPreference: function(food) { this.preferred = food }
})

All of these work. The following does not:

const Dog = (food) => { this.preferred = food }
// Does nothing: returns undefined

const Dog = function(food) { this.preferred = food }
Dog.prototype.getPreference = () => this.preferred
Dog.prototype.setPreference = (food) => { this.preferred = food }
// Returns undefined and does not change internal value

const Dog = (food) => ({
    preferred: food,
    getPreference: () => this.preferred,
    setPreference: (food) => { this.preferred = food }
})
// getPreference() returns undefined
// Error: setPreference is not a function

Why does this matter?

Since I split the actions and calculations from the data, this doesn't actually matter, but if I still planned to use objects and classes as entities with methods, this would matter. I could use any of the four ways I showed above: class, prototype, and objects. But remember the issue of prototype chains. I'm not going to use this.

Where it actually matters is as a coding standard. Am I going to use regular functions or arrow functions as variables? Since either retain the surrounding context, or parent scope, and there's no use of an object's context, the point of choosing is simply to have a consistent format.

Just my impression, it seems that nowadays, arrow functions are preferred.

What about Patterns?

Later post. I have not read or familiarized myself with enough patterns to compare, let alone select one for a project.

Conclusion

Structure:

  • Split data from calculations and actions

  • Pass only one argument: params
    Pass entities

  • Use arrow function declarations

File Structure (current)

  • Index.js
    - actions: move, render

  • Calc.js - calculations for direction, distance, and bounds

  • Player.js
    - data: position, speed, size, shape and color
    - calculations: apply ui-entered movements

  • Creature.js
    - data: position, velocity, speed, size, shape and color, perception_radius, separation
    - calculations: targeting

  • Herd.js (debatable)