How Can I Avoid Race Conditions When Users Try to Buy Tree Upgrades in My “Cookie Clicker” CO₂ Savings Game?

I’ve built the most basic CO₂ Tree Saver Game (still a work in progress, don’t judge). The idea is simple: users save CO₂, earn upgrades, and plant trees. However, there’s a small hiccup — when users try to buy a tree upgrade while their CO₂ savings are still being updated in the background, chaos ensues.

The logic around using CO₂ to buy a tree might be a little… unrefined, but that’s not the problem here. The issue is that when users return after being offline, the local localStorage value for CO₂ is out of sync, and they might end up trying to buy an upgrade they can’t afford.

Here’s a simplified version:

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Green Game (Beta)</title>
    <style>
        .tree {
            width: 50px;
            height: 50px;
            background-color: green;
            border-radius: 50%;
            margin-top: 20px;
        }
        .tree.upgraded {
            width: 100px;
            height: 100px;
            background-color: #8B4513; /* Tree brown color when upgraded */
        }
    </style>
</head>
<body>
    <h1>Welcome to the Green Game (Beta)</h1>
    <p id="game-status">Collecting CO₂ data...</p>
    <p id="co2-output">Current CO₂: 0kg</p>
    <p id="tree-status">You can plant a tree with 100kg of CO₂ saved.</p>

    <div id="tree" class="tree"></div>

    <button id="plant-tree-btn" disabled>Plant a Tree</button>
    <button id="upgrade-tree-btn" disabled>Upgrade Tree</button>

    <script>
        class GreenGame {
            constructor() {
                // Retrieve saved data from localStorage or use defaults
                this.co2Saved = parseInt(localStorage.getItem("co2Saved")) || 0; // Load CO₂ or default to 0
                this.treeThreshold = 100; // CO₂ needed to plant a tree
                this.treeUpgraded = localStorage.getItem("treeUpgraded") === "true"; // Load tree upgrade status

                this.co2Output = document.getElementById("co2-output");
                this.gameStatus = document.getElementById("game-status");
                this.treeStatus = document.getElementById("tree-status");
                this.treeElement = document.getElementById("tree");
                this.plantTreeBtn = document.getElementById("plant-tree-btn");
                this.upgradeTreeBtn = document.getElementById("upgrade-tree-btn");

                // Reflect saved tree upgrade state
                if (this.treeUpgraded) {
                    this.treeElement.classList.add("upgraded");
                    this.treeStatus.textContent = "Tree upgraded! Now it’s a mighty tree!";
                    this.upgradeTreeBtn.disabled = true; // Disable upgrade after upgrading
                }
            }

            updateCO2() {
                // Simulate receiving new CO₂ data (could be from an API)
                setTimeout(() => {
                    const newCO2 = Math.floor(Math.random() * 20); // Random CO₂ value
                    this.co2Saved += newCO2;
                    this.co2Output.textContent = `Current CO₂: ${this.co2Saved}kg`;

                    if (this.co2Saved >= this.treeThreshold) {
                        this.treeStatus.textContent = "You can now plant a tree!";
                        this.plantTreeBtn.disabled = false; // Enable plant button
                    } else {
                        this.treeStatus.textContent = "You need more CO₂ to plant a tree!";
                        this.plantTreeBtn.disabled = true; // Disable plant button
                    }

                    this.gameStatus.textContent = `New CO₂ input: +${newCO2}kg`;

                    // Save updated CO₂ value to localStorage
                    localStorage.setItem("co2Saved", this.co2Saved);

                    // Recursively simulate data updates
                    this.updateCO2();
                }, 3000); // Updates every 3 seconds
            }

            plantTree() {
                if (this.co2Saved >= this.treeThreshold) {
                    this.treeElement.classList.remove("upgraded"); // Reset upgrade on new tree
                    this.treeElement.style.display = "block";
                    this.treeStatus.textContent = "You planted a tree!";
                    this.co2Saved -= this.treeThreshold; // Subtract the CO₂ used to plant the tree
                    this.co2Output.textContent = `Current CO₂: ${this.co2Saved}kg`;

                    this.upgradeTreeBtn.disabled = false; // Enable upgrade button
                    this.plantTreeBtn.disabled = true; // Disable plant button after planting

                    // Save updated CO₂ value to localStorage
                    localStorage.setItem("co2Saved", this.co2Saved);
                }
            }

            upgradeTree() {
                this.treeElement.classList.add("upgraded");
                this.treeStatus.textContent = "Tree upgraded! Now it’s a mighty tree!";
                this.upgradeTreeBtn.disabled = true; // Disable upgrade button after upgrading

                // Save tree upgrade state to localStorage
                localStorage.setItem("treeUpgraded", "true");
            }
        }

        // Start the game
        const game = new GreenGame();
        game.updateCO2(); // Begin simulation

        // Attach event listeners to buttons
        document.getElementById("plant-tree-btn").addEventListener("click", () => game.plantTree());
        document.getElementById("upgrade-tree-btn").addEventListener("click", () => game.upgradeTree());
    </script>
</body>
</html>

The Issue:

  1. CO₂ savings are updated every few seconds, but when a user returns after being offline, their CO₂ value is out of sync.

  2. If they try to buy an upgrade, the app mistakenly thinks they have enough CO₂, even though it’s outdated.

The Question:

  1. What’s the best way to prevent race conditions when the user is offline and comes back to find their CO₂ savings completely out of whack?

My js:

// Initial CO2 savings and tree planting state
let co2Saved = 10;
let treeCount = 0;

const plantTreeButton = document.getElementById('plant-tree');
const upgradeTreeButton = document.getElementById('upgrade-tree');

const updateCO2 = (newCO2Value) => {
  co2Saved = newCO2Value;
  console.log(`CO₂ Saved: ${co2Saved}kg`);
};

const plantTree = () => {
  console.log('Planting tree...');
  treeCount++;
  console.log(`You have ${treeCount} trees now.`);
};

const upgradeTree = () => {
  console.log('Upgrading tree...');
  treeCount++;
  console.log(`You have ${treeCount} trees now.`);
};

// Simulating multiple clicks with race condition
plantTreeButton.addEventListener('click', () => {
  // Simulate async updates to CO2 savings and tree planting
  setTimeout(() => {
    updateCO2(co2Saved + 10); // Update CO2 after planting tree
    plantTree();
  }, Math.random() * 1000); // Random delay to simulate timing race
});

upgradeTreeButton.addEventListener('click', () => {
  // Simulate async updates to CO2 savings and tree upgrading
  setTimeout(() => {
    updateCO2(co2Saved + 5); // Update CO2 after upgrading tree
    upgradeTree();
  }, Math.random() * 1000); // Random delay to simulate timing race
});

This is what happens:

  1. Process 1 checks the balance and sees 100kg.

  2. Process 2 also checks the balance and sees 100kg.

  3. Both processes try to withdraw 50kg.

  4. After both processes finish, they both update the balance to 50kg, even though 100kg should have been the correct result after two withdrawals.

Why I think this is happening:

  • Multiple threads or processes are running concurrently.

  • These threads/processes share a common resource – the database that pulls in CO₂ live info.

This is a basic version of the game, and yes, it’s a work in progress. But if you can solve this, I’ll plant a tree (in the game, because it’s a work in progress).

Thanks in advance for any advice — or suggestions to fix the CO₂ as currency logic.