Compare commits

..

3 Commits

Author SHA1 Message Date
419e67f80a Add tests 2022-11-04 17:53:24 -03:00
38135762f6 Add exploding bunny rabbit 2022-11-04 17:53:17 -03:00
5c03647a4a Add assignment 4 2022-11-04 15:07:21 -03:00
6 changed files with 580 additions and 0 deletions

178
assignments/A4/life.js Normal file
View File

@ -0,0 +1,178 @@
// ES2015 classes based on https://eloquentjavascript.net/2nd_edition/07_elife.html
class Vector {
constructor(x, y) {
this.x = x;
this.y = y;
}
plus(other) {
return new Vector(this.x + other.x, this.y + other.y);
};
};
class Grid {
constructor (width, height) {
this.space = new Array(width * height);
this.width = width;
this.height = height;
};
isInside(vector) {
return vector.x >= 0 && vector.x < this.width &&
vector.y >= 0 && vector.y < this.height;
};
get(vector) {
return this.space[vector.x + this.width * vector.y];
};
set(vector, value) {
this.space[vector.x + this.width * vector.y] = value;
};
forEach(f, context) {
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
let value = this.space[x + y * this.width];
if (value != null)
f.call(context, value, new Vector(x, y));
}
}
};
}
let directions = {
"n": new Vector( 0, -1),
"ne": new Vector( 1, -1),
"e": new Vector( 1, 0),
"se": new Vector( 1, 1),
"s": new Vector( 0, 1),
"sw": new Vector(-1, 1),
"w": new Vector(-1, 0),
"nw": new Vector(-1, -1)
};
function randomElement(array) {
return array[Math.floor(Math.random() * array.length)];
}
let directionNames = "n ne e se s sw w nw".split(" ");
function BouncingCritter() {
this.direction = randomElement(directionNames);
};
BouncingCritter.prototype.act = function(view) {
if (view.look(this.direction) != " ")
this.direction = view.find(" ") || "s";
return {type: "move", direction: this.direction};
};
class View {
constructor(world, vector) {
this.world = world;
this.vector = vector;
}
look(dir) {
let target = this.vector.plus(directions[dir]);
if (this.world.grid.isInside(target))
return charFromElement(this.world.grid.get(target));
else
return "#";
}
findAll(ch) {
let found = [];
for (let dir in directions)
if (this.look(dir) == ch)
found.push(dir);
return found;
}
find(ch) {
let found = this.findAll(ch);
if (found.length == 0) return null;
return randomElement(found);
}
}
class World {
constructor(map, legend) {
let grid = new Grid(map[0].length, map.length);
this.grid = grid;
this.legend = legend;
map.forEach(function(line, y) {
for (let x = 0; x < line.length; x++)
grid.set(new Vector(x, y),
World.elementFromChar(legend, line[x]));
});
}
static elementFromChar(legend, ch) {
if (ch == " ")
return null;
let element = new legend[ch]();
element.originChar = ch;
return element;
}
toString() {
let output = "";
for (let y = 0; y < this.grid.height; y++) {
for (let x = 0; x < this.grid.width; x++) {
let element = this.grid.get(new Vector(x, y));
output += charFromElement(element);
}
output += "\n";
}
return output;
}
turn () {
let acted = [];
this.grid.forEach(function(critter, vector) {
if (critter.act && acted.indexOf(critter) == -1) {
acted.push(critter);
this.letAct(critter, vector);
}
}, this);
}
letAct(critter, vector) {
let action = critter.act(new View(this, vector));
if (action && action.type == "move") {
let dest = this.checkDestination(action, vector);
if (dest && this.grid.get(dest) == null) {
this.grid.set(vector, null);
this.grid.set(dest, critter);
}
}
}
checkDestination(action, vector) {
if (directions.hasOwnProperty(action.direction)) {
let dest = vector.plus(directions[action.direction]);
if (this.grid.isInside(dest))
return dest;
}
return undefined;
}
};
function charFromElement(element) {
if (element == null)
return " ";
else
return element.originChar;
}
function Wall() {};
exports.BouncingCritter=BouncingCritter;
exports.Grid=Grid;
exports.Wall=Wall;
exports.World=World;
exports.Vector=Vector;
exports.View=View;

122
assignments/A4/moarlife.js Normal file
View File

@ -0,0 +1,122 @@
let life=require("./life.js");
let View=life.View;
let actionTypes = Object.create(null);
actionTypes.grow = function(critter) {
critter.energy += 0.5;
return true;
};
actionTypes.move = function(critter, vector, action) {
let dest = this.checkDestination(action, vector);
if (dest == null ||
critter.energy <= 1 ||
this.grid.get(dest) != null)
return false;
critter.energy -= 1;
this.grid.set(vector, null);
this.grid.set(dest, critter);
return true;
};
actionTypes.eat = function(critter, vector, action) {
let dest = this.checkDestination(action, vector);
let atDest = dest != null && this.grid.get(dest);
if (!atDest || atDest.energy == null)
return false;
critter.energy += atDest.energy;
this.grid.set(dest, null);
return true;
};
actionTypes.reproduce = function(critter, vector, action) {
let baby = life.World.elementFromChar(this.legend,
critter.originChar);
let dest = this.checkDestination(action, vector);
if (dest == null ||
critter.energy <= 2 * baby.energy ||
this.grid.get(dest) != null)
return false;
critter.energy -= 2 * baby.energy;
this.grid.set(dest, baby);
return true;
};
actionTypes.die = function(critter, action) {
}
class LifelikeWorld extends life.World {
constructor(map,legend){
super(map,legend);
}
letAct(critter, vector) {
let action = critter.act(new View(this, vector));
let handled = action &&
action.type in actionTypes &&
actionTypes[action.type].call(this, critter,
vector, action);
if (!handled) {
critter.energy -= 0.2;
if (critter.energy <= 0)
this.grid.set(vector, null);
}
};
}
class Plant {
constructor() {
this.energy = 3 + Math.random() * 4;
}
act(view) {
if (this.energy > 15) {
let space = view.find(" ");
if (space)
return {type: "reproduce", direction: space};
}
if (this.energy < 20)
return {type: "grow"};
};
}
class PlantEater{
constructor () {
this.energy = 20;
}
act(view) {
let space = view.find(" ");
if (this.energy > 60 && space)
return {type: "reproduce", direction: space};
let plant = view.find("*");
if (plant)
return {type: "eat", direction: plant};
if (space)
return {type: "move", direction: space};
};
}
class ExplodingBunnyRabbit extends PlantEater {
constructor () {
super()
}
act(view) {
super.act(view);
if(this.energy > 55) {
if (Math.random() < 0.25) {
return {type: "die"}
}
}
}
}
exports.LifelikeWorld=LifelikeWorld;
exports.BouncingCritter=life.BouncingCritter;
exports.Wall=life.Wall;
exports.PlantEater = PlantEater;
exports.Plant = Plant;
exports.actionTypes = actionTypes;

View File

@ -0,0 +1,136 @@
let life=require ("../life.js");
let plan = ["############################",
"# # # o ##",
"# #",
"# ##### #",
"## # # ## #",
"### ## # #",
"# ### # #",
"# #### #",
"# ## o #",
"# o # o ### #",
"# # #",
"############################"];
let Vector = life.Vector;
describe("Grid",
function() {
it("initially undefined",
function() {
let grid = new life.Grid(5, 5);
expect(grid.get(new life.Vector(1, 1))).toBe(undefined);
});
it("setting a value",
function() {
let grid = new life.Grid(5, 5);
grid.set(new Vector(1, 1), "X");
expect(grid.get(new Vector(1, 1))).toEqual("X");
});
it("forEach",
function() {
let grid = new life.Grid(5, 5);
let test = {grid: grid, sum: 0,
method: function () {
this.grid.forEach(function() { this.sum++; }, this);
}
};
test.grid.set(new Vector(2,3), "#");
test.grid.set(new Vector(3,4), "#");
test.method();
expect(test.sum).toBe(2);
});
});
describe("BouncingCritter",
function() {
let bob = null;
beforeEach(function () {
spyOn(Math, 'random').and.returnValue(0.5);
bob=new life.BouncingCritter();
});
it("constructor",
function() {
expect('direction' in bob).toBe(true);
expect(bob.direction).toBe('s');
});
it("act, clear path",
function () {
let clear = {look: function () {return " ";}};
expect(bob.act(clear)).toEqual({type: "move", direction: "s"});
});
it("act, unclear path",
function () {
let unclear = {look: function () {return "#";}, find: function () { return "n";}};
expect(bob.act(unclear)).toEqual({type: "move", direction: "n"});
});
});
describe("World",
function () {
it("roundtrip",
function() {
let world = new life.World(plan, {"#": life.Wall, "o": life.BouncingCritter});
let rows = world.toString().split("\n");
// drop blank row
rows.pop();
expect(rows).toEqual(plan);
});
it("turn",
function () {
let world = new life.World(plan, {"#": life.Wall, "o": life.BouncingCritter});
let count=0;
spyOn(world, 'letAct').and.callFake(function(critter,vector) {count++;});
world.turn();
expect(count).toBe(4);
});
it("checkDestination",
function () {
let world = new life.World(plan, {"#": life.Wall, "o": life.BouncingCritter});
expect(world.checkDestination({direction: 's'},
new life.Vector(19,1))).toEqual(new life.Vector(19,2));
expect(world.checkDestination({direction: 'n'},
new life.Vector(0,0))).toEqual(undefined);
});
it("letAct",
function () {
let world = new life.World(plan, {"#": life.Wall, "o": life.BouncingCritter});
let src=new life.Vector(19,1);
let dest=new life.Vector(19,2);
let bob=world.grid.get(src);
spyOn(bob,'act').and.returnValue({type: 'move', direction: 's'});
world.letAct(bob, src);
expect(world.grid.get(dest)).toEqual(bob);
});
});
describe("View",
function () {
let world = new life.World(plan, {"#": life.Wall, "o": life.BouncingCritter});
let View=life.View;
let position=new Vector(15,9);
it("constructor",
function () {
let view=new View(world, position);
expect(view.vector).toEqual(position);
});
it("look",
function () {
let view=new View(world, position);
expect(view.look("s")).toBe(" ");
});
it("findAll",
function () {
let view=new View(world, position);
let directionNames = [ 'e', 'n', 'ne', 'nw', 's', 'se', 'sw', 'w' ];
expect(view.findAll(" ").sort()).toEqual(directionNames);
});
it("find",
function () {
let view=new View(world, position);
spyOn(Math, 'random').and.returnValue(0.5);
expect(view.find(" ")).toBe('s');
});
});

View File

@ -0,0 +1,103 @@
let life=require ("../moarlife.js");
let plan= ["############################",
"##### ######",
"## *** **##",
"# *##** ** O *##",
"# *** O ##** *#",
"# O ##*** #",
"# ##** #",
"# O #* #",
"#* #** O #",
"#*** ##** O **#",
"##**** ###*** *###",
"############################"];
describe("World",
function () {
let valley = new life.LifelikeWorld(plan,
{"#": life.Wall,
"O": life.PlantEater,
"*": life.Plant});
it("roundtrip",
function() {
let rows = valley.toString().split("\n");
// drop blank row
rows.pop();
expect(rows).toEqual(plan);
});
});
describe("actionTypes",
function () {
it("grow",
function () {
let critter = new life.Plant();
let energy = critter.energy;
life.actionTypes.grow(critter);
expect(critter.energy).toBeGreaterThan(energy);
});
})
describe("PlantEater",
function () {
it("constructor",
function () {
let pe = new life.PlantEater();
expect('energy' in pe).toBe(true);
expect(pe.energy).toBe(20);
});
it("act, reproduce",
function () {
let pe = new life.PlantEater();
pe.energy = 65
expect(pe.act({find: function (ch) { if (ch === " ") return "n"; } })).toEqual({ type: "reproduce", direction: "n" });
});
it("act, eat",
function () {
let pe = new life.PlantEater();
pe.energy = 20
expect(pe.act({find: function (ch ) { if (ch === "*") return "n"; } })).toEqual({ type: "eat", direction: "n" });
});
it("act, move",
function () {
let pe = new life.PlantEater();
expect(pe.act({find: function (ch) { if (ch === " ") return "n"; } })).toEqual({ type: "move", direction: "n" });
});
});
describe("ExplodingBunnyRabbit",
function () {
it("constructor",
function () {
let pe = new life.ExplodingBunnyRabbit();
expect('energy' in pe).toBe(true);
expect(pe.energy).toBe(20);
});
it("act, reproduce",
function () {
let pe = new life.ExplodingBunnyRabbit();
pe.energy = 65
expect(pe.act({find: function (ch) { if (ch === " ") return "n"; } })).toEqual({ type: "reproduce", direction: "n" });
});
it("act, eat",
function () {
let pe = new life.ExplodingBunnyRabbit();
pe.energy = 20
expect(pe.act({find: function (ch ) { if (ch === "*") return "n"; } })).toEqual({ type: "eat", direction: "n" });
});
it("act, move",
function () {
let pe = new life.ExplodingBunnyRabbit();
expect(pe.act({find: function (ch) { if (ch === " ") return "n"; } })).toEqual({ type: "move", direction: "n" });
});
it("act, explode",
function () {
let pe = new life.ExplodingBunnyRabbit();
expect(pe.act({find: function (ch) { if (ch === "O") return "n"; } })).toEqual({ type: "explode", direction: "n" });
});
});

View File

@ -0,0 +1,13 @@
{
"spec_dir": "spec",
"spec_files": [
"**/*[sS]pec.?(m)js"
],
"helpers": [
"helpers/**/*.?(m)js"
],
"env": {
"stopSpecOnExpectationFailure": false,
"random": true
}
}

28
assignments/A4/valley.js Normal file
View File

@ -0,0 +1,28 @@
let life=require("./moarlife.js");
let valley = new life.LifelikeWorld(
["############################",
"##### ######",
"## *** **##",
"# *##** ** O *##",
"# *** O ##** *#",
"# O ##*** #",
"# ##** #",
"# O #* #",
"#* #** O #",
"#*** ##** O **#",
"##**** ###*** *###",
"############################"],
{"#": life.Wall,
"O": life.PlantEater,
"*": life.Plant}
);
function loop () {
valley.turn();
console.log("\33c");
console.log(valley.toString());
setTimeout(function() { loop(); },250);
}
loop();