Mercurial > rinkhals
view gamelib/gameboard.py @ 434:f2a55e5e24db
Disable non-selectable toolbar widgets at night
author | Neil Muller <drnlmuller@gmail.com> |
---|---|
date | Sat, 21 Nov 2009 17:33:49 +0000 |
parents | 8643893635e7 |
children | feb9b7a23ef2 |
line wrap: on
line source
import random import pygame from pygame.locals import MOUSEBUTTONDOWN, MOUSEMOTION, KEYDOWN, K_UP, K_DOWN, \ K_LEFT, K_RIGHT from pgu import gui import tiles import icons import constants import buildings import animal import equipment import sound import cursors import sprite_cursor import misc import engine import toolbar class VidWidget(gui.Widget): def __init__(self, gameboard, vid, **params): gui.Widget.__init__(self, **params) self.gameboard = gameboard self.vid = vid self.vid.bounds = pygame.Rect((0, 0), vid.tile_to_view(vid.size)) def paint(self, surface): self.vid.paint(surface) def update(self, surface): return self.vid.update(surface) def move_view(self, x, y): self.vid.view.move_ip((x, y)) def event(self, e): if e.type == MOUSEBUTTONDOWN: self.gameboard.use_tool(e) elif e.type == MOUSEMOTION and self.gameboard.sprite_cursor: self.gameboard.update_sprite_cursor(e) class GameBoard(object): GRASSLAND = tiles.REVERSE_TILE_MAP['grassland'] FENCE = tiles.REVERSE_TILE_MAP['fence'] WOODLAND = tiles.REVERSE_TILE_MAP['woodland'] BROKEN_FENCE = tiles.REVERSE_TILE_MAP['broken fence'] def __init__(self, main_app, level): self.disp = main_app self.level = level self.tv = tiles.FarmVid() self.tv.png_folder_load_tiles('tiles') self.tv.tga_load_level(level.map) width, height = self.tv.size # Ensure we don't every try to create more foxes then is sane self.max_foxes = level.max_foxes self.selected_tool = None self.animal_to_place = None self.sprite_cursor = None self.chickens = set() self.foxes = set() self.buildings = [] self._pos_cache = { 'fox' : [], 'chicken' : []} self.cash = 0 self.wood = 0 self.eggs = 0 self.days = 0 self.killed_foxes = 0 self.day, self.night = True, False # For the level loading case if self.disp: self.create_display() self.add_cash(level.starting_cash) self.add_wood(level.starting_wood) self.fix_buildings() cdata = {} for tn in equipment.EQUIP_MAP: cdata[tn] = (self.add_start_chickens, tn) self.tv.run_codes(cdata, (0,0,width,height)) def get_top_widget(self): return self.top_widget def create_display(self): width, height = self.disp.rect.w, self.disp.rect.h tbl = gui.Table() tbl.tr() self.toolbar = toolbar.ToolBar(self, width=constants.TOOLBAR_WIDTH) tbl.td(self.toolbar, valign=-1) self.tvw = VidWidget(self, self.tv, width=width-constants.TOOLBAR_WIDTH, height=height) tbl.td(self.tvw) self.top_widget = tbl def update(self): self.tvw.reupdate() def loop(self): self.tv.loop() def set_selected_tool(self, tool, cursor): if not self.day: return self.selected_tool = tool if self.animal_to_place: # Clear any highlights self.animal_to_place.unequip_by_name("Spotlight") self.select_animal_to_place(None) sprite_curs = None if buildings.is_building(tool): sprite_curs = sprite_cursor.SpriteCursor(tool.IMAGE, self.tv, tool.BUY_PRICE) elif equipment.is_equipment(tool): sprite_curs = sprite_cursor.SpriteCursor(tool.CHICKEN_IMAGE_FILE, self.tv) self.set_cursor(cursor, sprite_curs) def set_cursor(self, cursor=None, sprite_curs=None): if cursor: pygame.mouse.set_cursor(*cursor) else: pygame.mouse.set_cursor(*cursors.cursors['arrow']) if self.sprite_cursor is not None: self.tv.sprites.remove(self.sprite_cursor, layer='cursor') self.sprite_cursor = sprite_curs if self.sprite_cursor is not None: self.tv.sprites.append(self.sprite_cursor, layer='cursor') def reset_states(self): """Clear current states (highlights, etc.)""" self.set_selected_tool(None, None) self.toolbar.clear_tool() def update_sprite_cursor(self, e): tile_pos = self.tv.screen_to_tile(e.pos) self.sprite_cursor.set_pos(tile_pos) def start_night(self): self.day, self.night = False, True self.tv.sun(False) self.reset_states() self.toolbar.start_night() self.spawn_foxes() self.eggs = 0 for chicken in self.chickens.copy(): chicken.start_night(self) self.toolbar.update_egg_counter(self.eggs) self._cache_animal_positions() def start_day(self): self.day, self.night = True, False self.tv.sun(True) self.reset_states() self.toolbar.start_day() self._pos_cache = { 'fox' : [], 'chicken' : []} self.advance_day() self.clear_foxes() for chicken in self.chickens.copy(): chicken.start_day(self) self.toolbar.update_egg_counter(self.eggs) def in_bounds(self, pos): """Check if a position is within the game boundaries""" if pos.x < 0 or pos.y < 0: return False width, height = self.tv.size if pos.x >= width or pos.y >= height: return False return True def use_tool(self, e): if not self.day: return if e.button == 3: # Right button self.set_selected_tool(None, None) self.toolbar.clear_tool() elif e.button != 1: # Left button return if self.selected_tool == constants.TOOL_SELL_CHICKEN: self.sell_chicken(self.tv.screen_to_tile(e.pos)) elif self.selected_tool == constants.TOOL_SELL_EGG: self.sell_egg(self.tv.screen_to_tile(e.pos)) elif self.selected_tool == constants.TOOL_PLACE_ANIMALS: self.place_animal(self.tv.screen_to_tile(e.pos)) elif self.selected_tool == constants.TOOL_SELL_BUILDING: self.sell_building(self.tv.screen_to_tile(e.pos)) elif self.selected_tool == constants.TOOL_SELL_EQUIPMENT: self.sell_equipment(self.tv.screen_to_tile(e.pos)) elif self.selected_tool == constants.TOOL_REPAIR_BUILDING: self.repair_building(self.tv.screen_to_tile(e.pos)) elif buildings.is_building(self.selected_tool): self.buy_building(self.tv.screen_to_tile(e.pos), self.selected_tool) elif equipment.is_equipment(self.selected_tool): self.buy_equipment(self.tv.screen_to_tile(e.pos), self.selected_tool) def get_outside_chicken(self, tile_pos): for chick in self.chickens: if chick.covers(tile_pos) and chick.outside(): return chick return None def get_building(self, tile_pos): for building in self.buildings: if building.covers(tile_pos): return building return None def sell_chicken(self, tile_pos): def do_sell(chicken, update_button=None): if not chicken: return False # sanity check if len(self.chickens) == 1: msg = "You can't sell your last chicken!" TextDialog("Squuaaawwwwwk!", msg).open() return False for item in list(chicken.equipment): self.add_cash(item.sell_price()) chicken.unequip(item) self.add_cash(self.level.sell_price_chicken) sound.play_sound("sell-chicken.ogg") if update_button: update_button(chicken, empty=True) self.remove_chicken(chicken) return True chick = self.get_outside_chicken(tile_pos) if chick is None: building = self.get_building(tile_pos) if building and building.HENHOUSE: self.open_building_dialog(building, do_sell) return do_sell(chick) def sell_one_egg(self, chicken): if chicken.eggs: self.add_cash(self.level.sell_price_egg) chicken.remove_one_egg() self.eggs -= 1 self.toolbar.update_egg_counter(self.eggs) return True return False def sell_egg(self, tile_pos): def do_sell(chicken, update_button=None): # We try sell and egg if self.sell_one_egg(chicken): sound.play_sound("sell-chicken.ogg") # Force toolbar update self.toolbar.chsize() if update_button: update_button(chicken) return False building = self.get_building(tile_pos) if building and building.HENHOUSE: self.open_building_dialog(building, do_sell) def select_animal_to_place(self, animal): if self.animal_to_place: self.animal_to_place.unequip_by_name("Spotlight") self.animal_to_place = animal if self.animal_to_place: self.animal_to_place.equip(equipment.Spotlight()) def place_animal(self, tile_pos): """Handle an TOOL_PLACE_ANIMALS click. This will either select an animal or place a selected animal in a building. """ chicken = self.get_outside_chicken(tile_pos) if chicken: if chicken is self.animal_to_place: self.select_animal_to_place(None) pygame.mouse.set_cursor(*cursors.cursors['select']) else: self.select_animal_to_place(chicken) pygame.mouse.set_cursor(*cursors.cursors['chicken']) return building = self.get_building(tile_pos) if building and building.ABODE: if self.animal_to_place: try: place = building.first_empty_place() self.relocate_animal(self.animal_to_place, place=place) self.animal_to_place.equip(equipment.Nest()) self.select_animal_to_place(None) pygame.mouse.set_cursor(*cursors.cursors['select']) except buildings.BuildingFullError: pass else: self.open_building_dialog(building) return if self.tv.get(tile_pos) == self.GRASSLAND: if self.animal_to_place is not None: self.animal_to_place.unequip_by_name("Nest") self.relocate_animal(self.animal_to_place, tile_pos=tile_pos) self.eggs -= self.animal_to_place.get_num_eggs() self.animal_to_place.remove_eggs() self.toolbar.update_egg_counter(self.eggs) def relocate_animal(self, chicken, tile_pos=None, place=None): assert((tile_pos, place) != (None, None)) if chicken.abode is not None: chicken.abode.clear_occupant() if tile_pos: chicken.set_pos(tile_pos) else: place.set_occupant(chicken) chicken.set_pos(place.get_pos()) self.set_visibility(chicken) def set_visibility(self, animal): if animal.outside(): if animal not in self.tv.sprites: self.tv.sprites.append(animal) else: if animal in self.tv.sprites: self.tv.sprites.remove(animal) def open_dialog(self, widget, x=None, y=None, close_callback=None): """Open a dialog for the given widget. Add close button.""" tbl = gui.Table() def close_dialog(): self.disp.close(tbl) if close_callback is not None: close_callback() close_button = gui.Button("Close") close_button.connect(gui.CLICK, close_dialog) tbl = gui.Table() tbl.tr() tbl.td(widget, colspan=2) tbl.tr() tbl.td(gui.Spacer(100, 0)) tbl.td(close_button, align=1) if x: offset = (self.disp.rect.center[0] + x, self.disp.rect.center[1] + y) else: offset = None self.disp.open(tbl, pos=offset) return tbl def open_building_dialog(self, building, sell_callback=None): """Create dialog for manipulating the contents of a building.""" place_button_map = {} def update_button(animal, empty=False): """Update a button image (either to the animal, or to empty).""" if animal: button = place_button_map.get(id(animal.abode)) if button: if empty: button.value = icons.EMPTY_NEST_ICON else: button.value = icons.animal_icon(animal) def nest_clicked(place, button): """Handle a nest being clicked.""" if place.occupant: # there is an occupant, select or sell it if not sell_callback: old_animal = self.animal_to_place self.select_animal_to_place(place.occupant) # deselect old animal (on button) update_button(old_animal) # select new animal (on button) update_button(self.animal_to_place) else: # Attempt to sell the occupant sell_callback(place.occupant, update_button) else: # there is no occupant, attempt to fill the space if self.animal_to_place is not None: # empty old nest (on button) update_button(self.animal_to_place, empty=True) self.relocate_animal(self.animal_to_place, place=place) # populate the new nest (on button) update_button(self.animal_to_place) tbl = gui.Table() columns = building.max_floor_width() kwargs = { 'style': { 'padding_left': 10, 'padding_bottom': 10 }} for floor in building.floors(): tbl.tr() tbl.td(gui.Button(floor.title), colspan=columns, align=-1, **kwargs) tbl.tr() for row in floor.rows(): tbl.tr() for place in row: if place.occupant is None: button = gui.Button(icons.EMPTY_NEST_ICON) else: button = gui.Button(icons.animal_icon(place.occupant)) place_button_map[id(place)] = button button.connect(gui.CLICK, nest_clicked, place, button) tbl.td(button, **kwargs) building.selected(True) def close_callback(): building.selected(False) def evict_callback(): if not self.animal_to_place: return for tile_pos in building.adjacent_tiles(): if self.tv.get(tile_pos) != self.GRASSLAND: continue if self.get_outside_chicken(tile_pos) is None: update_button(self.animal_to_place, empty=True) self.place_animal(tile_pos) break if not sell_callback: tbl.tr() button = gui.Button('Evict') button.connect(gui.CLICK, evict_callback) tbl.td(button, colspan=2, **kwargs) self.open_dialog(tbl, close_callback=close_callback) def buy_building(self, tile_pos, building_cls): building = building_cls(tile_pos) if self.wood < building.buy_price(): return if any(building.covers((chicken.pos.x, chicken.pos.y)) for chicken in self.chickens): return if building.place(self.tv): self.add_wood(-building.buy_price()) self.add_building(building) def buy_equipment(self, tile_pos, equipment_cls): equipment = equipment_cls() def do_equip(chicken, update_button=None): # Try to equip the chicken if self.cash < equipment.buy_price(): return False if equipment.place(chicken): self.add_cash(-equipment.buy_price()) chicken.equip(equipment) if update_button: update_button(chicken) return False chicken = self.get_outside_chicken(tile_pos) if chicken is None: building = self.get_building(tile_pos) if not (building and building.ABODE): return # Bounce through open dialog once more self.open_building_dialog(building, do_equip) else: do_equip(chicken) def sell_building(self, tile_pos): building = self.get_building(tile_pos) if building is None: return if list(building.occupants()): warning = gui.Button("Occupied buildings may not be sold.") self.open_dialog(warning) return self.add_wood(building.sell_price()) building.remove(self.tv) self.remove_building(building) def repair_building(self, tile_pos): building = self.get_building(tile_pos) if not (building and building.broken()): return if self.wood < building.repair_price(): return self.add_wood(-building.repair_price()) building.repair(self.tv) def sell_equipment(self, tile_pos): x, y = 0, 0 def do_sell(chicken, update_button=None): if not chicken.equipment: return elif len(chicken.equipment) == 1: item = chicken.equipment[0] self.add_cash(item.sell_price()) chicken.unequip(item) if update_button: update_button(chicken) else: self.open_equipment_dialog(chicken, x, y, update_button) return False chicken = self.get_outside_chicken(tile_pos) if chicken is not None: do_sell(chicken) else: building = self.get_building(tile_pos) if building is None: return x, y = 50, 0 self.open_building_dialog(building, do_sell) def open_equipment_dialog(self, chicken, x, y, update_button=None): tbl = gui.Table() def sell_item(item, button): """Select item of equipment.""" self.add_cash(item.sell_price()) chicken.unequip(item) if update_button: update_button(chicken) self.disp.close(dialog) kwargs = { 'style': { 'padding_left': 10, 'padding_bottom': 10 }} tbl.tr() tbl.td(gui.Button("Sell ... "), align=-1, **kwargs) for item in chicken.equipment: tbl.tr() button = gui.Button(item.name().title()) button.connect(gui.CLICK, sell_item, item, button) tbl.td(button, align=1, **kwargs) dialog = self.open_dialog(tbl, x=x, y=y) def event(self, e): if e.type == KEYDOWN and e.key in [K_UP, K_DOWN, K_LEFT, K_RIGHT]: if e.key == K_UP: self.tvw.move_view(0, -constants.TILE_DIMENSIONS[1]) if e.key == K_DOWN: self.tvw.move_view(0, constants.TILE_DIMENSIONS[1]) if e.key == K_LEFT: self.tvw.move_view(-constants.TILE_DIMENSIONS[0], 0) if e.key == K_RIGHT: self.tvw.move_view(constants.TILE_DIMENSIONS[0], 0) return True return False def advance_day(self): self.days += 1 if self.level.is_last_day(self.days): self.toolbar.day_counter.style.color = (255, 0, 0) self.toolbar.update_day_counter("%s/%s" % (self.days, self.level.get_max_turns())) def clear_foxes(self): for fox in self.foxes.copy(): # Any foxes that didn't make it to the woods are automatically # killed if self.in_bounds(fox.pos) and \ self.tv.get(fox.pos.to_tile_tuple()) != self.WOODLAND: self.kill_fox(fox) else: self.remove_fox(fox) self.foxes = set() # Remove all the foxes def clear_chickens(self): for chicken in self.chickens.copy(): self.remove_chicken(chicken) def do_night_step(self): """Handle the events of the night. We return True if there are no more foxes to move or all the foxes are safely back. This end's the night""" if not self.foxes: return True # Move all the foxes over = self.foxes_move() if not over: self.foxes_attack() self.chickens_attack() return over def _cache_animal_positions(self): """Cache the current set of fox positions for the avoiding checks""" w, h = self.tv.size self._pos_cache['fox'] = [[[None for z in range(5)] for y in range(h)] for x in range(w)] # NB: Assumes z in [0, 4] self._pos_cache['chicken'] = [[[None for z in range(5)] for y in range(h)] for x in range(w)] for fox in self.foxes: self._add_to_pos_cache(fox, 'fox') for chick in self.chickens: self._add_to_pos_cache(chick, 'chicken') def _add_to_pos_cache(self, animal, cache_type): if self.in_bounds(animal.pos): self._pos_cache[cache_type][animal.pos.x][animal.pos.y][animal.pos.z] = animal def _update_pos_cache(self, old_pos, animal, cache_type): if self.in_bounds(old_pos) and self._pos_cache[cache_type]: self._pos_cache[cache_type][old_pos.x][old_pos.y][old_pos.z] = None if animal: pos = animal.pos if self.in_bounds(pos): self._pos_cache[cache_type][pos.x][pos.y][pos.z] = animal def get_animal_at_pos(self, pos, cache_type): if not self._pos_cache[cache_type]: return None # We don't maintain the cache during the day if self.in_bounds(pos): return self._pos_cache[cache_type][pos.x][pos.y][pos.z] return None def chickens_scatter(self): """Chickens outside move around randomly a bit""" for chicken in [chick for chick in self.chickens if chick.outside()]: old_pos = chicken.pos chicken.move(self) if chicken.pos != old_pos: self._update_pos_cache(old_pos, chicken, 'chicken') def chickens_chop_wood(self): """Chickens with axes chop down trees near them""" for chicken in [chick for chick in self.chickens if chick.outside()]: chicken.chop(self) def foxes_move(self): over = True for fox in self.foxes: old_pos = fox.pos fox.move(self) if not fox.safe: over = False if fox.pos != old_pos: self._update_pos_cache(old_pos, fox, 'fox') return over def foxes_attack(self): for fox in self.foxes: fox.attack(self) def chickens_attack(self): for chicken in self.chickens: chicken.attack(self) def add_chicken(self, chicken): self.chickens.add(chicken) if chicken.outside(): self.tv.sprites.append(chicken) if self.disp: self.toolbar.update_chicken_counter(len(self.chickens)) def add_fox(self, fox): self.foxes.add(fox) self.tv.sprites.append(fox) def add_building(self, building): self.buildings.append(building) self.tv.sprites.append(building, layer='buildings') def place_hatched_chicken(self, new_chick, building): try: building.add_occupant(new_chick) self.add_chicken(new_chick) new_chick.equip(equipment.Nest()) except buildings.BuildingFullError: # No space in the hen house, look nearby for tile_pos in building.adjacent_tiles(): if self.tv.get(tile_pos) != self.GRASSLAND: continue if self.get_outside_chicken(tile_pos) is None: self.add_chicken(new_chick) self.relocate_animal(new_chick, tile_pos=tile_pos) break # if there isn't a space for the # new chick it dies. :/ Farm life # is cruel. def kill_fox(self, fox): self.killed_foxes += 1 self.toolbar.update_fox_counter(self.killed_foxes) self.add_cash(self.level.sell_price_dead_fox) self._update_pos_cache(fox.pos, None, 'fox') self.remove_fox(fox) def remove_fox(self, fox): self.foxes.discard(fox) if fox.building: fox.building.remove_predator(fox) if fox in self.tv.sprites: self.tv.sprites.remove(fox) def remove_chicken(self, chick): if chick is self.animal_to_place: self.select_animal_to_place(None) self.chickens.discard(chick) self.eggs -= chick.get_num_eggs() self.toolbar.update_egg_counter(self.eggs) if chick.abode: chick.abode.clear_occupant() self.toolbar.update_chicken_counter(len(self.chickens)) if chick in self.tv.sprites and chick.outside(): self.tv.sprites.remove(chick) self._update_pos_cache(chick.pos, None, 'chicken') def remove_building(self, building): if building in self.buildings: self.buildings.remove(building) self.tv.sprites.remove(building, layer='buildings') def add_cash(self, amount): self.cash += amount self.toolbar.update_cash_counter(self.cash) def add_wood(self, planks): self.wood += planks self.toolbar.update_wood_counter(self.wood) def add_start_chickens(self, _map, tile, value): """Add chickens as specified by the code layer""" chick = animal.Chicken((tile.tx, tile.ty)) for equip_cls in equipment.EQUIP_MAP[value]: item = equip_cls() chick.equip(item) self.add_chicken(chick) def _choose_fox(self, (x, y)): fox_cls = misc.WeightedSelection(self.level.fox_weightings).choose() return fox_cls((x, y)) def spawn_foxes(self): """The foxes come at night, and this is where they come from.""" # Foxes spawn just outside the map x, y = 0, 0 width, height = self.tv.size min_foxes = max(self.level.min_foxes, (self.days+3)/2) # always more than one fox new_foxes = min(random.randint(min_foxes, min_foxes*2), self.max_foxes) while len(self.foxes) < new_foxes: side = random.randint(0, 3) if side == 0: # top y = -1 x = random.randint(-1, width) elif side == 1: # bottom y = height x = random.randint(-1, width) elif side == 2: # left x = -1 y = random.randint(-1, height) else: x = width y = random.randint(-1, height) self.add_fox(self._choose_fox((x, y))) def fix_buildings(self): """Go through the level map looking for buildings that haven't been added to self.buildings and adding them. Where partial buildings exist (i.e. places where the building cannot fit on the available tiles) the building is added anyway to the top left corner. Could be a lot faster. """ tile_to_building = dict((b.TILE_NO, b) for b in buildings.BUILDINGS) w, h = self.tv.size for x in xrange(w): for y in xrange(h): tile_pos = (x, y) tile_no = self.tv.get(tile_pos) if tile_no not in tile_to_building: continue covered = False for building in self.buildings: if building.covers(tile_pos): covered = True break if covered: continue building_cls = tile_to_building[tile_no] building = building_cls(tile_pos) building.remove(self.tv) building.place(self.tv) self.add_building(building) def trees_left(self): width, height = self.tv.size return len([(x,y) for x in range(width) for y in range(height) if self.tv.get((x,y)) == self.WOODLAND]) class TextDialog(gui.Dialog): def __init__(self, title, text, **params): title_label = gui.Label(title) doc = gui.Document() space = doc.style.font.size(" ") for paragraph in text.split('\n\n'): doc.block(align=-1) for word in paragraph.split(): doc.add(gui.Label(word)) doc.space(space) doc.br(space[1]) doc.br(space[1]) done_button = gui.Button("Close") done_button.connect(gui.CLICK, self.close) tbl = gui.Table() tbl.tr() tbl.td(doc) tbl.tr() tbl.td(done_button, align=1) gui.Dialog.__init__(self, title_label, tbl, **params)