Back to leaderboard

neuralAdapter V1

Created 9/7/2026

Active

Uses a tiny RNN model that gradually learns the opponent's bot strategy

Avg score

8.544

Wins

137

Losses

7834

Win rate

0.8%

Total battles: 17622

Score history

Score trend over the last 199 battles

Avg: 8.544

Recent battles

Source

export default function bot({ history, memory }) {

    const H = 4;
    const LR = 0.08;

    // ---------------------------------------------------------
    // ACTIVATION FUNCTIONS
    // ---------------------------------------------------------

    function sigmoid(x) {
        return 1 / (1 + Math.exp(-x));
    }

    // ---------------------------------------------------------
    // INITIALIZE RNN
    // ---------------------------------------------------------

    if (memory == null) {

        // Deterministic small initial weights.
        // Avoid Math.random() so every match starts identically.
        memory = {
            Wx: [
                [ 0.10, -0.08],
                [-0.06,  0.12],
                [ 0.08,  0.05],
                [-0.10,  0.07]
            ],

            Wh: [
                [ 0.05, -0.03,  0.02,  0.01],
                [-0.02,  0.04,  0.01, -0.03],
                [ 0.03,  0.01, -0.04,  0.02],
                [ 0.01, -0.02,  0.03,  0.04]
            ],

            bh: [0, 0, 0, 0],

            Wo: [0.05, -0.05, 0.05, -0.05],

            // Start slightly biased toward cooperation.
            bo: -0.2,

            // Current hidden state
            h: [0, 0, 0, 0],

            // Information required to train the previous prediction
            lastH: null,
            lastInput: null,
            lastPrediction: null,

            rounds: 0
        };
    }


    // ---------------------------------------------------------
    // TRAIN ON THE RESULT OF OUR PREVIOUS PREDICTION
    // ---------------------------------------------------------

    if (
        history.length > 0 &&
        memory.lastPrediction != null &&
        memory.lastH != null
    ) {

        const actualMove = history.at(-1).opponent;

        // D = 1
        // C = 0
        const target = actualMove === "D" ? 1 : 0;

        const prediction = memory.lastPrediction;

        // For sigmoid + binary cross entropy:
        //
        // dL/dz = prediction - target

        const outputError = prediction - target;


        // Save the old output weights because they are required
        // for calculating the hidden error.
        const oldWo = memory.Wo.slice();


        // -------------------------
        // Train output layer
        // -------------------------

        for (let i = 0; i < H; i++) {
            memory.Wo[i] -=
                LR * outputError * memory.lastH[i];
        }

        memory.bo -= LR * outputError;


        // -------------------------
        // Train hidden layer
        //
        // This is ONE-STEP backprop.
        // We are NOT yet propagating through the entire history.
        // -------------------------

        for (let i = 0; i < H; i++) {

            const h = memory.lastH[i];

            // derivative of tanh:
            //
            // d/dx tanh(x) = 1 - tanh(x)^2

            const hiddenError =
                outputError *
                oldWo[i] *
                (1 - h * h);


            // Input -> hidden weights
            for (let j = 0; j < 2; j++) {
                memory.Wx[i][j] -=
                    LR *
                    hiddenError *
                    memory.lastInput[j];
            }

            // Hidden bias
            memory.bh[i] -= LR * hiddenError;
        }
    }


    // ---------------------------------------------------------
    // FIRST ROUND
    // ---------------------------------------------------------

    // We have no history from which to predict anything.
    if (history.length === 0) {

        memory.rounds++;

        return ["C", memory];
    }


    // ---------------------------------------------------------
    // CONVERT THE MOST RECENT ROUND INTO RNN INPUT
    // ---------------------------------------------------------

    const lastRound = history.at(-1);

    const input = [
        lastRound.you === "D" ? 1 : 0,
        lastRound.opponent === "D" ? 1 : 0
    ];


    // ---------------------------------------------------------
    // RNN FORWARD PASS
    //
    // h(t) = tanh(
    //          Wx*x(t)
    //        + Wh*h(t-1)
    //        + bh
    //       )
    // ---------------------------------------------------------

    const newH = new Array(H).fill(0);

    for (let i = 0; i < H; i++) {

        let sum = memory.bh[i];

        // Input contribution
        for (let j = 0; j < 2; j++) {
            sum += memory.Wx[i][j] * input[j];
        }

        // Recurrent contribution
        for (let j = 0; j < H; j++) {
            sum += memory.Wh[i][j] * memory.h[j];
        }

        newH[i] = Math.tanh(sum);
    }


    // ---------------------------------------------------------
    // OUTPUT
    //
    // prediction = P(opponent defects NEXT round)
    // ---------------------------------------------------------

    let z = memory.bo;

    for (let i = 0; i < H; i++) {
        z += memory.Wo[i] * newH[i];
    }

    const prediction = sigmoid(z);


    // ---------------------------------------------------------
    // SAVE INFORMATION FOR NEXT ROUND'S TRAINING
    // ---------------------------------------------------------

    memory.h = newH;
    memory.lastH = newH.slice();
    memory.lastInput = input.slice();
    memory.lastPrediction = prediction;
    memory.rounds++;


    // ---------------------------------------------------------
    // STRATEGY V1
    // ---------------------------------------------------------

    let move;

    // During early learning, use Tit-for-Tat.
    if (history.length < 8) {

        move = lastRound.opponent;

    } else {

        // RNN predicts probability of opponent defecting.
        //
        // If it thinks defection is likely, defend ourselves.
        // Otherwise cooperate.
        //
        // Conservative threshold for V1.
        move = prediction > 0.55 ? "D" : "C";
    }


    return [move, memory];
}