CSCI 136
Fundamentals of Computer Science II
Spring 2022

Montana Tech of The University of Montana
Computer Science & Software Engineering

Assignment 7 - Ultima 0.1

In this assignment, you will be extending your previous Ultima assignment. You will implement Python threads. You will learn how to create code that properly protects data/operations shared between different threads.

UPDATE: If you weren't able to get the last lab to work correctly, here is code that you can use as a basis for this week's assignment: Lab 6 Solution

Getting started. You can use your previous Ultima assignment as starting point for this assignment. Once everyone has turned in part 1 next week, we will provide you with working files for that in case you got stuck on the first part. Start by downloading This zip file includes all of the images you will need, the python library code necessary, and new configuration files for running the game. The file has been changed for the new and improved game, and you should use this as the main game loop - in other words, you don't need to make any changes to In your improved game, there are monsters you must go around and do battle with. We have provided a stub version of that you can fill in with your own code. You attack monsters by running into them. Monsters attack you in the same way. You can also walk on lava if you want, but it costs you one hit point.

Tile. As before, a Tile object represents an individual position in the Ultima world. You will be using the same tiles as in the previous version. You already have code to handle most of this. You will only need to add one method to the Tile class.

Below is the API for the Tile class. Most of these methods were already implemented in the previous lab, but you will need to add the getDamage() method to the Tile class for this lab.

class Tile
            __init__(self, string code)  # Create a new tile based on a String code
    boolean getLit(self)                 # Return whether this Tile is lit or not
            setLit(self, boolean value)  # Change the lit status of this Tile
            draw(self, int x, int y)     # Draw at index position (x, y)
    boolean isOpaque(self)               # Does this type of Tile block light?
    boolean isPassable(self)             # Can the Avatar walk on this Tile?
        int getDamage(self)              # Returns 1 if the tile is Lava, 0 otherwise
Avatar. The Avatar class has gotten somewhat more advanced. The Avatar now has a number of hit points (life, or health). Once your hit points reach 0, the game is over and you lose. The Avatar also has a damage amount, this is how many hit points of damage our hero causes when attacking a monster. You will need to update your constructor to handle these new attributes.

If the Avatar incurs damage (from a monster or from walking on lava), the new hit point value after the damage is displayed in the title line of the StdDraw canvas window. This is done for you automatically in the Ultima game loop. Your Avatar data type must implement the following additional API methods:
class Avatar
            Avatar(self, int x, int y, int hp, int damage, double torch) 
                                          # Modify constructor to accomodate additional
                                          # input parameters and functionality
        int getHitPoints(self)            # Get the number of hit points left
            incurDamage(self, int damage) # Reduce the Avatar's hit points by the amount damage
        int getDamage(self)               # Get the amount of damage the Avatar causes monsters
The Avatar's line in the game text file now has 5 numbers, for example:
11 2 20 3 100.0
The above means the Avatar starts at position (11, 2), starts with 20 hit points, causes 3 hit points of damage, and has an initial torch radius of 100.

Monster. The Monster class represents a monster that roams around the World randomly. A monster knows things like its location, its remaining hit points, the amount of damage it causes when it attacks, and the type of monster it is. Monster objects also keep a reference to the World object so they can call methods in World from their run() method (namely the monsterMove() method). If a monster is damaged it displays its remaining hit points in red text over the monster's image for three cycles of play. If a monster's hit points are 0 or less, the monster has been killed and no longer is shown, and its thread should terminate.

Each monster is its own Python thread that, every so many milliseconds, attempts to move itself around the World. They are not smart, they just choose north, south, east or west at random. If they can move in the randomly chosen direction they do, otherwise they skip their turn and remain in the same location. If the monster randomly moves into your Avatar, you will lose hit points according to the damage attribute of the monster. Just like the Avatar, monsters cannot walk through walls, on water, or through mountains. Monsters can walk on lava, but it causes damage just as it does for the Avatar.

class Monster
       __init__(self, World world, String code, int x, int y, int hp, int damage, int sleepMs)
                                        # Constructor initializes monster characteristics,
                                        # including a copy of the World object it has been
                                        # instanitaed in, a code for the monster type, several 
                                        # attributes that are the same as avatar (initial
                                        # position, hit points, and damage), and a number
                                        # representing the number of ms it should sleep
                                        # between moves
       incurDamage(self, int damage)    # Reduce this monster's hit points by the amount damage
       draw(self)                       # Draw the monster
   int getHitPoints(self)               # Get the number of remaining hit points of this monster
   int getDamage(self)                  # Get how much damage this monster causes
   int getX(self)                       # Get current x-position of this monster
   int getY(self)                       # Get current y-position of this monster
       setLocation(self, int x, int y)  # Move this monster to a new location
       run(self)                        # Worker thread that periodically moves this monster
Monsters are defined at the end of the game text file. You can assume the values in the file are valid starting locations, monsters won't start on top of each other, on top of a mountain, etc. Here is an example:
SK  3   3   10  3 1000
OR  6   19  8   2 1000
BA  20  10  4   1 500
SL  25  16  6   2 1500
The above defines: World. The World class has a number of changes. The constructor must now parse a file with more information about the Avatar as well as information about the monsters. The World object must create and keep track of the Monster objects. Each monster is a thread, so you'll need to fire up one thread per monster. You will also need to implement two methods that handle attempting to move the Avatar or a Monster. Finally, you will implement two methods that determine the end of the game: avatarAlive() and getMonsters(). The game ends when the avatar no longer has any hit points left, or all of the monsters have been eliminated.
class World
 boolean avatarAlive(self)                                # Is the Avatar still alive?
         monsterMove(self, int x, int y, Monster monster) # Attempt to move given monster to (x, y)
         avatarMove(self, int x, int y)                   # Attempt to move Avatar to (x, y)
     int getNumMonsters(self)                             # Return number of alive monsters
The monsterMove() should be called by a monster's run() method. If the proposed location is not valid or not passable, then nothing happens. If there is currently another monster at the proposed location, then nothing happens (monsters don't attack each other). If the Avatar is at the proposed location, then the monster gets to attack the Avatar and do the appropriate damage. In this case, the monster stays at its current location (Avatar and monsters never overlap). Otherwise, the monster makes its move to the new location, incurring any damage associated with the new location (i.e. if the new location is lava). Note: since only the World object knows the outcome of the monster's call to monsterMove(), the World object must update the calling Monster object by calling setLocation() and/or incurDamage().

The avatarMove() method should be called when the handleKey() method tries to move the Avatar. Similar to the monster, if the proposed location is not valid or passable, the Avatar stays put. If there is a monster at the location, the Avatar attacks it and the Avatar stays put. Otherwise, the Avatar moves to the new location incurring any damage associated with the new location (i.e. if the new location is lava).

Grade ItemPoints PossiblePoints Earned
Program Compiles and Runs
Comments on All Classes and Methods
Tile Changes
Avatar Changes
Monster Class
World Changes
Used Threads for Each Monster
No Concurrency Issues on World Instance

Do I need to follow the prescribed APIs? Yes. You must implement the methods as specified in the API, but you may add methods if they are helpful to your code.

Why does monsterMove() get passed a monster object? The monsterMove() method in the World class gets called by the monster's thread. In order for the method to do things like damage the monster for walking on lava or updating its location, it needs a reference to the object. While the monster run() method could do this, it could cause concurrency trouble.

But how do I pass monsterMove() the monster object? The run() method can use the self keyword which is a reference to the object running the method.

How did you change the font for your hit point display? StdDraw.setFontSize(12)

How do I display hit points for only three moves? You might want to implement a "timer" attribute in your Monster class to keep track. When a monster incurs damage, reset the "timer" to 0. When a monster is drawn, only draw hit points if the timer is less than 3, and if so increment the timer.

How do I test if I've handled concurrency correctly? Try running the 10x10_full.txt world. This world has monsters at all grid positions aside from the Avatar's position. The program shouldn't crash and the monsters shouldn't end up on top of each other.

If a monster calls say time.sleep(1) between moves, will the monster move exactly every second? No, in practice it might take a little longer than a second. time.sleep makes sure your thread isn't scheduled for execution for the specified period of time. Thereafter the thread must wait to be scheduled on the CPU which could take more time. For the purposes of our game, this is close enough.

Extra credit. Make the game better. One idea is to refactor the code so that it takes advantage of abstraction in the hierarchy of classes. Another idea is to create a source of health restoration for both the hero and the monsters. You might want to make additional maps, use additional tile types, or add sound to the game. Or come up with your own creative ideas. If you do submit extra credit work, be sure to submit the lab first, as defined by the assignment, and then submit your improved version to the Extra Credit dropbox.

Submission. Submit your programs,, and, using Moodle. Be sure each submitted source file has the required header with your name and a description of the program. For extra-credit, please submit a single zip file containing all your programs/graphics/sounds via the special extra-credit drop box.

Page last updated: March 02, 2022