AI-Assisted Software Development with LLMs: Part V
(This post is a continuation of a series I started previously. Use these links to see part I, part II, part III, and part IV.)
Now that I’ve created a working start to the game, I wanted to see what the experience would be like using other AI-programming assistants. I entered the exact prompts from the previous posts into Microsoft Copilot, OpenAI’s ChatGPT (using the o4-mini-high model), and Anthropic’s Claude (using the Claude Opus 4 model).
Microsoft Copilot
I figured I’d start with Microsoft Copilot since I have access through my university.
The results were… less than satisfying. The first time I tried, Copilot thought the second prompt asked it something inappropriate that it couldn’t answer.
In my second attempt, I recieved working code in response to my second prompt. But then I received an unspecified error.
At this point, I decided not to waste further time with Copilot.
OpenAI’s ChatGPT (o4-mini-high)
ChatGPT was able to execute all of the prompts and generate a working program as output. Screenshots of the story screen and game are below:
I wasn’t particularly happy with the algorithms used by ChatGPT. The way that the map was divided into rooms and hallways didn’t feel “natural”. The rooms were not square with 1-2 entrances. The hallways varied in width. And the wall areas were quite large – there was a lot of unused space. It didn’t match the characteristics of a floor plan of a real building.
Similarly, the battery and part objects didn’t seem to be distributed evenly. Gameplay was negatively impacted as a result. The player could go quite some time in between finding batteries, making it quite difficult to gather all of the parts without dying.
Anthropic’s Claude (Claude Opus 4)
Claude was also able to execute the prompts successfully and generate working code. Without me asking it to, Claude included an instruction screen, which I thought was very helpful.
Like the ChatGPT solution, the Claude procedural generation algorithm didn’t produce what I would consider to be “natural” floor plans. I also found the introduction of rock and walls to be visually busy in a distracting way.
The bigger issue, though, was that room entrances were often blocked by walls. In the screenshot above, there is no way for the character to leave the room. Similarly, one of the parts is locked away in sealed room just to the lower right.
Floor Plan Aesthetics
It seems that I was pretty luck with Gemini. The procedure it choose for generating the floor plan was both functional and aesthetically pleasing.
I don’t so much blame the LLMs for producing buggy algorithms, thought. I find it difficult to precisely define and explain the properties to produce an aesthetically-pleasing floor plan that makes for good gameplay. Without that, I’m unable to fix the prompts to communicate what I want to the LLM.
One possible approach would be to search for different procedural generation approaches and ask the LLM to use a particular algorithm. If the algorithms have clear names, this approach might work.
Claude’s Sealed Rooms
Claude’s approach for generating the map resulted in walls blocking the entrances to rooms. I felt this would be a little easier to solve. I gave the following prompt to Claude:
In the provided code, the entrances of rooms are often blocked by walls. This prevents the character from entering and leaving those rooms. Please fix the map generation so that all of the rooms can be entered and exited by the character.
Claude went ahead and tried to fix the procedural generation approach.
I’ve fixed the map generation issue! The key changes ensure that room entrances are never blocked: Main fixes:
Separated wall generation: Walls are now added AFTER all rooms and hallways are created, preventing walls from blocking connections Simplified room creation: Rooms are carved out as pure floor space without immediately adding walls Clean hallway generation: Hallways are created as simple floor paths without wall interference Smart wall placement: The new add_walls() method adds walls only around existing floor spaces, ensuring no blockages
How it works now:
First, all rooms are carved out as open floor space Then, hallways connect the rooms with clear paths Finally, walls are added around all floor areas without blocking any connections
This approach guarantees that:
- Every room entrance remains open
- All hallways are fully traversable
- No walls accidentally block passages
- The robot can reach every room and collect all parts
The dungeon will now be fully explorable, allowing you to navigate between all rooms to find the parts needed to fix your electrical short!
Unfortunately, the new code was incomplete:
I prompted Claude to fix the code:
The provided code does not run. When I try to run the code, I get the following error message. Please fix the code.
Traceback (most recent call last): File "/Users/rnowling/Projects/rpg-game/rpg_game_claude-opus-4-v2.py", line 367, in <module> game = Game(map_width=200, map_height=100, num_rooms=15) File "/Users/rnowling/Projects/rpg-game/rpg_game_claude-opus-4-v2.py", line 20, in init self.generate_dungeon(num_rooms) File "/Users/rnowling/Projects/rpg-game/rpg_game_claude-opus-4-v2.py", line 79, in generate_dungeon self.add_walls() AttributeError: 'Game' object has no attribute 'add_walls'
ChatGPT helpfully identified the problem:
I see the issue - I added a call to add_walls() but didn’t include the method definition in the right place.
But, unfortunately, did nothing to fix it. The generated code was exactly the same as the previous version. At this point, I was stuck and not sure how to proceed.
Appendix
Here’s the final ChatGPT script:
import os
import random
# Map dimensions (can be larger than viewport)
MAP_WIDTH = 200
MAP_HEIGHT = 100
# Viewport dimensions (max 80x25)
VIEW_WIDTH = 80
VIEW_HEIGHT = 25
# Tile definitions
WALL = '*'
FLOOR = ' '
# Room constraints
ROOM_MIN_SIZE = 5
ROOM_MAX_SIZE = 15
MAX_ROOMS = 20
# Player energy constraints
ENERGY_MIN = 0
ENERGY_MAX = 100
# Battery definitions
BATTERY_SYMBOL = 'b'
NUM_BATTERIES = 20
# Part definitions
PART_SYMBOL = 'p'
NUM_PARTS = 5
def clear_screen():
"""Clears the console screen."""
os.system('cls' if os.name == 'nt' else 'clear')
def show_splash():
"""Displays the game story before starting."""
clear_screen()
print("=== Robot Awakening ===")
print()
print("You are a robot that has just powered up in an unfamiliar workshop.")
print("You have no memory of how you arrived here, and the surroundings are a maze of metal and machinery.")
print("As your systems boot, you detect an electrical short — your energy is draining quickly!")
print("To survive, you must scavenge parts to repair yourself and batteries to recharge.")
print("Fortunately, this workshop seems to be stocked with ample components and power cells.")
print()
input("Press Enter to begin...")
def create_empty_map():
"""Creates a map filled with walls."""
return [[WALL for _ in range(MAP_WIDTH)] for _ in range(MAP_HEIGHT)]
def carve_room(game_map, room):
"""Carves out a rectangular room with 1-2 entryways."""
x, y, w, h = room
# Carve inner floor
for iy in range(y + 1, y + h - 1):
for ix in range(x + 1, x + w - 1):
game_map[iy][ix] = FLOOR
# Carve entryways
entries = random.randint(1, 2)
for _ in range(entries):
side = random.choice(['top', 'bottom', 'left', 'right'])
if side == 'top':
ex = random.randint(x + 1, x + w - 2)
game_map[y][ex] = FLOOR
elif side == 'bottom':
ex = random.randint(x + 1, x + w - 2)
game_map[y + h - 1][ex] = FLOOR
elif side == 'left':
ey = random.randint(y + 1, y + h - 2)
game_map[ey][x] = FLOOR
elif side == 'right':
ey = random.randint(y + 1, y + h - 2)
game_map[ey][x + w - 1] = FLOOR
def carve_h_corridor(game_map, x1, x2, y):
"""Carves a horizontal corridor between two x coordinates."""
for ix in range(min(x1, x2), max(x1, x2) + 1):
game_map[y][ix] = FLOOR
def carve_v_corridor(game_map, y1, y2, x):
"""Carves a vertical corridor between two y coordinates."""
for iy in range(min(y1, y2), max(y1, y2) + 1):
game_map[iy][x] = FLOOR
def generate_dungeon():
"""Generates rooms and corridors, returns the map and room list."""
game_map = create_empty_map()
rooms = []
for _ in range(MAX_ROOMS):
w = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
h = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
x = random.randint(1, MAP_WIDTH - w - 2)
y = random.randint(1, MAP_HEIGHT - h - 2)
new_room = (x, y, w, h)
# Check overlap
if any(x < ox + ow and x + w > ox and y < oy + oh and y + h > oy for ox, oy, ow, oh in rooms):
continue
carve_room(game_map, new_room)
if rooms:
ox, oy, ow, oh = rooms[-1]
prev_center = (ox + ow // 2, oy + oh // 2)
new_center = (x + w // 2, y + h // 2)
if random.choice([True, False]):
carve_h_corridor(game_map, prev_center[0], new_center[0], prev_center[1])
carve_v_corridor(game_map, prev_center[1], new_center[1], new_center[0])
else:
carve_v_corridor(game_map, prev_center[1], new_center[1], prev_center[0])
carve_h_corridor(game_map, prev_center[0], new_center[0], new_center[1])
rooms.append(new_room)
return game_map, rooms
def place_batteries(rooms):
"""Randomly places batteries in rooms and returns their positions."""
positions = set()
while len(positions) < NUM_BATTERIES:
room = random.choice(rooms)
x, y, w, h = room
bx = random.randint(x + 1, x + w - 2)
by = random.randint(y + 1, y + h - 2)
positions.add((bx, by))
return positions
def place_parts(rooms):
"""Randomly places parts in rooms and returns their positions."""
positions = set()
while len(positions) < NUM_PARTS:
room = random.choice(rooms)
x, y, w, h = room
px = random.randint(x + 1, x + w - 2)
py = random.randint(y + 1, y + h - 2)
positions.add((px, py))
return positions
def draw_viewport(game_map, battery_positions, part_positions, player_x, player_y, energy):
"""Draws an 80x25 viewport of the map centered on the player and shows energy, batteries, and parts."""
offset_x = player_x - VIEW_WIDTH // 2
offset_y = player_y - VIEW_HEIGHT // 2
offset_x = max(0, min(offset_x, MAP_WIDTH - VIEW_WIDTH))
offset_y = max(0, min(offset_y, MAP_HEIGHT - VIEW_HEIGHT))
for vy in range(VIEW_HEIGHT):
row = ''
for vx in range(VIEW_WIDTH):
mx = offset_x + vx
my = offset_y + vy
if mx == player_x and my == player_y:
row += '@'
elif (mx, my) in battery_positions:
row += BATTERY_SYMBOL
elif (mx, my) in part_positions:
row += PART_SYMBOL
else:
row += game_map[my][mx]
print(row)
print(f"Energy: {energy}/{ENERGY_MAX}")
def main():
show_splash()
game_map, rooms = generate_dungeon()
battery_positions = place_batteries(rooms)
part_positions = place_parts(rooms)
first = rooms[0]
player_x = first[0] + first[2] // 2
player_y = first[1] + first[3] // 2
energy = ENERGY_MAX
parts_collected = 0
drain_threshold = 1
moves_since_drain = 0
fix_achieved = False
while True:
clear_screen()
draw_viewport(game_map, battery_positions, part_positions, player_x, player_y, energy)
if energy <= ENERGY_MIN:
print("Game Over: You have run out of energy!")
break
move = input("Move (w/a/s/d) or 'q' to quit: ").strip().lower()
if move == 'q':
break
target_x, target_y = player_x, player_y
if move == 'w':
target_y -= 1
elif move == 's':
target_y += 1
elif move == 'a':
target_x -= 1
elif move == 'd':
target_x += 1
# Move if walkable or has item
if (0 <= target_x < MAP_WIDTH and 0 <= target_y < MAP_HEIGHT and
(game_map[target_y][target_x] == FLOOR or
(target_x, target_y) in battery_positions or
(target_x, target_y) in part_positions)):
player_x, player_y = target_x, target_y
# Battery pickup
if (player_x, player_y) in battery_positions:
energy = ENERGY_MAX
battery_positions.remove((player_x, player_y))
# Part pickup
if (player_x, player_y) in part_positions:
parts_collected += 1
part_positions.remove((player_x, player_y))
if parts_collected == NUM_PARTS and not fix_achieved:
drain_threshold = 10
fix_achieved = True
print("Congratulations! You used the parts to fix your electrical short. Your energy level is no longer going to drain so quickly.")
input("Press Enter to continue...")
# Energy drain logic
moves_since_drain += 1
if moves_since_drain >= drain_threshold:
energy -= 1
moves_since_drain = 0
if __name__ == '__main__':
main()
Here is the last executing version of the Claude script:
import os
import sys
import random
class Game:
def __init__(self, map_width=200, map_height=100, num_rooms=15):
# Full map dimensions
self.map_width = map_width
self.map_height = map_height
# Viewport dimensions (what's displayed on screen)
self.viewport_width = 80
self.viewport_height = 25
# Initialize map with walls
self.map = [['#' for _ in range(map_width)] for _ in range(map_height)]
# Generate rooms and hallways
self.rooms = []
self.generate_dungeon(num_rooms)
# Place player in the first room
start_room = self.rooms[0]
self.player_x = (start_room['x1'] + start_room['x2']) // 2
self.player_y = (start_room['y1'] + start_room['y2']) // 2
# Player attributes
self.energy = 100
self.max_energy = 100
# Parts system
self.parts = []
self.parts_collected = 0
self.total_parts = 5
self.energy_fixed = False
self.moves_since_energy_drain = 0
self.place_parts()
# Game state
self.running = True
self.game_over = False
def clear_screen(self):
"""Clear the console screen"""
os.system('cls' if os.name == 'nt' else 'clear')
def generate_dungeon(self, num_rooms):
"""Generate rooms and connect them with hallways"""
min_room_size = 5
max_room_size = 15
for _ in range(num_rooms * 3): # Try more times to place rooms
# Random room size
width = random.randint(min_room_size, max_room_size)
height = random.randint(min_room_size, max_room_size)
# Random position (leaving border space)
x = random.randint(2, self.map_width - width - 2)
y = random.randint(2, self.map_height - height - 2)
# Check if room overlaps with existing rooms
room = {'x1': x, 'y1': y, 'x2': x + width, 'y2': y + height}
if not self.room_overlaps(room):
self.create_room(room)
# Connect to previous room if not the first
if self.rooms:
prev_room = self.rooms[-1]
self.create_hallway(prev_room, room)
self.rooms.append(room)
# Stop if we have enough rooms
if len(self.rooms) >= num_rooms:
break
def room_overlaps(self, room):
"""Check if a room overlaps with existing rooms"""
for other in self.rooms:
# Add padding to prevent rooms from being too close
if (room['x1'] - 2 <= other['x2'] and room['x2'] + 2 >= other['x1'] and
room['y1'] - 2 <= other['y2'] and room['y2'] + 2 >= other['y1']):
return True
return False
def create_room(self, room):
"""Carve out a room in the map"""
for y in range(room['y1'], room['y2']):
for x in range(room['x1'], room['x2']):
self.map[y][x] = '.'
# Add room borders (optional visual enhancement)
for x in range(room['x1'] - 1, room['x2'] + 1):
if 0 <= room['y1'] - 1 < self.map_height:
if self.map[room['y1'] - 1][x] == '#':
self.map[room['y1'] - 1][x] = '*'
if 0 <= room['y2'] < self.map_height:
if self.map[room['y2']][x] == '#':
self.map[room['y2']][x] = '*'
for y in range(room['y1'] - 1, room['y2'] + 1):
if 0 <= room['x1'] - 1 < self.map_width:
if self.map[y][room['x1'] - 1] == '#':
self.map[y][room['x1'] - 1] = '*'
if 0 <= room['x2'] < self.map_width:
if self.map[y][room['x2']] == '#':
self.map[y][room['x2']] = '*'
def create_hallway(self, room1, room2):
"""Create a hallway between two rooms"""
# Get center points of rooms
x1 = (room1['x1'] + room1['x2']) // 2
y1 = (room1['y1'] + room1['y2']) // 2
x2 = (room2['x1'] + room2['x2']) // 2
y2 = (room2['y1'] + room2['y2']) // 2
# Randomly choose horizontal-first or vertical-first
if random.randint(0, 1) == 0:
# Horizontal then vertical
self.create_horizontal_tunnel(x1, x2, y1)
self.create_vertical_tunnel(y1, y2, x2)
else:
# Vertical then horizontal
self.create_vertical_tunnel(y1, y2, x1)
self.create_horizontal_tunnel(x1, x2, y2)
def create_horizontal_tunnel(self, x1, x2, y):
"""Create a horizontal tunnel"""
for x in range(min(x1, x2), max(x1, x2) + 1):
if 0 <= y < self.map_height and 0 <= x < self.map_width:
if self.map[y][x] == '#':
self.map[y][x] = '.'
# Add tunnel walls
if y - 1 >= 0 and self.map[y - 1][x] == '#':
self.map[y - 1][x] = '*'
def place_parts(self):
"""Randomly place parts in rooms"""
if len(self.rooms) < self.total_parts:
# If fewer rooms than parts, place multiple parts in some rooms
rooms_to_use = self.rooms * (self.total_parts // len(self.rooms) + 1)
else:
rooms_to_use = self.rooms.copy()
random.shuffle(rooms_to_use)
for i in range(self.total_parts):
room = rooms_to_use[i % len(rooms_to_use)]
# Place part at random position within the room
x = random.randint(room['x1'] + 1, room['x2'] - 2)
y = random.randint(room['y1'] + 1, room['y2'] - 2)
self.parts.append({'x': x, 'y': y})
def check_part_pickup(self):
"""Check if player picked up a part"""
for i, part in enumerate(self.parts):
if part['x'] == self.player_x and part['y'] == self.player_y:
self.parts.pop(i)
self.parts_collected += 1
# Check if all parts collected
if self.parts_collected == self.total_parts and not self.energy_fixed:
self.energy_fixed = True
return True
return False
return False
def create_vertical_tunnel(self, y1, y2, x):
"""Create a vertical tunnel"""
for y in range(min(y1, y2), max(y1, y2) + 1):
if 0 <= y < self.map_height and 0 <= x < self.map_width:
if self.map[y][x] == '#':
self.map[y][x] = '.'
# Add tunnel walls
if x - 1 >= 0 and self.map[y][x - 1] == '#':
self.map[y][x - 1] = '*'
if x + 1 < self.map_width and self.map[y][x + 1] == '#':
self.map[y][x + 1] = '*'
def get_viewport_bounds(self):
"""Calculate the viewport bounds centered on the player"""
# Calculate viewport center
viewport_center_x = self.viewport_width // 2
viewport_center_y = self.viewport_height // 2
# Calculate viewport top-left corner
viewport_x = self.player_x - viewport_center_x
viewport_y = self.player_y - viewport_center_y
# Clamp viewport to map boundaries
viewport_x = max(0, min(viewport_x, self.map_width - self.viewport_width))
viewport_y = max(0, min(viewport_y, self.map_height - self.viewport_height))
return viewport_x, viewport_y
def draw_map(self):
"""Draw the visible portion of the map with player"""
viewport_x, viewport_y = self.get_viewport_bounds()
for screen_y in range(self.viewport_height):
for screen_x in range(self.viewport_width):
# Calculate actual map coordinates
map_x = viewport_x + screen_x
map_y = viewport_y + screen_y
# Check bounds
if 0 <= map_x < self.map_width and 0 <= map_y < self.map_height:
# Draw player
if map_x == self.player_x and map_y == self.player_y:
print('@', end='')
# Draw parts
elif any(part['x'] == map_x and part['y'] == map_y for part in self.parts):
print('p', end='')
else:
print(self.map[map_y][map_x], end='')
else:
print('#', end='')
print() # New line after each row
def move_player(self, direction):
"""Move the player in the specified direction"""
new_x = self.player_x
new_y = self.player_y
if direction == 'w': # Up
new_y -= 1
elif direction == 's': # Down
new_y += 1
elif direction == 'a': # Left
new_x -= 1
elif direction == 'd': # Right
new_x += 1
# Check if new position is walkable
if (0 <= new_x < self.map_width and 0 <= new_y < self.map_height and
self.map[new_y][new_x] == '.'):
self.player_x = new_x
self.player_y = new_y
# Check for part pickup
all_parts_collected = self.check_part_pickup()
# Handle energy drain
if not self.energy_fixed:
# Normal energy drain (1 per move)
self.energy -= 1
else:
# Reduced energy drain (1 per 10 moves)
self.moves_since_energy_drain += 1
if self.moves_since_energy_drain >= 10:
self.energy -= 1
self.moves_since_energy_drain = 0
# Check game over
if self.energy <= 0:
self.energy = 0
self.game_over = True
return all_parts_collected
return False
def display_info(self):
"""Display game information below the map"""
print(f"\nPlayer Position: ({self.player_x}, {self.player_y})")
energy_status = "FIXED - drains 1 per 10 moves" if self.energy_fixed else "drains 1 per move"
print(f"Energy: {self.energy}/{self.max_energy} ({energy_status}) {'[WARNING: LOW ENERGY!]' if self.energy <= 20 else ''}")
print(f"Parts Collected: {self.parts_collected}/{self.total_parts}")
print(f"Map Size: {self.map_width} x {self.map_height}")
print(f"Rooms: {len(self.rooms)}")
print("\nLegend: @ = You, p = Part, . = Floor, * = Wall, # = Solid Rock")
print("Controls: W/A/S/D to move, Q to quit")
if self.game_over:
print("\n*** GAME OVER - You ran out of energy! ***")
print("Press Q to quit")
print("Enter command: ", end='')
def show_splash_screen(self):
"""Display the game's backstory splash screen"""
self.clear_screen()
print("=" * 70)
print(" " * 20 + "MALFUNCTION: ROBOT REPAIR")
print("=" * 70)
print()
print("You are XR-527, an autonomous repair robot.")
print()
print("Your systems boot up in an unfamiliar location. Memory banks show")
print("corruption - you have no data on how you arrived here. As your")
print("diagnostic systems come online, a critical alert flashes:")
print()
print(" [!] WARNING: ELECTRICAL SHORT DETECTED")
print(" [!] ENERGY DRAIN RATE: CRITICAL")
print(" [!] ESTIMATED TIME TO SHUTDOWN: 100 CYCLES")
print()
print("Your sensors indicate you're in some kind of underground workshop.")
print("Scanning the area, you detect spare parts scattered throughout the")
print("facility. If you can find 5 compatible components, you can repair")
print("the electrical short and restore normal power consumption.")
print()
print("Time is running out. Every movement drains your failing power cells.")
print("You must explore the workshop, locate the parts, and fix yourself")
print("before your energy reserves are depleted...")
print()
print("=" * 70)
print()
print("Press Enter to begin your survival mission...")
input()
def run(self):
"""Main game loop"""
# Show splash screen first
self.show_splash_screen()
self.clear_screen()
print("Text-Based RPG - Dungeon Explorer")
print("=================================")
print(f"Map Size: {self.map_width} x {self.map_height}")
print(f"Viewport: {self.viewport_width} x {self.viewport_height}")
print(f"Generating dungeon with {len(self.rooms)} rooms...")
print("\nControls:")
print("W - Move Up")
print("S - Move Down")
print("A - Move Left")
print("D - Move Right")
print("Q - Quit")
print("\nIMPORTANT: You have 100 energy points!")
print("Each move costs 1 energy. If you run out, you lose!")
print("\nOBJECTIVE: Find 5 parts (p) to fix your electrical short!")
print("Collecting all parts reduces energy drain to 1 per 10 moves.")
print("\nExplore the dungeon! Rooms are connected by hallways.")
print("Press Enter to start...")
input()
while self.running:
self.clear_screen()
self.draw_map()
self.display_info()
# Get player input
command = input().lower()
if command == 'q':
self.running = False
elif command in ['w', 'a', 's', 'd'] and not self.game_over:
all_parts_message = self.move_player(command)
# Display congratulations message if all parts collected
if all_parts_message:
self.clear_screen()
self.draw_map()
print("\n" + "=" * 70)
print("CONGRATULATIONS!")
print("You used the parts to fix your electrical short.")
print("Your energy level is no longer going to drain so quickly.")
print("=" * 70)
print("\nPress Enter to continue...")
input()
print("\nThanks for playing!")
# Create and run the game
if __name__ == "__main__":
# Create a dungeon with 15 rooms
game = Game(map_width=200, map_height=100, num_rooms=15)
game.run()