source: gamelib/gameboard.py @ 316:ae5989b3ce01

Last change on this file since 316:ae5989b3ce01 was 316:ae5989b3ce01, checked in by Neil Muller <drnlmuller@…>, 11 years ago

Fix test for time limit

File size: 37.7 KB
Line 
1import random
2
3import pygame
4from pygame.locals import MOUSEBUTTONDOWN, MOUSEMOTION, KEYDOWN, K_UP, K_DOWN, \
5        K_LEFT, K_RIGHT
6from pgu import gui
7
8import data
9import tiles
10import icons
11import constants
12import buildings
13import animal
14import equipment
15import sound
16import cursors
17import sprite_cursor
18import misc
19
20class OpaqueLabel(gui.Label):
21    def __init__(self, value, **params):
22        gui.Label.__init__(self, value, **params)
23        if 'width' in params:
24            self._width = params['width']
25        if 'height' in params:
26            self._height = params['height']
27        self._set_size()
28
29    def _set_size(self):
30        width, height = self.font.size(self.value)
31        width = getattr(self, '_width', width)
32        height = getattr(self, '_height', height)
33        self.style.width, self.style.height = width, height
34
35    def paint(self, s):
36        s.fill(self.style.background)
37        if self.style.align > 0:
38            r = s.get_rect()
39            w, _ = self.font.size(self.value)
40            s = s.subsurface(r.move((r.w-w, 0)).clip(r))
41        gui.Label.paint(self, s)
42
43    def update_value(self, value):
44        self.value = value
45        self._set_size()
46        self.repaint()
47
48def mklabel(text="", **params):
49    params.setdefault('color', constants.FG_COLOR)
50    params.setdefault('width', GameBoard.TOOLBAR_WIDTH/2)
51    return OpaqueLabel(text, **params)
52
53def mkcountupdate(counter):
54    def update_counter(self, value):
55        getattr(self, counter).update_value("%s  " % value)
56        self.repaint()
57    return update_counter
58
59class ToolBar(gui.Table):
60    def __init__(self, gameboard, **params):
61        gui.Table.__init__(self, **params)
62        self.group = gui.Group(name='toolbar', value=None)
63        self._next_tool_value = 0
64        self.gameboard = gameboard
65        self.cash_counter = mklabel(align=1)
66        self.chicken_counter = mklabel(align=1)
67        self.egg_counter = mklabel(align=1)
68        self.day_counter = mklabel(align=1)
69        self.killed_foxes = mklabel(align=1)
70
71        self.tr()
72        self.td(gui.Spacer(self.rect.w/2, 0))
73        self.td(gui.Spacer(self.rect.w/2, 0))
74        self.add_counter(mklabel("Day:"), self.day_counter)
75        self.add_counter(mklabel("Groats:"), self.cash_counter)
76        self.add_counter(mklabel("Eggs:"), self.egg_counter)
77        self.add_counter(icons.CHKN_ICON, self.chicken_counter)
78        self.add_counter(icons.KILLED_FOX, self.killed_foxes)
79        self.add_spacer(15)
80
81        self.add_tool_button("Move Hen", constants.TOOL_PLACE_ANIMALS,
82                None, cursors.cursors['select'])
83        self.add_tool_button("Cut Trees", constants.TOOL_LOGGING,
84                constants.LOGGING_PRICE, cursors.cursors['ball'])
85        self.add_spacer(15)
86
87        self.add_heading("Sell ...")
88        self.add_tool_button("Chicken", constants.TOOL_SELL_CHICKEN,
89                constants.SELL_PRICE_CHICKEN, cursors.cursors['sell'])
90        self.add_tool_button("Egg", constants.TOOL_SELL_EGG,
91                constants.SELL_PRICE_EGG, cursors.cursors['sell'])
92        self.add_tool_button("Building", constants.TOOL_SELL_BUILDING,
93                None, cursors.cursors['sell'])
94        self.add_tool_button("Equipment", constants.TOOL_SELL_EQUIPMENT,
95                None, cursors.cursors['sell'])
96        self.add_spacer(15)
97
98        self.add_heading("Buy ...")
99
100        self.add_tool_button("Fence", constants.TOOL_BUY_FENCE,
101                "%s/%s" % (constants.BUY_PRICE_FENCE,
102                           constants.REPAIR_PRICE_FENCE))
103
104        for building_cls in buildings.BUILDINGS:
105            self.add_tool_button(building_cls.NAME.title(), building_cls,
106                    None, cursors.cursors.get('build', None))
107
108        for equipment_cls in equipment.EQUIPMENT:
109            self.add_tool_button(equipment_cls.NAME.title(),
110                    equipment_cls,
111                    equipment_cls.BUY_PRICE,
112                    cursors.cursors.get('buy', None))
113
114        self.add_spacer(10)
115        self.add_tool("Price Reference", self.show_prices)
116        self.add_spacer(20)
117
118        self.add_tool("Finished Day", self.day_done)
119
120        self.anim_clear_tool = False # Flag to clear the tool on an anim loop
121        # pgu's tool widget fiddling happens after the tool action, so calling
122        # clear_tool in the tool's action doesn't work, so we punt it to
123        # the anim loop
124
125    def day_done(self):
126        import engine
127        pygame.event.post(engine.START_NIGHT)
128
129    def show_prices(self):
130        """Popup dialog of prices"""
131        def make_box(text):
132            style = {
133                    'border' : 1
134                    }
135            word = gui.Label(text, style=style)
136            return word
137
138        def fix_widths(doc):
139            """Loop through all the widgets in the doc, and set the
140               width of the labels to max + 10"""
141            # We need to do this because of possible font issues
142            max_width = 0
143            for thing in doc.widgets:
144                if hasattr(thing, 'style'):
145                    # A label
146                    if thing.style.width > max_width:
147                        max_width = thing.style.width
148            for thing in doc.widgets:
149                if hasattr(thing, 'style'):
150                    thing.style.width = max_width + 10
151
152        tbl = gui.Table()
153        tbl.tr()
154        doc = gui.Document(width=400)
155        space = doc.style.font.size(" ")
156        for header in ['Item', 'Buy Price', 'Sell Price']:
157            doc.add(make_box(header))
158        doc.br(space[1])
159        for building in buildings.BUILDINGS:
160            doc.add(make_box(building.NAME))
161            doc.add(make_box('%d' % building.BUY_PRICE))
162            doc.add(make_box('%d' % building.SELL_PRICE))
163            doc.br(space[1])
164        for equip in equipment.EQUIPMENT:
165            doc.add(make_box(equip.NAME))
166            doc.add(make_box('%d' % equip.BUY_PRICE))
167            doc.add(make_box('%d' % equip.SELL_PRICE))
168            doc.br(space[1])
169        fix_widths(doc)
170        for word in "Damaged equipment will be sold for less than" \
171                " the sell price.".split():
172            doc.add(gui.Label(word))
173            doc.space(space)
174        close_button = gui.Button("Close")
175        tbl.td(doc, colspan=3)
176        tbl.tr()
177        tbl.td(gui.Label(''))
178        tbl.td(gui.Label(''))
179        tbl.td(close_button)
180        dialog = gui.Dialog(gui.Label('Price Reference'), tbl)
181        close_button.connect(gui.CLICK, dialog.close)
182        dialog.open()
183        self.anim_clear_tool = True
184
185    update_cash_counter = mkcountupdate('cash_counter')
186    update_fox_counter = mkcountupdate('killed_foxes')
187    update_chicken_counter = mkcountupdate('chicken_counter')
188    update_egg_counter = mkcountupdate('egg_counter')
189    update_day_counter = mkcountupdate('day_counter')
190
191    def add_spacer(self, height):
192        self.tr()
193        self.td(gui.Spacer(0, height), colspan=2)
194
195    def add_heading(self, text):
196        self.tr()
197        self.td(mklabel(text), colspan=2)
198
199    def add_tool_button(self, text, tool, price=None, cursor=None):
200        if price is not None:
201            text = "%s  (%s)" % (text, price)
202        self.add_tool(text, lambda: self.gameboard.set_selected_tool(tool,
203            cursor))
204
205    def add_tool(self, text, func):
206        label = gui.basic.Label(text)
207        value = self._next_tool_value
208        self._next_tool_value += 1
209        tool = gui.Tool(self.group, label, value, width=self.rect.w, style={"padding_left": 0})
210        tool.connect(gui.CLICK, func)
211        self.tr()
212        self.td(tool, align=-1, colspan=2)
213
214    def clear_tool(self):
215        self.group.value = None
216        for item in self.group.widgets:
217            item.pcls = ""
218        self.anim_clear_tool = False
219
220    def add_counter(self, icon, label):
221        self.tr()
222        self.td(icon, width=self.rect.w/2)
223        self.td(label, width=self.rect.w/2)
224
225    def resize(self, width=None, height=None):
226        width, height = gui.Table.resize(self, width, height)
227        width = GameBoard.TOOLBAR_WIDTH
228        return width, height
229
230
231class VidWidget(gui.Widget):
232    def __init__(self, gameboard, vid, **params):
233        gui.Widget.__init__(self, **params)
234        self.gameboard = gameboard
235        self.vid = vid
236        self.vid.bounds = pygame.Rect((0, 0), vid.tile_to_view(vid.size))
237
238    def paint(self, surface):
239        self.vid.paint(surface)
240        # Blit animation frames on top of the drawing
241        x, y = self.vid.view.x, self.vid.view.y
242        for anim in self.gameboard.animations:
243            anim.fix_pos(self.vid)
244            anim.irect.x = anim.rect.x - anim.shape.x
245            anim.irect.y = anim.rect.y - anim.shape.y
246            surface.blit(anim.image, (anim.irect.x - x, anim.irect.y - y))
247            # We don't store anim._irect, since we only update anims if the
248            # image changes, which kills irect
249
250    def update(self, surface):
251        us = []
252        x, y = self.vid.view.x, self.vid.view.y
253        for anim in self.gameboard.animations[:]:
254            if anim.updated or anim.removed:
255                # We flag that we need to redraw stuff undeneath the animation
256                us.append(pygame.Rect(anim.irect.x - x, anim.irect.y - y,
257                    anim.irect.width, anim.irect.height))
258                self.vid.alayer[anim.pos.y][anim.pos.x]=1
259                self.vid.updates.append(anim.pos.to_tuple())
260            if anim.removed:
261                # Remove the animation from the draw loop
262                self.gameboard.animations.remove(anim)
263        us.extend(self.vid.update(surface))
264        for anim in self.gameboard.animations:
265            if anim.updated: 
266                anim.fix_pos(self.vid)
267                # setimage has happened, so redraw
268                anim.irect.x = anim.rect.x - anim.shape.x
269                anim.irect.y = anim.rect.y - anim.shape.y
270                surface.blit(anim.image, (anim.irect.x - x, anim.irect.y - y))
271                anim.updated = 0
272                us.append(pygame.Rect(anim.irect.x - x, anim.irect.y - y,
273                    anim.irect.width, anim.irect.height))
274                # This is enough, because sprite changes happen disjoint
275                # from the animation sequence, so we don't need to worry
276                # other changes forcing us to redraw the animation frame.
277        return us
278
279    def move_view(self, x, y):
280        self.vid.view.move_ip((x, y))
281
282    def event(self, e):
283        if e.type == MOUSEBUTTONDOWN:
284            self.gameboard.use_tool(e)
285        elif e.type == MOUSEMOTION and self.gameboard.sprite_cursor:
286            self.gameboard.update_sprite_cursor(e)
287
288class GameBoard(object):
289    TILE_DIMENSIONS = (20, 20)
290    TOOLBAR_WIDTH = 140
291
292    GRASSLAND = tiles.REVERSE_TILE_MAP['grassland']
293    FENCE = tiles.REVERSE_TILE_MAP['fence']
294    WOODLAND = tiles.REVERSE_TILE_MAP['woodland']
295    BROKEN_FENCE = tiles.REVERSE_TILE_MAP['broken fence']
296
297    # These don't have to add up to 100, but it's easier to think
298    # about them if they do.
299    FOX_WEIGHTINGS = (
300        (animal.Fox, 59),
301        (animal.GreedyFox, 30),
302        (animal.NinjaFox, 5),
303        (animal.DemoFox, 5),
304        (animal.Rinkhals, 1),
305        )
306
307    def __init__(self, main_app):
308        self.disp = main_app
309        self.tv = tiles.FarmVid()
310        self.tv.png_folder_load_tiles('tiles')
311        self.tv.tga_load_level(data.filepath('levels/farm.tga'))
312        self.create_display()
313
314        self.selected_tool = None
315        self.animal_to_place = None
316        self.sprite_cursor = None
317        self.chickens = set()
318        self.foxes = set()
319        self.buildings = []
320        self.animations = []
321        self.cash = 0
322        self.eggs = 0
323        self.days = 0
324        self.killed_foxes = 0
325        self.add_cash(constants.STARTING_CASH)
326        self.day, self.night = True, False
327
328        self.fix_buildings()
329
330        self.add_some_chickens()
331
332    def get_top_widget(self):
333        return self.top_widget
334
335    def create_display(self):
336        width, height = self.disp.rect.w, self.disp.rect.h
337        tbl = gui.Table()
338        tbl.tr()
339        self.toolbar = ToolBar(self, width=self.TOOLBAR_WIDTH)
340        tbl.td(self.toolbar, valign=-1)
341        self.tvw = VidWidget(self, self.tv, width=width-self.TOOLBAR_WIDTH, height=height)
342        tbl.td(self.tvw)
343        self.top_widget = tbl
344
345    def update(self):
346        self.tvw.reupdate()
347
348    def loop(self):
349        self.tv.loop()
350
351    def set_selected_tool(self, tool, cursor):
352        if not self.day:
353            return
354        self.selected_tool = tool
355        if self.animal_to_place:
356            # Clear any highlights
357            self.animal_to_place.unequip_by_name("Spotlight")
358        self.select_animal_to_place(None)
359        sprite_curs = None
360        if buildings.is_building(tool):
361            sprite_curs = sprite_cursor.SpriteCursor(tool.IMAGE, self.tv, tool.BUY_PRICE)
362        elif equipment.is_equipment(tool):
363            sprite_curs = sprite_cursor.SpriteCursor(tool.CHICKEN_IMAGE_FILE, self.tv)
364        elif tool == constants.TOOL_BUY_FENCE:
365            sprite_curs = sprite_cursor.SpriteCursor("tiles/fence.png", self.tv)
366        self.set_cursor(cursor, sprite_curs)
367
368    def set_cursor(self, cursor=None, sprite_curs=None):
369        if cursor:
370            pygame.mouse.set_cursor(*cursor)
371        else:
372            pygame.mouse.set_cursor(*cursors.cursors['arrow'])
373        self.sprite_cursor = sprite_curs
374        self.tv.sprites.set_cursor(sprite_curs)
375
376    def reset_states(self):
377        """Clear current states (highlights, etc.)"""
378        self.set_selected_tool(None, None)
379        self.toolbar.clear_tool()
380
381    def update_sprite_cursor(self, e):
382        tile_pos = self.tv.screen_to_tile(e.pos)
383        self.sprite_cursor.set_pos(tile_pos)
384
385    def start_night(self):
386        self.day, self.night = False, True
387        self.tv.sun(False)
388        self.reset_states()
389
390    def start_day(self):
391        self.day, self.night = True, False
392        self.tv.sun(True)
393        self.reset_states()
394
395    def in_bounds(self, pos):
396        """Check if a position is within the game boundaries"""
397        if pos.x < 0 or pos.y < 0:
398            return False
399        width, height = self.tv.size
400        if pos.x >= width or pos.y >= height:
401            return False
402        return True
403
404    def use_tool(self, e):
405        if not self.day:
406            return
407        if e.button == 3: # Right button
408            self.set_selected_tool(None, None)
409            self.toolbar.clear_tool()
410        elif e.button != 1: # Left button
411            return
412        if self.selected_tool == constants.TOOL_SELL_CHICKEN:
413            self.sell_chicken(self.tv.screen_to_tile(e.pos))
414        elif self.selected_tool == constants.TOOL_SELL_EGG:
415            self.sell_egg(self.tv.screen_to_tile(e.pos))
416        elif self.selected_tool == constants.TOOL_PLACE_ANIMALS:
417            self.place_animal(self.tv.screen_to_tile(e.pos))
418        elif self.selected_tool == constants.TOOL_BUY_FENCE:
419            self.buy_fence(self.tv.screen_to_tile(e.pos))
420        elif self.selected_tool == constants.TOOL_SELL_BUILDING:
421            self.sell_building(self.tv.screen_to_tile(e.pos))
422        elif self.selected_tool == constants.TOOL_SELL_EQUIPMENT:
423            self.sell_equipment(self.tv.screen_to_tile(e.pos))
424        elif self.selected_tool == constants.TOOL_LOGGING:
425            self.logging_forest(self.tv.screen_to_tile(e.pos))
426        elif buildings.is_building(self.selected_tool):
427            self.buy_building(self.tv.screen_to_tile(e.pos), self.selected_tool)
428        elif equipment.is_equipment(self.selected_tool):
429            self.buy_equipment(self.tv.screen_to_tile(e.pos), self.selected_tool)
430
431    def get_outside_chicken(self, tile_pos):
432        for chick in self.chickens:
433            if chick.covers(tile_pos) and chick.outside():
434                return chick
435        return None
436
437    def get_building(self, tile_pos):
438        for building in self.buildings:
439            if building.covers(tile_pos):
440                return building
441        return None
442
443    def sell_chicken(self, tile_pos):
444
445        def do_sell(chicken, update_button=None):
446            if not chicken:
447                return False # sanity check
448            if len(self.chickens) == 1:
449                print "You can't sell your last chicken!"
450                return False
451            for item in list(chicken.equipment):
452                self.add_cash(item.sell_price())
453                chicken.unequip(item)
454            self.add_cash(constants.SELL_PRICE_CHICKEN)
455            sound.play_sound("sell-chicken.ogg")
456            if update_button:
457                update_button(chicken, empty=True)
458            self.remove_chicken(chicken)
459            return True
460
461        chick = self.get_outside_chicken(tile_pos)
462        if chick is None:
463            building = self.get_building(tile_pos)
464            if building and building.NAME in buildings.HENHOUSES:
465                self.open_building_dialog(building, do_sell)
466            return
467        do_sell(chick)
468
469    def sell_one_egg(self, chicken):
470        if chicken.eggs:
471            self.add_cash(constants.SELL_PRICE_EGG)
472            chicken.remove_one_egg()
473            self.eggs -= 1
474            self.toolbar.update_egg_counter(self.eggs)
475            return True
476        return False
477
478    def sell_egg(self, tile_pos):
479        def do_sell(chicken, update_button=None):
480            # We try sell and egg
481            if self.sell_one_egg(chicken):
482                sound.play_sound("sell-chicken.ogg")
483                # Force toolbar update
484                self.toolbar.chsize()
485                if update_button:
486                    update_button(chicken)
487            return False
488
489        building = self.get_building(tile_pos)
490        if building and building.NAME in buildings.HENHOUSES:
491            self.open_building_dialog(building, do_sell)
492
493    def select_animal_to_place(self, animal):
494        if self.animal_to_place:
495            self.animal_to_place.unequip_by_name("Spotlight")
496        self.animal_to_place = animal
497        if self.animal_to_place:
498            self.animal_to_place.equip(equipment.Spotlight())
499
500    def place_animal(self, tile_pos):
501        """Handle an TOOL_PLACE_ANIMALS click.
502
503           This will either select an animal or
504           place a selected animal in a building.
505           """
506        chicken = self.get_outside_chicken(tile_pos)
507        if chicken:
508            if chicken is self.animal_to_place:
509                self.select_animal_to_place(None)
510                pygame.mouse.set_cursor(*cursors.cursors['select'])
511            else:
512                self.select_animal_to_place(chicken)
513                pygame.mouse.set_cursor(*cursors.cursors['chicken'])
514            return
515        building = self.get_building(tile_pos)
516        if building:
517            if self.animal_to_place:
518                try:
519                    place = building.first_empty_place()
520                    self.relocate_animal(self.animal_to_place, place=place)
521                    self.animal_to_place.equip(equipment.Nest())
522                    self.select_animal_to_place(None)
523                    pygame.mouse.set_cursor(*cursors.cursors['select'])
524                except buildings.BuildingFullError:
525                    pass
526            else:
527                self.open_building_dialog(building)
528            return
529        if self.tv.get(tile_pos) == self.GRASSLAND:
530            if self.animal_to_place is not None:
531                self.animal_to_place.unequip_by_name("Nest")
532                self.relocate_animal(self.animal_to_place, tile_pos=tile_pos)
533                self.eggs -= self.animal_to_place.get_num_eggs()
534                self.animal_to_place.remove_eggs()
535                self.toolbar.update_egg_counter(self.eggs)
536
537    def relocate_animal(self, chicken, tile_pos=None, place=None):
538        assert((tile_pos, place) != (None, None))
539        if chicken.abode is not None:
540            chicken.abode.clear_occupant()
541        if tile_pos:
542            chicken.set_pos(tile_pos)
543        else:
544            place.set_occupant(chicken)
545            chicken.set_pos(place.get_pos())
546        self.set_visibility(chicken)
547
548    def set_visibility(self, chicken):
549        if chicken.outside():
550            if chicken not in self.tv.sprites:
551                self.tv.sprites.append(chicken)
552        else:
553            if chicken in self.tv.sprites:
554                self.tv.sprites.remove(chicken)
555
556    def open_dialog(self, widget, x=None, y=None, close_callback=None):
557        """Open a dialog for the given widget. Add close button."""
558        tbl = gui.Table()
559
560        def close_dialog():
561            self.disp.close(tbl)
562            if close_callback is not None:
563                close_callback()
564
565        close_button = gui.Button("Close")
566        close_button.connect(gui.CLICK, close_dialog)
567
568        tbl = gui.Table()
569        tbl.tr()
570        tbl.td(widget, colspan=2)
571        tbl.tr()
572        tbl.td(gui.Spacer(100, 0))
573        tbl.td(close_button, align=1)
574
575        if x:
576            offset = (self.disp.rect.center[0] +  x,
577                    self.disp.rect.center[1] + y)
578        else:
579            offset = None
580        self.disp.open(tbl, pos=offset)
581        return tbl
582
583    def open_building_dialog(self, building, sell_callback=None):
584        """Create dialog for manipulating the contents of a building."""
585
586        place_button_map = {}
587
588        def update_button(animal, empty=False):
589            """Update a button image (either to the animal, or to empty)."""
590            if animal:
591                button = place_button_map.get(id(animal.abode))
592                if button:
593                    if empty:
594                        button.value = icons.EMPTY_NEST_ICON
595                    else:
596                        button.value = icons.animal_icon(animal)
597
598        def nest_clicked(place, button):
599            """Handle a nest being clicked."""
600            if place.occupant:
601                # there is an occupant, select or sell it
602                if not sell_callback:
603                    old_animal = self.animal_to_place
604                    self.select_animal_to_place(place.occupant)
605                    # deselect old animal (on button)
606                    update_button(old_animal)
607                    # select new animal (on button)
608                    update_button(self.animal_to_place)
609                else:
610                    # Attempt to sell the occupant
611                    sell_callback(place.occupant, update_button)
612            else:
613                # there is no occupant, attempt to fill the space
614                if self.animal_to_place is not None:
615                    # empty old nest (on button)
616                    update_button(self.animal_to_place, empty=True)
617                    self.relocate_animal(self.animal_to_place, place=place)
618                    # populate the new nest (on button)
619                    update_button(self.animal_to_place)
620
621        tbl = gui.Table()
622        columns = building.max_floor_width()
623        kwargs = { 'style': { 'padding_left': 10, 'padding_bottom': 10 }}
624        for floor in building.floors():
625            tbl.tr()
626            tbl.td(gui.Button(floor.title), colspan=columns, align=-1, **kwargs)
627            tbl.tr()
628            for row in floor.rows():
629                tbl.tr()
630                for place in row:
631                    if place.occupant is None:
632                        button = gui.Button(icons.EMPTY_NEST_ICON)
633                    else:
634                        button = gui.Button(icons.animal_icon(place.occupant))
635                    place_button_map[id(place)] = button
636                    button.connect(gui.CLICK, nest_clicked, place, button)
637                    tbl.td(button, **kwargs)
638
639        building.selected(True)
640        def close_callback():
641            building.selected(False)
642
643        def evict_callback():
644            if not self.animal_to_place:
645                return
646            for tile_pos in building.adjacent_tiles():
647                if self.tv.get(tile_pos) != self.GRASSLAND:
648                    continue
649                if self.get_outside_chicken(tile_pos) is None:
650                    update_button(self.animal_to_place, empty=True)
651                    self.place_animal(tile_pos)
652                    break
653
654        if not sell_callback:
655            tbl.tr()
656            button = gui.Button('Evict')
657            button.connect(gui.CLICK, evict_callback)
658            tbl.td(button, colspan=2, **kwargs)
659
660        self.open_dialog(tbl, close_callback=close_callback)
661
662    def buy_fence(self, tile_pos):
663        this_tile = self.tv.get(tile_pos)
664        if this_tile not in [self.GRASSLAND, self.BROKEN_FENCE]:
665            return
666        if this_tile == self.GRASSLAND:
667            cost = constants.BUY_PRICE_FENCE
668        else:
669            cost = constants.REPAIR_PRICE_FENCE
670        if any((chicken.pos.x, chicken.pos.y) == tile_pos for chicken in self.chickens):
671            return
672
673        if self.cash < cost:
674            print "You can't afford a fence."
675            return
676        self.add_cash(-cost)
677        self.tv.set(tile_pos, self.FENCE)
678
679    def sell_fence(self, tile_pos):
680        this_tile = self.tv.get(tile_pos)
681        if this_tile not in [self.FENCE, self.BROKEN_FENCE]:
682            return
683        if this_tile == self.FENCE:
684            self.add_cash(constants.SELL_PRICE_FENCE)
685        elif this_tile == self.BROKEN_FENCE:
686            self.add_cash(constants.SELL_PRICE_BROKEN_FENCE)
687        self.tv.set(tile_pos, self.GRASSLAND)
688
689    def logging_forest(self, tile_pos):
690        if self.tv.get(tile_pos) != self.WOODLAND:
691            return
692        if self.cash < constants.LOGGING_PRICE:
693            return
694        self.add_cash(-constants.LOGGING_PRICE)
695        self.tv.set(tile_pos, self.GRASSLAND)
696
697    def buy_building(self, tile_pos, building_cls):
698        building = building_cls(tile_pos)
699        if self.cash < building.buy_price():
700            return
701        if any(building.covers((chicken.pos.x, chicken.pos.y)) for chicken in self.chickens):
702            return
703        if building.place(self.tv):
704            self.add_cash(-building.buy_price())
705            self.add_building(building)
706
707    def buy_equipment(self, tile_pos, equipment_cls):
708
709        equipment = equipment_cls()
710
711        def do_equip(chicken, update_button=None):
712            # Try to equip the chicken
713            if equipment.place(chicken):
714                self.add_cash(-equipment.buy_price())
715                chicken.equip(equipment)
716                if update_button:
717                    update_button(chicken)
718            return False
719
720        chicken = self.get_outside_chicken(tile_pos)
721        if self.cash < equipment.buy_price():
722            return
723        if chicken is None:
724            building = self.get_building(tile_pos)
725            if building is None:
726                return
727            # Bounce through open dialog once more
728            self.open_building_dialog(building, do_equip)
729        else:
730            do_equip(chicken)
731
732    def sell_building(self, tile_pos):
733        if self.tv.get(tile_pos) in [self.FENCE, self.BROKEN_FENCE]:
734            return self.sell_fence(tile_pos)
735        building = self.get_building(tile_pos)
736        if building is None:
737            return
738        if list(building.occupants()):
739            warning = gui.Button("Occupied buildings may not be sold.")
740            self.open_dialog(warning)
741            return
742        self.add_cash(building.sell_price())
743        building.remove(self.tv)
744        self.remove_building(building)
745
746    def sell_equipment(self, tile_pos):
747        x, y = 0, 0
748        def do_sell(chicken, update_button=None):
749            if not chicken.equipment:
750                return
751            elif len(chicken.equipment) == 1:
752                item = chicken.equipment[0]
753                self.add_cash(item.sell_price())
754                chicken.unequip(item)
755                if update_button:
756                    update_button(chicken)
757            else:
758                self.open_equipment_dialog(chicken, x, y, update_button)
759            return False
760
761        chicken = self.get_outside_chicken(tile_pos)
762        if chicken is not None:
763            do_sell(chicken)
764        else:
765            building = self.get_building(tile_pos)
766            if building is None:
767                return
768            x, y = 50, 0
769            self.open_building_dialog(building, do_sell)
770
771    def open_equipment_dialog(self, chicken, x, y, update_button=None):
772        tbl = gui.Table()
773
774        def sell_item(item, button):
775            """Select item of equipment."""
776            self.add_cash(item.sell_price())
777            chicken.unequip(item)
778            if update_button:
779                update_button(chicken)
780            self.disp.close(dialog)
781
782        kwargs = { 'style': { 'padding_left': 10, 'padding_bottom': 10 }}
783
784        tbl.tr()
785        tbl.td(gui.Button("Sell ...     "), align=-1, **kwargs)
786
787        for item in chicken.equipment:
788            tbl.tr()
789            button = gui.Button(item.name().title())
790            button.connect(gui.CLICK, sell_item, item, button)
791            tbl.td(button, align=1, **kwargs)
792
793        dialog = self.open_dialog(tbl, x=x, y=y)
794
795    def event(self, e):
796        if e.type == KEYDOWN and e.key in [K_UP, K_DOWN, K_LEFT, K_RIGHT]:
797            if e.key == K_UP:
798                self.tvw.move_view(0, -self.TILE_DIMENSIONS[1])
799            if e.key == K_DOWN:
800                self.tvw.move_view(0, self.TILE_DIMENSIONS[1])
801            if e.key == K_LEFT:
802                self.tvw.move_view(-self.TILE_DIMENSIONS[0], 0)
803            if e.key == K_RIGHT:
804                self.tvw.move_view(self.TILE_DIMENSIONS[0], 0)
805            return True
806        return False
807
808    def advance_day(self):
809        self.days += 1
810        self.toolbar.update_day_counter(self.days)
811
812    def clear_foxes(self):
813        for fox in self.foxes.copy():
814            # Any foxes that didn't make it to the woods are automatically
815            # killed
816            if self.in_bounds(fox.pos) and self.tv.get(fox.pos.to_tuple()) \
817                    != self.WOODLAND:
818                self.kill_fox(fox)
819            else:
820                self.tv.sprites.remove(fox)
821        self.foxes = set() # Remove all the foxes
822
823    def run_animations(self):
824        for anim in self.animations:
825            anim.animate()
826        if self.toolbar.anim_clear_tool:
827            self.toolbar.clear_tool()
828
829    def move_foxes(self):
830        """Move the foxes.
831       
832           We return True if there are no more foxes to move or all the
833           foxes are safely back. This end's the night"""
834        if not self.foxes:
835            return True
836        over = True
837        for fox in self.foxes:
838            fox.move(self)
839            if not fox.safe:
840                over = False
841        for chicken in self.chickens:
842            chicken.attack(self)
843        return over
844
845    def add_chicken(self, chicken):
846        self.chickens.add(chicken)
847        if chicken.outside():
848            self.tv.sprites.append(chicken)
849        self.toolbar.update_chicken_counter(len(self.chickens))
850
851    def add_fox(self, fox):
852        self.foxes.add(fox)
853        self.tv.sprites.append(fox)
854
855    def add_building(self, building):
856        self.buildings.append(building)
857        self.tv.sprites.append(building)
858
859    def lay_eggs(self):
860        self.eggs = 0
861        for building in self.buildings:
862            if building.NAME in buildings.HENHOUSES:
863                for chicken in building.occupants():
864                    chicken.lay()
865                    self.eggs += chicken.get_num_eggs()
866        self.toolbar.update_egg_counter(self.eggs)
867
868    def hatch_eggs(self):
869        for building in self.buildings:
870            if building.NAME in buildings.HENHOUSES:
871                for chicken in building.occupants():
872                    new_chick = chicken.hatch(self)
873                    if new_chick:
874                        try:
875                            building.add_occupant(new_chick)
876                            self.add_chicken(new_chick)
877                            new_chick.equip(equipment.Nest())
878                        except buildings.BuildingFullError:
879                            # No space in the hen house, look nearby
880                            for tile_pos in building.adjacent_tiles():
881                                if self.tv.get(tile_pos) != self.GRASSLAND:
882                                    continue
883                                if self.get_outside_chicken(tile_pos) is None:
884                                    self.add_chicken(new_chick)
885                                    self.relocate_animal(new_chick, tile_pos=tile_pos)
886                                    break
887                            # if there isn't a space for the
888                            # new chick it dies. :/ Farm life
889                            # is cruel.
890        self.toolbar.update_egg_counter(self.eggs)
891
892    def kill_fox(self, fox):
893        self.killed_foxes += 1
894        self.toolbar.update_fox_counter(self.killed_foxes)
895        self.add_cash(constants.SELL_PRICE_DEAD_FOX)
896        self.remove_fox(fox)
897
898    def remove_fox(self, fox):
899        self.foxes.discard(fox)
900        if fox in self.tv.sprites:
901            self.tv.sprites.remove(fox)
902
903    def remove_chicken(self, chick):
904        if chick is self.animal_to_place:
905            self.select_animal_to_place(None)
906        self.chickens.discard(chick)
907        self.eggs -= chick.get_num_eggs()
908        self.toolbar.update_egg_counter(self.eggs)
909        if chick.abode:
910            chick.abode.clear_occupant()
911        self.toolbar.update_chicken_counter(len(self.chickens))
912        if chick in self.tv.sprites and chick.outside():
913            self.tv.sprites.remove(chick)
914
915    def remove_building(self, building):
916        if building in self.buildings:
917            self.buildings.remove(building)
918            self.tv.sprites.remove(building)
919
920    def add_cash(self, amount):
921        self.cash += amount
922        self.toolbar.update_cash_counter(self.cash)
923
924    def add_some_chickens(self):
925        """Add some random chickens to start the game"""
926        x, y = 0, 0
927        width, height = self.tv.size
928        tries = 0
929        while len(self.chickens) < 10:
930            if x < width:
931                tile = self.tv.get((x, y))
932            else:
933                y += 1
934                if y >= height:
935                    y = 0
936                    tries += 1
937                    if tries > 3:
938                        break # Things have gone wierd
939                x = 0
940                continue
941            # See if we place a chicken
942            if 'grassland' == tiles.TILE_MAP[tile]:
943                # Farmland
944                roll = random.randint(1, 20)
945                # We don't place within a tile of the fence, this is to make things
946                # easier
947                for xx in range(x-1, x+2):
948                    if xx >= width or xx < 0:
949                        continue
950                    for yy in range(y-1, y+2):
951                        if yy >= height or yy < 0:
952                            continue
953                        neighbour = self.tv.get((xx, yy))
954                        if 'fence' == tiles.TILE_MAP[neighbour]:
955                            # Fence
956                            roll = 10
957                if roll == 1:
958                    # Create a chicken
959                    chick = animal.Chicken((x, y))
960                    self.add_chicken(chick)
961            x += 1
962
963    def _choose_fox(self, (x, y)):
964        fox_cls = misc.WeightedSelection(self.FOX_WEIGHTINGS).choose()
965        return fox_cls((x, y))
966
967    def spawn_foxes(self):
968        """The foxes come at night, and this is where they come from."""
969        # Foxes spawn just outside the map
970        x, y = 0, 0
971        width, height = self.tv.size
972        min_foxes = (self.days+3)/2 # always more than one fox
973        new_foxes = random.randint(min_foxes, min_foxes*2)
974        while len(self.foxes) < new_foxes:
975            side = random.randint(0, 3)
976            if side == 0:
977                # top
978                y = -1
979                x = random.randint(-1, width)
980            elif side == 1:
981                # bottom
982                y = height
983                x = random.randint(-1, width)
984            elif side == 2:
985                # left
986                x = -1
987                y = random.randint(-1, height)
988            else:
989                x = width
990                y = random.randint(-1, height)
991            skip = False
992            for other_fox in self.foxes:
993                if other_fox.pos.x == x and other_fox.pos.y == y:
994                    skip = True # Choose a new position
995                    break
996            if not skip:
997                self.add_fox(self._choose_fox((x, y)))
998
999    def fix_buildings(self):
1000        """Go through the level map looking for buildings that haven't
1001           been added to self.buildings and adding them.
1002
1003           Where partial buildings exist (i.e. places where the building
1004           cannot fit on the available tiles) the building is added anyway
1005           to the top left corner.
1006
1007           Could be a lot faster.
1008           """
1009        tile_to_building = dict((b.TILE_NO, b) for b in buildings.BUILDINGS)
1010
1011        w, h = self.tv.size
1012        for x in xrange(w):
1013            for y in xrange(h):
1014                tile_pos = (x, y)
1015                tile_no = self.tv.get(tile_pos)
1016                if tile_no not in tile_to_building:
1017                    continue
1018
1019                covered = False
1020                for building in self.buildings:
1021                    if building.covers(tile_pos):
1022                        covered = True
1023                        break
1024
1025                if covered:
1026                    continue
1027
1028                building_cls = tile_to_building[tile_no]
1029                building = building_cls(tile_pos)
1030                building.remove(self.tv)
1031                building.place(self.tv)
1032                self.add_building(building)
1033
1034    def trees_left(self):
1035        width, height = self.tv.size
1036        return len([(x,y) for x in range(width) for y in range(height) if self.tv.get((x,y)) == self.WOODLAND])
1037
1038    def is_game_over(self):
1039        """Return true if we're complete"""
1040        if self.trees_left() == 0:
1041            return True
1042        if constants.TURN_LIMIT > 0 and self.days >= constants.TURN_LIMIT:
1043            return True
1044        if len(self.chickens) == 0:
1045            return True
Note: See TracBrowser for help on using the repository browser.