Search code examples
javascriptpythoncellular-automata

Cellular automata works in Python, not Javascript


Edit: I put a JavaScript snippet at the bottom.

I've made two versions of the same celluar automata in JavaScript and Python.

The Python version works perfectly as expected, whereas the JavaScript doesn't...

The problem is the third generation.

How it looks in Python:

enter image description here

How it looks in JavaScript:

enter image description here

Two cells (C and D) should change from defectors back to cooperators (blue cell). This is because these cells have a cooperator in their neighbourhood that scores a 6 (6 > 5.7). On the other hand, cells A and B change rightfully back to cooperators. My confusion is that this automata is perfectly symmetrical. Why is A and B working, but not C and D when they are, in theory, existing under the exact same conditions?

Some where in the update function I suspected the cells C and D have a neighbor whose score in generation= 3 is 6 (a cooperator. Only a cooperator could score this. See picture of generation = 2 below to see what I mean) but this neighbor's actual strategy is for some reason being written as a defector, when is should be a cooperator.

 this.updateCell = function(row, col) {
        let player = this.active_array[row][col];
        let best_strat = 0;
        let highest_score = 0; 
        player.neighbours.forEach(coords => {
            let x_coord = coords[0];
            let y_coord = coords[1];
            let neighbour = this.active_array[x_coord][y_coord];
            if (neighbour.score > highest_score) {
                highest_score = neighbour.score;
                best_strat = neighbour.strat;
            }
        });
        if (player.score < highest_score) {
            return best_strat;
        }
        if (player.score >= highest_score) {
            return player.strat;
        }
    }

I ran some console logs in the third generation and it seems to be the case. When cells C and D update, highest_score is returning a 6 (hence, a cooperator) but best_strategy is returning a 1 (meaning a defector). Meaning, cells C and D must have a neighbor whose strategy should be a cooperator but it's actually a defector (even though the scores are correct). If I'm right, I'm not sure how to fix this.

What's equally annoying is that I've tried basically copying the logic from the working Python version in JavaScript exactly, but no matter what I change, nothing seems to fix it. I can't find any difference between the two! :<

Again, the first two generations of the JavaScript version work perfectly.

enter image description here

enter image description here

enter image description here

For reference, the working Python code (JavaScript snippet below):

from calendar import c
from random import choices, sample, shuffle
import numpy as np
import pandas as pd
import copy
import matplotlib.pyplot as plt
from collections import Counter

v = 1.96
generations = 100
width = 100
height = 100
# cooperate = 0, defect = 1 
strategies = ['0', '1']
next_cells = {}
temp = [0.2, 0.8]
payoffs = np.array([[1, 0], [1 + v, 0]])
rpm = pd.DataFrame(payoffs, columns=['0', '1'])
rpm.index = ['0', '1']
cpm = rpm.T
output2 = []

for x in range(width):
    for y in range(height):
        if (x == width/2) and (y == height/2):
            strat = '1'
        else:
            strat = '0'
        next_cells[(x, y)] = {
            'strategy': strat, 
            'prev_strat': None, 
            'score': 0, 
            'neighbours': [((x + 1) % width, y), ((x - 1) % width, y), (x, (y - 1) % height), (x, (y + 1) % height),
            ((x + 1) % width, (y - 1) % height), ((x + 1) % width, (y + 1) % height), ((x - 1) % width, (y - 1) % height), ((x - 1) % width, (y + 1) % height)
            ]
        }

for gen in range(generations):
    output = np.zeros(shape=(width, height))
    cells = copy.deepcopy(next_cells)
    for coord, cell in cells.items():
        score = 0
        if cell['strategy'] == '0':
                score += 1
        for neighbour in cell['neighbours']:
            if cell['strategy'] == '0' and cells[neighbour]['strategy'] == '0':
                score += 1
            if cell['strategy'] == '1' and cells[neighbour]['strategy'] == '0':
                score += v
        cell['score'] = score
                
    for coord, cell in cells.items():
        highest_score = 0
        best_strat = None
        for neighbour in cell['neighbours']:
            if cells[neighbour]['score'] > highest_score:
                highest_score = cells[neighbour]['score']
                best_strat = cells[neighbour]['strategy']
        if cell['score'] < highest_score:
            next_cells[coord]['strategy'] = best_strat
            next_cells[coord]['prev_strat'] = cell['strategy']
        if cell['score'] >= highest_score:
            next_cells[coord]['strategy'] = cell['strategy']
            next_cells[coord]['prev_strat'] = cell['strategy']      
        
        x, y = coord[0], coord[1]
        if next_cells[coord]['strategy'] == '0' and next_cells[coord]['prev_strat'] == '1':
            output[x][y] = 2
        elif next_cells[coord]['strategy'] == '1' and next_cells[coord]['prev_strat'] == '0':
            output[x][y] = 3
        else:
            output[x][y] = int(next_cells[coord]['strategy'])
    

    plt.imshow(output, interpolation='nearest')
    plt.colorbar()
    plt.pause(0.01)
    plt.savefig(f"images/foo{gen}.png")
    plt.close("all")

plt.show()

class Cell4 {

    constructor() {

        this.score = 0;
        this.neighbours;
        this.strat;
        this.prev_strat;

        this.initNeighbours = function(row, col) {
            let array = [];
            array.push(
            [(((row - 1) % rows) + rows) % rows, (((col - 1) % cols) + cols) % cols],
            [(((row + 1) % rows) + rows) % rows, (((col + 1) % cols) + cols) % cols],
            [(((row - 1) % rows) + rows) % rows, (((col + 1) % cols) + cols) % cols],
            [(((row + 1) % rows) + rows) % rows, (((col - 1) % cols) + cols) % cols],
            [(((row - 1) % rows) + rows) % rows, col],
            [(((row + 1) % rows) + rows) % rows, col],  
            [row, (((col - 1) % cols) + cols) % cols],
            [row, (((col + 1) % cols) + cols) % cols],
            )
            this.neighbours = array;
        };
    }
}

class FractalPD {

    constructor() {

        this.u = 1.96;
        this.cooperator_color = 'blue';
        this.defect_color = 'red';
        this.was_defect_color = 'cyan';
        this.was_coop_color = 'yellow';
        this.active_array = [];
        this.inactive_array = [];
        
        this.makeGrid = function() {
            for (let i = 0; i < rows; i++) {
                this.active_array[i] = [];
                for (let j = 0; j < cols; j++) {
                   this.active_array[i][j] = 0;
                }
            }
            this.inactive_array = this.active_array;
        };

        this.randomizeGrid = function() {
            for (let i = 0; i < rows; i++) {
                for (let j = 0; j < cols; j++) {
                    const cell = new Cell4();
                    cell.strat = 0;
                    cell.prev_strat = 0;
                    cell.initNeighbours(i, j);
                    this.active_array[i][j] = cell;
                    if (i === parseInt(rows/2) && j === parseInt(cols/2)) {
                        this.active_array[i][j].strat = 1;
                        this.active_array[i][j].prev_strat = 1;
                    }
                }
            }
        };

        this.fillGrid = function() {
            // cooperate = 0 defect = 1
            for (let i = 0; i < rows; i++) {
                for (let j = 0; j < cols; j++) {
                    if (this.active_array[i][j].strat === 0 && this.active_array[i][j].prev_strat === 0) {
                        ctx.fillStyle = this.cooperator_color;
                    }
                    
                    if (this.active_array[i][j].strat === 1 && this.active_array[i][j].prev_strat === 1) {
                        ctx.fillStyle = this.defect_color;
                        
                    }
                    if (this.active_array[i][j].strat === 1 && this.active_array[i][j].prev_strat === 0) {
                        ctx.fillStyle = this.was_coop_color;
                        
                    }
                    if (this.active_array[i][j].strat === 0 && this.active_array[i][j].prev_strat === 1) {
                        ctx.fillStyle = this.was_defect_color;
                        
                    }
                    ctx.fillRect(j * cell_size, i * cell_size, cell_size - 1, cell_size - 1);
                    ctx.textAlign="center"; 
                    ctx.textBaseline = "middle";
                    ctx.fillStyle = "black";
                    ctx.fillText(`${this.active_array[i][j].score.toFixed(1)}`, (j * cell_size) + cell_size/2 , (i * cell_size) + cell_size/2)
                }
            }
        };
        
        this.playRound = function(row, col) {
            const player = this.active_array[row][col];
            const neighbours = player.neighbours;
            let score = 0;
            if (player.strat === 0) {
                score += 1;
            }
            neighbours.forEach(coords => {
                let x_coord = coords[0];
                let y_coord = coords[1];
                let neighbour = this.active_array[x_coord][y_coord];
                if (player.strat === 0 && neighbour.strat === 0) {
                   score += 1;
                }
                if (player.strat === 1 && neighbour.strat === 0) {
                   score += this.u;
                }
            });
            player.score = score;
        };

        this.updateCell = function(row, col) {
            let player = this.active_array[row][col];
            let best_strat = 0;
            let highest_score = 0; 
            player.neighbours.forEach(coords => {
                let x_coord = coords[0];
                let y_coord = coords[1];
                let neighbour = this.active_array[x_coord][y_coord];
                if (neighbour.score > highest_score) {
                    highest_score = neighbour.score;
                    best_strat = neighbour.strat;
                }
            });
            if (player.score < highest_score) {
                return best_strat;
            }
            if (player.score >= highest_score) {
                return player.strat;
            }
        }

        this.updateGrid = function() {

            for (let i = 0; i < rows; i++) {
                for (let j = 0; j < cols; j++) {
                    this.playRound(i, j);
                }
            }
            for (let i = 0; i < rows; i++) {
                for (let j = 0; j < cols; j++) {
                    let old_state = this.active_array[i][j].strat;
                    let new_state = this.updateCell(i, j);
                    this.inactive_array[i][j].strat = new_state;
                    this.inactive_array[i][j].prev_strat = old_state;
                }
            }
            this.active_array = this.inactive_array;
        };

        this.gameSetUp = () => {
            this.makeGrid();
        };

        this.runGame = () => {
            this.updateGrid();
            this.fillGrid();
        };
    }    
}

const canvas = document.querySelector("#gamefield");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const ctx = canvas.getContext("2d");
let PD = new FractalPD();
let cell_size = 19;
let cols = Math.floor(canvas.width / cell_size);
let rows = Math.floor(canvas.height / cell_size);

PD.gameSetUp();
PD.randomizeGrid();
PD.fillGrid();
setInterval(function() {
    PD.runGame();
}, 3000)
body {
    font-family: 'Poppins', sans-serif;
    background: #1d1d1d;
}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="style.css">
</head>

<body>
    <div id="content">
        <canvas id="gamefield" width="1500" height="1000"></canvas> 
    </div>
    <script src="fractalPD.js"></script>
</body>
</html>


Solution

  • At a glance, I would guess the problem is this line in your makeGrid method:

    this.inactive_array = this.active_array;
    

    This a reference assignment, not a deep copy, so you don't actually have two arrays, but just two references to the same array. When you try to update inactive_array in updateGrid, you're actually also updating active_array, since they're the same array.

    To fix this issue, you could make a deep copy of the array with structuredClone(). Or just have a method that creates a single array and returns it, and call it twice to get two independent arrays.