Browse code
implemented item stacks
Ed L authored on 26/07/2012 04:23:38
Showing 8 changed files
Showing 8 changed files
... | ... |
@@ -51,8 +51,11 @@ class GameBase: |
51 | 51 |
color_light_ground = libtcod.Color(200,200,200) |
52 | 52 |
|
53 | 53 |
|
54 |
- def message(self, msg, color): |
|
55 |
- utilities.message(self.game_msgs, self.MSG_HEIGHT, self.MSG_WIDTH, msg, color) |
|
54 |
+ def message(self, msg, color=None): |
|
55 |
+ if color is not None: |
|
56 |
+ utilities.message(self.game_msgs, self.MSG_HEIGHT, self.MSG_WIDTH, msg, color) |
|
57 |
+ else: |
|
58 |
+ utilities.message(self.game_msgs, self.MSG_HEIGHT, self.MSG_WIDTH, msg) |
|
56 | 59 |
|
57 | 60 |
def __init__(self, app_name='test app', screen_width=SCREEN_WIDTH, screen_height=SCREEN_HEIGHT): |
58 | 61 |
print '__init__' |
... | ... |
@@ -150,10 +153,12 @@ class GameBase: |
150 | 153 |
libtcod.console_blit(self.panel, 0,0, self.SCREEN_WIDTH,self.PANEL_HEIGHT, 0,0, self.PANEL_Y) |
151 | 154 |
|
152 | 155 |
def inventory_menu(self, header): |
153 |
- index = self.menu(self.con, header, self.player.get_item_names(), self.INVENTORY_WIDTH) |
|
156 |
+ data = [(item.display_name, item.ident) for item in self.player.get_items()] |
|
157 |
+ display = [x[0] for x in data] |
|
158 |
+ index = self.menu(self.con, header, display, self.INVENTORY_WIDTH) |
|
154 | 159 |
|
155 | 160 |
if index is not None: |
156 |
- return self.player.get_item(index) |
|
161 |
+ return self.player.get_item(data[index][1]) |
|
157 | 162 |
|
158 | 163 |
def get_names_under_mouse(self): |
159 | 164 |
x,y = self.mouse.cx, self.mouse.cy |
... | ... |
@@ -1,4 +1,7 @@ |
1 | 1 |
from __future__ import division |
2 |
+import os.path |
|
3 |
+import glob |
|
4 |
+import yaml |
|
2 | 5 |
|
3 | 6 |
import libtcodpy as libtcod |
4 | 7 |
import game |
... | ... |
@@ -6,13 +9,59 @@ import objects |
6 | 9 |
import utilities |
7 | 10 |
import monsters |
8 | 11 |
import random |
9 |
-from main import game_instance, Game |
|
12 |
+from main import Game |
|
10 | 13 |
|
11 | 14 |
|
12 | 15 |
class Item(object): |
16 |
+ def __init__(self, stackable=False): |
|
17 |
+ self.stackable = stackable |
|
18 |
+ self.stacks_with = [] |
|
19 |
+ self.stack_limit = 5 |
|
20 |
+ |
|
13 | 21 |
def __new__(*args): |
14 | 22 |
res = object.__new__(*args) |
15 | 23 |
return res |
24 |
+ def bind_game(self, game): |
|
25 |
+ self.game = game |
|
26 |
+ def bind_user(self, user): |
|
27 |
+ self.user = user |
|
28 |
+ return self.owner |
|
29 |
+ def free_user(self): |
|
30 |
+ self.user = None |
|
31 |
+ return self.owner |
|
32 |
+ |
|
33 |
+class ItemLoader(object): |
|
34 |
+ def __init__(self, dir): |
|
35 |
+ self.dir = dir |
|
36 |
+ |
|
37 |
+ def load_items(self): |
|
38 |
+ for fn in glob.glob(os.path.join(self.dir,'*.yml')): |
|
39 |
+ print 'fn', fn |
|
40 |
+ for doc in yaml.safe_load_all(file(fn)): |
|
41 |
+ self.load_item(doc) |
|
42 |
+ |
|
43 |
+ def load_item(self, doc): |
|
44 |
+ _color = doc.get('color', None) |
|
45 |
+ if _color is None: |
|
46 |
+ _color = libtcod.green |
|
47 |
+ elif hasattr(_color, 'upper'): |
|
48 |
+ _color = getattr(libtcod, _color) |
|
49 |
+ else: |
|
50 |
+ _color = libtcod.Color(*_color) |
|
51 |
+ |
|
52 |
+ item_class = doc['item_class'] |
|
53 |
+ module, clas = item_class.rsplit('.',1) |
|
54 |
+ module = __import__(module) |
|
55 |
+ item_class = getattr(module, clas) |
|
56 |
+ print 'item class:', item_class |
|
57 |
+ |
|
58 |
+ print 'loading', doc |
|
59 |
+ @Game.register_item_type(doc['spawn_chance']) |
|
60 |
+ class LoadedItem(item_class): |
|
61 |
+ name = doc.get('item_description') |
|
62 |
+ char = doc.get('char', '!') |
|
63 |
+ color = _color |
|
64 |
+ |
|
16 | 65 |
|
17 | 66 |
@Game.register_item_type(5) |
18 | 67 |
class HealingPotion(Item): |
... | ... |
@@ -24,10 +73,10 @@ class HealingPotion(Item): |
24 | 73 |
|
25 | 74 |
result = True |
26 | 75 |
if fighter.hp == fighter.max_hp: |
27 |
- game.message('You\'re full, can\'t heal', libtcod.red) |
|
76 |
+ self.game.message('You\'re full, can\'t heal', libtcod.red) |
|
28 | 77 |
result = False |
29 | 78 |
else: |
30 |
- game.message('Healing...') |
|
79 |
+ self.game.message('Healing...') |
|
31 | 80 |
fighter.heal(10) |
32 | 81 |
|
33 | 82 |
return result |
... | ... |
@@ -54,7 +103,7 @@ class Confusion(Item): |
54 | 103 |
|
55 | 104 |
result = False |
56 | 105 |
if monster is not None: |
57 |
- game.message('%s becomes confused' % monster.name) |
|
106 |
+ self.game.message('%s becomes confused' % monster.name) |
|
58 | 107 |
monsters.ConfusedMonster(random.randrange(10,18)).attach( |
59 | 108 |
monster |
60 | 109 |
) |
... | ... |
@@ -69,13 +118,13 @@ class Strengthen(Item): |
69 | 118 |
color = libtcod.chartreuse |
70 | 119 |
def use(self): |
71 | 120 |
if self.user.fighter: |
72 |
- game.message('%s feels a surge of strength' % self.user.name) |
|
121 |
+ self.game.message('%s feels a surge of strength' % self.user.name) |
|
73 | 122 |
self.user.fighter.stat_adjust(20, self.adj) |
74 | 123 |
return True |
75 | 124 |
|
76 | 125 |
def adj(self, owner): |
77 | 126 |
return ( |
78 |
- lambda _: game.message('The surge of strength has subsided'), |
|
127 |
+ lambda _: self.game.message('The surge of strength has subsided'), |
|
79 | 128 |
owner.fighter.defense, |
80 | 129 |
owner.fighter.power+3 |
81 | 130 |
) |
... | ... |
@@ -87,13 +136,13 @@ class Protect(Item): |
87 | 136 |
color = libtcod.chartreuse |
88 | 137 |
def use(self): |
89 | 138 |
if self.user.fighter: |
90 |
- game.message('%s is surrounded by a protecting aura' % self.user.name) |
|
91 |
- self.user.fighter.stat_adjust(10, self.adj) |
|
139 |
+ self.game.message('%s is surrounded by a protecting aura' % self.user.name) |
|
140 |
+ self.user.fighter.stat_adjust(15, self.adj) |
|
92 | 141 |
return True |
93 | 142 |
|
94 | 143 |
def adj(self, owner): |
95 | 144 |
return ( |
96 |
- lambda _: game.message('The protecting aura dissipates'), |
|
145 |
+ lambda _: self.game.message('The protecting aura dissipates'), |
|
97 | 146 |
owner.fighter.defense+6, |
98 | 147 |
owner.fighter.power |
99 | 148 |
) |
... | ... |
@@ -107,11 +156,11 @@ class LightningBolt(Item): |
107 | 156 |
monster = monsters.get_closest_monster(self.user) |
108 | 157 |
result = False |
109 | 158 |
if monster and self.user.can_see(monster.x, monster.y): |
110 |
- game.message('Monster %s has been struck by lightning' % monster.name) |
|
159 |
+ self.game.message('Monster %s has been struck by lightning' % monster.name) |
|
111 | 160 |
monster.fighter.take_damage(13) |
112 | 161 |
result = True |
113 | 162 |
else: |
114 |
- game.message('No target') |
|
163 |
+ self.game.message('No target') |
|
115 | 164 |
return result |
116 | 165 |
|
117 | 166 |
@Game.register_item_type(5) |
... | ... |
@@ -121,19 +170,19 @@ class Jump(Item): |
121 | 170 |
color= libtcod.dark_green |
122 | 171 |
jump_distance = 3 |
123 | 172 |
def use(self): |
124 |
- game_instance.select(self.jump) |
|
173 |
+ self.game.select(self.jump) |
|
125 | 174 |
return True |
126 | 175 |
def jump(self, x,y): |
127 | 176 |
dist = self.user.distance(x,y) |
128 | 177 |
|
129 | 178 |
if dist <= self.jump_distance: |
130 | 179 |
self.user.x, self.user.y = x,y |
131 |
- game.message('you are transported to a new place') |
|
180 |
+ self.game.message('you are transported to a new place') |
|
132 | 181 |
elif random.random() < self.jump_distance/dist: |
133 | 182 |
self.user.x, self.user.y = x,y |
134 |
- game.message('you strain all your power to move %d squares' % int(dist)) |
|
183 |
+ self.game.message('you strain all your power to move %d squares' % int(dist)) |
|
135 | 184 |
else: |
136 |
- game.message('you didn\'t make it') |
|
185 |
+ self.game.message('you didn\'t make it') |
|
137 | 186 |
self.user.fighter.take_damage( int(round(2 * dist/self.jump_distance)) ) |
138 | 187 |
|
139 | 188 |
@Game.register_item_type(3) |
... | ... |
@@ -143,8 +192,8 @@ class Acquire(Item): |
143 | 192 |
color= libtcod.dark_green |
144 | 193 |
effect_distance = 5 |
145 | 194 |
def use(self): |
146 |
- game.message('what do you want?') |
|
147 |
- game_instance.select(self.get) |
|
195 |
+ self.game.message('what do you want?') |
|
196 |
+ self.game.select(self.get) |
|
148 | 197 |
return True |
149 | 198 |
def get(self, x,y): |
150 | 199 |
if self.user.distance(x,y) < self.effect_distance: |
... | ... |
@@ -157,7 +206,7 @@ class Smite(Item): |
157 | 206 |
char = '\x0f' |
158 | 207 |
color = libtcod.red |
159 | 208 |
def use(self): |
160 |
- game_instance.select(self.smite) |
|
209 |
+ self.game.select(self.smite) |
|
161 | 210 |
return True |
162 | 211 |
|
163 | 212 |
def smite(self, x,y): |
... | ... |
@@ -165,9 +214,9 @@ class Smite(Item): |
165 | 214 |
if monster: |
166 | 215 |
monster.fighter.take_damage(10) |
167 | 216 |
if monster.fighter: |
168 |
- game.message('%s is smitten, he only retains %s hp' % (monster.name, monster.fighter.hp)) |
|
217 |
+ self.game.message('%s is smitten, he only retains %s hp' % (monster.name, monster.fighter.hp)) |
|
169 | 218 |
else: |
170 |
- game.message('%s thought it better to go elsewhere' % monster.name) |
|
219 |
+ self.game.message('%s thought it better to go elsewhere' % monster.name) |
|
171 | 220 |
|
172 | 221 |
|
173 | 222 |
@Game.register_item_type(2) |
... | ... |
@@ -178,7 +227,7 @@ class Fireball(Item): |
178 | 227 |
effect_radius = 3 |
179 | 228 |
|
180 | 229 |
def use(self): |
181 |
- game_instance.select(self.smite) |
|
230 |
+ self.game.select(self.smite) |
|
182 | 231 |
return True |
183 | 232 |
|
184 | 233 |
def smite(self, x,y): |
... | ... |
@@ -189,7 +238,7 @@ class Fireball(Item): |
189 | 238 |
for obj in self.owner.level.objects: |
190 | 239 |
if obj.fighter and obj is not self.user: |
191 | 240 |
if (obj.x, obj.y) == (x,y): |
192 |
- game.message('%s takes a direct hit from the fireball' % obj.name) |
|
241 |
+ self.game.message('%s takes a direct hit from the fireball' % obj.name) |
|
193 | 242 |
obj.fighter.take_damage(20) |
194 | 243 |
elif obj.distance(x,y) < self.effect_radius: |
195 | 244 |
obj.fighter.take_damage(6) |
... | ... |
@@ -197,5 +246,5 @@ class Fireball(Item): |
197 | 246 |
strikes.append('%s %s' % (obj.name, obj.fighter.hp)) |
198 | 247 |
else: |
199 | 248 |
strikes.append('%s dead' % obj.name) |
200 |
- game.message('The names of those who were to close for comfort: %s' % ', '.join(strikes)) |
|
249 |
+ self.game.message('The names of those who were to close for comfort: %s' % ', '.join(strikes)) |
|
201 | 250 |
|
... | ... |
@@ -57,7 +57,7 @@ if __name__ == 'main': |
57 | 57 |
|
58 | 58 |
|
59 | 59 |
def player_death(self, player): |
60 |
- utilities.message(self.game_msgs, self.MSG_HEIGHT, self.MSG_WIDTH, 'You died!') |
|
60 |
+ self.message('You died!', libtcod.red) |
|
61 | 61 |
self.game_state = 'dead' |
62 | 62 |
player.char = '%' |
63 | 63 |
player.color = libtcod.dark_red |
... | ... |
@@ -82,7 +82,7 @@ if __name__ == 'main': |
82 | 82 |
def register_item_type(cls, chance): |
83 | 83 |
def _inner(typ): |
84 | 84 |
cls.item_types[typ] = chance |
85 |
- return cls |
|
85 |
+ return typ |
|
86 | 86 |
return _inner |
87 | 87 |
|
88 | 88 |
|
... | ... |
@@ -90,6 +90,7 @@ if __name__ == 'main': |
90 | 90 |
@classmethod |
91 | 91 |
def register_monster_type(cls, typ, chance): |
92 | 92 |
cls.monster_types[typ] = chance |
93 |
+ return typ |
|
93 | 94 |
|
94 | 95 |
@property |
95 | 96 |
def level(self): |
... | ... |
@@ -198,6 +199,7 @@ if __name__ == 'main': |
198 | 199 |
def mvkeyhandler(self): |
199 | 200 |
item = self.inventory_menu('choose item\n') |
200 | 201 |
if item is not None: |
202 |
+ item.bind_game(self) |
|
201 | 203 |
self.player.use(item) |
202 | 204 |
|
203 | 205 |
@mvkeyhandler.handle('d') |
... | ... |
@@ -298,8 +300,11 @@ if __name__ == '__main__': |
298 | 300 |
from functools import partial |
299 | 301 |
render_bar = partial(render_bar, game_instance.panel) |
300 | 302 |
|
301 |
- a = MonsterLoader(os.path.join('.','data','monsters')) |
|
302 |
- a.load_monsters() |
|
303 |
+ ml = MonsterLoader(os.path.join('.','data','monsters')) |
|
304 |
+ ml.load_monsters() |
|
305 |
+ |
|
306 |
+ il = ItemLoader(os.path.join('.','data','items')) |
|
307 |
+ il.load_items() |
|
303 | 308 |
|
304 | 309 |
game_instance.setup_map() |
305 | 310 |
game_instance.main() |
... | ... |
@@ -216,6 +216,14 @@ class Smoother(AutomataEngine): |
216 | 216 |
else: |
217 | 217 |
return cell |
218 | 218 |
|
219 |
+class NewSmoother(AutomataEngine): |
|
220 |
+ def rule(self, x,y, cell): |
|
221 |
+ avg = self.sum_area( (x,y), 2 ) / 16 |
|
222 |
+ if avg < .5: |
|
223 |
+ return 0 |
|
224 |
+ else: |
|
225 |
+ return 1 |
|
226 |
+ |
|
219 | 227 |
import collections |
220 | 228 |
class Map(collections.MutableSequence): |
221 | 229 |
def __init__(self, width, height, con, level): |
... | ... |
@@ -258,7 +266,7 @@ class Map(collections.MutableSequence): |
258 | 266 |
def __getitem__(self, k): |
259 | 267 |
return self.data[k] |
260 | 268 |
def __setitem__(self, k,v): |
261 |
- return self.data[k][v] |
|
269 |
+ self.data[k] = v |
|
262 | 270 |
def __delitem__(self, k): |
263 | 271 |
del self.data[k] |
264 | 272 |
|
... | ... |
@@ -143,7 +143,10 @@ class MonsterLoader(object): |
143 | 143 |
(lambda doc: |
144 | 144 |
lambda map,level,con,x,y: objects.Object( map, con, x,y, |
145 | 145 |
doc['char'], |
146 |
- doc.get('name_fmt', '%s the %s') % (libtcod.namegen_generate(doc['namegen_class']).capitalize(), doc['race_name'].capitalize()), |
|
146 |
+ doc.get('name_fmt', '%s the %s') % ( |
|
147 |
+ libtcod.namegen_generate(doc['namegen_class']).capitalize(), |
|
148 |
+ doc['race_name'].capitalize() |
|
149 |
+ ), |
|
147 | 150 |
color, |
148 | 151 |
True, |
149 | 152 |
fighter=objects.Fighter( |
... | ... |
@@ -11,6 +11,91 @@ def get_pos_pair(x,y): |
11 | 11 |
x,y = x.pos |
12 | 12 |
return x,y |
13 | 13 |
|
14 |
+import collections |
|
15 |
+class Slot(object): |
|
16 |
+ def __init__(self): |
|
17 |
+ self.limit = None |
|
18 |
+ self.items = [] |
|
19 |
+ |
|
20 |
+ @property |
|
21 |
+ def display_name(self): |
|
22 |
+ if self.items != []: |
|
23 |
+ return '%s (x%s)' % (self.items[0].name, len(self.items)) |
|
24 |
+ |
|
25 |
+ @property |
|
26 |
+ def ident(self): |
|
27 |
+ if self.items != []: |
|
28 |
+ return self.items[0].name |
|
29 |
+ |
|
30 |
+ |
|
31 |
+ def empty(self): |
|
32 |
+ return len(self.items) == 0 |
|
33 |
+ |
|
34 |
+ def _add_item(self, item): |
|
35 |
+ self.items.append(item) |
|
36 |
+ return True |
|
37 |
+ |
|
38 |
+ def add_item(self, item): |
|
39 |
+ result = False |
|
40 |
+ if self.limit is None or len(self.items) <= self.limit: |
|
41 |
+ if self.items == []: |
|
42 |
+ self.limit = item.item.stack_limit |
|
43 |
+ print 'no items' |
|
44 |
+ return self._add_item(item) |
|
45 |
+ elif (len(self.items) < self.limit) and (self.ident == item.name): |
|
46 |
+ print 'add_items %d' % len(self.items) |
|
47 |
+ return self._add_item(item) |
|
48 |
+ elif self.ident != item.name: |
|
49 |
+ raise ValueError('Cannot stack %s with %s' % (self.ident, item.ident)) |
|
50 |
+ return result |
|
51 |
+ |
|
52 |
+ def get_item(self, default=None): |
|
53 |
+ result = default |
|
54 |
+ if self.items != []: |
|
55 |
+ result = self.items[-1] |
|
56 |
+ return result |
|
57 |
+ |
|
58 |
+ def consume(self): |
|
59 |
+ self.items.pop() |
|
60 |
+ |
|
61 |
+ |
|
62 |
+ |
|
63 |
+class Inventory(object): |
|
64 |
+ def __init__(self): |
|
65 |
+ self.objects = {} |
|
66 |
+ |
|
67 |
+ def __iter__(self): |
|
68 |
+ for v in self.objects.itervalues(): |
|
69 |
+ for i in v: |
|
70 |
+ yield i |
|
71 |
+ |
|
72 |
+ def __len__(self): |
|
73 |
+ return sum(len(x) for x in self.objects.values()) |
|
74 |
+ |
|
75 |
+ def __contains__(self, it): |
|
76 |
+ return it.ident in self.objects |
|
77 |
+ |
|
78 |
+ def __getitem__(self, k): |
|
79 |
+ return self.objects[k][-1].get_item() |
|
80 |
+ |
|
81 |
+ def __setitem__(self, k,v): |
|
82 |
+ if v.ident != k: raise ValueError('Inventory key must equal the item\'s name') |
|
83 |
+ self.add_item(v) |
|
84 |
+ |
|
85 |
+ def add_item(self, item): |
|
86 |
+ slot = self.objects.setdefault(item.name, [Slot()]) |
|
87 |
+ while not slot[-1].add_item(item): |
|
88 |
+ print 'add slot' |
|
89 |
+ slot.append(Slot()) |
|
90 |
+ |
|
91 |
+ def __delitem__(self, k): |
|
92 |
+ self.objects[k][-1].consume() |
|
93 |
+ while self.objects[k] != [] and self.objects[k][-1].empty(): |
|
94 |
+ self.objects[k].pop() |
|
95 |
+ else: |
|
96 |
+ if self.objects[k] == []: |
|
97 |
+ del self.objects[k] |
|
98 |
+ |
|
14 | 99 |
class Player(Object): |
15 | 100 |
def triggers_recompute(func): |
16 | 101 |
def _inner(self, *a, **kw): |
... | ... |
@@ -25,7 +110,7 @@ class Player(Object): |
25 | 110 |
) |
26 | 111 |
|
27 | 112 |
map.player = self |
28 |
- self.inventory = [] |
|
113 |
+ self.inventory = Inventory() |
|
29 | 114 |
|
30 | 115 |
def draw(self, player=None): |
31 | 116 |
if player is None: |
... | ... |
@@ -33,38 +118,37 @@ class Player(Object): |
33 | 118 |
return Object.draw(self, player) |
34 | 119 |
|
35 | 120 |
def pick_up(self, obj): |
121 |
+ # TODO: limit number of items |
|
36 | 122 |
if len(self.inventory) >= 26: |
37 | 123 |
game.message('Your inventory is full, cannot pick up %s' % obj.name, libtcod.red) |
38 |
- else: |
|
39 |
- self.inventory.append( |
|
40 |
- self.level.claim_object(obj) |
|
124 |
+ elif obj is not None: |
|
125 |
+ self.inventory.add_item( |
|
126 |
+ self.level.claim_object(obj).item.bind_user(self) # returns item.owner |
|
41 | 127 |
) |
42 | 128 |
game.message('you picked up a %s!' % obj.name, libtcod.green) |
43 |
- obj.item.user = self |
|
44 | 129 |
return self |
45 | 130 |
|
46 | 131 |
def drop(self, obj): |
47 |
- self.level.objects.insert(0, obj) |
|
132 |
+ obj = self.inventory[obj.name] |
|
48 | 133 |
obj.x, obj.y = self.x, self.y |
49 |
- game.message('you dropped a %s' % obj.name, libtcod.yellow) |
|
134 |
+ self.level.add_object(obj) |
|
135 |
+ del self.inventory[obj.name] |
|
50 | 136 |
|
51 | 137 |
def use(self, item): |
52 | 138 |
item.owner.enter_level(self.level) |
53 | 139 |
success = item.use() |
54 | 140 |
|
55 |
- try: |
|
56 |
- index = self.inventory.index(item.owner) |
|
57 |
- except ValueError: |
|
58 |
- index = -1 |
|
141 |
+ if success: |
|
142 |
+ del self.inventory[item.name] |
|
59 | 143 |
|
60 |
- if success and index != -1: |
|
61 |
- self.inventory.pop(index) |
|
62 | 144 |
|
63 | 145 |
def get_item(self, index): |
64 | 146 |
return self.inventory[index].item |
65 | 147 |
|
66 | 148 |
def get_item_names(self): |
67 |
- return [item.name for item in self.inventory] |
|
149 |
+ return [item.display_name for item in self.inventory] |
|
150 |
+ def get_items(self): |
|
151 |
+ return [item for item in self.inventory] |
|
68 | 152 |
|
69 | 153 |
def tick(self): |
70 | 154 |
if self.fighter: |
... | ... |
@@ -90,6 +90,8 @@ class MovementKeyListener(object): |
90 | 90 |
result = self.handlers[key.vk](*a, **kw) or True |
91 | 91 |
elif key.c in self.char_handlers: |
92 | 92 |
result = self.char_handlers[key.c](*a, **kw) or True |
93 |
+ if result is None: |
|
94 |
+ result = 'didnt-take-turn' |
|
93 | 95 |
return (key, result) |
94 | 96 |
|
95 | 97 |
def handle(self, key): |