Build an Infinite World Engine
Build a Python world engine with procedural terrain, LRU caching, and SQLite.
Introduction
30 Second Summary
A game world can feel endless even though no computer can keep every tile ready at once. The illusion works because the game creates each nearby area only when a player needs it.
In this project, you will build a terminal world engine that uses deterministic generation to recreate terrain from a seed. You will make exploration responsive while keeping player changes after a restart.
What You'll Build
Your final demo is a terminal world that reconstructs familiar terrain during exploration while preserving only the tiles you changed.
By the end of this project, you'll have:
- A deterministic terrain demo where the same seed and coordinates always reveal the same 8 by 8 map.
- An observable LRU cache that reports hits, misses, evictions, and a maximum occupancy of two chunks as you travel.
- A restart-proof SQLite change store whose live storage statistics match the tradeoffs documented in ARCHITECTURE.md.
- Secret Mission: Reclaim storage space by deleting an override when a tile returns to its generated value.
Are there any prerequisites?
You should be comfortable writing Python classes, functions, loops, and dictionaries. Basic database familiarity is helpful but optional.
Everything runs locally with no paid services.
Before We Start
Before any hands-on work, take a moment to commit to the simulator you are building and the design question it answers. Separating terrain regenerated from a seed from stored player changes lets the world feel vast without saving every untouched tile.
Set Up the Local Workspace
A controlled local workspace keeps each later experiment isolated. Without that boundary, generation tests can become tangled with memory or persistence work.
You will use Visual Studio Code as the project home. You will confirm that Python can run a local file without cloud services or third-party packages.
In this step, get ready to:
- Open the minecraft-world-engine folder as a Visual Studio Code workspace.
- Confirm that Python 3.11 or newer is available.
- Run main.py to print the engine title.
Open the VS Code workspace
A workspace gives the project files one predictable location. The integrated terminal also starts inside the selected folder.
- Press Cmd+Space to open macOS search.
- Type Visual Studio Code into the search bar.
- Press Enter to open Visual Studio Code.
You will see the Visual Studio Code window.
- Click File in the top menu bar.
- Select Open Folder....
You will see the folder picker.
- Select your Desktop in the folder picker.
The folder picker now shows the contents of your Desktop.
- Create a folder named minecraft-world-engine with the folder picker's new-folder control.
You will see minecraft-world-engine listed on your Desktop.
- Select the minecraft-world-engine folder.
- Confirm the folder selection.
You will see minecraft-world-engine at the top of the Explorer sidebar. Your engine now has one local home.
Confirm Python is ready
The engine requires Python 3.11 or newer. Checking the interpreter now prevents version-related failures in later steps.
- Click Terminal in the Visual Studio Code menu bar.
- Select New Terminal.
You will see the integrated terminal open inside minecraft-world-engine.
- Check the installed Python version by running this command:
python3 --version
What Does This Command Check?
Python prints its version number. The command then exits.
The result places your Mac in one of three setup states.
✔️ I see a supported Python version
Check that the reported version is Python 3.11 or newer.
That version check proves your interpreter is ready for the engine.
ⓧ I see an older Python version
Your current interpreter is below the project minimum. The current reference release is Python 3.15.0.
The installer may request administrator approval. This permission allows Python to be installed locally.
- Open the Python.org downloads page.
- Download the macOS installer for Python 3.15.0.
You will see the installer in your downloads.
- Run the downloaded installer.
The installer guides you through the local installation.
- Approve the installation if macOS requests administrator access.
- Complete the installer.
You will see the installer confirm that Python is installed.
- Switch back to Visual Studio Code.
Your minecraft-world-engine workspace remains open.
- Click Terminal in the menu bar.
- Select New Terminal.
- Confirm the updated interpreter by running this command:
python3 --version
What Should You See?
The fresh terminal should report Python 3.11 or newer. The installed reference release reports Python 3.15.0.
Still Seeing an Older Version?
Close every Visual Studio Code window. Reopen the minecraft-world-engine workspace before checking again.
If the older version remains, help me identify which Python installation the integrated terminal is using.
ⓧ Command not found
The integrated terminal cannot currently access Python. Installing Python 3.15.0 supplies the interpreter required by the project.
The installer may request administrator approval. This permission allows Python to be installed locally.
- Open the Python.org downloads page.
- Download the macOS installer for Python 3.15.0.
You will see the installer in your downloads.
- Run the downloaded installer.
The installer guides you through the local installation.
- Approve the installation if macOS requests administrator access.
- Complete the installer.
You will see the installer confirm that Python is installed.
- Switch back to Visual Studio Code.
Your minecraft-world-engine workspace remains open.
- Click Terminal in the menu bar.
- Select New Terminal.
- Confirm the installed interpreter by running this command:
python3 --version
What Should You See?
The fresh terminal should report Python 3.11 or newer. The installed reference release reports Python 3.15.0.
Still Missing a Version Number?
Confirm that the installer reached its completion screen. Reopen Visual Studio Code after the installation completes.
If the command remains unavailable, help me make Python available in the integrated terminal.
Create and run the starter file
A one-line starter file tests the full path from your editor to the interpreter. Its terminal output gives you the first visible result from the engine.
- Create main.py inside minecraft-world-engine with the new-file control in the Explorer sidebar.
- Add the engine title to main.py by pasting this code:
print("Minecraft-Style Infinite World Engine")
What Does This Code Do?
- The print() function writes text to the terminal.
- The title proves that Python executed the project file.
- Press Cmd+S to save main.py.
Before you run the file, what exact title do you expect the terminal to print?
- Run the starter file in the integrated terminal with this command:
python3 main.py
What Should You See?
The command asks Python to execute main.py. You will see Minecraft-Style Infinite World Engine on its own line.
That is your first visible result from the local engine workspace.
Does the Title Fail to Appear?
Confirm that main.py is saved inside minecraft-world-engine. Check that the integrated terminal is using the same folder.
Compare the capitalization plus quotation marks with the code above. If the title still does not appear, help me debug the starter file.
✔️ Awesome, I've got everything!
Your saved starter file prints the engine title. The workspace is ready for the terrain generator.
ⓧ I'd like to double check the full code
- Compare main.py with the complete file below.
print("Minecraft-Style Infinite World Engine")
Your first Python file now runs inside the project workspace. Next, you will use one seed to generate the same terrain twice.
Generate Deterministic Terrain
Your title script proves that the local workspace runs. The world engine now needs terrain that can be recreated after the process exits.
Deterministic generation turns stable inputs into repeatable output. This step turns one seed plus global coordinates into the same 8 by 8 chunk every time.
In this step, get ready to:
- Map global coordinates to repeatable terrain symbols.
- Generate an 8 by 8 chunk from its chunk address.
- Render the same chunk twice to prove the results match.
Map global coordinates to terrain
A stable hash converts the seed plus each coordinate into a repeatable number. The hashlib.sha256() function gives every tile a consistent terrain range.
- In the VS Code Explorer sidebar, click New File beside the minecraft-world-engine folder.
- Type generator.py in the file name field.
- Press Enter to create the file.
- Define the generator foundations by pasting this code:
import hashlib
CHUNK_SIZE = 8
TERRAIN_LABELS = {
"~": "water",
".": "plains",
"T": "forest",
"^": "mountain",
}
What Does This Code Set Up?
- The hashlib import provides the stable hashing function used for every coordinate.
- CHUNK_SIZE fixes each chunk at eight tiles per side.
- TERRAIN_LABELS connects each map symbol to a readable terrain name.
- Save generator.py.
- Confirm the Explorer sidebar lists generator.py beside main.py.
Is the Generator File Missing?
Confirm that generator.py sits directly inside the minecraft-world-engine folder. Check that the file name ends with .py.
Help me check my generator file.
Each tile needs a pure calculation based on the same inputs. The generator stores the seed as text before combining it with x plus y.
- Place your cursor two blank lines below the closing brace of TERRAIN_LABELS.
- Add the tile generator by pasting this code:
class TerrainGenerator:
def __init__(self, seed):
self.seed = str(seed)
self.chunk_generations = 0
def generate_tile(self, x, y):
payload = f"{self.seed}:{x}:{y}".encode("utf-8")
value = hashlib.sha256(payload).digest()[0]
if value < 48:
return "~"
if value < 144:
return "."
if value < 224:
return "T"
return "^"
How Does One Tile Get Chosen?
- The seed gives every generated coordinate a shared world identity.
- payload combines the seed with one global coordinate pair.
- value uses the first digest byte as a repeatable number.
- The threshold checks map that number to water, plains, forest, or mountain terrain.
- Save generator.py.
- Confirm generate_tile() contains four possible terrain returns.
Does the Tile Method Look Misaligned?
Keep generate_tile() aligned with __init__() inside TerrainGenerator. Keep each return indented beneath its matching condition.
Help me fix my tile method.
Build an 8 by 8 chunk
Spatial partitioning divides the virtual world into addressed regions. A chunk address becomes a global origin before the generator calculates its 64 tiles.
- Place your cursor one blank line below return "^" inside TerrainGenerator.
- Add chunk generation by pasting this code:
def generate_chunk(self, chunk_x, chunk_y):
self.chunk_generations += 1
origin_x = chunk_x * CHUNK_SIZE
origin_y = chunk_y * CHUNK_SIZE
return [
[
self.generate_tile(origin_x + local_x, origin_y + local_y)
for local_x in range(CHUNK_SIZE)
]
for local_y in range(CHUNK_SIZE)
]
How Does Chunk Generation Work?
- chunk_generations counts how many full chunks the generator creates.
- origin_x converts the horizontal chunk address into its first global tile coordinate.
- origin_y converts the vertical chunk address into its first global tile coordinate.
- The nested list builds eight rows containing eight generated tiles each.
- Save generator.py.
- Confirm generate_chunk() remains inside TerrainGenerator at the same indentation level as generate_tile().
Is the Chunk Method Outside the Class?
Check that def generate_chunk begins with four spaces. Confirm that the nested list closes with two matching square brackets.
Help me check my chunk method.
✔️ Awesome, I've got everything!
Your saved generator.py now converts stable inputs into complete terrain chunks.
ⓧ I'd like to double check the full code
Compare your saved generator.py with this complete reference.
import hashlib
CHUNK_SIZE = 8
TERRAIN_LABELS = {
"~": "water",
".": "plains",
"T": "forest",
"^": "mountain",
}
class TerrainGenerator:
def __init__(self, seed):
self.seed = str(seed)
self.chunk_generations = 0
def generate_tile(self, x, y):
payload = f"{self.seed}:{x}:{y}".encode("utf-8")
value = hashlib.sha256(payload).digest()[0]
if value < 48:
return "~"
if value < 144:
return "."
if value < 224:
return "T"
return "^"
def generate_chunk(self, chunk_x, chunk_y):
self.chunk_generations += 1
origin_x = chunk_x * CHUNK_SIZE
origin_y = chunk_y * CHUNK_SIZE
return [
[
self.generate_tile(origin_x + local_x, origin_y + local_y)
for local_x in range(CHUNK_SIZE)
]
for local_y in range(CHUNK_SIZE)
]
What Should Match?
The file defines the terrain constants before TerrainGenerator. The class contains seed setup, tile generation, plus chunk generation in that order.
Prove the chunk is repeatable
A deterministic claim needs an observable test. The demo creates chunk (0, 0) twice from the same seed before comparing the resulting lists.
- In the VS Code Explorer sidebar, click New File beside the minecraft-world-engine folder.
- Type demo_generation.py in the file name field.
- Press Enter to create the file.
- Build the generation demo by pasting this code:
from generator import TerrainGenerator
def render(chunk):
return "\n".join("".join(row) for row in chunk)
def main():
generator = TerrainGenerator("nextwork-world-42")
first = generator.generate_chunk(0, 0)
second = generator.generate_chunk(0, 0)
print("First generation:\n")
print(render(first))
print("\nSecond generation:\n")
print(render(second))
result = "PASS" if first == second else "FAIL"
print(f"\nDeterministic check: {result}")
if __name__ == "__main__":
main()
What Does the Demo Prove?
- render() joins each tile row into a terminal-friendly map.
- main() creates one generator with the seed nextwork-world-42.
- first plus second hold two independently generated copies of chunk (0, 0).
- result reports whether the complete nested lists match.
- Save demo_generation.py.
✔️ Awesome, I've got everything!
Your demo is saved. It is ready to compare two independently generated chunks.
ⓧ I'd like to double check the full code
Compare your saved demo_generation.py with this complete reference.
from generator import TerrainGenerator
def render(chunk):
return "\n".join("".join(row) for row in chunk)
def main():
generator = TerrainGenerator("nextwork-world-42")
first = generator.generate_chunk(0, 0)
second = generator.generate_chunk(0, 0)
print("First generation:\n")
print(render(first))
print("\nSecond generation:\n")
print(render(second))
result = "PASS" if first == second else "FAIL"
print(f"\nDeterministic check: {result}")
if __name__ == "__main__":
main()
What Should Match?
The file imports TerrainGenerator before defining render() plus main(). The final guard calls main() when you run the file.
Before you run the demo, predict whether both rendered maps will match.
- Return to the integrated terminal from earlier.
- Test deterministic generation by running this command:
python3 demo_generation.py
What Does This Command Do?
The python3 interpreter executes demo_generation.py. The file calls main() to generate both maps.
You'll see two identical 8 by 8 maps followed by Deterministic check: PASS. You have now proved that the same seed plus coordinates reconstruct the same terrain.
Does the Demo Fail?
Confirm that generator.py plus demo_generation.py are saved inside the same folder. Compare the indentation of both generator methods with the full-code reference.
Help me diagnose my generation demo.
System Design Checkpoint
The seed plus the algorithm act as compact source data for untouched terrain. The engine can reconstruct those tiles without saving every symbol.
A future algorithm change could alter regenerated terrain. Production systems pair procedural generation with generator versions plus migration rules.
Your world now rebuilds the same chunk on demand. Next, you will keep recently visited chunks fast with a bounded LRU cache.
Stream Chunks Through an LRU Cache
Your deterministic generation code can rebuild any chunk from its seed plus global coordinates. Recent chunks still cost CPU each time the generator rebuilds them.
A bounded LRU cache keeps the hottest chunks in memory. Lazy loading generates each chunk only when the explorer first requests it.
In this step, get ready to:
- Build a two-entry LRU cache.
- Map global positions into generated chunks.
- Explore the world while observing bounded cache activity.
Build the bounded cache
The least recently used policy treats every successful lookup as a recency update. When the capacity is exceeded, the oldest untouched entry leaves the cache.
Why Use an Explicit LRU Cache?
An explicit cache object exposes every hit. It also exposes every miss.
Python's OrderedDict also makes recency changes plus evictions visible. Those counters turn the cache policy into something you can inspect.
- Return to Visual Studio Code from earlier.
- Create cache.py inside the minecraft-world-engine workspace using the new-file control in the Explorer sidebar.
- Add the cache state plus lookup behavior to cache.py by pasting this code:
from collections import OrderedDict
class LRUChunkCache:
def __init__(self, capacity=2):
if capacity < 1:
raise ValueError("Cache capacity must be at least 1")
self.capacity = capacity
self._items = OrderedDict()
self.hits = 0
self.misses = 0
self.evictions = 0
def get(self, key):
if key not in self._items:
self.misses += 1
return None
self.hits += 1
self._items.move_to_end(key)
return self._items[key]
What Does This Code Do?
- The constructor stores the capacity. It also creates counters for observable cache activity.
- The get() method records a miss when a key is absent.
- A successful lookup calls move_to_end(key). This marks the chunk as the most recently used entry.
- Save cache.py.
- Confirm the editor shows capacity plus the three cache counters.
Seeing a Cache Syntax Problem?
Check that get() remains indented inside LRUChunkCache. Confirm that the import sits at the top of cache.py.
Help me check the first part of my cache class.
The cache can now recognize hot chunks. It still needs insertion plus eviction behavior to keep its size bounded.
- Place the cursor below the final line of get() in cache.py.
- Add the insertion plus inspection methods by pasting this code:
def put(self, key, value):
if key in self._items:
self._items[key] = value
self._items.move_to_end(key)
return None
self._items[key] = value
if len(self._items) > self.capacity:
evicted_key, _ = self._items.popitem(last=False)
self.evictions += 1
return evicted_key
return None
def size(self):
return len(self._items)
def keys(self):
return list(self._items.keys())
How Does Eviction Work?
- The put() method refreshes an existing key without increasing the cache size.
- The popitem(last=False) call removes the least recently used entry.
- The inspection methods expose the current occupancy plus the order from least recent to most recent.
- Save cache.py.
- Confirm the class now ends with size() plus keys().
Does the Cache Class Look Incomplete?
Confirm that put() aligns with get(). Check that size() plus keys() stay inside the class.
Help me check my completed cache class.
✔️ Awesome, I've got everything!
Good work. Your cache can now record activity while enforcing a fixed capacity.
ⓧ I'd like to double check the full code
Compare your saved cache.py with this complete file.
from collections import OrderedDict
class LRUChunkCache:
def __init__(self, capacity=2):
if capacity < 1:
raise ValueError("Cache capacity must be at least 1")
self.capacity = capacity
self._items = OrderedDict()
self.hits = 0
self.misses = 0
self.evictions = 0
def get(self, key):
if key not in self._items:
self.misses += 1
return None
self.hits += 1
self._items.move_to_end(key)
return self._items[key]
def put(self, key, value):
if key in self._items:
self._items[key] = value
self._items.move_to_end(key)
return None
self._items[key] = value
if len(self._items) > self.capacity:
evicted_key, _ = self._items.popitem(last=False)
self.evictions += 1
return evicted_key
return None
def size(self):
return len(self._items)
def keys(self):
return list(self._items.keys())
What Should Match?
Your file should contain one constructor plus four cache methods. The counters should begin at zero.
Connect coordinates to cached chunks
The engine converts each global position into a chunk address plus a local tile address. Python's divmod(a, b) supplies the quotient plus remainder needed for positive or negative coordinates.
- Create engine.py inside minecraft-world-engine using the new-file control in the Explorer sidebar.
- Add the engine state plus coordinate conversion by pasting this code:
from cache import LRUChunkCache
from generator import CHUNK_SIZE, TERRAIN_LABELS, TerrainGenerator
ALLOWED_TILES = set(TERRAIN_LABELS) | {"#"}
class WorldEngine:
def __init__(self, seed, cache_capacity=2, store=None):
self.seed = str(seed)
self.generator = TerrainGenerator(self.seed)
self.cache = LRUChunkCache(cache_capacity)
self.store = store
self.explored_chunks = set()
self.modified_positions = set()
def chunk_address(self, x, y):
chunk_x, local_x = divmod(x, CHUNK_SIZE)
chunk_y, local_y = divmod(y, CHUNK_SIZE)
return chunk_x, chunk_y, local_x, local_y
What Does the Engine Track?
- Each engine owns a generator plus its own bounded cache.
- The explored_chunks set records unique chunk addresses visited during the session.
- The chunk_address() method separates global coordinates from positions inside an 8 by 8 chunk.
- Save engine.py.
- Confirm WorldEngine creates one generator plus one cache.
Seeing an Import or Indentation Problem?
Confirm cache.py sits beside engine.py. Check that chunk_address() remains inside WorldEngine.
Help me check the start of my world engine.
A chunk request now has enough information to locate its cache entry. A miss regenerates the chunk before placing it into the bounded hot set.
- Place the cursor below chunk_address() in engine.py.
- Add chunk retrieval plus tile access by pasting this code:
def get_chunk(self, chunk_x, chunk_y):
key = (chunk_x, chunk_y)
self.explored_chunks.add(key)
chunk = self.cache.get(key)
if chunk is None:
chunk = self.generator.generate_chunk(chunk_x, chunk_y)
self.cache.put(key, chunk)
return chunk
def get_tile(self, x, y):
chunk_x, chunk_y, local_x, local_y = self.chunk_address(x, y)
return self.get_chunk(chunk_x, chunk_y)[local_y][local_x]
How Does Lazy Loading Work?
- Every request first checks the cache using the chunk coordinates as its key.
- A cache miss calls generate_chunk() exactly when the chunk is needed.
- The get_tile() method reads one local position from the effective chunk.
- Save engine.py.
- Confirm get_chunk() checks the cache before calling the generator.
Is a Generated Chunk Missing From the Cache?
Check that self.cache.put(key, chunk) stays inside the cache-miss branch. Confirm that return chunk aligns with the branch.
Help me trace my chunk lookup flow.
The final engine methods expose deterministic checks plus cache statistics. Tile mutation is included so the next experiment can test whether memory survives a restart.
- Place the cursor below get_tile() in engine.py.
- Add mutation plus observability behavior by pasting this code:
def set_tile(self, x, y, tile):
if tile not in ALLOWED_TILES:
allowed = " ".join(sorted(ALLOWED_TILES))
raise ValueError(f"Choose one symbol: {allowed}")
chunk_x, chunk_y, local_x, local_y = self.chunk_address(x, y)
self.get_chunk(chunk_x, chunk_y)[local_y][local_x] = tile
self.modified_positions.add((x, y))
def verify_determinism(self, x, y):
first = self.generator.generate_tile(x, y)
second = self.generator.generate_tile(x, y)
return first == second, first, second
def stats(self):
return {"unique_chunks_explored": len(self.explored_chunks), "chunk_generations": self.generator.chunk_generations, "cache_hits": self.cache.hits, "cache_misses": self.cache.misses, "cache_evictions": self.cache.evictions, "cache_size": self.cache.size(), "cache_capacity": self.cache.capacity, "cached_chunks_lru_to_mru": self.cache.keys()}
def close(self):
pass
What Becomes Observable?
- The verify_determinism() method regenerates one base tile twice for comparison.
- The stats() method reports generation activity plus cache activity.
- The close() method provides a cleanup hook for the persistent store added later.
- Save engine.py.
- Confirm the file ends with close().
Does the Statistics Method Fail to Parse?
Confirm the statistics dictionary has matching braces. Check that each key remains inside the single returned dictionary.
Help me check my engine statistics method.
✔️ Awesome, I've got everything!
That is the engine layer complete. Global positions now resolve through a bounded chunk cache.
ⓧ I'd like to double check the full code
Compare your saved engine.py with this complete file.
from cache import LRUChunkCache
from generator import CHUNK_SIZE, TERRAIN_LABELS, TerrainGenerator
ALLOWED_TILES = set(TERRAIN_LABELS) | {"#"}
class WorldEngine:
def __init__(self, seed, cache_capacity=2, store=None):
self.seed = str(seed)
self.generator = TerrainGenerator(self.seed)
self.cache = LRUChunkCache(cache_capacity)
self.store = store
self.explored_chunks = set()
self.modified_positions = set()
def chunk_address(self, x, y):
chunk_x, local_x = divmod(x, CHUNK_SIZE)
chunk_y, local_y = divmod(y, CHUNK_SIZE)
return chunk_x, chunk_y, local_x, local_y
def get_chunk(self, chunk_x, chunk_y):
key = (chunk_x, chunk_y)
self.explored_chunks.add(key)
chunk = self.cache.get(key)
if chunk is None:
chunk = self.generator.generate_chunk(chunk_x, chunk_y)
self.cache.put(key, chunk)
return chunk
def get_tile(self, x, y):
chunk_x, chunk_y, local_x, local_y = self.chunk_address(x, y)
return self.get_chunk(chunk_x, chunk_y)[local_y][local_x]
def set_tile(self, x, y, tile):
if tile not in ALLOWED_TILES:
allowed = " ".join(sorted(ALLOWED_TILES))
raise ValueError(f"Choose one symbol: {allowed}")
chunk_x, chunk_y, local_x, local_y = self.chunk_address(x, y)
self.get_chunk(chunk_x, chunk_y)[local_y][local_x] = tile
self.modified_positions.add((x, y))
def verify_determinism(self, x, y):
first = self.generator.generate_tile(x, y)
second = self.generator.generate_tile(x, y)
return first == second, first, second
def stats(self):
return {"unique_chunks_explored": len(self.explored_chunks), "chunk_generations": self.generator.chunk_generations, "cache_hits": self.cache.hits, "cache_misses": self.cache.misses, "cache_evictions": self.cache.evictions, "cache_size": self.cache.size(), "cache_capacity": self.cache.capacity, "cached_chunks_lru_to_mru": self.cache.keys()}
def close(self):
pass
What Should Match?
Your engine should convert coordinates before requesting a chunk. Its statistics should expose the cache capacity plus current occupancy.
Explore the cached world
The terminal interface turns cache behavior into a repeatable experiment. Jumping across chunk boundaries produces misses plus evictions that you can inspect through stats.
- Select the existing main.py file in the Explorer sidebar.
- Select the existing title-print line.
- Replace the selected line with the renderer setup below:
from engine import WorldEngine
from generator import TERRAIN_LABELS
SEED = "nextwork-world-42"
CACHE_CAPACITY = 2
PERSISTENCE_ENABLED = False
def print_help():
print("Commands: w a s d, go <x> <y>, set <symbol>, verify, stats, help, quit")
def render(engine, player_x, player_y):
chunk_x, chunk_y, local_x, local_y = engine.chunk_address(player_x, player_y)
chunk = engine.get_chunk(chunk_x, chunk_y)
current_tile = engine.get_tile(player_x, player_y)
print(f"\nChunk: ({chunk_x}, {chunk_y}) | Player: ({player_x}, {player_y})")
for row_index, row in enumerate(chunk):
display_row = list(row)
if row_index == local_y:
display_row[local_x] = "@"
print(" ".join(display_row))
print(f"Standing on: {current_tile} ({TERRAIN_LABELS.get(current_tile, 'player-built')})")
How Does the Renderer Work?
- The constants select one seed plus a cache capacity of two chunks.
- The renderer asks the engine for the player's current chunk.
- The @ symbol replaces the player's local tile only in the displayed copy.
- Save main.py.
- Confirm the renderer ends with the current terrain description.
Does the Renderer Look Misaligned?
Check that the row loop remains inside render(). Confirm that the player replacement remains inside the matching-row branch.
Help me check my terminal renderer.
The main loop renders before every prompt. Movement changes global coordinates before the next loop requests a chunk.
- Place the cursor below render() in main.py.
- Add the loop setup plus command parsing by pasting this code:
def main():
print("Minecraft-Style Infinite World Engine")
engine = WorldEngine(SEED, CACHE_CAPACITY)
player_x = player_y = 0
moves = {"w": (0, -1), "a": (-1, 0), "s": (0, 1), "d": (1, 0)}
try:
while True:
render(engine, player_x, player_y)
parts = input("world> ").strip().split()
if not parts:
continue
command = parts[0].lower()
What Does the Loop Prepare?
- The engine starts at global position zero with a two-chunk cache.
- The movement dictionary translates one-letter commands into coordinate changes.
- The prompt splits each entry into a command plus any supplied values.
- Save main.py.
- Confirm the final visible line assigns command from the first prompt value.
Is the Main Loop Nested Incorrectly?
Confirm the prompt remains inside while True. Check that the loop remains inside the try block.
Help me check my interactive loop structure.
The next paste is indentation-sensitive, so take this one slowly. Each branch must line up with the first movement check.
- Place the cursor directly below the command assignment in main.py.
- Complete the command branches plus cleanup behavior by pasting this code:
if command in moves:
delta_x, delta_y = moves[command]
player_x += delta_x
player_y += delta_y
elif command == "go" and len(parts) == 3:
player_x, player_y = int(parts[1]), int(parts[2])
elif command == "set" and len(parts) == 2:
engine.set_tile(player_x, player_y, parts[1])
elif command == "verify":
matches, first, second = engine.verify_determinism(player_x, player_y)
print(f"Deterministic generator check: {'PASS' if matches else 'FAIL'} ({first} == {second})")
elif command == "stats":
print(engine.stats())
elif command == "help":
print_help()
elif command in {"quit", "q"}:
break
finally:
engine.close()
if __name__ == "__main__":
main()
Which Commands Are Available?
- Movement commands update the player's global coordinates.
- The go command jumps directly to another position.
- The verify command checks the generated base tile twice.
- The stats command prints generation plus cache counters.
- Save main.py.
- Confirm the file ends with a call to main().
Seeing an Indentation Error in Main?
Align every elif branch with the first if command in moves line. Keep finally aligned with try.
Help me check the completed command loop.
✔️ Awesome, I've got everything!
Your terminal explorer is ready to stream generated chunks through its bounded cache.
ⓧ I'd like to double check the full code
Compare your saved main.py with this complete file.
from engine import WorldEngine
from generator import TERRAIN_LABELS
SEED = "nextwork-world-42"
CACHE_CAPACITY = 2
PERSISTENCE_ENABLED = False
def print_help():
print("Commands: w a s d, go <x> <y>, set <symbol>, verify, stats, help, quit")
def render(engine, player_x, player_y):
chunk_x, chunk_y, local_x, local_y = engine.chunk_address(player_x, player_y)
chunk = engine.get_chunk(chunk_x, chunk_y)
current_tile = engine.get_tile(player_x, player_y)
print(f"\nChunk: ({chunk_x}, {chunk_y}) | Player: ({player_x}, {player_y})")
for row_index, row in enumerate(chunk):
display_row = list(row)
if row_index == local_y:
display_row[local_x] = "@"
print(" ".join(display_row))
print(f"Standing on: {current_tile} ({TERRAIN_LABELS.get(current_tile, 'player-built')})")
def main():
print("Minecraft-Style Infinite World Engine")
engine = WorldEngine(SEED, CACHE_CAPACITY)
player_x = player_y = 0
moves = {"w": (0, -1), "a": (-1, 0), "s": (0, 1), "d": (1, 0)}
try:
while True:
render(engine, player_x, player_y)
parts = input("world> ").strip().split()
if not parts:
continue
command = parts[0].lower()
if command in moves:
delta_x, delta_y = moves[command]
player_x += delta_x
player_y += delta_y
elif command == "go" and len(parts) == 3:
player_x, player_y = int(parts[1]), int(parts[2])
elif command == "set" and len(parts) == 2:
engine.set_tile(player_x, player_y, parts[1])
elif command == "verify":
matches, first, second = engine.verify_determinism(player_x, player_y)
print(f"Deterministic generator check: {'PASS' if matches else 'FAIL'} ({first} == {second})")
elif command == "stats":
print(engine.stats())
elif command == "help":
print_help()
elif command in {"quit", "q"}:
break
finally:
engine.close()
if __name__ == "__main__":
main()
What Should Match?
Your file should keep PERSISTENCE_ENABLED set to False. The cache capacity should remain two.
Before you run the explorer, what do you expect to see at global position zero?
- Run the terminal explorer in the integrated terminal by running this command:
python3 main.py
What Does This Command Start?
Python loads the cache plus engine modules before calling main(). The application then waits at the world prompt.
You will see an 8 by 8 terrain map. The @ symbol marks the player at global position zero.
Does the Explorer Stop Before Showing a Map?
Check that cache.py plus engine.py sit beside main.py. Use the reported filename plus line number to find an indentation mismatch.
Help me diagnose why the terminal explorer does not start.
Before you test the cache, do you think visiting three distinct chunks can leave all three in a cache with capacity two?
- Enter go 8 0 at the world prompt.
- Enter go 16 0 at the next world prompt.
- Enter go 8 0 at the next world prompt.
- Enter go 0 0 at the next world prompt.
- Enter stats at the next world prompt.
What Should the Report Prove?
- The cache_hits value is greater than zero.
- The cache_misses value is greater than zero.
- The cache_evictions value shows that at least one old chunk left the cache.
- The cache_size value is no greater than two.
You have proved that recent chunks can be reused without allowing memory to grow with every location visited. The explorer now has a measurable bounded hot set.
- Enter quit at the world prompt.
Your world now streams chunks through a bounded cache. Next, you will change a cached tile. A restart will expose what memory cannot preserve.
Expose the Persistence Failure
Your two-entry LRU cache keeps recent terrain available during one process. A tile changed inside that cache continues to look durable while the process stays alive.
This step puts that apparent durability under pressure. You will change one tile before restarting the world to test whether the player-authored value survives.
In this step, get ready to:
- Expose the number of tiles modified during the current run.
- Change the tile beneath the player to a player-built symbol.
- Restart the engine to test whether the cached change survives.
Expose volatile changes
The existing modified_positions set records each coordinate changed during one process. Adding its size to stats() makes that temporary state visible.
- Select engine.py in the Visual Studio Code Explorer sidebar.
- Find the current stats() function:
def stats(self):
return {"unique_chunks_explored": len(self.explored_chunks), "chunk_generations": self.generator.chunk_generations, "cache_hits": self.cache.hits, "cache_misses": self.cache.misses, "cache_evictions": self.cache.evictions, "cache_size": self.cache.size(), "cache_capacity": self.cache.capacity, "cached_chunks_lru_to_mru": self.cache.keys()}
What Do the Current Statistics Show?
This dictionary reports generation activity plus cache activity. It does not report how many coordinates the current process has modified.
- Replace the current stats() function with this version:
def stats(self):
return {"unique_chunks_explored": len(self.explored_chunks), "modified_tiles_this_run": len(self.modified_positions), "chunk_generations": self.generator.chunk_generations, "cache_hits": self.cache.hits, "cache_misses": self.cache.misses, "cache_evictions": self.cache.evictions, "cache_size": self.cache.size(), "cache_capacity": self.cache.capacity, "cached_chunks_lru_to_mru": self.cache.keys()}
What Does the New Metric Measure?
The modified_tiles_this_run metric counts coordinates held in modified_positions. A new process begins with an empty set.
- Save engine.py.
- Return to the integrated terminal from earlier.
- Start the engine by running this command:
python3 main.py
What Does This Run Establish?
The terminal renders the chunk at global coordinate (0, 0). The interactive world> prompt confirms that the engine is ready.
- Inspect the new session metric by entering:
stats
What Should the Metric Report?
The statistics include 'modified_tiles_this_run': 0. No tile has been changed during this process.
- Stop the current session by entering:
quit
What Does Quit Do?
The command exits the interactive loop. The terminal returns to its normal prompt.
The terminal also needs to display the active persistence mode. A confirmation after set makes each mutation visible before the next render.
- Select main.py in the Explorer sidebar.
- Find the opening lines of main() near the middle of the file:
def main():
print("Minecraft-Style Infinite World Engine")
engine = WorldEngine(SEED, CACHE_CAPACITY)
What Does Startup Show Now?
The current startup prints the project title. It does not reveal whether durable storage is active.
- Replace those opening lines with this version:
def main():
print("Minecraft-Style Infinite World Engine")
print(f"Persistence enabled: {PERSISTENCE_ENABLED}")
engine = WorldEngine(SEED, CACHE_CAPACITY)
What Does the Startup Message Prove?
Every launch now prints the value of PERSISTENCE_ENABLED. The experiment uses volatile memory while that value remains False.
- Find the current set command branch:
elif command == "set" and len(parts) == 2:
engine.set_tile(player_x, player_y, parts[1])
What Does the Current Branch Do?
The branch passes the current coordinates plus the requested symbol to set_tile(). The next render exposes the changed tile beneath the player marker.
- Replace the current branch with this version:
elif command == "set" and len(parts) == 2:
engine.set_tile(player_x, player_y, parts[1])
print(f"Set ({player_x}, {player_y}) to {parts[1]}")
What Does the Confirmation Add?
The new line prints the changed global coordinate plus the requested symbol. This gives the mutation an immediate terminal signal.
- Save main.py.
Seeing a Python Error After the Edits?
Check that modified_tiles_this_run remains inside the dictionary returned by stats().
Check that the new confirmation line has the same indentation as engine.set_tile(player_x, player_y, parts[1]).
Ask for targeted help if the terminal reports a syntax or indentation problem: Help me check the observability edits in engine.py and main.py.
Change a cached tile
A cached chunk is a mutable grid. Changing one cell updates later reads from the same in-memory chunk.
- Start the memory-only engine by running this command:
python3 main.py
What Does This Session Confirm?
The startup output includes Persistence enabled: False. The rendered map shows the generated terrain at (0, 0).
- Record the symbol shown after Standing on: as your generated base tile symbol.
Before you change the tile, what value do you expect modified_tiles_this_run to report after one successful mutation?
- Change the current tile to # by entering:
set #
What Does the Mutation Change?
The command changes the cell inside the cached chunk. It also adds coordinate (0, 0) to modified_positions.
Good work. The next render shows Standing on: # beneath the player marker.
- Inspect the session statistics by entering:
stats
What Do the Statistics Prove?
The output includes 'modified_tiles_this_run': 1. The current process knows that one coordinate has changed.
- End the modified session by entering:
quit
What State Just Ended?
The process containing the modified chunk has stopped. The restart can now test whether any durable source remembers #.
Restart the world
Before you restart, do you expect the tile beneath the player to remain # or return to its generated symbol?
- Start a fresh process by running this command:
python3 main.py
What Does the Restart Reveal?
The restarted world shows the same symbol you recorded as your generated base tile symbol. The player-authored # has disappeared.
The TerrainGenerator.generate_tile(x, y) method can reconstruct deterministic base terrain from stable inputs. It has no input that describes the previous player's edit.
Why Did the Change Disappear?
The cached chunk belonged to the earlier process. Restarting created a new cache before deterministic generation rebuilt the original terrain.
That's the persistence failure exposed. Volatile memory preserved the change only while its process remained alive.
- Inspect the fresh process state by entering:
stats
What Does the Fresh Count Show?
The output includes 'modified_tiles_this_run': 0. This new process has no record of the earlier mutation.
- End the restarted session by entering:
quit
What Has the Experiment Proved?
The generated base tile is repeatable across restarts. A player-authored override needs a durable source of truth outside the cache.
The next checkpoint names the local database path while keeping persistence disabled. This preserves the failure you just observed.
- Return to main.py in the Explorer sidebar.
- Find the configuration block near the top of the file:
SEED = "nextwork-world-42"
CACHE_CAPACITY = 2
PERSISTENCE_ENABLED = False
What Controls the Current Mode?
The PERSISTENCE_ENABLED flag remains False. The current engine still passes no store to WorldEngine.
- Replace the configuration block with this version:
SEED = "nextwork-world-42"
CACHE_CAPACITY = 2
DATABASE_PATH = "world_changes.db"
PERSISTENCE_ENABLED = False
What Does the Path Define?
The DATABASE_PATH constant gives the local database file a stable name. The disabled flag means this step still creates no database connection.
- Save main.py.
✔️ Awesome, I've got everything!
Your project now exposes session modifications plus the active persistence mode. Double-check that every edited file is saved.
ⓧ I'd like to double check the full code
Compare your project files with these checkpoint versions. Match any differences before the final verification.
from collections import OrderedDict
class LRUChunkCache:
def __init__(self, capacity=2):
if capacity < 1:
raise ValueError("Cache capacity must be at least 1")
self.capacity = capacity
self._items = OrderedDict()
self.hits = self.misses = self.evictions = 0
def get(self, key):
if key not in self._items:
self.misses += 1
return None
self.hits += 1
self._items.move_to_end(key)
return self._items[key]
def put(self, key, value):
if key in self._items:
self._items[key] = value
self._items.move_to_end(key)
return None
self._items[key] = value
if len(self._items) > self.capacity:
self.evictions += 1
return self._items.popitem(last=False)[0]
return None
def size(self):
return len(self._items)
def keys(self):
return list(self._items.keys())
from generator import TerrainGenerator
def render(chunk):
return "\n".join("".join(row) for row in chunk)
def main():
generator = TerrainGenerator("nextwork-world-42")
first = generator.generate_chunk(0, 0)
second = generator.generate_chunk(0, 0)
print("First generation:\n")
print(render(first))
print("\nSecond generation:\n")
print(render(second))
result = "PASS" if first == second else "FAIL"
print(f"\nDeterministic check: {result}")
if __name__ == "__main__":
main()
from cache import LRUChunkCache
from generator import CHUNK_SIZE, TERRAIN_LABELS, TerrainGenerator
ALLOWED_TILES = set(TERRAIN_LABELS) | {"#"}
class WorldEngine:
def __init__(self, seed, cache_capacity=2, store=None):
self.seed = str(seed)
self.generator = TerrainGenerator(self.seed)
self.cache = LRUChunkCache(cache_capacity)
self.store = store
self.explored_chunks = set()
self.modified_positions = set()
def chunk_address(self, x, y):
chunk_x, local_x = divmod(x, CHUNK_SIZE)
chunk_y, local_y = divmod(y, CHUNK_SIZE)
return chunk_x, chunk_y, local_x, local_y
def get_chunk(self, chunk_x, chunk_y):
key = (chunk_x, chunk_y)
self.explored_chunks.add(key)
chunk = self.cache.get(key)
if chunk is None:
chunk = self.generator.generate_chunk(chunk_x, chunk_y)
self.cache.put(key, chunk)
return chunk
def get_tile(self, x, y):
chunk_x, chunk_y, local_x, local_y = self.chunk_address(x, y)
return self.get_chunk(chunk_x, chunk_y)[local_y][local_x]
def set_tile(self, x, y, tile):
if tile not in ALLOWED_TILES:
raise ValueError(f"Choose one symbol: {' '.join(sorted(ALLOWED_TILES))}")
chunk_x, chunk_y, local_x, local_y = self.chunk_address(x, y)
self.get_chunk(chunk_x, chunk_y)[local_y][local_x] = tile
self.modified_positions.add((x, y))
def verify_determinism(self, x, y):
first = self.generator.generate_tile(x, y)
second = self.generator.generate_tile(x, y)
return first == second, first, second
def stats(self):
return {"unique_chunks_explored": len(self.explored_chunks), "modified_tiles_this_run": len(self.modified_positions), "chunk_generations": self.generator.chunk_generations, "cache_hits": self.cache.hits, "cache_misses": self.cache.misses, "cache_evictions": self.cache.evictions, "cache_size": self.cache.size(), "cache_capacity": self.cache.capacity, "cached_chunks_lru_to_mru": self.cache.keys()}
def close(self):
pass
import hashlib
CHUNK_SIZE = 8
TERRAIN_LABELS = {"~": "water", ".": "plains", "T": "forest", "^": "mountain"}
class TerrainGenerator:
def __init__(self, seed):
self.seed = str(seed)
self.chunk_generations = 0
def generate_tile(self, x, y):
value = hashlib.sha256(f"{self.seed}:{x}:{y}".encode("utf-8")).digest()[0]
if value < 48:
return "~"
if value < 144:
return "."
if value < 224:
return "T"
return "^"
def generate_chunk(self, chunk_x, chunk_y):
self.chunk_generations += 1
origin_x = chunk_x * CHUNK_SIZE
origin_y = chunk_y * CHUNK_SIZE
return [[self.generate_tile(origin_x + local_x, origin_y + local_y) for local_x in range(CHUNK_SIZE)] for local_y in range(CHUNK_SIZE)]
from engine import WorldEngine
from generator import TERRAIN_LABELS
SEED = "nextwork-world-42"
CACHE_CAPACITY = 2
DATABASE_PATH = "world_changes.db"
PERSISTENCE_ENABLED = False
def print_help():
print("Commands: w a s d, go <x> <y>, set <symbol>, verify, stats, help, quit")
def render(engine, player_x, player_y):
chunk_x, chunk_y, local_x, local_y = engine.chunk_address(player_x, player_y)
chunk = engine.get_chunk(chunk_x, chunk_y)
current_tile = engine.get_tile(player_x, player_y)
print(f"\nChunk: ({chunk_x}, {chunk_y}) | Player: ({player_x}, {player_y})")
for row_index, row in enumerate(chunk):
display_row = list(row)
if row_index == local_y:
display_row[local_x] = "@"
print(" ".join(display_row))
print(f"Standing on: {current_tile} ({TERRAIN_LABELS.get(current_tile, 'player-built')})")
def main():
print("Minecraft-Style Infinite World Engine")
print(f"Persistence enabled: {PERSISTENCE_ENABLED}")
engine = WorldEngine(SEED, CACHE_CAPACITY)
player_x = player_y = 0
moves = {"w": (0, -1), "a": (-1, 0), "s": (0, 1), "d": (1, 0)}
try:
while True:
render(engine, player_x, player_y)
parts = input("world> ").strip().split()
if not parts:
continue
command = parts[0].lower()
if command in moves:
delta_x, delta_y = moves[command]
player_x += delta_x
player_y += delta_y
elif command == "go" and len(parts) == 3:
player_x, player_y = int(parts[1]), int(parts[2])
elif command == "set" and len(parts) == 2:
engine.set_tile(player_x, player_y, parts[1])
print(f"Set ({player_x}, {player_y}) to {parts[1]}")
elif command == "verify":
matches, first, second = engine.verify_determinism(player_x, player_y)
print(f"Deterministic generator check: {'PASS' if matches else 'FAIL'} ({first} == {second})")
elif command == "stats":
print(engine.stats())
elif command == "help":
print_help()
elif command in {"quit", "q"}:
break
finally:
engine.close()
if __name__ == "__main__":
main()
Before the final run, what tile do you expect a fresh memory-only process to display at (0, 0)?
- Run the completed checkpoint with this command:
python3 main.py
What Should You See?
The terminal prints Persistence enabled: False. The Standing on: line shows your generated base tile symbol instead of #.
- End the final verification by entering:
quit
What Has the Checkpoint Isolated?
The cache preserves a mutation during one process. The deterministic generator restores only the base terrain after restart.
You have reproduced the durability gap with a controlled restart. Next, you will add sparse storage so player changes can outlive the cache.
Persist Sparse Tile Overrides
The restart test exposed the limit of your LRU cache. A cached change disappears when the Python process ends.
Your Visual Studio Code workspace now needs a durable source for player changes. SQLite provides sparse persistence while the generator continues reconstructing untouched terrain.
In this step, get ready to:
- Create a SQLite change store for player-authored tile overrides.
- Overlay saved changes whenever the engine regenerates a chunk.
- Measure the logical storage savings across a restart.
Create the sparse change store
The generated world remains the default state. The database stores one row for each player-authored seed plus coordinate combination.
Why SQLite for This Store?
SQLite supports selective reads plus durable updates in one local file. This keeps the project focused on sparse records without requiring a database server.
A JSON file would require the application to manage partial updates plus crash-safe writes. SQLite already provides those storage boundaries.
- Create store.py inside the minecraft-world-engine workspace with this code:
import sqlite3
class ChangeStore:
def __init__(self, database_path="world_changes.db"):
self.connection = sqlite3.connect(database_path)
self.connection.execute("CREATE TABLE IF NOT EXISTS tile_overrides (world_seed TEXT NOT NULL, x INTEGER NOT NULL, y INTEGER NOT NULL, tile TEXT NOT NULL, PRIMARY KEY (world_seed, x, y))")
self.connection.commit()
def get_chunk_overrides(self, seed, chunk_x, chunk_y, chunk_size):
min_x, min_y = chunk_x * chunk_size, chunk_y * chunk_size
cursor = self.connection.execute("SELECT x, y, tile FROM tile_overrides WHERE world_seed = ? AND x >= ? AND x < ? AND y >= ? AND y < ?", (str(seed), min_x, min_x + chunk_size, min_y, min_y + chunk_size))
return {(x, y): tile for x, y, tile in cursor.fetchall()}
def save_override(self, seed, x, y, tile):
self.connection.execute("INSERT OR REPLACE INTO tile_overrides(world_seed, x, y, tile) VALUES(?, ?, ?, ?)", (str(seed), x, y, tile))
self.connection.commit()
def count_overrides(self, seed):
return self.connection.execute("SELECT COUNT(*) FROM tile_overrides WHERE world_seed = ?", (str(seed),)).fetchone()[0]
def override_positions(self, seed):
return self.connection.execute("SELECT x, y FROM tile_overrides WHERE world_seed = ?", (str(seed),)).fetchall()
def close(self):
self.connection.close()
What Does This Store Do?
- The tile_overrides table uses the world seed plus global coordinates as its primary key.
- The chunk query reads overrides within one chunk's coordinate boundaries.
- The replacement write keeps one effective symbol for each saved position.
- The remaining queries expose persisted records for runtime statistics.
- Save store.py.
- Check the workspace file list for store.py.
Is the Store File Missing?
Check that store.py sits directly inside minecraft-world-engine. The file name must use lowercase letters.
Confirm that the file ends with .py. A different extension prevents the later import.
Help me check my change store file.
✔️ Awesome, I've got everything!
Your saved store.py now defines the complete SQLite storage boundary.
ⓧ I'd like to double check the full code
import sqlite3
class ChangeStore:
def __init__(self, database_path="world_changes.db"):
self.connection = sqlite3.connect(database_path)
self.connection.execute("CREATE TABLE IF NOT EXISTS tile_overrides (world_seed TEXT NOT NULL, x INTEGER NOT NULL, y INTEGER NOT NULL, tile TEXT NOT NULL, PRIMARY KEY (world_seed, x, y))")
self.connection.commit()
def get_chunk_overrides(self, seed, chunk_x, chunk_y, chunk_size):
min_x, min_y = chunk_x * chunk_size, chunk_y * chunk_size
cursor = self.connection.execute("SELECT x, y, tile FROM tile_overrides WHERE world_seed = ? AND x >= ? AND x < ? AND y >= ? AND y < ?", (str(seed), min_x, min_x + chunk_size, min_y, min_y + chunk_size))
return {(x, y): tile for x, y, tile in cursor.fetchall()}
def save_override(self, seed, x, y, tile):
self.connection.execute("INSERT OR REPLACE INTO tile_overrides(world_seed, x, y, tile) VALUES(?, ?, ?, ?)", (str(seed), x, y, tile))
self.connection.commit()
def count_overrides(self, seed):
return self.connection.execute("SELECT COUNT(*) FROM tile_overrides WHERE world_seed = ?", (str(seed),)).fetchone()[0]
def override_positions(self, seed):
return self.connection.execute("SELECT x, y FROM tile_overrides WHERE world_seed = ?", (str(seed),)).fetchall()
def close(self):
self.connection.close()
What Should You Compare?
Confirm that the class includes chunk reads plus override writes. Confirm that it also includes statistics queries plus connection cleanup.
The storage class now exists. The application needs a small factory that creates it only when persistence is enabled.
- Select main.py from the workspace file list.
- Replace everything from the imports through PERSISTENCE_ENABLED = False with this section:
from engine import WorldEngine
from generator import TERRAIN_LABELS
SEED = "nextwork-world-42"
CACHE_CAPACITY = 2
DATABASE_PATH = "world_changes.db"
PERSISTENCE_ENABLED = True
def build_store():
if not PERSISTENCE_ENABLED:
return None
from store import ChangeStore
return ChangeStore(DATABASE_PATH)
How Does the Store Start?
- The PERSISTENCE_ENABLED switch activates the durable path.
- The build_store() function connects ChangeStore to world_changes.db.
- The conditional import keeps storage creation behind the switch.
- Save main.py.
Before you run the program, which persistence state do you expect the opening message to report?
- Check the persistence switch by running this command:
python3 main.py
What Does This Run Prove?
The opening message reports Persistence enabled: True. This proves that the application is using the new configuration value.
- Enter quit at the application prompt.
The store still needs to enter the engine through its existing third constructor argument.
- Find this line near the start of main() in main.py:
engine = WorldEngine(SEED, CACHE_CAPACITY)
What Are You Locating?
This line currently creates the engine without a store. The third constructor argument is available because the earlier engine already accepts store=None.
- Replace that line with this version:
engine = WorldEngine(SEED, CACHE_CAPACITY, build_store())
What Does the Injection Change?
The third argument gives WorldEngine access to the open change store. The engine can now coordinate generated terrain with persisted rows.
- Save main.py.
- Create the local database by running this command:
python3 main.py
What Does This Run Create?
The call to build_store() opens the database. The store creates the tile_overrides table when it is absent.
- Enter quit at the application prompt.
- Check the workspace file list for world_changes.db.
Good progress. Your local database now exists beside the Python files.
Does the Database Fail to Appear?
Confirm that PERSISTENCE_ENABLED is set to True. Confirm that build_store() is the third argument passed to WorldEngine.
Check that store.py defines ChangeStore. The class name must match the import exactly.
Help me diagnose the missing database file.
Overlay saved tiles and measure storage
The database can now hold durable rows. A cache miss must regenerate the base chunk before applying the saved coordinates within that chunk.
- Select engine.py from the workspace file list.
- Replace the existing get_chunk() method with this version:
def get_chunk(self, chunk_x, chunk_y):
key = (chunk_x, chunk_y)
self.explored_chunks.add(key)
chunk = self.cache.get(key)
if chunk is None:
chunk = self.generator.generate_chunk(chunk_x, chunk_y)
if self.store is not None:
for (world_x, world_y), tile in self.store.get_chunk_overrides(self.seed, chunk_x, chunk_y, CHUNK_SIZE).items():
_, _, local_x, local_y = self.chunk_address(world_x, world_y)
chunk[local_y][local_x] = tile
self.cache.put(key, chunk)
return chunk
How Does a Chunk Reappear?
- A cache hit returns the effective chunk already held in memory.
- A cache miss regenerates the deterministic base chunk.
- The store query returns persisted rows within the requested chunk.
- The engine converts each global position into a local array position before caching the chunk.
- Save engine.py.
- Check the empty override path by running this command:
python3 main.py
What Does the Map Confirm?
The starting chunk renders from deterministic terrain. The successful render proves that a cache miss can query the empty override table.
- Enter quit at the application prompt.
Does Chunk Loading Stop?
Check the indentation inside get_chunk(). The override loop belongs inside the cache-miss branch.
Confirm that get_chunk_overrides() receives the seed plus both chunk coordinates plus CHUNK_SIZE.
Help me debug chunk override loading.
The read path can restore saved rows. The write path must send each accepted tile change to the same store.
- Replace the existing set_tile() method in engine.py with this version:
def set_tile(self, x, y, tile):
if tile not in ALLOWED_TILES:
raise ValueError(f"Choose one symbol: {' '.join(sorted(ALLOWED_TILES))}")
chunk_x, chunk_y, local_x, local_y = self.chunk_address(x, y)
self.get_chunk(chunk_x, chunk_y)[local_y][local_x] = tile
self.modified_positions.add((x, y))
if self.store is not None:
self.store.save_override(self.seed, x, y, tile)
How Does a Change Become Durable?
The method updates the cached chunk immediately. It also writes the seed plus coordinates plus tile symbol when a store is available.
- Save engine.py.
- Start the persistence-enabled engine by running this command:
python3 main.py
What Is Ready to Test?
The current process now has both durable read behavior and durable write behavior. The terminal command set remains unchanged.
- Change the tile beneath the player by entering:
set #
What Does This Command Change?
The engine changes the cached tile at the current global coordinate. It saves the same coordinate as an override in world_changes.db.
The next map reports Standing on: # (player-built). The modified symbol is visible beneath the player marker.
- Enter quit at the application prompt.
Before you restart the process, do you expect the generated base symbol or the player-authored symbol to appear?
- Test the durable read path by running this command:
python3 main.py
What Does the Restart Prove?
The regenerated chunk reports Standing on: # (player-built). The persisted row now survives outside the previous cache plus process.
- Enter quit at the application prompt.
That is the durability gap closed. Your player-authored tile now survives a complete restart.
Does the Base Tile Return?
Confirm that set_tile() calls save_override() after changing the cached chunk.
Confirm that get_chunk() applies returned overrides before placing the chunk in the cache.
Help me debug the lost override.
Durability fixes the restart failure. The statistics can now compare a small persisted set with the larger logical world represented by those records.
- Replace the existing stats() plus close() methods in engine.py with this section:
def stats(self):
represented_chunks = set(self.explored_chunks)
persisted_records = 0
if self.store is not None:
persisted_records = self.store.count_overrides(self.seed)
for x, y in self.store.override_positions(self.seed):
chunk_x, chunk_y, _, _ = self.chunk_address(x, y)
represented_chunks.add((chunk_x, chunk_y))
represented_tiles = len(represented_chunks) * CHUNK_SIZE * CHUNK_SIZE
return {"unique_chunks_explored": len(self.explored_chunks), "logical_tiles_represented": represented_tiles, "chunk_generations": self.generator.chunk_generations, "modified_tiles_this_run": len(self.modified_positions), "persisted_override_records": persisted_records, "persistence_ratio": persisted_records / represented_tiles * 100 if represented_tiles else 0.0, "cache_hits": self.cache.hits, "cache_misses": self.cache.misses, "cache_evictions": self.cache.evictions, "cache_size": self.cache.size(), "cache_capacity": self.cache.capacity, "cached_chunks_lru_to_mru": self.cache.keys()}
def close(self):
if self.store is not None:
self.store.close()
What Do These Metrics Measure?
- The represented chunk set combines chunks explored during this run with chunks implied by persisted positions.
- The represented tile count multiplies those chunks by the fixed chunk area.
- The persistence ratio compares persisted override rows with logical terrain tiles.
- The close path releases the database connection when the program exits.
- Save engine.py.
- Inspect the completed statistics by running this command:
python3 main.py
What Does This Run Prepare?
The engine loads the saved override through the completed read path. It also starts fresh runtime counters for the new process.
- Display the runtime report by entering:
stats
How Should You Read the Report?
The persisted_override_records value is greater than zero. The logical_tiles_represented value is larger because each represented chunk contains multiple generated tiles.
The cache values still expose hits plus misses plus evictions. The occupancy remains no greater than the capacity.
- Enter quit at the application prompt.
Are the New Statistics Missing?
Confirm that the replacement stats() method remains inside WorldEngine. Incorrect indentation can place it outside the class.
Check that the returned dictionary contains logical_tiles_represented plus persisted_override_records plus persistence_ratio.
Help me debug the storage metrics.
✔️ Awesome, I've got everything!
Your engine now regenerates base chunks. It overlays saved edits plus reports cache and storage metrics.
ⓧ I'd like to double check the full code
from cache import LRUChunkCache
from generator import CHUNK_SIZE, TERRAIN_LABELS, TerrainGenerator
ALLOWED_TILES = set(TERRAIN_LABELS) | {"#"}
class WorldEngine:
def __init__(self, seed, cache_capacity=2, store=None):
self.seed = str(seed)
self.generator = TerrainGenerator(self.seed)
self.cache = LRUChunkCache(cache_capacity)
self.store = store
self.explored_chunks = set()
self.modified_positions = set()
def chunk_address(self, x, y):
chunk_x, local_x = divmod(x, CHUNK_SIZE)
chunk_y, local_y = divmod(y, CHUNK_SIZE)
return chunk_x, chunk_y, local_x, local_y
def get_chunk(self, chunk_x, chunk_y):
key = (chunk_x, chunk_y)
self.explored_chunks.add(key)
chunk = self.cache.get(key)
if chunk is None:
chunk = self.generator.generate_chunk(chunk_x, chunk_y)
if self.store is not None:
for (world_x, world_y), tile in self.store.get_chunk_overrides(self.seed, chunk_x, chunk_y, CHUNK_SIZE).items():
_, _, local_x, local_y = self.chunk_address(world_x, world_y)
chunk[local_y][local_x] = tile
self.cache.put(key, chunk)
return chunk
def get_tile(self, x, y):
chunk_x, chunk_y, local_x, local_y = self.chunk_address(x, y)
return self.get_chunk(chunk_x, chunk_y)[local_y][local_x]
def set_tile(self, x, y, tile):
if tile not in ALLOWED_TILES:
raise ValueError(f"Choose one symbol: {' '.join(sorted(ALLOWED_TILES))}")
chunk_x, chunk_y, local_x, local_y = self.chunk_address(x, y)
self.get_chunk(chunk_x, chunk_y)[local_y][local_x] = tile
self.modified_positions.add((x, y))
if self.store is not None:
self.store.save_override(self.seed, x, y, tile)
def verify_determinism(self, x, y):
first = self.generator.generate_tile(x, y)
second = self.generator.generate_tile(x, y)
return first == second, first, second
def stats(self):
represented_chunks = set(self.explored_chunks)
persisted_records = 0
if self.store is not None:
persisted_records = self.store.count_overrides(self.seed)
for x, y in self.store.override_positions(self.seed):
chunk_x, chunk_y, _, _ = self.chunk_address(x, y)
represented_chunks.add((chunk_x, chunk_y))
represented_tiles = len(represented_chunks) * CHUNK_SIZE * CHUNK_SIZE
return {"unique_chunks_explored": len(self.explored_chunks), "logical_tiles_represented": represented_tiles, "chunk_generations": self.generator.chunk_generations, "modified_tiles_this_run": len(self.modified_positions), "persisted_override_records": persisted_records, "persistence_ratio": persisted_records / represented_tiles * 100 if represented_tiles else 0.0, "cache_hits": self.cache.hits, "cache_misses": self.cache.misses, "cache_evictions": self.cache.evictions, "cache_size": self.cache.size(), "cache_capacity": self.cache.capacity, "cached_chunks_lru_to_mru": self.cache.keys()}
def close(self):
if self.store is not None:
self.store.close()
What Should You Compare?
Confirm that get_chunk() overlays stored changes. Confirm that set_tile() saves changes.
Confirm that stats() measures persisted records plus represented terrain. Confirm that close() closes the store.
Document the design and prove the savings
The working engine now separates deterministic base state from durable player state. The final files make that design visible in both code and documentation.
- Select main.py from the workspace file list.
- Find this command branch:
elif command == "set" and len(parts) == 2:
engine.set_tile(player_x, player_y, parts[1])
print(f"Set ({player_x}, {player_y}) to {parts[1]}")
What Are You Locating?
The renderer already exposes the effective tile beneath the player. The extra confirmation line duplicates that visible result.
- Replace that branch with this version:
elif command == "set" and len(parts) == 2:
engine.set_tile(player_x, player_y, parts[1])
What Does This Edit Keep?
The branch still delegates the tile change to set_tile(). The next render remains the visible confirmation.
- Save main.py.
✔️ Awesome, I've got everything!
Your completed main.py enables persistence plus injects the change store.
ⓧ I'd like to double check the full code
from engine import WorldEngine
from generator import TERRAIN_LABELS
SEED = "nextwork-world-42"
CACHE_CAPACITY = 2
DATABASE_PATH = "world_changes.db"
PERSISTENCE_ENABLED = True
def build_store():
if not PERSISTENCE_ENABLED:
return None
from store import ChangeStore
return ChangeStore(DATABASE_PATH)
def print_help():
print("Commands: w a s d, go <x> <y>, set <symbol>, verify, stats, help, quit")
def render(engine, player_x, player_y):
chunk_x, chunk_y, local_x, local_y = engine.chunk_address(player_x, player_y)
chunk = engine.get_chunk(chunk_x, chunk_y)
current_tile = engine.get_tile(player_x, player_y)
print(f"\nChunk: ({chunk_x}, {chunk_y}) | Player: ({player_x}, {player_y})")
for row_index, row in enumerate(chunk):
display_row = list(row)
if row_index == local_y:
display_row[local_x] = "@"
print(" ".join(display_row))
print(f"Standing on: {current_tile} ({TERRAIN_LABELS.get(current_tile, 'player-built')})")
def main():
print("Minecraft-Style Infinite World Engine")
print(f"Persistence enabled: {PERSISTENCE_ENABLED}")
engine = WorldEngine(SEED, CACHE_CAPACITY, build_store())
player_x = player_y = 0
moves = {"w": (0, -1), "a": (-1, 0), "s": (0, 1), "d": (1, 0)}
try:
while True:
render(engine, player_x, player_y)
parts = input("world> ").strip().split()
if not parts:
continue
command = parts[0].lower()
if command in moves:
delta_x, delta_y = moves[command]
player_x += delta_x
player_y += delta_y
elif command == "go" and len(parts) == 3:
player_x, player_y = int(parts[1]), int(parts[2])
elif command == "set" and len(parts) == 2:
engine.set_tile(player_x, player_y, parts[1])
elif command == "verify":
matches, first, second = engine.verify_determinism(player_x, player_y)
print(f"Deterministic generator check: {'PASS' if matches else 'FAIL'} ({first} == {second})")
elif command == "stats":
print(engine.stats())
elif command == "help":
print_help()
elif command in {"quit", "q"}:
break
finally:
engine.close()
if __name__ == "__main__":
main()
What Should You Compare?
Confirm that PERSISTENCE_ENABLED is True. Confirm that build_store() is passed to WorldEngine.
Confirm that the set branch calls set_tile() without the previous confirmation print.
The generator behavior remains unchanged. A named payload now makes its stable seed plus coordinate input easier to identify.
- Select generator.py from the workspace file list.
- Replace the file contents with this version:
import hashlib
CHUNK_SIZE = 8
TERRAIN_LABELS = {
"~": "water",
".": "plains",
"T": "forest",
"^": "mountain",
}
class TerrainGenerator:
def __init__(self, seed):
self.seed = str(seed)
self.chunk_generations = 0
def generate_tile(self, x, y):
payload = f"{self.seed}:{x}:{y}".encode("utf-8")
value = hashlib.sha256(payload).digest()[0]
if value < 48:
return "~"
if value < 144:
return "."
if value < 224:
return "T"
return "^"
def generate_chunk(self, chunk_x, chunk_y):
self.chunk_generations += 1
origin_x = chunk_x * CHUNK_SIZE
origin_y = chunk_y * CHUNK_SIZE
return [[self.generate_tile(origin_x + local_x, origin_y + local_y) for local_x in range(CHUNK_SIZE)] for local_y in range(CHUNK_SIZE)]
What Does This Clarify?
The payload variable holds the encoded seed plus global coordinates. The existing hash plus terrain thresholds still produce the same world.
- Save generator.py.
- Check the deterministic result by running this command:
python3 demo_generation.py
What Should the Demo Show?
The terminal prints two identical terrain maps. It finishes with Deterministic check: PASS.
Does the Deterministic Check Fail?
Confirm that payload contains the seed plus x plus y in that order. Confirm that it still feeds hashlib.sha256().
Help me debug the deterministic generator.
✔️ Awesome, I've got everything!
Your generator still reconstructs identical terrain from the same seed plus coordinates.
ⓧ I'd like to double check the full code
import hashlib
CHUNK_SIZE = 8
TERRAIN_LABELS = {
"~": "water",
".": "plains",
"T": "forest",
"^": "mountain",
}
class TerrainGenerator:
def __init__(self, seed):
self.seed = str(seed)
self.chunk_generations = 0
def generate_tile(self, x, y):
payload = f"{self.seed}:{x}:{y}".encode("utf-8")
value = hashlib.sha256(payload).digest()[0]
if value < 48:
return "~"
if value < 144:
return "."
if value < 224:
return "T"
return "^"
def generate_chunk(self, chunk_x, chunk_y):
self.chunk_generations += 1
origin_x = chunk_x * CHUNK_SIZE
origin_y = chunk_y * CHUNK_SIZE
return [[self.generate_tile(origin_x + local_x, origin_y + local_y) for local_x in range(CHUNK_SIZE)] for local_y in range(CHUNK_SIZE)]
What Should You Compare?
Confirm that generate_tile() hashes the seed plus global coordinates. Confirm that generate_chunk() still builds each chunk from global positions.
The code now demonstrates the complete request path. A short architecture document records that path plus the tradeoffs behind it.
- Create ARCHITECTURE.md inside the minecraft-world-engine workspace with this content:
# Minecraft-Style Infinite World Engine Architecture
## Data flow
Terminal UI -> WorldEngine -> LRUChunkCache -> TerrainGenerator -> ChangeStore -> SQLite database
## Request path
1. Convert global coordinates into chunk and local coordinates.
2. Ask the bounded LRU cache for the chunk.
3. On a miss, regenerate the base chunk from the seed and coordinates.
4. Read saved overrides within that chunk and apply them.
5. Return the effective chunk to the renderer and cache it for reuse.
## Tradeoffs
- Deterministic generation avoids records for untouched terrain but generator changes can alter old terrain.
- Chunk loading limits work to explored areas but chunk size affects latency and cache efficiency.
- A bounded LRU cache controls memory but can thrash during wide exploration.
- Sparse SQLite overrides preserve edits without storing base terrain, but savings shrink when most tiles change.
- Local SQLite avoids server setup but is not intended for many networked writers.
## Metric caveat
The terminal compares logical tile counts with persisted override rows, not physical SQLite bytes because pages, indexes, and schema metadata add overhead.
What Does the Architecture Capture?
- The data flow shows how a terminal request reaches cache memory plus generation plus durable storage.
- The request path places saved overrides on regenerated base terrain.
- The tradeoffs connect bounded memory plus sparse writes with their limitations.
- The metric caveat separates logical record efficiency from physical database size.
- Save ARCHITECTURE.md.
- Check the document for the four section headings.
Why Measure Logical Records?
The ratio compares persisted override rows with terrain represented by explored or saved chunks. Database pages plus indexes plus schema metadata create separate physical storage overhead.
✔️ Awesome, I've got everything!
Your architecture document now explains the final request path plus its storage tradeoffs.
ⓧ I'd like to double check the full code
# Minecraft-Style Infinite World Engine Architecture
## Data flow
Terminal UI -> WorldEngine -> LRUChunkCache -> TerrainGenerator -> ChangeStore -> SQLite database
## Request path
1. Convert global coordinates into chunk and local coordinates.
2. Ask the bounded LRU cache for the chunk.
3. On a miss, regenerate the base chunk from the seed and coordinates.
4. Read saved overrides within that chunk and apply them.
5. Return the effective chunk to the renderer and cache it for reuse.
## Tradeoffs
- Deterministic generation avoids records for untouched terrain but generator changes can alter old terrain.
- Chunk loading limits work to explored areas but chunk size affects latency and cache efficiency.
- A bounded LRU cache controls memory but can thrash during wide exploration.
- Sparse SQLite overrides preserve edits without storing base terrain, but savings shrink when most tiles change.
- Local SQLite avoids server setup but is not intended for many networked writers.
## Metric caveat
The terminal compares logical tile counts with persisted override rows, not physical SQLite bytes because pages, indexes, and schema metadata add overhead.
What Should You Compare?
Confirm that the request path regenerates terrain before applying saved overrides. Confirm that the metric caveat distinguishes logical records from physical bytes.
One final restart ties the full design together. You will prove that the override survives while the statistics still show sparse storage.
- Start the completed engine by running this command:
python3 main.py
What Is Running Now?
The engine starts with persistence enabled. It opens world_changes.db through ChangeStore.
- Save the player-authored tile again by entering:
set #
What Does This Save?
The command writes the current global coordinate as a persisted override. The renderer reports the player-built symbol beneath @.
- Enter quit at the application prompt.
Before you restart, which state do you expect the regenerated chunk to show beneath the player?
- Run the completed engine again with this command:
python3 main.py
What Should You See After Restart?
The starting map reports Standing on: # (player-built). The database has restored the saved override onto regenerated terrain.
- Display the final storage report by entering:
stats
What Proves the Savings?
The persisted override count is nonzero. The logical tile count is larger than the persisted record count.
The ratio measures logical rows across represented terrain. It does not estimate the physical size of world_changes.db.
- Enter quit at the application prompt.
Do the Final Counts Look Wrong?
Confirm that stats() adds chunks represented by saved override positions. Confirm that it multiplies the represented chunk count by CHUNK_SIZE twice.
Check that the saved tile still appears after the restart. A missing tile means the durable read or write path needs attention first.
Help me check my persistence statistics.
Secret mission
Reclaim a Redundant Override
Keep your sparse database lean by removing overrides that match generated terrain. Restore a modified tile to its base symbol, then prove the redundant row stays gone after cache eviction and restart.
Clean Up Your Resources
Clean Up Your Resources
All of your project resources live on your Mac, so there are no ongoing costs. Decide whether to keep the files, pause your work, or remove the local artifacts.
Resources you used:
- The local minecraft-world-engine folder. This contains your Python source files plus ARCHITECTURE.md.
- The world_changes.db file. This contains your local SQLite database of persisted tile overrides.
Keep everything running
No action needed. Choose this if you are still testing the world engine or demonstrating sparse persistence.
- Leave the minecraft-world-engine folder in its current location.
- Keep world_changes.db so your persisted tile overrides remain available.
- Keep ARCHITECTURE.md with the source files so the design decisions remain documented.
Pause - I'll come back to this later
Shut down the world explorer if it is running. Your local files remain ready for another session.
- Return to the integrated terminal from earlier.
- Enter quit at the world> prompt if the explorer is active.
- Close Visual Studio Code.
Your world engine is safely paused. The database stays on disk with its remaining tile overrides.
Delete - I don't want to use this again
Deleting local files is permanent. Your separate Python installation and Visual Studio Code remain untouched.
- Select the cleanup scope that matches what you want to remove.
Reset saved changes only
A database reset removes every persisted tile override. Your source files remain available for a fresh world session.
- Return to the integrated terminal from earlier.
- Enter quit at the world> prompt if the explorer is active.
- Locate world_changes.db in the Visual Studio Code file sidebar.
- Delete world_changes.db from the workspace.
- Confirm the deletion if Visual Studio Code asks.
You should no longer see world_changes.db in the file sidebar. The next run creates a fresh database with no persisted overrides.
Remove the whole project
Complete removal clears the world engine from your Mac. This includes its source code, architecture document, and saved changes.
- Return to the integrated terminal from earlier.
- Enter quit at the world> prompt if the explorer is active.
- Close Visual Studio Code.
- Use Finder to locate minecraft-world-engine in the parent folder where you created it.
- Delete the minecraft-world-engine folder.
- Confirm the deletion if Finder asks.
Confirm that minecraft-world-engine no longer appears in Finder. That clears every local artifact created by this project.
Nice Work!
Nice Work!
You did it! You built a terminal world engine powered by deterministic generation, a bounded LRU cache, and sparse SQLite overrides.
You've learned how to:
- Generate deterministic terrain from a seed and global coordinates. Prove that two 8 by 8 chunks match.
- Stream chunks on demand through a two-chunk LRU cache. Observe hits, misses, and evictions while occupancy stays at or below 2.
- Recover from a deliberate persistence failure with sparse tile overrides in world_changes.db. Document the request path, logical persistence ratio, and system tradeoffs in ARCHITECTURE.md.
- Secret Mission: Reclaim redundant override records when a tile returns to its generated base value.
Ready to quiz yourself?