AgentSet.js

import AgentArray from './AgentArray.js'

/**
 * A model's {@link Patches}, {@link Turtles}, {@link Links},
 * are all subclasses of AgentSet.
 *
 * AgentSets are {@link AgentArray}s that are factories for their own Agents.
 * That means you do *not* call `new Turtle()`, rather Turtles
 * will create the Turtle instances, adding them to itself.
 *
 * Finally, a Breed is simply a subarray of Patches, Turtles, Links.
 * Patches could have a Streets breed, Turtles could have Cops and Robbers
 * breeds, and Links Spokes and Rim breeds
 *
 * AgentSets are not created directly by modelers.
 * Instead, class {@link Model} creates them along with their Breeds.
 * You can easily skip this initially, instead simply understand AgentSets
 * are the basis for Patches, Turtles, Links & Breeds
 *
 * @param {Model} model Instance of Class Model to which I belong
 * @param {Patch|Turtle|Link} AgentClass Class of items stored in this AgentSet
 * @param {String} name Name of this AgentSet. Ex: Patches
 * @param {Patches|Turtles|Links} [baseSet=null] If a Breed, it's parent AgentSet
 */

class AgentSet extends AgentArray {
    // Inherited by Patches, Turtles, Links
    model
    name
    baseSet
    AgentClass

    /**
     * Magic to return AgentArrays rather than AgentSets
     * [Symbol.species](https://goo.gl/Zsxwxd)
     *
     * @readonly
     */
    static get [Symbol.species]() {
        return AgentArray
    }

    constructor(model, AgentClass, name, baseSet = null) {
        super() // create empty AgentArray
        baseSet = baseSet || this // if not a breed, set baseSet to this
        Object.assign(this, { model, name, baseSet, AgentClass })
        // BaseSets know their breeds and keep the ID global
        if (this.isBaseSet()) {
            this.breeds = {} // will contain breedname: breed entries
            this.ID = 0
            // Breeds inherit frm their baseSet and add themselves to baseSet
        } else {
            Object.setPrototypeOf(this, Object.getPrototypeOf(baseSet))
            this.baseSet.breeds[name] = this
        }
        // Keep a list of this set's variables; see `own` below
        // REMIND: not really used. Remove? Create after setup()?
        this.ownVariables = []
        // Create a proto for our agents by having a defaults and instance layer
        // this.AgentClass = AgentClass
        this.agentProto = new AgentClass(this)
        this.protoMixin(this.agentProto, AgentClass)
        // }
    }
    /**
     * Add common variables to an Agent being added to this AgentSet.
     *
     * Each Agent has it's AgentSet and the Model instance.
     * It also has an id, set by the AgentSet's global ID.
     *
     * The Agent also has three methods added: setBreed, getBreed, isBreed.
     *
     * @param {Object} agentProto A new instance of the Agent being added
     * @param {Patch|Turtle|Link} AgentClass It's Class
     */
    protoMixin(agentProto, AgentClass) {
        Object.assign(agentProto, {
            agentSet: this,
            model: this.model,
            // world: this.world
        })
        agentProto[this.baseSet.name] = this.baseSet

        // if (this.isBaseSet()) {
        // Model.reset should not redefine these.
        if (!AgentClass.prototype.setBreed) {
            Object.assign(AgentClass.prototype, {
                setBreed(breed) {
                    breed.setBreed(this)
                },
                getBreed() {
                    return this.agentSet
                },
                isBreed(breed) {
                    return this.agentSet === breed
                },
            })
            Object.defineProperty(AgentClass.prototype, 'breed', {
                get: function () {
                    return this.agentSet
                },
            })
        }
    }

    /**
     * Create a subarray of this AgentSet.
     * Example: create a people breed of Turtles:
     *
     * `people = turtles.newBreed('people')`
     *
     * @param {String} name The name of the new breed AgentSet
     * @returns {AgentSet} A subarray of me
     */
    newBreed(name) {
        return new AgentSet(this.model, this.AgentClass, name, this)
    }

    /**
     * @returns {boolean} true if I am a baseSet subarray
     */
    isBreedSet() {
        return this.baseSet !== this
    }
    /**
     * @returns {boolean} true if I am a Patches, Turtles or Links AgentSet
     */
    isBaseSet() {
        return this.baseSet === this
    }

    /**
     * Return breeds in a subset of an AgentSet
     *
     * Ex: patches.inRect(5).withBreed(houses)
     *
     * @param {AgentSet} breed A breed AgentSet
     * @returns {AgentArray}
     */
    withBreed(breed) {
        return this.filter(a => a.agentSet === breed)
    }

    // Abstract method used by subclasses to create and add their instances.
    create() {
        console.log(`AgentSet: Abstract method called: ${this}`)
    }

    /**
     * @param {Object} o An Agent to be added to this AgentSet
     * @returns {Object} The input Agent, bound to this AgentSet.
     * @description
     * Add an Agent to this AgentSet.  Only used by factory methods.
     * Adds the `id` property to Agent. Increment AgentSet `ID`.
     */
    addAgent(o = undefined) {
        // o only for breeds adding themselves to their baseSet
        o = o || Object.create(this.agentProto) // REMIND: Simplify! Too slick.
        if (this.isBreedSet()) {
            this.baseSet.addAgent(o)
        } else {
            o.id = this.ID++
            if (o.agentConstructor) o.agentConstructor()
        }
        this.push(o)
        return o
    }
    /**
     * Remove all Agents from this AgentSet using agent.die() for each agent.
     *
     */
    clear() {
        // die() is an agent method. sets it's id to -1
        while (!this.isEmpty()) this.last().die()
    }
    /**
     * Remove an Agent from this AgentSet
     *
     * @param {Object} o The Agent to be removed
     * @returns {AgentSet} This AgentSet with the Agent removed
     */
    removeAgent(o) {
        // Note removeAgent(agent) different than remove(agent) which
        // simply removes the agent from it's array

        if (o.id != -1) {
            // Remove me from my baseSet
            if (this.isBreedSet()) this.baseSet.remove(o, 'id')
            // Remove me from my set.
            this.remove(o, 'id')
        }

        return this
    }

    /**
     * Set a default value shared by all Agents in this AgentSet
     *
     * @param {String} name The name of the shared value
     * @param {any} value
     * @returns {AgentSet} This AgentSet
     */
    setDefault(name, value) {
        this.agentProto[name] = value
        return this
    }
    /**
     * Return a default, shared value
     *
     * @param {String} name The name of the default
     * @returns {any} The default value
     */
    getDefault(name) {
        return this.agentProto[name]
    }
    // Used when getter/setter's need to know if get/set default
    // settingDefault(agent) {
    //     return agent.id == null
    // }

    // Declare variables of an agent class. May deprecate if not needed.
    // `varnames` is a string of space separated names
    // own(varnames) {
    //     // if (this.isBreedSet())
    //     //   this.ownVariables = util.clone(this.baseSet.ownVariables)
    //     for (const name of varnames.split(' ')) {
    //         this.setDefault(name, null)
    //         this.ownVariables.push(name)
    //     }
    // }

    /**
     * Move an agent from its AgentSet/breed to be in this AgentSet/breed
     *
     * @param {Agent} a An agent, a member of another AgentSet
     * @returns {Agent} The updated agent
     */
    setBreed(a) {
        // change agent a to be in this breed
        // Return if `a` is already of my breed
        if (a.agentSet === this) return
        // Remove/insert breeds (not baseSets) from their agentsets
        if (a.agentSet.isBreedSet()) a.agentSet.remove(a, 'id')
        if (this.isBreedSet()) this.insert(a, 'id')

        // Make list of `a`'s vars and my ownvars.
        const avars = a.agentSet.ownVariables
        // First remove `a`'s vars not in my ownVariables
        for (const avar of avars) {
            if (!this.ownVariables.includes(avar)) delete a[avar]
        }
        // Now add ownVariables to `a`'s vars, default to 0.
        // If ownvar already in avars, it is not modified.
        for (const ownvar of this.ownVariables) {
            if (!avars.includes(ownvar)) a[ownvar] = 0
        } // NOTE: NL uses 0, maybe we should use null?

        // Give `a` my defaults/statics
        return Object.setPrototypeOf(a, this.agentProto)
    }

    /**
     * Call fcn(agent, index, array) for each item in AgentArray.
     * Index & array optional. Overrides AgentArray's ask with
     * additional guards for modifications in AgentSet's array.
     *
     * @param {Function} fcn fcn(agent, index?, array?)
     */
    ask(fcn) {
        if (this.length === 0) return
        const lastID = this.last().id // would fail w/o 0 check above
        // for (let i = 0; this[i].id <= lastID; i++) { // nope.
        for (let i = 0; i < this.length && this[i].id <= lastID; i++) {
            fcn(this[i], i, this)
        }
    }

    /**
     * A much stronger version of ask(fcn) with stronger mutability guards.
     *
     * @param {Function} fcn fcn(agent, index?, array?)
     */
    askSet(fcn) {
        // Manages immutability reasonably well.
        if (this.length === 0) return
        // Patches are static
        if (this.name === 'patches') super.forLoop(fcn)
        else if (this.isBaseSet()) this.baseSetAsk(fcn)
        else if (this.isBreedSet()) this.cloneAsk(fcn)
    }

    // An ask function for mutable baseSets.
    // BaseSets can only add past the end of the array.
    // This allows us to manage mutations by allowing length change,
    // and managing deletions only within the original length.
    baseSetAsk(fcn) {
        if (this.length === 0) return
        const lastID = this.last().id

        // Added obj's have id > lastID. Just check for deletions.
        // There Be Dragons:
        // - AgentSet can become length 0 if all deleted
        // - For loop tricky:
        //   - i can become negative w/in loop:
        //   - i can become bigger than current AgentSet
        //   - Guard w/ i<len & i>=0
        for (let i = 0; i < this.length; i++) {
            const obj = this[i]
            const id = obj.id
            if (id > lastID) break
            fcn(obj, i, this)
            if (i >= this.length) break
            if (this[i].id > id) {
                while (i >= 0 && this[i].id > id) i-- // ok if -1
            }
        }
    }

    // For breeds, mutations can occur in many ways.
    // This solves this by cloning the initial array and
    // managing agents that have died or changed breed.
    // In other words, we can be concerned only with mutations
    // of the agents themselves.
    cloneAsk(fcn) {
        const clone = this.clone()
        for (let i = 0; i < clone.length; i++) {
            const obj = clone[i]
            // obj.id > 0: obj.die() sets id to -1
            if (obj.breed == this && obj.id > 0) {
                fcn(obj, i, clone)
            }
        }
    }
}

export default AgentSet