Introduction
I have always held a deep appreciation for the games I encountered when I was first introduced to computing in the 1980s. Many of these games were text-based or combined text with static images to illustrate the narrative, often referred to as "Interactive Fiction." These games frequently had connections to well-known literary works, such as The Prisoner and The Hitchhiker’s Guide to the Galaxy. At that time, these games appeared exceedingly challenging to me. It is possible that some of these games required familiarity with the source material upon which they were based, thus contributing to my perception of their difficulty.
Nevertheless, they were fun!
Coding in the early 1980s was challenging. BASIC was one of the few high-level languages available then, with advanced compilers emerging later in the decade. Many game developers bypassed BASIC, using proprietary or obscure operating systems and 6502 assembly to limit piracy and complicate reverse engineering.
Fast forward to today. I wanted to see if ChatGPT could be prompted to develop a fully functional engine, using a world defined by JSON in short time. In the 1980s, this would have taken days, weeks, months to publish or release a functional game.
I was able to get ChatGPT to give me the Python-based Interactive Fiction I was looking for in less than an hour.
I’ll go over the process here and also provide some sample source code in case you want to use this. Hopefully, this will also introduce some concepts of game design as well.
Aside: Computer Magazines in the 1980s
One thing that today’s generation never experienced was that coding examples, technical information, or gaming tricks/techniques, could only be delivered by magazine.
A few years back I found that the publisher of one of my favorite Apple II magazines (Nibble) made all issues available as PDF on CD-ROM. I jumped at the opportunity to enjoy these magazines once again.
I hope that this post invokes a little bit of that feeling that some reading may have experienced in these early days of computing.
Working with ChatGPT
Often, when working with ChatGPT, an outline is initially created. A few bullet points are written, and then ChatGPT is asked to refine the content or add any considerations that may have been overlooked. The outline is provided below for reference.
Subsequently, it essentially involves requesting ChatGPT to provide the code in a format that is suitable for your needs. In certain instances, ChatGPT may even inquire, "How do you want to start?". In this situation, I focused on refining the JSON specifications to ensure that the Python code had the necessary data to operate effectively.
My Requirements
Capability to load the game world via the command line.
Option to specify a save data file through the command line.
Implementation of encryption and compression for the save data file to mitigate the risk of cheating or tampering.
The game world should be three-dimensional.
Inventory management system.
Health management system.
I have not tested whether the coordinate system supports negative values for the X, Y, and Z axes, but I see no reason why it should not function correctly. At present, I do not observe any limitations regarding world size or object/inventory limits, aside from those imposed by inherent Python or system constraints. In any case, this should be well beyond the limits of an 1980s Apple II system!
I chunked my prompting in the following manner (some of it was due to the default behaviors ChatGPT was proposing).
The Refinement Process
Game mechanics such as loading and saving: It was important to ensure that the command line arguments functioned as intended, such as creating a save data file if it did not already exist for updates, or generating the encryption key used to modestly obfuscate the save data file. Note: The term "obfuscate" is used here rather than implying that this method keeps the save data "secure".
The `game_loop` function was implemented to manage basic input and direct the flow to various functions.
Movement logic was incorporated: The JSON file associates directions with "doors" that each room has access to. Although this was not tested, it is observed that a doorway could theoretically serve as a "secret passage" to a non-adjacent room/cell.
Implementing the LOOK and Inventory system.
Add logic for the game to note if a player has been in a room before, so that users don’t receive the full description of the room upon each entry. Although the descriptions are brief in the sample.json, there do not appear to be any limitations on this.
Initially, the game allowed entry into non-defined rooms or cells simply because the current room had a “doorway”. Once a player entered the non-defined room, they would become trapped. This issue has been addressed and fixed.
Testing of the health system. The goal was to make a room capable of being a hazardous area; spending too much time in the room without taking action would result in a health penalty. This can be extended to objects; for example, a MEDICINE object has been added to one of the rooms to counteract the effect of standing in the hazardous area for too long. Additionally, setting health_modifer to a positive integer can make a room have some sort of healing effect.
A USE capability for objects in inventory was required. While there may be ways to make this more dynamic within the game, for simplicity, the “torch”, “medicine”, and “portalkey” have their functionality coded.
A method to trigger a "win game" scenario was needed. This could be by locating the exit cell in the 3-D grid or using a special object.
Sample “World” JSON
{
"metadata": {
"title": "The Lost Dungeon",
"author": "Game Creator",
"turn_limit": 50,
"starting_health": 100,
"starting_position": [0, 0, 0]
},
"rooms": {
"0,0,0": {
"description": "You are in a dark stone chamber. The air is damp, and you find it hard to breathe.",
"exits": {
"N": "0,1,0",
"E": "1,0,0"
},
"visited": false,
"light": false,
"health_modifier": -1
},
"0,1,0": {
"description": "A long, narrow hallway with torches flickering on the walls. You notice a ladder that goes upwards.",
"exits": {
"S": "0,0,0",
"U": "0,1,1"
},
"visited": false,
"light": true,
"health_modifier": 0
},
"0,1,1": {
"description": "You find yourself in a small storage room. A faint glow from a crack in the ceiling provides some light.",
"exits": {
"D": "0,1,0",
"U": "0,1,2"
},
"visited": false,
"light": true,
"health_modifier": 0
},
"0,1,2": {
"description": "You step into a glowing chamber. A mystical energy fills the air. The exit to freedom lies ahead!",
"exits": {
"D": "0,1,1"
},
"visited": false,
"light": true,
"health_modifier": 0,
"win_room": true
}
},
"objects": {
"torch": {
"name": "Torch",
"description": "A wooden torch. Might help in dark places.",
"location": "0,0,0",
"open": false
},
"key": {
"name": "Rusty Key",
"description": "An old iron key with some corrosion.",
"location": "torch",
"open": false
},
"medicine": {
"name": "Medicine",
"description": "A small bottle of medicine. It looks like it could restore health.",
"location": "0,1,1",
"open": false
},
"portalkey": {
"name": "Portal Key",
"description": "A mystical key that unlocks the way to another dimension.",
"location": "0,1,1",
"open": false
}
}
}
The Python Script
import json
import os
import zlib
import argparse
from cryptography.fernet import Fernet
# Default savegame structure
DEFAULT_SAVEGAME = {
"player": {
"position": [0, 0, 0], # Starting position
"health": 100,
"inventory": []
},
"visited_rooms": []
}
# Direction translator
DIRECTION_MAP = {
"NORTH": "N",
"SOUTH": "S",
"EAST": "E",
"WEST": "W",
"UP": "U",
"DOWN": "D"
}
def load_or_create_key(key_path):
"""Load encryption key from file or create a new one if it doesn't exist."""
if os.path.exists(key_path):
with open(key_path, 'rb') as key_file:
key = key_file.read()
print(f"Using existing encryption key from '{key_path}'.")
else:
key = Fernet.generate_key()
with open(key_path, 'wb') as key_file:
key_file.write(key)
print(f"Generated new encryption key and saved to '{key_path}'.")
return Fernet(key)
def compress_and_encrypt(data, cipher):
"""Compress and encrypt JSON data."""
json_string = json.dumps(data)
compressed_data = zlib.compress(json_string.encode('utf-8'))
encrypted_data = cipher.encrypt(compressed_data)
return encrypted_data
def decrypt_and_decompress(encrypted_data, cipher):
"""Decrypt and decompress JSON data."""
decrypted_data = cipher.decrypt(encrypted_data)
decompressed_data = zlib.decompress(decrypted_data)
return json.loads(decompressed_data.decode('utf-8'))
def load_json(file_path):
"""Load a JSON file and return its contents."""
try:
with open(file_path, 'r', encoding='utf-8') as file:
return json.load(file)
except FileNotFoundError:
print(f"Error: File '{file_path}' not found.")
return None
except json.JSONDecodeError as e:
print(f"Error parsing JSON file '{file_path}': {e}")
return None
def save_game(save_path, save_data, cipher):
"""Save game progress to a file using compression and encryption."""
encrypted_data = compress_and_encrypt(save_data, cipher)
with open(save_path, 'wb') as file:
file.write(encrypted_data)
print(f"Game saved to '{save_path}' (compressed & encrypted).")
def load_or_create_savegame(save_path, world_data, cipher):
"""Load an existing savegame or create a new one if it doesn't exist."""
if os.path.exists(save_path):
print(f"Loading existing savegame from '{save_path}'...")
try:
with open(save_path, 'rb') as file:
encrypted_data = file.read()
return decrypt_and_decompress(encrypted_data, cipher)
except Exception as e:
print(f"Error loading savegame: {e}")
print("Creating a new savegame...")
else:
print("No savegame found. Creating a new one...")
save_data = {
"player": {
"position": world_data["metadata"]["starting_position"],
"health": 100,
"inventory": []
},
"visited_rooms": []
}
save_game(save_path, save_data, cipher) # Save new game immediately
return save_data
def validate_world(world_data):
"""Basic validation of the world JSON structure."""
required_keys = ["metadata", "rooms", "objects"]
for key in required_keys:
if key not in world_data:
print(f"Error: Missing required key '{key}' in world JSON.")
return False
return True
def game_loop(world_data, save_data, cipher, save_file):
"""Main game loop to handle player input and movement."""
while True:
user_input = input("\nWhat do you want to do? ").strip().upper()
# Exit game without applying health modifier
if user_input in ["EXIT", "QUIT"]:
print("Saving game and exiting...")
save_game(save_file, save_data, cipher)
break
# Apply health modifier before processing command
apply_health_modifier(world_data, save_data)
if user_input == "LOOK":
display_room_description(world_data, save_data)
elif user_input == "INVENTORY":
display_inventory(world_data, save_data)
elif user_input.startswith("GO "):
direction = user_input[3:]
move_player(world_data, save_data, direction)
elif user_input.startswith("TAKE "):
object_name = user_input[5:]
take_object(world_data, save_data, object_name)
elif user_input.startswith("USE "):
object_name = user_input[4:]
use_object(world_data, save_data, object_name)
else:
print("Invalid command. Try 'LOOK', 'INVENTORY', 'TAKE <object>', 'USE <object>', or 'GO <direction>'.")
def display_inventory(world_data, save_data):
"""Show the player's current inventory."""
inventory = save_data["player"]["inventory"]
if not inventory:
print("You are carrying nothing.")
return
item_names = [world_data["objects"][item]["name"] for item in inventory if item in world_data["objects"]]
print("You are carrying: " + ", ".join(item_names))
def get_current_room(world_data, player_position):
"""Retrieve the current room based on the player's position."""
position_key = ",".join(map(str, player_position))
return world_data["rooms"].get(position_key, None)
def move_player(world_data, save_data, direction):
"""Attempt to move the player in the given direction, ensuring the target room exists."""
direction = direction.upper().strip()
direction = DIRECTION_MAP.get(direction, direction) # Convert full word to short form if needed
current_position = save_data["player"]["position"]
current_room = get_current_room(world_data, current_position)
if not current_room:
print("Error: Current room not found!")
return
# Check if movement is blocked due to darkness
if not current_room.get("light", True):
if "torch" not in save_data["player"]["inventory"]:
print("It's too dark to move! You need a light source.")
return
# Validate if direction exists
if direction not in current_room.get("exits", {}):
print("You can't go that way.")
return
# Check if the destination room actually exists before moving
new_position_key = current_room["exits"][direction]
if new_position_key not in world_data["rooms"]:
print("You try to go that way, but there's nothing there.")
return
# Move player to new position
new_position = list(map(int, new_position_key.split(",")))
save_data["player"]["position"] = new_position
print(f"You move {direction}.")
# Check if the new room is a win room
new_room = world_data["rooms"].get(new_position_key, {})
if new_room.get("win_room", False):
print("You have found the exit! YOU WIN!")
exit() # End the game immediately
# Display the room description
display_room_description(world_data, save_data)
def apply_health_modifier(world_data, save_data):
"""Adjust the player's health based on the room's health modifier and display it."""
current_position = save_data["player"]["position"]
current_room = get_current_room(world_data, current_position)
if not current_room:
print("Error: Current room not found!")
return
health_modifier = current_room.get("health_modifier", 0) # Default to 0 if missing
save_data["player"]["health"] += health_modifier # Apply health change
# Display current health after applying damage/healing
print(f"Health: {save_data['player']['health']} HP")
# Check for game over condition
if save_data["player"]["health"] <= 0:
print("You have run out of health. GAME OVER.")
exit() # End the game
def display_room_description(world_data, save_data):
"""Display the room description, marking it as visited only after first display."""
current_position = save_data["player"]["position"]
position_key = ",".join(map(str, current_position))
current_room = get_current_room(world_data, current_position)
if not current_room:
print("Error: Room data not found!")
return
# Check if the room has been visited before
if position_key in save_data["visited_rooms"]:
print(f"You are in {current_room['description']} (visited before).")
else:
print(f"{current_room['description']} (first time here).")
save_data["visited_rooms"].append(position_key) # Mark as visited
# Show objects in the room
objects_in_room = [obj["name"] for obj in world_data["objects"].values() if obj["location"] == position_key]
if objects_in_room:
print(f"You see: {', '.join(objects_in_room)}")
def take_object(world_data, save_data, object_name):
"""Allow the player to pick up an object if it's in the room."""
current_position = ",".join(map(str, save_data["player"]["position"]))
for obj_id, obj in world_data["objects"].items():
if obj["location"] == current_position and obj["name"].upper() == object_name:
save_data["player"]["inventory"].append(obj_id) # Add to inventory
obj["location"] = "INVENTORY" # Mark as taken
print(f"You take the {obj['name']}.")
return
print(f"There is no {object_name} here to take.")
def use_object(world_data, save_data, object_name):
"""Allow the player to use an object from inventory."""
for obj_id in save_data["player"]["inventory"]:
obj = world_data["objects"].get(obj_id)
if obj and obj["name"].upper() == object_name:
if obj_id == "torch":
current_position = ",".join(map(str, save_data["player"]["position"]))
world_data["rooms"][current_position]["light"] = True # Light up room
print("You light the torch. The room is now illuminated.")
return
elif obj_id == "medicine":
save_data["player"]["health"] = 100 # Restore health
save_data["player"]["inventory"].remove(obj_id) # Remove medicine after use
print("You take the medicine and feel fully restored.")
return
elif obj_id == "portalkey":
print("You activate the Portal Key... a bright light surrounds you!")
print("YOU WIN!")
exit() # End the game immediately
print(f"You don't have a {object_name} to use.")
def main():
"""Main function to load the game world and handle savegame loading."""
parser = argparse.ArgumentParser(description="Load a text-based adventure game world.")
parser.add_argument("world_file", help="Path to the world definition JSON file.")
parser.add_argument("save_file", help="Path to the savegame file.")
parser.add_argument("key_file", help="Path to the encryption key file.")
args = parser.parse_args()
# Load the encryption key (or create one if it doesn't exist)
cipher = load_or_create_key(args.key_file)
# Load the world JSON
world_data = load_json(args.world_file)
if not world_data or not validate_world(world_data):
print("World file is invalid. Exiting.")
return
# Load or create the savegame
save_data = load_or_create_savegame(args.save_file, world_data, cipher)
# Show initial health
print(f"Health: {save_data['player']['health']} HP")
# Show initial room description
display_room_description(world_data, save_data)
# Start game loop
game_loop(world_data, save_data, cipher, args.save_file)
if __name__ == "__main__":
main()
The “Outline” with ChatGPT
Text-Based Adventure Game Development Plan
1. Project Setup
Define project structure (folders, files)
Set up Python environment
Install any necessary libraries (e.g., json, zlib for compression, cryptography for encryption)
Establish core game loop
2. Defining the Game World
Use JSON for world storage
Rooms (cells) stored as JSON objects
Three-dimensional addressing system (X, Y, Z coordinates)
Descriptions for each cell
Movement limitations based on available paths (bit flags for U, D, N, E, S, W)
Tracking visited rooms (bit flags to avoid redundant text)
Light/Dark flag for illumination needs
Health modifier for environmental effects (e.g., losing HP over time in certain areas)
Turn limit (optional, 0 for free play)
Implement game world mechanics in Python
Load world from JSON
Retain, update, and modify the world in memory
Allow changes based on player interactions (e.g., unlocking doors, revealing hidden paths)
3. Implementing Movement System
Command Parsing: Interpret player input (e.g., "GO WEST", "CLIMB LADDER")
Movement Rules:
Ensure valid moves (e.g., can't go "UP" without stairs/ladder)
Update player position in 3D space
Modify room states if necessary (e.g., opening doors)
Tracking State:
Update visited room flags
Change descriptions dynamically if the environment changes (e.g., collapsed bridge)
4. Object System (Items, Containers, and Interaction)
Item Properties
Object ID, Name, Description
Open/Closed flag (e.g., doors, chests)
Parent Object ID (e.g., a key inside a box)
Child Object IDs (e.g., multiple items inside a container)
Item Interactions
Allow objects to be picked up, used, opened, or combined
Example actions: "OPEN BOX", "TAKE KEY", "USE TORCH"
5. Inventory System
Storing Items in Memory
Player inventory stored in a JSON structure
Objects should be removable from world and placed in inventory
Handling Inventory Commands
"TAKE FLASHLIGHT", "DROP SWORD"
"USE KEY" should interact with world objects
Persistence
Inventory included in RAM JSON for game saves
6. Implementing a Verb-Noun System
Command Structure
Ensure valid verb-noun relationships
Examples:
✅ "TAKE FLASHLIGHT" (valid)
❌ "TAKE WEST" (invalid)
✅ "USE TORCH" (valid)
❌ "USE GO" (invalid)
Parsing and Handling Actions
Split user input into verb/noun
Match against allowed actions
Give appropriate feedback if an action is not possible
7. Events & Triggers System
Dynamic Changes in the Game World
Rooms unlocking or changing states after actions
Example: A "HIDDEN DOOR" is revealed after "MOVE BOOKCASE"
Timed events (e.g., fire spreads after 5 turns)
NPCs & Dialogue (Optional)
Implement simple NPC interactions
Example: "TALK GUARD" may lead to unlocking a path
8. Health System & Turn Limit
Health Tracking
Numeric health indicator for the player
Negative effects (e.g., losing HP in extreme environments)
Healing options (e.g., "DRINK WATER" restores HP)
Turn Limit
Optional mode where game ends after X turns
Default mode (0 turns) for unlimited play
9. Game End Events
Victory Conditions
Example: "USE KEY" in the correct location unlocks the final exit
Trigger an ending message upon completion
Defeat Conditions
Running out of health
Reaching the turn limit without success
10. Saving & Loading Game State
Saving Game Data
Store game progress (player position, inventory, world changes) in JSON
Compress and encrypt saved game files for security
Loading Saved Data
Allow resuming from previous save state
Multiple Save Slots (Optional)
Support different saves instead of overwriting progress
11. Debugging & Testing
Error Handling
Handle incorrect inputs gracefully
Prevent invalid actions from breaking the game
Automated Testing (Optional)
Use unit tests to validate movement, inventory, and object interactions
Logging System (Optional)
Track player actions for debugging or replay features
12. Optional Enhancements
Automated Mapping
Show explored rooms visually or through text descriptions
Combat System (If Added)
Implement basic turn-based fights with simple mechanics
Puzzles & Mini-Games
More complex interactions, like levers, riddles, or crafting
Conclusion
At this point, you should be able to utilize the Python code and JSON example, and develop your own adventure (Interactive Fiction) game. If there’s interest, place a comment here and I’ll see about creating a JSON world editor. Enjoy!
Booookmarked!!! I’ve been thinking about making one of these!