Part 14: Shall We Play a Game?
This series of posts is dedicated to building tools that support a simulation of simplified D&D gameplay. To implement the simulation I’ll be using git, Python, PostgreSQL, Elastic, and Docker. The idea behind these blog posts is to document my progress through this project and interesting bits I find along the way. If you’d like to start from the beginning, have a look at part 1. It talks about where this idea came from, and what I’d like to accomplish. The github repository for this project holds the latest version of the files mentioned in these posts. To follow along, part 6 describes how to set up the environment. In the last post, a battlefield was built and the logic to place combatants was defined. Since the Encounter class will be changing in future posts, a release has been created for experimenting with this post’s content. See release 0.1.1. Yay! This project’s first release. Now it’s time to test that out a bit by playing a game of tag.
With all the pieces that have been developed through the earlier posts, it should be pretty straightforward to play an easy game of tag. Here’s how the game operates:
- Create two parties of players (Heroes and Opponents).
- Each party can be one or more characters.
- Place both parties on the grid.
- Each character roles for initiative and that value is used to assign an order to the character’s turn in a round.
- Rounds are tracked so that at the end of the game the length of the game can be noted.
- When the game starts, each character has to figure out what’s going on (pass a perception check).
- Find the closest enemy.
- Run towards the closest enemy.
- If the enemy is very far away, use their action to move, effectively doubling their movement.
- If the enemy is just outside of the characters movement range, use their action to prepare an attack.
- If the enemy close enough for the character to get within melee range, get there and ATTACK!
- When enemies get into melee range, any characters who have been readying their attack go first in initiative order, then the current character gets their action.
- First Player to attack another eliminates that player’s party from the competition.
- Report the winning group, the number of rounds the game ran, and the remaining characters in the winning group.
The main driver for this game is going to be a python class called Encounter. It will handle the setup and gameplay. Since the Encounter class will change in later posts the first release of this project, 0.1.1, has the code demonstrated here.
An Encounter takes a few parameters to run. The parameters:
- Two lists of PlayerCharacters or Foes are required. These two lists are referred to as the Hero and Opponent parties in the encounter. They can contain one or more characters.
- The size of the playing field defaults to 500 ft squared. The simulation sees the field in 5 foot sectors, so the field_size value devaults to 100, unless it is overridden
- To show all the debug info, the debug_ind can be set to 1. The default is suppress this output.
All the action in the Encounter comes from the master_loop method. In there, the iterations for turns in each of the rounds takes place. Fortunately, most of what happens for this game is pretty linear. When it’s the player’s turn they can move up to their max movement, then decide whether to move again, wait on an attack if an opponent is out of range, or attack.
Movement
Once a character passes a perception check the character is free to persue the closest opponent. That movement is angular until it’s a straight line path. Each destination sector is checked for availability. If not available, a sector on either side of the original choice is checked. If available, then the source sector is uninhabited and the new one claimed. If none are available no movement is made.
Waiting for Attack
One of the interesting non-linear things a player can do is wait for an incoming opponent and attack when they get into range. This causes an interesting feature to happen in the game. Before a player takes their action, a check is made to see if there are any players waiting to attack them. Any waiting players get their turns at that point in initiative order. This will be carried forward into the future simulation, being a bit more restrictive. But for now, this will be good start.
And We have a Winner
For this game, the first player to attack wins the game for their party. To implement this, each player in the losing party defends against a killing blow, effectively eliminating the entire party.
Once the game is complete, dump out some debugging info, report out the winning party and how many rounds the encounter took.
The gen_encounter.py script will execute a run of an encounter.
from InvokePSQL import InvokePSQL from PlayerCharacter import PlayerCharacter from Foe import Foe from Encounter import Encounter
This script uses the classes built in earlier posts. They handle how to work with the database and how the players are generated and behave.
db = InvokePSQL() Heroes = [] Opponents = []
Then it sets up a database connection and creates two empty lists to be used in the game.
Heroes.append(PlayerCharacter(db, debug_ind=0)) Heroes.append(PlayerCharacter(db, debug_ind=0)) Opponents.append(Foe(db, foe_candidate='Skeleton', debug_ind=0)) Opponents.append(Foe(db, foe_candidate='Skeleton', debug_ind=0)) Opponents.append(Foe(db, foe_candidate='Skeleton', debug_ind=0)) print(f"For the Heroes:") for Hero in Heroes: print(f" {Hero.get_name()}") print(f"Against:") for Opponent in Opponents: print(f" {Opponent.get_name()}")
Two randomly created Heroes and three Skeletons are created, then added to the lists. The names are printed out to give the game a nice starting point. To see most everything going on behind the scenes when this game is run, turn on some or all of the debug_ind flags. That will print all of the debugging information being generated by the classes.
e1 = Encounter(Heroes, Opponents, debug_ind=0)
The one line that will run the entire encounter, decide on a winner, and note how long it took.
print(f"The winning party was: {e1.winning_list_name} in {e1.round} rounds.") print(f"The surviving {e1.winning_list_name} members:") for i in range(len(e1.winning_list)): if e1.winning_list[i].alive: print(f'{e1.winning_list[i].get_name()}')
Then print out the results. That looks like:
$ gen_encounter.py For the Heroes: Luric Galanodel Drulx Frizzlewhiz Against: Skeleton Skeleton Skeleton The winning party was: Heroes in 5 rounds. The surviving Heroes members: Luric Galanodel Drulx Frizzlewhiz
Yay! The Heroes won! That won’t always be the case.
Now the simulation is starting to take shape, It can create and place players on a game field; figure out who to attack and persue them; and it can deal damage and kill. Next time, seeing into how long each of the steps in the process takes will be the priority. Until then, Go play!