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:
-
CO₂ savings are updated every few seconds, but when a user returns after being offline, their CO₂ value is out of sync.
-
If they try to buy an upgrade, the app mistakenly thinks they have enough CO₂, even though it’s outdated.
The Question:
- 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:
-
Process 1 checks the balance and sees 100kg.
-
Process 2 also checks the balance and sees 100kg.
-
Both processes try to withdraw 50kg.
-
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.