UNPKG

agentscript

Version:

AgentScript Model in Model/View architecture

465 lines (443 loc) 15 kB
import AgentList from './AgentList.js' import * as util from './utils.js' /** * Class Turtle instances represent the dynamic, behavioral element of modeling. * Each turtle knows the patch it is on, and interacts with that and other * patches, as well as other turtles. Turtles are also the end points of Links. * * You do not call `new Turtle()`, instead class Turtles creates Turtle instances * via {@link Turtles#create} or {@link Turtles#createOne} * * I.e. class Turtles is a factory for all of it's Turtle instances. * So *don't* do this: */ class Turtle { static defaults = { atEdge: 'wrap', hidden: false, z: 0, // Set by AgentSet agentSet: null, model: null, name: null, } static variables = { id: null, theta: 0, x: 0, y: 0, } constructor() { Object.assign(this, Turtle.defaults) } newInstance(agentProto) { const insstance = Object.create(agentProto) Object.assign(insstance, Turtle.variables) return insstance } /** * Ask this turtle to "die" * - Removes itself from the Turtles array * - Removes itself from any Turtles breeds * - Removes all my Links if any exist * - Removes me from my Patch list of turtles on it * - Set it's id to -1 to indicate to others it's gone */ die() { if (this.id === -1) return this.agentSet.removeAgent(this) // remove me from my baseSet and breeds // Remove my links if any exist. // Careful: don't promote links if (this.hasOwnProperty('links')) { while (this.links.length > 0) this.links[0].die() } // Remove me from patch.turtles cache if patch.turtles array exists // if (this.patch.turtles != null) { // util.removeArrayItem(this.patch.turtles, this) // } if (this.patch && this.patch.turtles) util.removeArrayItem(this.patch.turtles, this) // Set id to -1, indicates that I've died. // Useful when other JS objects contain turtles. Views for example. this.id = -1 } isDead() { return this.id === -1 } /** * Factory method: create num new turtles at this turtle's location. * * @param {number} [num=1] The number of new turtles to create * @param {AgentSet} [breed=this.agentSet] The type of turtles to create, * defaults to my type * @param {Function} [init=turtle => {}] A function to initialize the new * turtles, defaults to no-op * @returns {Array} An Array of the new Turtles, generally ignored * due to the init function */ hatch(num = 1, breed = this.agentSet, init = turtle => {}) { return breed.create(num, turtle => { // turtle.setxy(this.x, this.y) turtle.setxy(this.x, this.y, this.z) turtle.theta = this.theta // // hatched turtle inherits parents' ownVariables // for (const key of breed.ownVariables) { // if (turtle[key] == null) turtle[key] = this[key] // } init(turtle) }) } // Getter for links for this turtle. // Uses lazy evaluation to promote links to instance variables. /** * Returns an array of the Links that have this Turtle as one of the end points * @returns {Array} An AgentList Array of my Links */ get links() { // lazy promote links from getter to instance prop. Object.defineProperty(this, 'links', { value: new AgentList(this.model), enumerable: true, }) return this.links } /** * Return the patch this Turtle is on. Return null if Turtle off-world. */ get patch() { return this.model.patches.patch(this.x, this.y) } /** * Return this Turtle's heading */ get heading() { return this.model.fromRads(this.theta) } /** * Sets this Turtle's heading */ set heading(heading) { this.theta = this.model.toRads(heading) } /** * Computes the difference between the my heading and the given heading, * the smallest angle by which t could be rotated to produce heading. * * @param {Angle} heading The heading I wish to be roated to. * @returns {Angle} */ subtractHeading(heading) { // // Using rads so will work with any geometry. // const rads1 = this.model.toRads(this.heading) // const rads2 = this.model.toRads(heading) // const diff = util.subtractRadians(rads2, rads1) // return this.model.fromRads(diff) return util.subtractHeadings(heading, this.heading) } /** * Set Turtles x, y position. If z given, override default z of 0. * * @param {number} x Turtle's x coord, a Float in patch space * @param {number} y Turtle's Y coord, a Float in patch space * @param {number|undefined} [z=undefined] Turtle's Z coord if given */ setxy(x, y, z = undefined) { const p0 = this.patch this.x = x this.y = y if (z != null) this.z = z this.checkXYZ(p0) } checkXYZ(p0) { this.checkEdge() this.checkPatch(p0) } checkEdge() { const { x, y, z } = this // if (!(this.model.world.isOnWorld(x, y, z) || this.atEdge === 'OK')) { if (!this.model.world.isOnWorld(x, y, z) && this.atEdge !== 'OK') { this.handleEdge(x, y, z) } } checkPatch(p0) { const p = this.patch // both can be null if (p != p0) { if (p0 && p0.turtles) util.removeArrayItem(p0.turtles, this) if (p && p.turtles) p.turtles.push(this) } } /** * Handle turtle x,y,z if turtle off-world. * Uses the Turtle's atEdge property to determine how to manage the Turtle. * Defaults to 'wrap', wrapping the x,y,z to the opposite edge. * * atEdge can be: * - 'die' * - 'wrap' * - 'bounce' * - 'clamp' * - 'random' * - a function called with the Turtle as it's argument * * @param {number} x Turtle's x coord * @param {number} y Turtle's y coord * @param {number|undefined} [z=undefined] Turtle's z coord if not undefined */ handleEdge(x, y, z = undefined) { let atEdge = this.atEdge if (util.isString(atEdge)) { const { minXcor, maxXcor, minYcor, maxYcor, minZcor, maxZcor } = this.model.world if (atEdge === 'wrap') { this.x = util.wrap(x, minXcor, maxXcor) this.y = util.wrap(y, minYcor, maxYcor) if (z != null) this.z = util.wrap(z, minZcor, maxZcor) } else if (atEdge === 'die') { this.die() } else if (atEdge === 'random') { this.setxy(...this.model.world.randomPoint()) } else if (atEdge === 'clamp' || atEdge === 'bounce') { this.x = util.clamp(x, minXcor, maxXcor) this.y = util.clamp(y, minYcor, maxYcor) if (z != null) this.z = util.clamp(z, minZcor, maxZcor) if (atEdge === 'bounce') { if (this.x === minXcor || this.x === maxXcor) { this.theta = Math.PI - this.theta } else if (this.y === minYcor || this.y === maxYcor) { this.theta = -this.theta } else if (this.z === minZcor || this.z === maxZcor) { if (this.pitch) { this.pitch = -this.pitch } else { this.z = util.wrap(z, minZcor, maxZcor) } } } } else { throw Error(`turtle.handleEdge: bad atEdge: ${atEdge}`) } } else { this.atEdge(this) } } /** * Place the turtle at the given patch/turtle location * * @param {Patch|Turtle} agent A Patch or Turtle who's location is used */ moveTo(agent) { // this.setxy(agent.x, agent.y) this.setxy(agent.x, agent.y, agent.z) } /** * Move forward, along the Turtle's heading d units in Patch coordinates * * @param {number} d The distance to move */ forward(d) { this.setxy( this.x + d * Math.cos(this.theta), this.y + d * Math.sin(this.theta) ) } /** * Change Turtle's heading by angle * * @param {number} angle The angle to rotate by */ rotate(angle) { angle = this.model.toCCW(angle) this.heading += angle } /** * Turn Turtle right by angle * * @param {number} angle The angle to rotate by */ right(angle) { this.rotate(-angle) } /** * Turn Turtle left by angle * * @param {number} angle The angle to rotate by */ left(angle) { this.rotate(angle) } /** * Turn turtle so at to be facing the given Turtle or Patch * * @param {Patch|Turtle} agent The agent to face towards */ face(agent) { // this.theta = this.towards(agent) this.heading = this.towards(agent) } /** * Turn turtle so at to be facing the given x, y patch coordinate * * @param {number} x The x coordinate * @param {number} y The y coordinate */ facexy(x, y) { // this.theta = this.towardsXY(x, y) this.heading = this.towardsXY(x, y) } /** * Return the patch ahead of this turtle by distance. * Return undefined if the distance puts the patch off-world * @param {number} distance The distance ahead * @returns {Patch|undefined} The patch at the distance ahead of this Turtle */ patchAhead(distance) { return this.patchAtHeadingAndDistance(this.heading, distance) } /** * Return the patch angle to the right and ahead by distance * Return undefined if the distance puts the patch off-world * @param {number} angle The angle to the right * @param {number} distance The distance ahead * @returns {Patch|undefined} The patch found, or undefined if off-world */ patchRightAndAhead(angle, distance) { // if (this.model.geometry === 'heading') angle = -angle angle = this.model.toCCW(angle) return this.patchAtHeadingAndDistance(this.heading - angle, distance) } /** * Return the patch angle to the left and ahead by distance * Return undefined if the distance puts the patch off-world * @param {number} angle The angle to the left * @param {number} distance The distance ahead * @returns {Patch|undefined} The patch found, or undefined if off-world */ patchLeftAndAhead(angle, distance) { return this.patchRightAndAhead(-angle, distance) } /** * Can I move forward by distance and not be off-world? * @param {number} distance The distance ahead * @returns {Boolean} True if moving forward by distance is on-world */ canMove(distance) { return this.patchAhead(distance) != null } /** * Distance from this turtle to x, y * No off-world test done. * * 2.5D: use z too if both z & this.z exist. * @param {number} x * @param {number} y * @param {number|undefined} [z=null] * @returns {number} distance in patch coordinates. */ distanceXY(x, y, z = null) { const useZ = z != null && this.z != null return useZ ? util.distance3(this.x, this.y, this.z, x, y, z) : util.distance(this.x, this.y, x, y) } /** * Return distance from me to the Patch or Turtle * * 2.5D: use z too if both agent.z and this.z exist * @param {Patch|Turtle} agent * @returns {number} distance in patch coordinates. */ distance(agent) { const { x, y, z } = agent return this.distanceXY(x, y, z) } /** * A property for the x-increment if the turtle were to take one step * forward in its current heading. * @readonly */ get dx() { return Math.cos(this.theta) } /** * A property for the y-increment if the turtle were to take one step * forward in its current heading. * @readonly */ get dy() { return Math.sin(this.theta) } /** * Return the heading towards the Patch or Turtle given. * @param {Patch|Turtle} agent The agent who's angle from this Turtle we use * @returns {number} The angle towards the agent */ towards(agent) { return this.towardsXY(agent.x, agent.y) } /** * Return the heading towards the given x,y coordinates. * @param {number} x The x coordinarte * @param {number} y The y coordinarte * @returns {number} The angle towards x,y */ towardsXY(x, y) { // return util.radiansTowardXY(this.x, this.y, x, y) let rads = util.radiansTowardXY(this.x, this.y, x, y) // rads = this.model.toCCW(rads) return this.model.fromRads(rads) } /** * The patch at dx, dy from my current position. * Return undefined if off-world * @param {number} dx The delta x ahead * @param {number} dy The delta y ahead * @returns {Patch|undefined} The patch dx, dy ahead; undefined if off-world */ patchAt(dx, dy) { return this.model.patches.patch(this.x + dx, this.y + dy) } /** * Return the patch at the absolute, not relative heading and distance * from this turtle. Return undefined if off-world * * Use the Left/Right versions for relative heading. * @param {number} heading The absolute angle from this turtle * @param {number} distance The distance ahead * @returns {Patch|undefined} The Patch, or undefined if off-world */ patchAtHeadingAndDistance(heading, distance) { return this.model.patches.patchAtHeadingAndDistance( this, heading, distance ) } /** * Return the other end of this link from me. Link must include me! * * See links property for all my links, if any. * @param {Link} l * @returns {Turtle} The other turtle making this Link */ otherEnd(l) { return l.end0 === this ? l.end1 : l.end0 } // Return all turtles linked to me /** * Return all turtles linked to me. Basically me.otherEnd of all my links. * @returns {Array} All the turtles linked to me */ linkNeighbors() { return this.links.map(l => this.otherEnd(l)) } /** * Is the given Turtle linked to me? * @param {Turtle} t * @returns {Boolean} */ isLinkNeighbor(t) { return t in this.linkNeighbors() } } export default Turtle