본문 바로가기
Practical Python Design Patterns

The Prototype Pattern

by 자동매매 2023. 3. 28.

CHAPTER 3 The Prototype Pattern

Reality doesn t care if you believe in it.

Boba Fett, StarWars extended universe

The Problem

I can still remember the very first time I wanted to program anything. This was at a time when DOS could not address more than 20 MB at a time, and thus our massive 40 MB hard drive had to be partitioned into two drives. It was a beige Olivetti XT. The local library had a section with computer books in it. One of them, a very thin softcover, had a picture of a game character on it. The title promised to teach you to program your very own games. In a classic case of youthful ignorance swayed by false advertising, I worked through every example, typing out the characters one by one (I could not read English at the time, so I really had no idea what I was doing). After about the tenth time, I got everything entered correctly into the GW Basic interface that came with the computer. I did not know about saving and loading programs, so every mistake meant starting from scratch. My crowning moment fell completely flat. The game I had worked so hard on turned out to be a simple for loop and an if statement. The premise of the game was that you were in a runaway car and got three chances to guess a number before the car ended up in a dam. That was it

no graphics, no sound, no pretty colors, just the same text question three times and then:

You are dead. Getting it right resulted in a simple message: Yay! You got it right.

Beyond the First Steps

Even though my first program was a complete let-down, that magical moment when I picked up the book for the first time and believed that I could program a game, or anything I could dream of, stuck with me.

37

' Wessel Badenhorst 2017

W. Badenhorst, Practical Python Design Patterns, https://doi.org/10.1007/978-1-4842-2680-3_3
CHAPTER 3 THE PROTOTYPE PATTERN

I guess a lot of people first become interested in programming in some derivative of interest in games and the desire to program games. Sadly, games tend to be vast and complex systems, and making a game that will be fun and popular is a massive undertaking without any guarantees of success. In the end, relatively few programmers pursue their initial interest in game programming. In this chapter, we will imagine that we are indeed part of this select group of programmers.

Base for an Actual Game

Suppose we want to write an RTS (short for real-time strategy game) like StarCraft, where you have a player that controls a group of characters. The player needs to construct buildings, generate units, and ultimately meet some sort of strategic objective. Let s consider one unit, a Knight. The Knight is produced in a building called the Barracks. A player can have multiple such buildings in a single scenario so as to create knight units more rapidly.

Looking at this description of the unit and its interaction with the building, a fairly obvious solution would involve defining a Barracks class that has a generate_knight function that returns a Knight object, which is an instance of the Knight class. We will be implementing the following basic properties for the Knight class:

Life

Speed

Attack power Attack range Weapon

Generating a new Knight instance would simply involve instantiating an object from the Knight class and setting the values.

Here is the code for doing exactly that:

rts_simple.py

class Knight(object): def __init__(

self,

life,

38
CHAPTER 3 THE PROTOTYPE PATTERN

speed, attack_power, attack_range, weapon

):

self.life = life

self.speed = speed self.attack_power = attack_power self.attack_range = attack_range self.weapon = weapon

def __str__(self):

return "Life: {0}\n" \

"Speed: {1}\n" \

"Attack Power: {2}\n" \

"Attack Range: {3}\n" \

"Weapon: {4}".format( self.life,

self.speed,

self.attack_power, self.attack_range,

self.weapon

)

class Barracks(object):

def generate_knight(self):

return Knight(400, 5, 3, 1, "short sword") if __name__ == "__main__":

barracks = Barracks()

knight1 = barracks.generate_knight()

print("[knight1] {}".format(knight1))

Running this code from the command line will create a barracks instance of the Barracks class, and will then use that instance to generate knight1, which is just an instance of the Knight class with values set for the properties we defined in the previous section. Running the code prints the following text to the terminal so you can check that the knight instance matches what you expect it to be based on the values set in the generate_knight function.

39
CHAPTER 3 THE PROTOTYPE PATTERN

[knight1] Life: 400 Speed: 5

Attack Power: 3 Attack Range: 1 Weapon: short sword

Even though we do not have any draw logic or interaction code, it is quite easy to generate a new knight with some default values. For the rest of this chapter, we will be concerning ourselves with this generation code and will see what it teaches us about creating multiple objects that are almost exactly alike.

Note If you are interested in learning more about game programming, I challenge you to look at the PyGame package; see the “Exercises” section of this chapter for

more information.

If you have never played or seen an RTS before, it is important to know that a big part of the fun in these games comes from the many different units you can generate. Each unit has its own strengths and weaknesses, and how you leverage these unique characteristics shapes the strategy you end up adopting. The better you are at understanding trade-offs, the better you become at developing effective strategies.

The next step is to add one more character to the cast that can be generated by a barracks. For example, I am going to add an Archer class, but feel free to add some of your own unit types, giving them unique strengths and weaknesses. You get to create your dream cast of units. While you are at it, feel free to think of other attributes your units will need to add depth to the game. When you are done with this chapter, go through the code you have written and add these ideas. Not only will it make your RTS more interesting, but it will also help you better grasp the arguments presented throughout the chapter.

With the addition of the Archer class, our rts_simple.py now looks like this:

rts_simple.py

class Knight(object): def __init__(

self,

life,

speed,

40
CHAPTER 3 THE PROTOTYPE PATTERN

attack_power, attack_range, weapon

):

self.unit_type = "Knight" self.life = life

self.speed = speed self.attack_power = attack_power self.attack_range = attack_range self.weapon = weapon

def __str__(self):

return "Type: {0}\n" \

"Life: {1}\n"
*
"Speed: {2}\n" \ "Attack Power: {3}\n" \ "Attack Range: {4}\n" \ "Weapon: {5}".format(

self.unit_type,

self.life,

self.speed,

self.attack_power, self.attack_range, self.weapon

)

class Archer(object): def __init__(

self,

life,

speed, attack_power, attack_range, weapon

):

41
CHAPTER 3 THE PROTOTYPE PATTERN

self.unit_type = "Archer"

self.life = life

self.speed = speed

self.attack_power = attack_power self.attack_range = attack_range self.weapon = weapon

def __str__(self):

return "Type: {0}\n" \

"Life: {1}\n" \

"Speed: {2}\n" \

"Attack Power: {3}\n" \

"Attack Range: {4}\n" \

"Weapon: {5}".format( self.unit_type,

self.life,

self.speed,

self.attack_power, self.attack_range, self.weapon

)

class Barracks(object):

def generate_knight(self):

return Knight(400, 5, 3, 1, "short sword")

def generate_archer(self):

return Archer(200, 7, 1, 5, "short bow")

if __name__ == "__main__":

barracks = Barracks() knight1 = barracks.generate_knight()

archer1 = barracks.generate_archer()

print("[knight1] {}".format(knight1))

print("[archer1] {}".format(archer1))

Next, you will see the result of running this program. We now have a knight and an archer, each with its very own unique values for the unit attributes. Read through the code and try to understand what each line is doing before you continue to the explanation of the results.

42
CHAPTER 3 THE PROTOTYPE PATTERN

[knight1] Type: Knight Life: 400

Speed: 5

Attack Power: 3 Attack Range: 1 Weapon: short sword [archer1] Type: Archer Life: 200

Speed: 7

Attack Power: 1 Attack Range: 5 Weapon: short bow

At the moment, there are only the two units to think about, but since you already had a moment to contemplate all the other units you are going to add to your unit generator, it should be pretty clear that having a separate function for each type of unit you plan

on generating in a specific building is not a great idea. To hammer this point home a bit, imagine what would happen if you wanted to upgrade the units that a barracks could generate. As an example, consider upgrading the Archer class so that the weapon it gets created with is no longer a short bow but rather a long bow, and its attack range is upped by five points along with a two-point increase in attack power. Suddenly, you double

the number of functions needed in the Barracks class, and you need to keep some sort of record of the state of the barracks and the units that it can generate to make sure you generate the right level of unit.

Alarm bells should be going off by now.

There must be a better way to implement unit generation, one that is aware of not only the type of unit you want it to generate, but also of the level of the unit in question. One way to implement this would be to replace the individual generate_knight and generate_archer methods with one method called generate_unit. This method would take as its parameters the type of unit to be generated along with the level of unit you wanted it to generate. The single method would use this information to split out into

the units to be created. We should also extend the individual unit classes to alter the parameters used for the different unit attributes, based on a level parameter passed to the constructor when the unit is instantiated.

43
CHAPTER 3 THE PROTOTYPE PATTERN

Your upgraded unit-generation code would now look like this:

rts_multi_unit.py

class Knight(object):

def __init__(self, level):

self.unit_type = "Knight"

if level == 1:

self.life = 400

self.speed = 5

self.attack_power = 3

self.attack_range = 1

self.weapon = "short sword" elif level == 2:

self.life = 400

self.speed = 5 self.attack_power = 6 self.attack_range = 2 self.weapon = "long sword"

def __str__(self):

return "Type: {0}\n" \

"Life: {1}\n" \

"Speed: {2}\n" \

"Attack Power: {3}\n" \

"Attack Range: {4}\n" \

"Weapon: {5}".format( self.unit_type,

self.life,

self.speed,

self.attack_power, self.attack_range, self.weapon

)

class Archer(object):

def __init__(self, level):

self.unit_type = "Archer"

44
CHAPTER 3 THE PROTOTYPE PATTERN

if level == 1:

self.life = 200

self.speed = 7

self.attack_power = 1

self.attack_range = 5

self.weapon = "short bow" elif level == 2:

self.life = 200

self.speed = 7 self.attack_power = 3 self.attack_range = 10 self.weapon = "long bow"

def __str__(self):

return "Type: {0}\n" \

"Life: {1}\n" \

"Speed: {2}\n" \

"Attack Power: {3}\n" \

"Attack Range: {4}\n" \

"Weapon: {5}".format( self.unit_type,

self.life,

self.speed,

self.attack_power, self.attack_range, self.weapon

)

class Barracks(object):

def build_unit(self, unit_type, level):

if unit_type == "knight":

return Knight(level)

elif unit_type == "archer":

return Archer(level)

if __name__ == "__main__": barracks = Barracks()

45
CHAPTER 3 THE PROTOTYPE PATTERN

knight1 = barracks.build_unit("knight", 1) archer1 = barracks.build_unit("archer", 2) print("[knight1] {}".format(knight1))

print("[archer1] {}".format(archer1))

In the results that follow, you will see that the code generates a level 1 knight and a level 2 archer, and their individual parameters match what you would expect them to be, without the need for the Barracks class to keep track of every level of every unit and the relevant parameters associated with them.

[knight1] Type: Knight Life: 400

Speed: 5

Attack Power: 3 Attack Range: 1 Weapon: short sword [archer1] Type: Archer Life: 200

Speed: 7

Attack Power: 3 Attack Range: 10 Weapon: long bow

I like this implementation a little more than the previous one, as we have reduced the methods needed and isolated the unit-level parameters inside the unit class, where it makes more sense to have them.

In modern RTS games, balance is a big issue one of the main challenges facing game designers. The idea behind game balance is that sometimes users find a way

to exploit a specific unit s characteristics in such a way that it overpowers every other strategy or situation in the game. Even though that sounds like exactly the type of strategy you would want to find, this is actually a guarantee that players will lose interest in your game. Alternatively, some characters might suffer from a weakness that makes

it practically useless for the game. In both these cases, the unit in question (or the game as a whole) is said to be imbalanced. A game designer wants to make alterations to the parameters of each unit (like attack power) in order to address these imbalances.

46
CHAPTER 3 THE PROTOTYPE PATTERN

Digging through hundreds of thousands of lines of code to find values for the parameters of each class and alter them, especially if the developers must do that

hundreds of times throughout the development lifecycle. Imagine what a mess digging through lines upon lines of code would be for a game like Eve Online, which relies heavily on its Python base for game logic.

We could store the parameters in a separate JSON file or a database to allow game designers to alter unit parameters in a single place. It would be easy to create a nice GUI (Graphical User Interface) for game designers where they could quickly and easily make changes without even having to alter the file in a text editor.

When we want to instantiate a unit, we load the relevant file or entry, extract the values we need, and create the instance as before, like this:

knight_1.dat

400

5

3

1

short sword

archer_1.dat

200

7

3

10

Long bow

rts_file_based.py

class Knight(object):

def __init__(self, level):

self.unit_type = "Knight"

filename = "{}_{}.dat".format(self.unit_type, level)

with open(filename, ’r’) as parameter_file:

lines = parameter_file.read().split("\n") self.life = lines[0]

self.speed = lines[1]

47
CHAPTER 3 THE PROTOTYPE PATTERN

self.attack_power = lines[2] self.attack_range = lines[3] self.weapon = lines[4]

def __str__(self):

return "Type: {0}\n" \

"Life: {1}\n" \

"Speed: {2}\n" \

"Attack Power: {3}\n" \

"Attack Range: {4}\n" \

"Weapon: {5}".format( self.unit_type,

self.life,

self.speed,

self.attack_power, self.attack_range, self.weapon

)

class Archer(object):

def __init__(self, level):

self.unit_type = "Archer"

filename = "{}_{}.dat".format(self.unit_type, level)

with open(filename, ’r’) as parameter_file:

lines = parameter_file.read().split("\n") self.life = lines[0]

self.speed = lines[1]

self.attack_power = lines[2] self.attack_range = lines[3]

self.weapon = lines[4]

48
CHAPTER 3 THE PROTOTYPE PATTERN

def __str__(self):

return "Type: {0}\n" \

"Life: {1}\n" \

"Speed: {2}\n" \

"Attack Power: {3}\n" \

"Attack Range: {4}\n" \

"Weapon: {5}".format( self.unit_type,

self.life,

self.speed,

self.attack_power, self.attack_range, self.weapon

)

class Barracks(object):

def build_unit(self, unit_type, level):

if unit_type == "knight":

return Knight(level)

elif unit_type == "archer":

return Archer(level)

if __name__ == "__main__":

baracks = Baracks()

knight1 = barracks.build_unit("knight", 1) archer1 = barracks.build_unit("archer", 2) print("[knight1] {}".format(knight1))

print("[archer1] {}".format(archer1))

Since the unit data files store the data in a predictable order, it makes it very easy to grab the file from disk and then read in the parameters needed to construct the relevant unit. The code still delivers the same result as before, but now we are ready to balance many different unit types and levels. In our example, the import from file looks the same for both the Archer and Knight classes, but we must keep in mind that we will have units that have to import different parameters from their file, so a single import file would be impractical in a real-world scenario.

49
CHAPTER 3 THE PROTOTYPE PATTERN

You can verify that your results match these results:

[knight1] Type: Knight Life: 400

Speed: 5

Attack Power: 3 Attack Range: 1 Weapon: short sword [archer1] Type: Archer Life: 200

Speed: 7

Attack Power: 3 Attack Range: 10 Weapon: long bow

Players will be able to build multiple versions of the same building, as we discussed before, and we want the levels and types of units that a specific building can generate

to change based on the level of the building. A level 1 barracks generates only level 1 knights, but once the barracks is upgraded to level 2, it unlocks the archer unit, and as an added bonus it now generates level 2 knights instead of the level 1 knights from before. Upgrading one building only has an effect on the units that it is able to generate, and not on the capabilities of all the buildings of the same type that the player built. We cannot simply keep track of a single instance of a unit; now every building needs to track its own version of the unit.

Every time a building wants to create a unit, it needs to look up what units it can create and then issue a command to create the selected unit. The unit class then has

to query the storage system to find the relevant parameters, which are then read from storage before they are passed into the class constructor of the instance being created. This is all very inefficient. If one building has to generate 500 units of the same type, you have to make 499 duplicate requests of the storage system you chose. Multiply this by

the number of buildings, and then add the lookups each building needs to do to decide which units it should be capable of generating. A massive game like Eve Online, or any other modern RTS for that matter, will kill your system in short order if it is required to go through this process every time it needs to generate a unit or build a building. Some games take this a step further by allowing specific add-ons on buildings that give units generated in that building different capabilities from the general unit type, which would make the system even more resource hungry.

50
CHAPTER 3 THE PROTOTYPE PATTERN

What we have here is a very real need to create a large number of objects that are mostly the same, with one or two small tweaks differentiating them. Loading these objects from scratch every time is not a scalable solution, as we saw.

Which brings us to . . .

Implementing the Prototype Pattern

In the prototype pattern, we favor composition over inheritance. Classes that are composed of parts allow you to substitute those parts at runtime, profoundly affecting the testability and maintainability of the system. The classes to instantiate are specified at runtime by means of dynamic loading. The result of this characteristic of the prototype pattern is that sub-classing is reduced significantly. The complexities of creating a

new instance are hidden from the client. All of this is great, but the main benefit of this pattern is that it forces you to program to an interface, which leads to better design.

Note Just beware that deep-cloning classes with circular references can and will cause issues. See the next section for more information on shallow versus deep copy.

We simply want to make a copy of some object we have on hand. To make sure the copy is set up the way it should be, and to isolate the functionality of the object from the rest of the system, the instance you are going to copy should provide the copying feature. A clone() method on the instance that clones the object and then modifies its values accordingly would be ideal.

The three components needed for the prototype pattern are as follows:

Client creates a new object by asking a prototype to clone itself Prototype declares an interface for cloning itself

Concrete prototype implements the operation for cloning itself

(Credit for the tree components of the prototype pattern: http://radek.io/2011/ 08/03/design-pattern-prototype/)

51
CHAPTER 3 THE PROTOTYPE PATTERN

In the RTS example, every building should keep a list of the prototypes it can use to generate units, like a list of units of the same level, with the properties to match the current state of the building. When a building is upgraded, this list is updated to match the new capabilities of the building. We get to remove the 499 redundant calls from the equation. This is also the point where the prototype design pattern deviates from the abstract factory design pattern, which we will look at later in this book. By swapping the prototypes that a building can use at runtime, we allow the building to dynamically switch to generating completely different units without any form of sub-classing of the building class in question. Our buildings effectively become prototype managers.

The idea of a prototype pattern is great, but we need to look at one more concept before we can implement the pattern in our buildings.

Shallow Copy vs. Deep Copy

Python handles variables a little differently than do other programming languages you may have come across. Apart from the basic types, like integers and strings, variables in Python are closer to tags or labels than to the buckets other programming languages use as a metaphor. The Python variables are essentially pointers to addresses in memory where the relevant values are stored.

Let s look at the following example:

a = range(1,6)

print("[a] {}".format(a)) b = a

print("[b] {}".format(b)) b.append(6)

print("[a] {}".format(a)) print("[b] {}".format(b))

The first statement points the variable a to a list of numbers from 1 to 6 (excluding 6). Next, variable b is assigned to the same list of numbers that a is pointing to. Finally, the digit 6 is added to the end of list pointed to by b.

52
CHAPTER 3 THE PROTOTYPE PATTERN

What do you expect the results of the print statements to be? Look at the following output. Does it match what you expected?

  1. [1, 2, 3, 4, 5]
  2. [1, 2, 3, 4, 5]
  3. [1, 2, 3, 4, 5, 6]
  4. [1, 2, 3, 4, 5, 6]

Most people find it surprising that the digit 6 was appended to the end of both the list a points to and the list b points to. If you remember that in fact a and b point to the same list, it is clear that adding an element to the end of that list should show the element added to the end of the list no matter which variable you are looking at.

Shallow Copy

When we want to make an actual copy of the list, and have two independent lists at the end of the process, we need to use a different strategy.

Just as before, we are going to load the same digits into a list and point the variable a at the list. This time, we are going to use the slice operator to copy the list.

a = range(1,6)

print("[a] {}".format(a)) b = a[:]

print("[b] {}".format(b)) b.append(6)

print("[a] {}".format(a)) print("[b] {}".format(b))

The slice operator is a very useful tool, since it allows us to cut up a list (or string) in many different ways. Want to get all the elements from a list, while leaving out the first two elements? No problem just slice the list with [2:]. What about all elements but the last two? Slice does that: [:-2]. You could even ask the slice operator to only give you every second element. I challenge you to figure out how that is done.

When we assigned b to a[:], we told b to point to a slice created by duplicating the elements in list a, from the first to the last element, effectively copying all the values in list a to another location in memory and pointing the variable b to this list.

53
CHAPTER 3 THE PROTOTYPE PATTERN

Does the result you see surprise you?

  1. [1, 2, 3, 4, 5]
  2. [1, 2, 3, 4, 5]
  3. [1, 2, 3, 4, 5]
  4. [1, 2, 3, 4, 5, 6]

The slice operator works perfectly when dealing with a shallow list (a list containing only actual values, not references to other complex objects like lists or dictionaries).

Dealing with Nested Structures

Look at the code that follows. What do you think the result will be?

lst1 = [’a’, ’b’, [’ab’, ’ba’]] lst2 = lst1[:]

lst2[0] = ’c’

print("[lst1] {}".format(lst1)) print("[lst2] {}".format(lst2))

Here, you see an example of a deep list. Take a look at the results do they match what you predicted?

[lst1] [’a’, ’b’, [’ab’, ’ba’]] [lst2] [’c’, ’b’, [’ab’, ’ba’]]

As you might have expected from the shallow copy example, altering the first element of lst2 does not alter the first element of lst1, but what happens when we alter one of the elements in the list within the list?

lst1 = [’a’, ’b’, [’ab’, ’ba’]] lst2 = lst1[:]

lst2[2][1] = ’d’

print("[lst1] {}".format(lst1)) print("[lst2] {}".format(lst2))

Can you explain the results we get this time?

[lst1] [’a’, ’b’, [’ab’, ’d’]] [lst2] [’a’, ’b’, [’ab’, ’d’]]

54
CHAPTER 3 THE PROTOTYPE PATTERN

Were you surprised to see what happened?

The list lst1 contained three elements, ’a’, ’b’, and a pointer to another list that looked like this: [’ab’, ’ba’]. When we did a shallow copy of lst1 to create the list that lst2 points to, only the elements in the list on one level were duplicated. The structures contained at the addresses for the element at position 2 in lst1 were not cloned; only the value pointing to the position of the list [’ab’, ’ba’] in memory was. As a result, both lst1 and lst2 ended up pointing to separate lists containing the characters ’a’ and ’b’ followed by a pointer to the same list containing [’ab’, ’ba’], which would result in an issue whenever some function changes an element at this list, since it would have an effect on the contents of the other list.

Deep Copy

Clearly, we will need another solution when making clones of objects. How can we

force Python to make a complete copy, a deep copy, of everything contained in the list and its sub-lists or objects? Luckily, we have the copy module as part of the standard library. The copy contains a method, deep-copy, that allows a complete deep copy of an arbitrary list; i.e., shallow and other lists. We will now use deep copy to alter the previous example so we get the output we expect.

from copy import deepcopy

lst1 = [’a’, ’b’, [’ab’, ’ba’]] lst2 = deepcopy(lst1) lst2[2][1] = ’d’

print("[lst1] {}".format(lst1)) print("[lst2] {}".format(lst2))

resulting in:

[lst1] [’a’, ’b’, [’ab’, ’ba’]] [lst2] [’a’, ’b’, [’ab’, ’d’]]

There. Now we have the expected result, and after that fairly long detour, we are ready to see what this means for the buildings in our RTS.

55
CHAPTER 3 THE PROTOTYPE PATTERN

Using What We Have Learned in Our Project

At its heart, the prototype pattern is just a clone() function that accepts an object as an input parameter and returns a clone of it.

The skeleton of a prototype pattern implementation should declare an abstract base class that specifies a pure virtual clone() method. Any class that needs a polymorphic constructor (the class decides which constructor to use based on the number of arguments it receives upon instantiation) capability derives itself from the abstract base class and implements the clone() method. Every unit needs to derive itself from this abstract base class. The client, instead of writing code that invokes the new operator on a hard-coded class name, calls the clone() method on the prototype.

This is what the prototype pattern would look like in general terms:

prototype_1.py

from abc import ABCMeta, abstractmethod

class Prototype(metaclass=ABCMeta): @abstractmethod

def clone(self):

pass

concrete.py

from prototype_1 import Prototype from copy import deepcopy

class Concrete(Prototype): def clone(self):

return deepcopy(self)

Finally, we can implement our unit-generating building with the prototype pattern, using the same prototype_1.py file.

rts_prototype.py

from prototype_1 import Prototype from copy import deepcopy

56
CHAPTER 3 THE PROTOTYPE PATTERN

class Knight(Prototype):

def __init__(self, level):

self.unit_type = "Knight"

filename = "{}_{}.dat".format(self.unit_type, level)

with open(filename, ’r’) as parameter_file:

lines = parameter_file.read().split("\n") self.life = lines[0]

self.speed = lines[1]

self.attack_power = lines[2] self.attack_range = lines[3]

self.weapon = lines[4]

def __str__(self):

return "Type: {0}\n" \

"Life: {1}\n" \

"Speed: {2}\n" \

"Attack Power: {3}\n" \

"Attack Range: {4}\n" \

"Weapon: {5}".format( self.unit_type,

self.life,

self.speed,

self.attack_power, self.attack_range, self.weapon

)

def clone(self):

return deepcopy(self)

class Archer(Prototype):

def __init__(self, level):

self.unit_type = "Archer"

filename = "{}_{}.dat".format(self.unit_type, level)

57
CHAPTER 3 THE PROTOTYPE PATTERN

with open(filename, ’r’) as parameter_file:

lines = parameter_file.read().split("\n") self.life = lines[0]

self.speed = lines[1]

self.attack_power = lines[2] self.attack_range = lines[3]

self.weapon = lines[4]

def __str__(self):

return "Type: {0}\n" \

"Life: {1}\n" \

"Speed: {2}\n" \

"Attack Power: {3}\n" \

"Attack Range: {4}\n" \

"Weapon: {5}".format( self.unit_type,

self.life,

self.speed,

self.attack_power, self.attack_range, self.weapon

)

def clone(self):

return deepcopy(self)

class Barracks(object): def __init__(self):

self.units = { "knight": {

1: Knight(1),

2: Knight(2) },

"archer": {

1: Archer(1),

2: Archer(2) }

}

58
CHAPTER 3 THE PROTOTYPE PATTERN

def build_unit(self, unit_type, level):

return self.units[unit_type][level].clone()

if __name__ == "__main__":

barracks = Baracks()

knight1 = barracks.build_unit("knight", 1) archer1 = barracks.build_unit("archer", 2) print("[knight1] {}".format(knight1))

print("[archer1] {}".format(archer1))

When we extended the abstract base class in the unit classes, that forced us to implement the clone method, which we used when we had the barracks generate new units. The other thing we did was generate all the options that a Barracks instance can generate and keep it in a unit s array. Now, we can simply clone the unit with the right level without opening a file or loading any data from an external source.

[knight1] Type: Knight Life: 400

Speed: 5

Attack Power: 3 Attack Range: 1 Weapon: short sword [archer1] Type: Archer Life: 200

Speed: 7

Attack Power: 3 Attack Range: 10 Weapon: long bow

Well done; you have not only taken your first steps to creating your very own RTS, but you have also dug deeply into a very useful design pattern.

Exercises

Take the prototype example code and extend the Archer class to

handle its upgrades.

Experiment with adding more units to the barracks building.

59
CHAPTER 3 THE PROTOTYPE PATTERN

Add a second type of building.

Take a look at the PyGame package (http://pygame.org/hifi.html).

How can you extend your Knight class so it can draw itself in a game loop?

As an exercise, you can look at the PyGame package and implement a

draw() method for the Knight class to be able to draw it on a map.

If you are interested, try to write your own mini RTS game with a

barracks and two units using PyGame.

Extend the clone method of each unit to generate a random name, so

each cloned unit will have a different name.

60

'Practical Python Design Patterns' 카테고리의 다른 글

Builder Pattern  (0) 2023.03.28
Factory Pattern  (0) 2023.03.28
The Singleton Pattern  (0) 2023.03.28
Before We Begin  (0) 2023.03.28
Index  (0) 2023.03.28

댓글