Skip to main content
Tweeted twitter.com/StackCodeReview/status/1582431335091576832
included code
Source Link
Jeff
  • 33
  • 4

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];
    }

    getAdjacentCoords(x, y) {
        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;
    }

    getEmptyAdjacentCoords(x, y) {
        var coords = this.getAdjacentCoords(x, y);
        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;
    }

    getAdjacentFishCoords(x, y) {
        var coords = this.getAdjacentCoords(x, y);
        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;
    }

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

    addShark(origin, 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) {
            // already moved            
        } else {
            // yet to move
            var empty = this.sea.getEmptyAdjacentCoords(this.x, this.y);
            if (empty.length === 0) {
                // no empty adjacent cells
            } 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);
                    this.sea.addFish(origin, this.chronon + 1);
                } 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) {
            // already moved
        } else {
            // yet to move
            var fish = this.sea.getAdjacentFishCoords(this.x, this.y);
            if (fish.length === 0) {
                // no adjacent fish
                var empty = this.sea.getEmptyAdjacentCoords(this.x, this.y);
                if (empty.length === 0) {
                    // no empty adjacent cells
                } 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);
                        this.sea.addShark(origin, this.chronon + 1);
                    } 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);
                    this.sea.addShark(origin, this.chronon + 1);
                } 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});
            }
        }
    }
}

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];
    }

    getAdjacentCoords(x, y) {
        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;
    }

    getEmptyAdjacentCoords(x, y) {
        var coords = this.getAdjacentCoords(x, y);
        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;
    }

    getAdjacentFishCoords(x, y) {
        var coords = this.getAdjacentCoords(x, y);
        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;
    }

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

    addShark(origin, 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) {
            // already moved            
        } else {
            // yet to move
            var empty = this.sea.getEmptyAdjacentCoords(this.x, this.y);
            if (empty.length === 0) {
                // no empty adjacent cells
            } 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);
                    this.sea.addFish(origin, this.chronon + 1);
                } 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) {
            // already moved
        } else {
            // yet to move
            var fish = this.sea.getAdjacentFishCoords(this.x, this.y);
            if (fish.length === 0) {
                // no adjacent fish
                var empty = this.sea.getEmptyAdjacentCoords(this.x, this.y);
                if (empty.length === 0) {
                    // no empty adjacent cells
                } 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);
                        this.sea.addShark(origin, this.chronon + 1);
                    } 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);
                    this.sea.addShark(origin, this.chronon + 1);
                } 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});
            }
        }
    }
}
Source Link
Jeff
  • 33
  • 4

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.

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.