source: gamelib/gameboard.py @ 320:9bf0e701a36e

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

Switch between 'Finished Day' and 'Fast Forward' modes

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