Skip to content

Commit

Permalink
Merge pull request #75 from dstrain115/qrpg3
Browse files Browse the repository at this point in the history
Quantum RPG Battle Engine

Add class to actually run a Quantum RPG battle.
This pits players versus NPCs in a battle.
Player actions and NPC actions can be triggered via functions.
Also adds a simple test NPC.
  • Loading branch information
dstrain115 authored Dec 21, 2022
2 parents 59958b7 + ab5897d commit 9a345df
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 2 deletions.
124 changes: 124 additions & 0 deletions unitary/examples/quantum_rpg/battle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import io
import sys
from typing import List, Optional

from unitary.examples.quantum_rpg.qaracter import Qaracter


class Battle:
"""Class representing a battle between players and NPCs.
This class encapsulates a list of players and enemies (NPCs),
each of which are `QuantumWorld` objects.
This class has functions for each side to take a turn using
rules of the Quantum RPG, as well as a function to print out
the status.
Args:
player_side: a list of player QuantumWorld objects representing
their character sheets (initial state).
enemy_side: a list of NPC QuantumWorld objects.
file: Optional IOBase file object to write output to.
This enables the battle to write status to a file or string
for testing.
"""

def __init__(self,
player_side: List[Qaracter],
enemy_side: List[Qaracter],
file: io.IOBase = sys.stdout):
self.player_side = player_side
self.enemy_side = enemy_side
self.file = file

def print_screen(self):
"""Prints a two-column output of the battle status.
Left side includes the players and their qubits. Right
side includes the NPCs and their qubits.
Output will be written to the `file` attribute.
"""
print('-----------------------------------------------', file=self.file)
for i in range(max(len(self.player_side), len(self.enemy_side))):
status = ''
if i < len(self.player_side):
status += f'{self.player_side[i].name} {type(self.player_side[i]).__name__}'
else:
status += '\t\t'
status += '\t\t\t'
if i < len(self.enemy_side):
status += f'{self.enemy_side[i].name} {type(self.enemy_side[i]).__name__}'

status += '\n'

if i < len(self.player_side):
status += self.player_side[i].status_line()
else:
status += '\t\t'
status += '\t\t\t'
if i < len(self.enemy_side):
status += self.enemy_side[i].status_line()
print(status, file=self.file)
print('-----------------------------------------------', file=self.file)

def take_player_turn(self, user_input: Optional[List[str]] = None):
"""Take a player's turn and record results in the battle.
1) Retrieve the possible actions from the player.
2) Prompt the player for which action to use.
3) Prompt the player which NPC and qubit to target.
4) Call the player's action to perform the action.
Args:
user_input: List of strings that substitute for the user's
raw input.
"""

# If user input is provided as an argument, then use that.
# Otherwise, prompt from raw input.
if user_input is not None:
user_input = iter(user_input)
get_user_input = lambda _: next(user_input)
else:
get_user_input = input

for current_player in self.player_side:
self.print_screen()
print(f'{current_player.name} turn:', file=self.file)
if not current_player.is_active():
print(f'{current_player.name} is DOWN!', file=self.file)
continue
actions = current_player.actions()
for key in actions:
print(key, file=self.file)
action = get_user_input('Choose your action: ')
if action in current_player.actions():
monster = int(get_user_input('Which enemy number: ')) - 1
if monster < len(self.enemy_side):
qubit = int(get_user_input('Which enemy qubit number: '))
selected_monster = self.enemy_side[monster]
qubit_name = selected_monster.quantum_object_name(qubit)
if qubit_name in selected_monster.active_qubits():
res = actions[action](selected_monster, qubit)
if isinstance(res, str):
print(res, file=self.file)
else:
print(f'{qubit_name} is not an active qubit',
file=self.file)
else:
print(f'{monster + 1} is not a valid monster',
file=self.file)

def take_npc_turn(self):
"""Take all NPC turns.
Loop through all NPCs and call each function.
"""
for npc in self.enemy_side:
if not npc.is_active():
print(f'{npc.name} is DOWN!', file=self.file)
continue
result = npc.npc_action(self)
print(result, file=self.file)
60 changes: 60 additions & 0 deletions unitary/examples/quantum_rpg/battle_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import io
import unitary.examples.quantum_rpg.battle as battle
import unitary.examples.quantum_rpg.classes as classes
import unitary.examples.quantum_rpg.npcs as npcs


def test_battle():
output = io.StringIO()
c = classes.Analyst('Aaronson')
e = npcs.Observer('watcher')
b = battle.Battle([c], [e], file=output)
b.take_player_turn(user_input=['s', '1', '1'])
b.take_npc_turn()
assert output.getvalue().replace('\t', ' ').strip() == r"""
-----------------------------------------------
Aaronson Analyst watcher Observer
1QP (0|1> 0|0> 1?) 1QP (0|1> 0|0> 1?)
-----------------------------------------------
Aaronson turn:
s
m
Sample result HealthPoint.HURT
Observer watcher measures Aaronson at qubit Aaronson_1
""".strip()


def test_bad_monster():
output = io.StringIO()
c = classes.Analyst('Aaronson')
e = npcs.Observer('watcher')
b = battle.Battle([c], [e], file=output)
b.take_player_turn(user_input=['s', '2', '1'])
assert output.getvalue().replace('\t', ' ').strip() == r"""
-----------------------------------------------
Aaronson Analyst watcher Observer
1QP (0|1> 0|0> 1?) 1QP (0|1> 0|0> 1?)
-----------------------------------------------
Aaronson turn:
s
m
2 is not a valid monster
""".strip()


def test_bad_qubit():
output = io.StringIO()
c = classes.Analyst('Aaronson')
e = npcs.Observer('watcher')
b = battle.Battle([c], [e], file=output)
b.take_player_turn(user_input=['s', '1', '2'])
assert output.getvalue().replace('\t', ' ').strip() == r"""
-----------------------------------------------
Aaronson Analyst watcher Observer
1QP (0|1> 0|0> 1?) 1QP (0|1> 0|0> 1?)
-----------------------------------------------
Aaronson turn:
s
m
watcher_2 is not an active qubit
""".strip()
26 changes: 26 additions & 0 deletions unitary/examples/quantum_rpg/npcs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import random

import unitary.alpha as alpha
from unitary.examples.quantum_rpg import qaracter


class Npc(qaracter.Qaracter):
"""Base class for non-player character `Qaracter` objects.
"""

def is_npc(self):
return True


class Observer(Npc):
"""Simple test NPC that measures a random qubit each turn."""

def npc_action(self, battle) -> str:
enemy_target = random.randint(0, len(battle.player_side) - 1)
enemy_name = battle.player_side[enemy_target].name
enemy_qubit = random.choice(
battle.player_side[enemy_target].active_qubits())

battle.player_side[enemy_target].sample(enemy_qubit, True)
return f'Observer {self.name} measures {enemy_name} at qubit {enemy_qubit}'
12 changes: 12 additions & 0 deletions unitary/examples/quantum_rpg/npcs_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import unitary.alpha as alpha
import unitary.examples.quantum_rpg.battle as battle
import unitary.examples.quantum_rpg.classes as classes
import unitary.examples.quantum_rpg.npcs as npcs


def test_observer():
qar = npcs.Observer(name='glasses')
c = classes.Analyst('cat')
b = battle.Battle([c], [qar])
assert qar.is_npc()
assert qar.npc_action(b) == "Observer glasses measures cat at qubit cat_1"
4 changes: 2 additions & 2 deletions unitary/examples/quantum_rpg/qaracter.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ def is_down(self) -> bool:

def is_escaped(self) -> bool:
"""Returns True if all HP are measured and at least half are `HEALTHY`."""
return len(
self.health_status) == self.level and self.virtue >= (self.level / 2)
return len(self.health_status) == self.level and self.virtue >= (
self.level / 2)

def is_active(self) -> bool:
"""Returns True if the Qaracter is not down yet and there are HPs left to measure."""
Expand Down

0 comments on commit 9a345df

Please sign in to comment.