Added Text Based RPG Game in Python folder#123
Added Text Based RPG Game in Python folder#123x0lg0n merged 1 commit intox0lg0n:mainfrom Arni005:add-text_based_RPG_game
Conversation
Reviewer's GuideThis PR introduces a standalone terminal-based text RPG in Python by adding a single script that defines character and enemy classes, a turn-based combat engine with special abilities and item management, a main game loop with exploration and random events, and a JSON-based save/load system. Sequence diagram for save/load game processsequenceDiagram
actor User
participant "main()"
participant "save_game(player)"
participant "load_game()"
participant "rpg_save.json"
User->>main(): Choose to save or load game
main->>save_game: Save game (on user request)
save_game->>rpg_save.json: Write player data
rpg_save.json-->>save_game: Confirm save
save_game-->>main: Save complete
main->>load_game: Load game (on user request)
load_game->>rpg_save.json: Read player data
rpg_save.json-->>load_game: Return data
load_game-->>main: Player loaded
Sequence diagram for turn-based combatsequenceDiagram
actor User
participant combat
participant Player
participant Enemy
User->>combat: Choose action (attack/special/item/flee)
combat->>Player: Execute chosen action
Player->>Enemy: Attack or use special ability
Enemy->>Player: Perform special ability or attack
combat->>User: Show combat results
alt Player wins
combat->>Player: Award gold/XP, check level up
else Player loses
combat->>User: Show defeat message
end
Class diagram for Player, Enemy, and CharacterClassclassDiagram
class CharacterClass {
<<enum>>
WARRIOR
MAGE
THIEF
}
class Player {
- name: str
- character_class: CharacterClass
- level: int
- experience: int
- gold: int
- inventory: list[str]
- max_health: int
- health: int
- attack: int
- defense: int
- dodge_chance: float
- special_charges: int
+ is_alive(): bool
+ level_up(): bool
+ use_special_ability(enemy: Enemy): bool
+ use_item(item_index: int): bool
}
class Enemy {
- name: str
- level: int
- max_health: int
- health: int
- attack: int
- defense: int
- gold_reward: int
- exp_reward: int
- special_ability: str|None
+ is_alive(): bool
+ perform_special_ability(player: Player): bool
}
Player --> CharacterClass
Enemy --> Player
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
WalkthroughA self-contained text-based RPG game is added with character creation, level-based progression, turn-based combat, inventory management, random exploration events, JSON-based save/load functionality, and a main game loop supporting exploration, resting, combat, and item usage. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Game as Main Loop
participant Explore as Exploration
participant Combat as Combat System
participant Event as Random Events
participant Save as Save/Load
User->>Game: Start Game
Game->>Save: Load existing or create new character
Save-->>Game: Player instance
loop Main Loop
Game->>User: Display menu (Explore/Rest/Use Item/Save/Quit)
User->>Game: Select action
alt Explore
Game->>Explore: Roll for event/enemy
alt Random Event
Explore->>Event: Trigger event
Event-->>Game: Effect applied
else Combat
Explore->>Combat: Initialize with generated enemy
loop Combat Turn
Combat->>User: Display options
User->>Combat: Choose action
alt Attack/Special
Combat->>Combat: Calculate damage
Combat-->>User: Show result
else Use Item
Combat->>Combat: Apply item effect
else Flee
Combat-->>Game: Return to explore
end
Combat->>Combat: Enemy counter-attack
Combat-->>User: Show counter damage
alt Player/Enemy defeated
Combat-->>Game: End combat (Victory/Defeat)
end
end
Game->>Game: Process rewards & level-up if victory
end
else Rest
Game->>Event: Trigger rest event
Event-->>Game: Effect applied
else Save
Game->>Save: Serialize player state to JSON
else Quit
Game-->>User: Exit game
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR Compliance Guide 🔍Below is a summary of compliance checks for this PR:
Compliance status legend🟢 - Fully Compliant🟡 - Partial Compliant 🔴 - Not Compliant ⚪ - Requires Further Human Verification 🏷️ - Compliance label |
||||||||||||||||||
There was a problem hiding this comment.
Hey there - I've reviewed your changes - here's some feedback:
- The file currently mixes game logic, I/O prompts, and data definitions in one 475-line script—consider splitting into modules (e.g., combat engine, UI layer, persistence) for better organization and testability.
- There’s a lot of duplicated patterns in special abilities, combat flow, and event handling—extract shared behavior into helper functions or strategy classes to reduce repetition.
- Hard-coded constants (enemy names, item tables, balance numbers) could be moved into a config or constants module to make future balancing and tweaking easier.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The file currently mixes game logic, I/O prompts, and data definitions in one 475-line script—consider splitting into modules (e.g., combat engine, UI layer, persistence) for better organization and testability.
- There’s a lot of duplicated patterns in special abilities, combat flow, and event handling—extract shared behavior into helper functions or strategy classes to reduce repetition.
- Hard-coded constants (enemy names, item tables, balance numbers) could be moved into a config or constants module to make future balancing and tweaking easier.
## Individual Comments
### Comment 1
<location> `Python/text_based_RPG_game.py:435` </location>
<code_context>
+ if random.random() < 0.3: # 30% chance for random event
+ random_event(player)
+ else:
+ enemy = Enemy(random.choice(enemies), player.level + random.randint(-1, 2))
+ if not combat(player, enemy):
+ break
</code_context>
<issue_to_address>
**issue:** Enemy level can be less than 1, which may cause unintended behavior.
random.randint(-1, 2) can produce enemy levels below 1, which may not be supported by the Enemy class. Please ensure enemy level is at least 1.
</issue_to_address>
### Comment 2
<location> `Python/text_based_RPG_game.py:345-354` </location>
<code_context>
+ def load_game():
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Generic exception handling may mask specific errors during game load.
Consider handling specific exceptions like JSONDecodeError to give clearer feedback and prevent hiding underlying issues.
Suggested implementation:
```python
def load_game():
"""Load game from file"""
try:
with open('rpg_save.json', 'r') as f:
data = json.load(f)
player = Player(data['name'], CharacterClass(data['character_class']))
player.level = data['level']
player.experience = data['experience']
player.gold = data['gold']
player.inventory = data['inventory']
except json.JSONDecodeError:
print("❌ Failed to load game: Save file is corrupted or not valid JSON.")
return None
except FileNotFoundError:
print("❌ Save file not found. Start a new game first.")
return None
except Exception as e:
print(f"❌ An unexpected error occurred while loading the game: {e}")
return None
```
If `json` is not already imported at the top of the file, add:
```python
import json
```
Also, ensure that the function returns the `player` object at the end if loading is successful.
</issue_to_address>
### Comment 3
<location> `Python/text_based_RPG_game.py:402-411` </location>
<code_context>
+ def main():
</code_context>
<issue_to_address>
**suggestion:** Game loop does not handle KeyboardInterrupt gracefully.
Wrap the main loop in a try/except block to catch KeyboardInterrupt and exit cleanly, optionally prompting the user to save before quitting.
Suggested implementation:
```python
def main():
"""Main game loop"""
print("🐉 Welcome to Enhanced Python RPG! 🐉")
try:
# Load or create character
player = None
if os.path.exists('rpg_save.json'):
load_choice = input("Load saved game? (y/n): ").strip().lower()
if load_choice == 'y':
player = load_game()
# ... rest of the main loop logic goes here ...
except KeyboardInterrupt:
print("\n⚠️ Game interrupted by user (Ctrl+C).")
save_choice = input("Would you like to save your progress before quitting? (y/n): ").strip().lower()
if save_choice == 'y':
if player is not None:
save_game(player)
print("💾 Game saved successfully.")
else:
print("No game in progress to save.")
print("👋 Goodbye!")
sys.exit(0)
```
- Ensure that `save_game` and `player` are accessible in this scope.
- Make sure to import `sys` at the top of the file if not already present: `import sys`
- If the main loop logic is not yet inside the try block, move it there.
</issue_to_address>
### Comment 4
<location> `Python/text_based_RPG_game.py:80` </location>
<code_context>
+ return True
+ return False
+
+ def use_special_ability(self, enemy):
+ """Class-specific special abilities"""
+ if self.special_charges <= 0:
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring special ability logic and the combat loop using dispatch maps and a CombatEngine class to flatten nesting and reduce duplication.
Here are three small focused refactor ideas that keep all of your new functionality but drastically flatten nesting, remove duplication, and split responsibilities.
1) **Consolidate Player special‐ability logic via a dispatch map**
Move each class’s special into its own private method, then call via a simple dict instead of repeating `if/elif`:
```python
# inside Player:
def use_special_ability(self, enemy):
if self.special_charges <= 0:
print("❌ No special charges remaining!")
return False
self.special_charges -= 1
# map class ⇒ handler method
handlers = {
CharacterClass.WARRIOR: self._warrior_strike,
CharacterClass.MAGE: self._mage_fireball,
CharacterClass.THIEF: self._thief_poison,
}
handlers[self.character_class](enemy)
return True
def _warrior_strike(self, enemy):
dmg = int(self.attack * 1.8 - enemy.defense * 0.5)
enemy.health -= max(1, dmg)
print(f"💥 WARRIOR'S MIGHT! You strike for {dmg} damage!")
def _mage_fireball(self, enemy):
dmg = self.attack + random.randint(5, 15)
enemy.health -= dmg
print(f"🔥 FIREBALL! You burn for {dmg} damage!")
def _thief_poison(self, enemy):
base = self.attack // 2
extra = base // 2
enemy.health -= (base + extra)
print(f"☠️ POISON! You deal {base}+{extra} damage!")
```
2) **Extract Enemy special abilities into a lookup**
Same idea for enemies—no more long `if/elif` chains:
```python
# at module top:
ENEMY_SPECIALS = {
"double_attack": lambda e, p: (
setattr(p, 'health', p.health - max(1, e.attack - p.defense + random.randint(-2,2))),
print(f"⚡ {e.name} DOUBLE STRIKE!")
),
"heal": lambda e, p: (
setattr(e, 'health', min(e.max_health, e.health + e.max_health//4)),
print(f"💚 {e.name} heals!")
),
"poison": lambda e, p: (
setattr(p, 'health', p.health - e.attack//3),
print(f"☠️ {e.name} poisons you!")
),
}
class Enemy:
def perform_special_ability(self, player):
if not self.special_ability or random.random() > 0.3:
return False
ENEMY_SPECIALS[self.special_ability](self, player)
return True
```
3) **Encapsulate the combat loop in a CombatEngine class**
Break out the 30‐line nested loop into clear `player_turn()` / `enemy_turn()` methods:
```python
class CombatEngine:
def __init__(self, player, enemy):
self.p, self.e = player, enemy
def player_turn(self):
choice = get_combat_choice(self.p)
{
'1': lambda: self._attack(self.p, self.e),
'2': lambda: self.p.use_special_ability(self.e),
'3': lambda: use_item_in_combat(self.p),
'4': lambda: self._try_flee()
}[choice]()
def enemy_turn(self):
if not self.e.perform_special_ability(self.p):
damage = attack_target(self.e, self.p)
print(f"💢 {self.e.name} deals {damage} damage!")
def start(self):
turn = 0
while self.p.is_alive() and self.e.is_alive():
turn += 1
print(f"\n— Turn {turn} —")
self.player_turn()
if not self.e.is_alive(): break
print(f"\n{self.e.name}'s turn...")
time.sleep(1)
self.enemy_turn()
return self.p.is_alive()
# then in combat():
def combat(player, enemy):
print(f"\n⚔️ A wild {enemy.name} appears!")
engine = CombatEngine(player, enemy)
if engine.start():
victory_rewards(player, enemy)
return True
print("💀 You have been defeated…")
return False
```
By pulling out these three pieces:
- you remove deep nesting in `combat()`
- you eliminate duplicated `if/elif` blocks
- you isolate event/ability logic into self-contained units
You can do the same for `random_event` (e.g. an `Event` base class + subclasses) or even move each into its own file without touching any existing game behavior.
</issue_to_address>
### Comment 5
<location> `Python/text_based_RPG_game.py:375-377` </location>
<code_context>
name = input("Enter your character's name: ").strip()
if not name:
name = "Hero"
</code_context>
<issue_to_address>
**suggestion (code-quality):** Use `or` for providing a fallback value ([`use-or-for-fallback`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/use-or-for-fallback))
```suggestion
name = input("Enter your character's name: ").strip() or "Hero"
```
<br/><details><summary>Explanation</summary>Thanks to the flexibility of Python's `or` operator, you can use a single
assignment statement, even if a variable can retrieve its value from various
sources. This is shorter and easier to read than using multiple assignments with
`if not` conditions.
</details>
</issue_to_address>
### Comment 6
<location> `Python/text_based_RPG_game.py:54` </location>
<code_context>
def level_up(self):
levels_gained = 0
while self.experience >= self.level * XP_MULTIPLIER:
self.experience -= self.level * XP_MULTIPLIER
self.level += 1
levels_gained += 1
# Stat increases based on class
if self.character_class == CharacterClass.WARRIOR:
self.max_health += 20
self.attack += 4
self.defense += 3
elif self.character_class == CharacterClass.MAGE:
self.max_health += 10
self.attack += 6
self.defense += 1
else: # THIEF
self.max_health += 15
self.attack += 3
self.defense += 2
self.dodge_chance = min(0.4, self.dodge_chance + 0.02)
self.health = self.max_health # Full heal on level up
self.special_charges += 1
if levels_gained > 0:
print(f"🎉 {self.name} leveled up to level {self.level}!")
print(f"❤️ Max Health: {self.max_health}")
print(f"⚔️ Attack: {self.attack}")
print(f"🛡️ Defense: {self.defense}")
if self.character_class == CharacterClass.THIEF:
print(f"🌀 Dodge Chance: {self.dodge_chance:.1%}")
return True
return False
</code_context>
<issue_to_address>
**issue (code-quality):** We've found these issues:
- Extract duplicate code into method ([`extract-duplicate-method`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/extract-duplicate-method/))
- Lift code into else after jump in control flow ([`reintroduce-else`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/reintroduce-else/))
- Replace if statement with if expression ([`assign-if-exp`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/assign-if-exp/))
- Extract code out into method ([`extract-method`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/extract-method/))
</issue_to_address>
### Comment 7
<location> `Python/text_based_RPG_game.py:153` </location>
<code_context>
def perform_special_ability(self, player):
"""Enemy special abilities"""
if not self.special_ability or random.random() > 0.3: # 30% chance to use special
return False
if self.special_ability == "double_attack":
damage = max(1, self.attack - player.defense + random.randint(-2, 2))
player.health -= damage
print(f"⚡ {self.name} uses DOUBLE STRIKE! Deals {damage} damage!")
return True
elif self.special_ability == "heal":
heal_amount = self.max_health // 4
self.health = min(self.max_health, self.health + heal_amount)
print(f"💚 {self.name} heals for {heal_amount} HP!")
return True
elif self.special_ability == "poison":
poison_damage = self.attack // 3
player.health -= poison_damage
print(f"☠️ {self.name} poisons you for {poison_damage} damage!")
return True
return False
</code_context>
<issue_to_address>
**issue (code-quality):** Extract duplicate code into method ([`extract-duplicate-method`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/extract-duplicate-method/))
</issue_to_address>
### Comment 8
<location> `Python/text_based_RPG_game.py:196` </location>
<code_context>
def attack_target(attacker, defender, is_critical=False):
"""Handle attack between two entities"""
if random.random() < getattr(defender, 'dodge_chance', 0):
print(f"💨 {defender.name} dodged the attack!")
return 0
base_damage = attacker.attack - defender.defense + random.randint(-2, 3)
damage = max(1, base_damage)
if is_critical or random.random() < 0.1: # 10% critical chance
damage = int(damage * CRITICAL_MULTIPLIER)
print(f"💥 CRITICAL HIT! ", end="")
defender.health -= damage
return damage
</code_context>
<issue_to_address>
**issue (code-quality):** Replace f-string with no interpolated values with string ([`remove-redundant-fstring`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-redundant-fstring/))
</issue_to_address>
### Comment 9
<location> `Python/text_based_RPG_game.py:285-286` </location>
<code_context>
def use_item_in_combat(player):
"""Handle item usage during combat"""
if not player.inventory:
print("❌ Your inventory is empty!")
return False
print("\n🎒 Inventory:")
for i, item in enumerate(player.inventory, 1):
print(f"{i}. {item}")
print(f"{len(player.inventory) + 1}. Cancel")
try:
choice = int(input("Choose item to use: ")) - 1
if choice == len(player.inventory):
return False # Cancel
return player.use_item(choice)
except (ValueError, IndexError):
print("❌ Invalid choice.")
return False
</code_context>
<issue_to_address>
**issue (code-quality):** We've found these issues:
- Lift code into else after jump in control flow ([`reintroduce-else`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/reintroduce-else/))
- Replace if statement with if expression ([`assign-if-exp`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/assign-if-exp/))
</issue_to_address>
### Comment 10
<location> `Python/text_based_RPG_game.py:348` </location>
<code_context>
def load_game():
"""Load game from file"""
try:
with open('rpg_save.json', 'r') as f:
data = json.load(f)
player = Player(data['name'], CharacterClass(data['character_class']))
player.level = data['level']
player.experience = data['experience']
player.gold = data['gold']
player.inventory = data['inventory']
player.health = data['health']
player.max_health = data['max_health']
player.attack = data['attack']
player.defense = data['defense']
player.dodge_chance = data['dodge_chance']
player.special_charges = data['special_charges']
print("📂 Game loaded successfully!")
return player
except FileNotFoundError:
print("❌ No saved game found.")
return None
except Exception as e:
print(f"❌ Error loading game: {e}")
return None
</code_context>
<issue_to_address>
**issue (code-quality):** Extract code out into function ([`extract-method`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/extract-method/))
</issue_to_address>
### Comment 11
<location> `Python/text_based_RPG_game.py:402` </location>
<code_context>
def main():
"""Main game loop"""
print("🐉 Welcome to Enhanced Python RPG! 🐉")
# Load or create character
player = None
if os.path.exists('rpg_save.json'):
load_choice = input("Load saved game? (y/n): ").strip().lower()
if load_choice == 'y':
player = load_game()
if not player:
player = create_character()
enemies = ["Goblin", "Skeleton", "Orc", "Dragon", "Witch", "Zombie", "Vampire", "Giant Spider"]
while player.is_alive():
display_stats(player)
print("\nChoose your action:")
print("1. 🗺️ Explore")
print("2. 🛌 Rest (restore health)")
print("3. 🎒 Use Item")
print("4. 💾 Save Game")
print("5. 🚪 Quit Game")
choice = input("Enter your choice (1-5): ").strip()
if choice == '1': # Explore
# Random event chance
if random.random() < 0.3: # 30% chance for random event
random_event(player)
else:
enemy = Enemy(random.choice(enemies), player.level + random.randint(-1, 2))
if not combat(player, enemy):
break
elif choice == '2': # Rest
heal_amount = min(30, player.max_health - player.health)
player.health += heal_amount
print(f"💤 You rest and recover {heal_amount} health")
# Random encounter while resting
if random.random() < 0.2:
print("🌙 You were ambushed during rest!")
enemy = Enemy(random.choice(enemies), player.level)
if not combat(player, enemy):
break
elif choice == '3': # Use Item
if not use_item_in_combat(player):
print("❌ No valid item used.")
elif choice == '4': # Save Game
save_game(player)
elif choice == '5': # Quit
save_choice = input("Save before quitting? (y/n): ").strip().lower()
if save_choice == 'y':
save_game(player)
print("Thanks for playing! 👋")
break
else:
print("❌ Invalid choice. Please try again.")
# Small delay for better pacing
time.sleep(1)
if not player.is_alive():
print(f"\nGame Over! Final stats:")
display_stats(player)
</code_context>
<issue_to_address>
**issue (code-quality):** Low code quality found in main - 25% ([`low-code-quality`](https://docs.sourcery.ai/Reference/Default-Rules/comments/low-code-quality/))
<br/><details><summary>Explanation</summary>The quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.
How can you solve this?
It might be worth refactoring this function to make it shorter and more readable.
- Reduce the function length by extracting pieces of functionality out into
their own functions. This is the most important thing you can do - ideally a
function should be less than 10 lines.
- Reduce nesting, perhaps by introducing guard clauses to return early.
- Ensure that variables are tightly scoped, so that code using related concepts
sits together within the function rather than being scattered.</details>
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| if random.random() < 0.3: # 30% chance for random event | ||
| random_event(player) | ||
| else: | ||
| enemy = Enemy(random.choice(enemies), player.level + random.randint(-1, 2)) |
There was a problem hiding this comment.
issue: Enemy level can be less than 1, which may cause unintended behavior.
random.randint(-1, 2) can produce enemy levels below 1, which may not be supported by the Enemy class. Please ensure enemy level is at least 1.
| def load_game(): | ||
| """Load game from file""" | ||
| try: | ||
| with open('rpg_save.json', 'r') as f: | ||
| data = json.load(f) | ||
|
|
||
| player = Player(data['name'], CharacterClass(data['character_class'])) | ||
| player.level = data['level'] | ||
| player.experience = data['experience'] | ||
| player.gold = data['gold'] |
There was a problem hiding this comment.
suggestion (bug_risk): Generic exception handling may mask specific errors during game load.
Consider handling specific exceptions like JSONDecodeError to give clearer feedback and prevent hiding underlying issues.
Suggested implementation:
def load_game():
"""Load game from file"""
try:
with open('rpg_save.json', 'r') as f:
data = json.load(f)
player = Player(data['name'], CharacterClass(data['character_class']))
player.level = data['level']
player.experience = data['experience']
player.gold = data['gold']
player.inventory = data['inventory']
except json.JSONDecodeError:
print("❌ Failed to load game: Save file is corrupted or not valid JSON.")
return None
except FileNotFoundError:
print("❌ Save file not found. Start a new game first.")
return None
except Exception as e:
print(f"❌ An unexpected error occurred while loading the game: {e}")
return NoneIf json is not already imported at the top of the file, add:
import jsonAlso, ensure that the function returns the player object at the end if loading is successful.
| def main(): | ||
| """Main game loop""" | ||
| print("🐉 Welcome to Enhanced Python RPG! 🐉") | ||
|
|
||
| # Load or create character | ||
| player = None | ||
| if os.path.exists('rpg_save.json'): | ||
| load_choice = input("Load saved game? (y/n): ").strip().lower() | ||
| if load_choice == 'y': | ||
| player = load_game() |
There was a problem hiding this comment.
suggestion: Game loop does not handle KeyboardInterrupt gracefully.
Wrap the main loop in a try/except block to catch KeyboardInterrupt and exit cleanly, optionally prompting the user to save before quitting.
Suggested implementation:
def main():
"""Main game loop"""
print("🐉 Welcome to Enhanced Python RPG! 🐉")
try:
# Load or create character
player = None
if os.path.exists('rpg_save.json'):
load_choice = input("Load saved game? (y/n): ").strip().lower()
if load_choice == 'y':
player = load_game()
# ... rest of the main loop logic goes here ...
except KeyboardInterrupt:
print("\n⚠️ Game interrupted by user (Ctrl+C).")
save_choice = input("Would you like to save your progress before quitting? (y/n): ").strip().lower()
if save_choice == 'y':
if player is not None:
save_game(player)
print("💾 Game saved successfully.")
else:
print("No game in progress to save.")
print("👋 Goodbye!")
sys.exit(0)- Ensure that
save_gameandplayerare accessible in this scope. - Make sure to import
sysat the top of the file if not already present:import sys - If the main loop logic is not yet inside the try block, move it there.
| return True | ||
| return False | ||
|
|
||
| def use_special_ability(self, enemy): |
There was a problem hiding this comment.
issue (complexity): Consider refactoring special ability logic and the combat loop using dispatch maps and a CombatEngine class to flatten nesting and reduce duplication.
Here are three small focused refactor ideas that keep all of your new functionality but drastically flatten nesting, remove duplication, and split responsibilities.
- Consolidate Player special‐ability logic via a dispatch map
Move each class’s special into its own private method, then call via a simple dict instead of repeatingif/elif:
# inside Player:
def use_special_ability(self, enemy):
if self.special_charges <= 0:
print("❌ No special charges remaining!")
return False
self.special_charges -= 1
# map class ⇒ handler method
handlers = {
CharacterClass.WARRIOR: self._warrior_strike,
CharacterClass.MAGE: self._mage_fireball,
CharacterClass.THIEF: self._thief_poison,
}
handlers[self.character_class](enemy)
return True
def _warrior_strike(self, enemy):
dmg = int(self.attack * 1.8 - enemy.defense * 0.5)
enemy.health -= max(1, dmg)
print(f"💥 WARRIOR'S MIGHT! You strike for {dmg} damage!")
def _mage_fireball(self, enemy):
dmg = self.attack + random.randint(5, 15)
enemy.health -= dmg
print(f"🔥 FIREBALL! You burn for {dmg} damage!")
def _thief_poison(self, enemy):
base = self.attack // 2
extra = base // 2
enemy.health -= (base + extra)
print(f"☠️ POISON! You deal {base}+{extra} damage!")- Extract Enemy special abilities into a lookup
Same idea for enemies—no more longif/elifchains:
# at module top:
ENEMY_SPECIALS = {
"double_attack": lambda e, p: (
setattr(p, 'health', p.health - max(1, e.attack - p.defense + random.randint(-2,2))),
print(f"⚡ {e.name} DOUBLE STRIKE!")
),
"heal": lambda e, p: (
setattr(e, 'health', min(e.max_health, e.health + e.max_health//4)),
print(f"💚 {e.name} heals!")
),
"poison": lambda e, p: (
setattr(p, 'health', p.health - e.attack//3),
print(f"☠️ {e.name} poisons you!")
),
}
class Enemy:
def perform_special_ability(self, player):
if not self.special_ability or random.random() > 0.3:
return False
ENEMY_SPECIALS[self.special_ability](self, player)
return True- Encapsulate the combat loop in a CombatEngine class
Break out the 30‐line nested loop into clearplayer_turn()/enemy_turn()methods:
class CombatEngine:
def __init__(self, player, enemy):
self.p, self.e = player, enemy
def player_turn(self):
choice = get_combat_choice(self.p)
{
'1': lambda: self._attack(self.p, self.e),
'2': lambda: self.p.use_special_ability(self.e),
'3': lambda: use_item_in_combat(self.p),
'4': lambda: self._try_flee()
}[choice]()
def enemy_turn(self):
if not self.e.perform_special_ability(self.p):
damage = attack_target(self.e, self.p)
print(f"💢 {self.e.name} deals {damage} damage!")
def start(self):
turn = 0
while self.p.is_alive() and self.e.is_alive():
turn += 1
print(f"\n— Turn {turn} —")
self.player_turn()
if not self.e.is_alive(): break
print(f"\n{self.e.name}'s turn...")
time.sleep(1)
self.enemy_turn()
return self.p.is_alive()
# then in combat():
def combat(player, enemy):
print(f"\n⚔️ A wild {enemy.name} appears!")
engine = CombatEngine(player, enemy)
if engine.start():
victory_rewards(player, enemy)
return True
print("💀 You have been defeated…")
return FalseBy pulling out these three pieces:
- you remove deep nesting in
combat() - you eliminate duplicated
if/elifblocks - you isolate event/ability logic into self-contained units
You can do the same for random_event (e.g. an Event base class + subclasses) or even move each into its own file without touching any existing game behavior.
| name = input("Enter your character's name: ").strip() | ||
| if not name: | ||
| name = "Hero" |
There was a problem hiding this comment.
suggestion (code-quality): Use or for providing a fallback value (use-or-for-fallback)
| name = input("Enter your character's name: ").strip() | |
| if not name: | |
| name = "Hero" | |
| name = input("Enter your character's name: ").strip() or "Hero" | |
Explanation
Thanks to the flexibility of Python'sor operator, you can use a singleassignment statement, even if a variable can retrieve its value from various
sources. This is shorter and easier to read than using multiple assignments with
if not conditions.
|
|
||
| if is_critical or random.random() < 0.1: # 10% critical chance | ||
| damage = int(damage * CRITICAL_MULTIPLIER) | ||
| print(f"💥 CRITICAL HIT! ", end="") |
There was a problem hiding this comment.
issue (code-quality): Replace f-string with no interpolated values with string (remove-redundant-fstring)
PR Code Suggestions ✨Explore these optional code suggestions:
|
||||||||||||||||
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (5)
Python/text_based_RPG_game.py (5)
301-305: Make dropped items usable or trim the drop table.You award Magic Scroll, Iron Sword, Wooden Shield, but Player.use_item handles only Health Potion. Either remove them or add simple effects.
Apply this diff to implement basic effects:
@@ def victory_rewards(player, enemy): - if random.random() < 0.4: # 40% chance for item - items = ["Health Potion", "Health Potion", "Health Potion", "Magic Scroll", "Iron Sword", "Wooden Shield"] + if random.random() < 0.4: # 40% chance for item + items = ["Health Potion", "Health Potion", "Health Potion", "Magic Scroll", "Iron Sword", "Wooden Shield"] found_item = random.choice(items) player.inventory.append(found_item) print(f"🎁 Found {found_item}!") @@ class Player: - def use_item(self, item_index): + def use_item(self, item_index): """Use item from inventory""" if item_index < 0 or item_index >= len(self.inventory): return False @@ - if item == "Health Potion": + if item == "Health Potion": heal_amount = min(POTION_HEAL, self.max_health - self.health) self.health += heal_amount self.inventory.pop(item_index) print(f"🧪 Used Health Potion! Healed {heal_amount} HP.") return True + elif item == "Magic Scroll": + self.special_charges += 2 + self.inventory.pop(item_index) + print("📜 Used Magic Scroll! Gained +2 special charges.") + return True + elif item == "Iron Sword": + self.attack += 2 + self.inventory.pop(item_index) + print("🗡️ Equipped Iron Sword! +2 Attack.") + return True + elif item == "Wooden Shield": + self.defense += 2 + self.inventory.pop(item_index) + print("🛡️ Equipped Wooden Shield! +2 Defense.") + return TrueAlso applies to: 111-126
341-343: Make save atomic to avoid file corruption.Crash/power loss mid-write can corrupt rpg_save.json. Write to a temp file and replace.
Apply this diff:
- with open('rpg_save.json', 'w') as f: - json.dump(data, f) - print("💾 Game saved successfully!") + tmp_path = 'rpg_save.json.tmp' + with open(tmp_path, 'w') as f: + json.dump(data, f) + os.replace(tmp_path, 'rpg_save.json') + print("💾 Game saved successfully!")
258-270: Use theplayerarg to show special-charge status; also fix an f-string nit.Small UX win and resolves the “unused arg” warning. Also remove unnecessary f-string.
Apply this diff:
def get_combat_choice(player): """Get valid combat choice from player""" while True: - print(f"\nChoose action:") + print("\nChoose action:") - print("1. ⚔️ Attack") - print("2. ✨ Special Ability") + print("1. ⚔️ Attack") + special_label = "2. ✨ Special Ability" if player.special_charges > 0 else "2. ✨ Special Ability (0 left)" + print(special_label) print("3. 🎒 Use Item") print("4. 🏃 Flee")Based on static analysis hints.
196-196: Remove unnecessary f-strings.No placeholders present; drop the
fprefix.Apply this diff:
- print(f"💥 CRITICAL HIT! ", end="") + print("💥 CRITICAL HIT! ", end="") @@ - print(f"\nGame Over! Final stats:") + print("\nGame Over! Final stats:")Based on static analysis hints.
Also applies to: 471-471
1-6: About S311 (non-crypto RNG): safe to ignore; prefer config-level suppression.random is appropriate for gameplay. To silence Ruff without cluttering code, add per-file ignore in pyproject:
# pyproject.toml [tool.ruff.lint.per-file-ignores] "Python/text_based_RPG_game.py" = ["S311"]If you prefer inline, add “# noqa: S311” to the specific random.* lines. Based on static analysis hints.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
Python/text_based_RPG_game.py(1 hunks)
🧰 Additional context used
🪛 Ruff (0.14.1)
Python/text_based_RPG_game.py
96-96: Standard pseudo-random generators are not suitable for cryptographic purposes
(S311)
137-137: Standard pseudo-random generators are not suitable for cryptographic purposes
(S311)
141-141: Standard pseudo-random generators are not suitable for cryptographic purposes
(S311)
148-148: Standard pseudo-random generators are not suitable for cryptographic purposes
(S311)
152-152: Standard pseudo-random generators are not suitable for cryptographic purposes
(S311)
187-187: Standard pseudo-random generators are not suitable for cryptographic purposes
(S311)
191-191: Standard pseudo-random generators are not suitable for cryptographic purposes
(S311)
194-194: Standard pseudo-random generators are not suitable for cryptographic purposes
(S311)
196-196: f-string without any placeholders
Remove extraneous f prefix
(F541)
229-229: Standard pseudo-random generators are not suitable for cryptographic purposes
(S311)
258-258: Unused function argument: player
(ARG001)
261-261: f-string without any placeholders
Remove extraneous f prefix
(F541)
301-301: Standard pseudo-random generators are not suitable for cryptographic purposes
(S311)
303-303: Standard pseudo-random generators are not suitable for cryptographic purposes
(S311)
319-319: Standard pseudo-random generators are not suitable for cryptographic purposes
(S311)
364-364: Consider moving this statement to an else block
(TRY300)
368-368: Do not catch blind exception: Exception
(BLE001)
432-432: Standard pseudo-random generators are not suitable for cryptographic purposes
(S311)
435-435: Standard pseudo-random generators are not suitable for cryptographic purposes
(S311)
435-435: Standard pseudo-random generators are not suitable for cryptographic purposes
(S311)
444-444: Standard pseudo-random generators are not suitable for cryptographic purposes
(S311)
446-446: Standard pseudo-random generators are not suitable for cryptographic purposes
(S311)
471-471: f-string without any placeholders
Remove extraneous f prefix
(F541)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Sourcery review
🔇 Additional comments (1)
Python/text_based_RPG_game.py (1)
171-184: Nice: clear stats view and straightforward save payload.Readability is good and the saved fields cover everything needed to reconstruct Player state.
Also applies to: 324-344
| damage = int(self.attack * 1.8 - (enemy.defense * 0.5)) | ||
| enemy.health -= max(1, damage) | ||
| print(f"💥 WARRIOR'S MIGHT! You strike for {damage} damage!") | ||
|
|
There was a problem hiding this comment.
Print actual applied damage for Warrior special.
You clamp applied damage but print the unclamped value; can display negatives.
Apply this diff:
- damage = int(self.attack * 1.8 - (enemy.defense * 0.5))
- enemy.health -= max(1, damage)
- print(f"💥 WARRIOR'S MIGHT! You strike for {damage} damage!")
+ damage = int(self.attack * 1.8 - (enemy.defense * 0.5))
+ actual_damage = max(1, damage)
+ enemy.health -= actual_damage
+ print(f"💥 WARRIOR'S MIGHT! You strike for {actual_damage} damage!")🤖 Prompt for AI Agents
In Python/text_based_RPG_game.py around lines 90 to 93, the code computes damage
then clamps it with max(1, damage) but prints the original (unclamped) value;
change the logic so you compute damage = int(self.attack * 1.8 - (enemy.defense
* 0.5)), then compute applied_damage = max(1, damage), subtract applied_damage
from enemy.health, and print applied_damage in the message (e.g., use
applied_damage instead of damage) so negatives are not shown.
| elif action == '4': # Flee | ||
| if random.random() < 0.6: # 60% chance to flee | ||
| print("🏃 You successfully fled!") | ||
| return False | ||
| else: | ||
| print("❌ You couldn't escape!") |
There was a problem hiding this comment.
Fix flee flow: fleeing should not end the game loop.
combat() returns False both on death and on flee, and callers treat False as “exit.” Return a sentinel for flee and handle it at call sites.
Apply this diff:
@@
- elif action == '4': # Flee
- if random.random() < 0.6: # 60% chance to flee
- print("🏃 You successfully fled!")
- return False
+ elif action == '4': # Flee
+ if random.random() < 0.6: # 60% chance to flee
+ print("🏃 You successfully fled!")
+ return "fled"
else:
print("❌ You couldn't escape!")
@@
- enemy = Enemy(random.choice(enemies), player.level + random.randint(-1, 2))
- if not combat(player, enemy):
- break
+ enemy = Enemy(random.choice(enemies), player.level + random.randint(-1, 2))
+ result = combat(player, enemy)
+ if result is False: # defeated
+ break
@@
- enemy = Enemy(random.choice(enemies), player.level)
- if not combat(player, enemy):
- break
+ enemy = Enemy(random.choice(enemies), player.level)
+ result = combat(player, enemy)
+ if result is False: # defeated
+ breakAlso applies to: 436-438, 446-448
🧰 Tools
🪛 Ruff (0.14.1)
229-229: Standard pseudo-random generators are not suitable for cryptographic purposes
(S311)
| def load_game(): | ||
| """Load game from file""" | ||
| try: | ||
| with open('rpg_save.json', 'r') as f: | ||
| data = json.load(f) | ||
|
|
||
| player = Player(data['name'], CharacterClass(data['character_class'])) | ||
| player.level = data['level'] | ||
| player.experience = data['experience'] | ||
| player.gold = data['gold'] | ||
| player.inventory = data['inventory'] | ||
| player.health = data['health'] | ||
| player.max_health = data['max_health'] | ||
| player.attack = data['attack'] | ||
| player.defense = data['defense'] | ||
| player.dodge_chance = data['dodge_chance'] | ||
| player.special_charges = data['special_charges'] | ||
|
|
||
| print("📂 Game loaded successfully!") | ||
| return player | ||
| except FileNotFoundError: | ||
| print("❌ No saved game found.") | ||
| return None | ||
| except Exception as e: | ||
| print(f"❌ Error loading game: {e}") | ||
| return None | ||
|
|
There was a problem hiding this comment.
Harden load_game: narrow exceptions and use try/else.
Catching broad Exception (BLE001) and missing try/else (TRY300). Also handle JSON decoding errors explicitly.
Apply this diff (includes a small import addition):
@@
-import json
+import json
+from json import JSONDecodeError
@@
def load_game():
"""Load game from file"""
- try:
- with open('rpg_save.json', 'r') as f:
- data = json.load(f)
-
- player = Player(data['name'], CharacterClass(data['character_class']))
- player.level = data['level']
- player.experience = data['experience']
- player.gold = data['gold']
- player.inventory = data['inventory']
- player.health = data['health']
- player.max_health = data['max_health']
- player.attack = data['attack']
- player.defense = data['defense']
- player.dodge_chance = data['dodge_chance']
- player.special_charges = data['special_charges']
-
- print("📂 Game loaded successfully!")
- return player
- except FileNotFoundError:
- print("❌ No saved game found.")
- return None
- except Exception as e:
- print(f"❌ Error loading game: {e}")
- return None
+ try:
+ with open('rpg_save.json', 'r') as f:
+ data = json.load(f)
+ except FileNotFoundError:
+ print("❌ No saved game found.")
+ return None
+ except (JSONDecodeError, KeyError, ValueError) as e:
+ print(f"❌ Error loading game: {e}")
+ return None
+ else:
+ player = Player(data['name'], CharacterClass(data['character_class']))
+ player.level = data['level']
+ player.experience = data['experience']
+ player.gold = data['gold']
+ player.inventory = data['inventory']
+ player.health = data['health']
+ player.max_health = data['max_health']
+ player.attack = data['attack']
+ player.defense = data['defense']
+ player.dodge_chance = data['dodge_chance']
+ player.special_charges = data['special_charges']
+ print("📂 Game loaded successfully!")
+ return playerBased on static analysis hints.
Also applies to: 3-3
🧰 Tools
🪛 Ruff (0.14.1)
364-364: Consider moving this statement to an else block
(TRY300)
368-368: Do not catch blind exception: Exception
(BLE001)
🤖 Prompt for AI Agents
In Python/text_based_RPG_game.py around lines 345 to 371, replace the broad
try/except block with a narrow, explicit handling flow: open the file in a try
that catches FileNotFoundError, then call json.load() inside a try/except that
catches json.JSONDecodeError (or import JSONDecodeError) and KeyError for
missing fields; use a try/else so that player construction and attribute
assignment occur only in the else branch when loading succeeded, and avoid a
blanket except Exception—also catch OSError if you want IO errors; return None
on handled failures and print specific error messages for each case.
| enemy = Enemy(random.choice(enemies), player.level + random.randint(-1, 2)) | ||
| if not combat(player, enemy): |
There was a problem hiding this comment.
Clamp enemy level to at least 1.
Random offset can produce level 0. Clamp to avoid zero-level enemies and zero rewards.
Apply this diff:
- enemy = Enemy(random.choice(enemies), player.level + random.randint(-1, 2))
+ enemy_level = max(1, player.level + random.randint(-1, 2))
+ enemy = Enemy(random.choice(enemies), enemy_level)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| enemy = Enemy(random.choice(enemies), player.level + random.randint(-1, 2)) | |
| if not combat(player, enemy): | |
| enemy_level = max(1, player.level + random.randint(-1, 2)) | |
| enemy = Enemy(random.choice(enemies), enemy_level) | |
| if not combat(player, enemy): |
🧰 Tools
🪛 Ruff (0.14.1)
435-435: Standard pseudo-random generators are not suitable for cryptographic purposes
(S311)
435-435: Standard pseudo-random generators are not suitable for cryptographic purposes
(S311)
🤖 Prompt for AI Agents
In Python/text_based_RPG_game.py around lines 435 to 436, the enemy level
calculation can produce 0 because of the random offset; change the enemy
instantiation to clamp the computed level to a minimum of 1 (e.g., compute level
= max(1, player.level + random.randint(-1, 2)) and pass that to Enemy) so
enemies and rewards never use level 0.
Added a terminal-based Role-Playing Game built entirely in Python with no external dependencies. This game features character classes, turn-based combat, level progression, inventory management, and a complete save/load system.
Summary by Sourcery
Add a complete terminal-based RPG game in Python featuring character classes, turn-based combat, exploration, inventory management, and game persistence.
New Features:
Summary by CodeRabbit