# I made a webpage that simulates sharks eating fish in the torrid world of Wa-tor

I've been self-studying HTML/CSS/JS. I read about Wa-tor. It's a population dynamics simulation which simulates fish and sharks breeding and predating. This seemed cool so I decided to implement it. I don't know much about javascript, my background is mostly in python and R.

Here's my implementation. Just hit Play. And here's the source code.

Wa-tor

var intervalId;
var isPaused;

function randomChoice(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}

function tick(sea) {
sea.tick();
sea.draw();
}

function play() {
var sea = new Sea(seaCanvas.width, seaCanvas.height);
sea.generate();
sea.draw();
clearInterval(intervalId);
intervalId = setInterval(tick, 1, sea);
}


Sea

class Sea {

constructor(width, height) {
this.width = width;
this.height = height;
this.numFish = 0;
this.numSharks = 0;
this.chronon = 0;
this.ctx = document.getElementById('seaCanvas').getContext('2d', { willReadFrequently: true });
this.array = new Array();
for (let j = 0; j < this.height; j++) {
var row = new Array();
for (let i = 0; i < this.width; i++) {
row.push(null);
}
this.array.push(row)
}
}

getNorthCellCoords(x, y) {
if (y !== 0) {
return {
x: x,
y: y - 1
};
} else {
return {
x: x,
y: this.height - 1
};
}
}

getWestCellCoords(x, y) {
if (x !== 0) {
return {
x: x - 1,
y: y
};
} else {
return {
x: this.width - 1,
y: y
};
}
}

getSouthCellCoords(x, y) {
if (y !== this.height - 1) {
return {
x: x,
y: y + 1
};
} else {
return {
x: x,
y: 0
};
}
}

getEastCellCoords(x, y) {
if (x !== this.width - 1) {
return {
x: x + 1,
y: y
};
} else {
return {
x: 0,
y: y
}
}
}

getCell(x, y) {
return this.array[y][x];
}

var coords = new Array();
coords.push(this.getNorthCellCoords(x, y));
coords.push(this.getWestCellCoords(x,y));
coords.push(this.getSouthCellCoords(x,y));
coords.push(this.getEastCellCoords(x,y));
return coords;
}

var empty = new Array();
for (let i = 0; i < coords.length; i++) {
let testX = coords[i].x;
let testY = coords[i].y;
if (this.array[testY][testX] == null) {
empty.push(coords[i]);
}
}
return empty;
}

var fish = new Array();
for (let i = 0; i < coords.length; i++) {
let testX = coords[i].x;
let testY = coords[i].y;
if (this.array[testY][testX] != null) {
if (this.array[testY][testX].species === 'fish') {
fish.push(coords[i]);
}
}
}
return fish;
}

move(origin, destination) {
this.array[origin.y][origin.x].x = destination.x;
this.array[origin.y][origin.x].y = destination.y;
this.array[destination.y][destination.x] = this.array[origin.y][origin.x];
this.array[origin.y][origin.x] = null;
}

this.numFish++;
this.array[origin.y][origin.x] = new Fish(this, origin.x, origin.y, chronon);
}

this.numSharks++;
this.array[origin.y][origin.x] = new Shark(this, origin.x, origin.y, chronon);
}

kill(coord) {
if (this.array[coord.y][coord.x].species === 'fish') {
this.numFish--;
} else if (this.array[coord.y][coord.x].species === 'shark') {
this.numSharks--;
}
this.array[coord.y][coord.x] = null;
}

generate() {
let fishPct = document.getElementById('fishPct').value / 100;
let sharkPct = document.getElementById('sharkPct').value / 100;
for (let x = 0; x < this.width; x++) {
for (let y = 0; y < this.height; y++) {
let r = Math.random();
if (r < fishPct) {
this.array[y][x] = new Fish(this, x, y, 0);
this.numFish++;
} else if (r < fishPct + sharkPct) {
this.array[y][x] = new Shark(this, x, y, 0);
this.numSharks++;
} else {
this.array[y][x] = null;
}
}
}
}

draw() {
const imageData = this.ctx.getImageData(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
const data = imageData.data;
for (let x = 0; x < this.width; x++) {
for (let y = 0; y < this.height; y++) {
let i = 4 * (y * this.width + x);
if (this.array[y][x] == null) {
// white water
data[i] = 255;
data[i + 1] = 255;
data[i + 2] = 255;
data[i + 3] = 255;
} else if (this.array[y][x].species === 'fish') {
// yellow fish
data[i] = 255;
data[i + 1] = 255;
data[i + 2] = 0;
data[i + 3] = 255;
} else if (this.array[y][x].species === 'shark') {
// red sharks
data[i] = 255;
data[i + 1] = 0;
data[i + 2] = 0;
data[i + 3] = 255;
}
}
}
this.ctx.putImageData(imageData, 0, 0);
document.getElementById('chrononText').innerHTML = this.chronon;
document.getElementById('fishText').innerHTML = this.numFish;
document.getElementById('sharksText').innerHTML = this.numSharks;
}

tick() {
this.chronon++;
for (let x = 0; x < this.width; x++) {
for (let y = 0; y < this.height; y++) {
if (this.array[y][x] == null) {
// do nothing
} else {
this.array[y][x].tick(this.chronon);
}
}
}
}
}


Animal

class Animal {
constructor(sea, x, y, chronon) {
this.sea = sea;
this.x = x;
this.y = y;
this.chronon = chronon;
this.species = null;
this.hasMoved = false;
this.turnsToBreed = 0;
this.turnsToStarve = 0;
}
}


Fish

class Fish extends Animal {
constructor(sea, x, y, chronon) {
super(sea, x, y, chronon);
this.turnsToBreed = document.getElementById('fishBreedTurns').value;
this.species = 'fish';
}

tick(chronon) {
if (chronon === this.chronon) {
} else {
// yet to move
if (empty.length === 0) {
} else {
// there is at least one empty adjacent cell available
var origin = {x: this.x, y: this.y};
var destination = randomChoice(empty);
if (this.turnsToBreed <= 0) {
this.turnsToBreed = document.getElementById('fishBreedTurns').value;
this.sea.move(origin, destination);
} else {
this.sea.move(origin, destination);
}
}
this.chronon++;
this.turnsToBreed--;
}
}
}


Shark

class Shark extends Animal {
constructor(sea, x, y, chronon) {
super(sea, x, y, chronon);
this.turnsToBreed = document.getElementById('sharkBreedTurns').value;
this.turnsToStarve = document.getElementById('sharkStarveTurns').value;
this.species = 'shark';
}

tick(chronon) {
if (chronon === this.chronon) {
} else {
// yet to move
if (fish.length === 0) {
if (empty.length === 0) {
} else {
// there is at least one empty adjacent cell available
var origin = {x: this.x, y: this.y};
var destination = randomChoice(empty);
if (this.turnsToBreed <= 0) {
this.turnsToBreed = document.getElementById('sharkBreedTurns').value;
this.sea.move(origin, destination);
} else {
this.sea.move(origin, destination);
}
}
} else {
// devourin' time!
var origin = {x: this.x, y: this.y};
var destination = randomChoice(fish);
this.turnsToStarve = document.getElementById('sharkStarveTurns').value;
if (this.turnsToBreed <= 0) {
this.turnsToBreed = document.getElementById('sharkBreedTurns').value + 1;
this.sea.kill(destination);
this.sea.move(origin, destination);
} else {
this.sea.kill(destination);
this.sea.move(origin, destination);
}
}
this.chronon++;
this.turnsToBreed--;
this.turnsToStarve--;
if (this.turnsToStarve === 0) {
this.sea.kill({x: this.x, y: this.y});
}
}
}
}


I had a lot of questions pop up while I was writing this. Some of them:

1. Did I take the right approach by using the Canvas? I considered SVG but read that Canvas is better for many entities, especially if I did one-to-one pixels. Would the next step be GL for this kind of thing?
2. Did I use JS's "class" system properly? I don't think I took full advantage of Fish and Shark inheriting from Animal.
3. Did I organize my files properly? I feel like three .js files may be overkill.
4. What does my code smell like?
5. Did I miss out on any helpful design patterns?

I don't have much experience doing this, so some guidance would be enormously helpful. Thank you.

• Thank you. Added...is it too much?
– Jeff
Commented Oct 18, 2022 at 13:45
• We accept quite large programs for review - that's not a problem here! Commented Oct 18, 2022 at 14:09

Pretty good!

1. Definitely, for pixel-sized entities, bitmap is imho perfect choice :)
2. You could definitely design your code more polymorphic. tick method could be already in your Animal class and possibly taking care of some of the common stuff like checking for correct chronon value automatically. Imagine one method in animal, that does this check and if this check passes, it calls another method and that method actually does the job (and is implemented in your subclasses Shark and Fish). There could be more methods in Animal probably depending on your design. It seems weird to call sea.move(fish) (should sea move the fish? shouldn't fish move on it's own?).
3. It's always good to split into more smaller pieces :) Although it's good practice to merge all your .js files into one big when in browser. webpack is one of common tools for that.
4. Hah, smells fine!
5. Take a look at "game loop", polymorphism, "Law of demeter".

Some things to modify:

• Is the check of chronon chronon === this.chronon really necessary? Does that really happen? I would expect the calling code makes sure it is always called only once per frame. This just increases nesting and doesn't look too good. In case you need this in your code, I would use negative condition with return to reduce nesting:

if (chronon !== this.chronon) return

• I don't like referencing settings from your classes through html divs. Is it because you want the settings to be dynamic and it's possible to modify them while the game is running? Even so, I would prefer a wrapper that has access to those settings so that UI is not so tightly bound to your logic.

• Very often you have branching and then there's a lot of shared code in both pathways. That is unnecessary duplication, that you could leave outside.

For example this:

if (this.turnsToBreed <= 0) {
this.turnsToBreed = document.getElementById('sharkBreedTurns').value + 1;
this.sea.kill(destination);
this.sea.move(origin, destination);
} else {
this.sea.kill(destination);
this.sea.move(origin, destination);
}


Can turn into this:

this.sea.kill(destination);
this.sea.move(origin, destination);
if (this.turnsToBreed <= 0) {
this.turnsToBreed = document.getElementById('sharkBreedTurns').value + 1;

• Your tick methods are too big, extract them into smaller ones with more specific purpose - moving, breeding, devouring, etc.