class StateMachine(object): def __init__(self, sprite, tick): self.sprite = sprite self.states = {} self.active_state = None self.last_state = None self.timer = Timer(tick) self.timer.begin() def add_state(self, state): self.states[state.id] = state def run(self): new_state_id = None # ai event tick, interrupt event will trigger condition calculation at once if self.sprite.brain.interrupt or self.timer.exceed(): self.timer.begin() new_state_id = self.active_state.check_conditions() if new_state_id is not None: self.set_state(new_state_id) self.sprite.brain.actions = self.active_state.send_actions() def set_state(self, new_state_id): if self.active_state is not None: self.last_state = self.active_state self.active_state.exit() self.active_state = self.states[new_state_id] self.active_state.enter(self.last_state)
class SpriteDefence(State): def __init__(self, sprite, ai): super(SpriteDefence, self).__init__(cfg.SpriteState.DEFENCE) self.sprite = sprite self.ai = ai self.enter_timer = Timer() #self.action_to_do = cfg.EnemyAction.STAND def enter(self, last_state): self.enter_timer.begin(gauss(self.ai.DEFENCE_TIME_MU, self.ai.DEFENCE_TIME_SIGMA)) sp = self.sprite sp.direction = cal_face_direct(sp.pos.as_tuple(), sp.brain.target.pos.as_tuple()) #if sp.hp_status == cfg.HpStatus.DANGER and happen(self.ai.DEFENCE_BACKWARD_PROB): # self.action_to_do = cfg.EnemyAction.BACKWARD #else: # self.action_to_do = cfg.EnemyAction.STAND def send_actions(self): return (cfg.EnemyAction.STAND, ) #return (self.action_to_do, ) def check_conditions(self): sp = self.sprite if self.enter_timer.exceed(): if happen(self.ai.DEFENCE_TO_OFFENCE_PROB) and sp.attacker.chance(sp.brain.target): return cfg.SpriteState.OFFENCE distance_to_target = sp.pos.get_distance_to(sp.brain.target.pos) if happen(self.ai.DEFENCE_TO_CHASE_PROB) and distance_to_target <= self.ai.CHASE_RANGE : return cfg.SpriteState.CHASE if distance_to_target > self.ai.CHASE_RANGE: sp.brain.target = None return cfg.SpriteState.STAY return cfg.SpriteState.DEFENCE else: sp.direction = cal_face_direct(sp.pos.as_tuple(), sp.brain.target.pos.as_tuple()) def exit(self): self.enter_timer.clear()
class SpriteOffence(State): def __init__(self, sprite, ai): super(SpriteOffence, self).__init__(cfg.SpriteState.OFFENCE) self.sprite = sprite self.ai = ai self.enter_timer = Timer() def enter(self, last_state): sp = self.sprite sp.brain.persistent = True sp.direction = cal_face_direct(sp.pos.as_tuple(), sp.brain.target.pos.as_tuple()) self.enter_timer.begin(gauss(self.ai.OFFENCE_GO_DELAY_TIME_MU, self.ai.OFFENCE_GO_DELAY_TIME_SIGMA)) def send_actions(self): if not self.enter_timer.exceed(): # add delay time for attack return (cfg.EnemyAction.STAND, ) sp = self.sprite return (cfg.EnemyAction.ATTACK, ) if sp.brain.persistent else (cfg.EnemyAction.STAND, ) def check_conditions(self): sp = self.sprite if sp.brain.persistent: return if sp.attacker.chance(sp.brain.target): return cfg.SpriteState.OFFENCE if happen(self.ai.OFFENCE_TO_CHASE_PROB): return cfg.SpriteState.CHASE return cfg.SpriteState.DEFENCE def exit(self): self.enter_timer.clear() self.sprite.brain.persistent = False
class SpritePatrol(State): def __init__(self, sprite, ai): super(SpritePatrol, self).__init__(cfg.SpriteState.PATROL) self.sprite = sprite self.ai = ai self.enter_timer = Timer() self.view_sensor_timer = Timer(ai.VIEW_SENSOR_TICK) def choose_a_backside_direction(self, current_direction): total = cfg.Direction.TOTAL opposite_direction = (current_direction + 4) % total return choice([(opposite_direction - 1) % total, opposite_direction, (opposite_direction + 1) % total]) def enter(self, last_state): self.enter_timer.begin(gauss(self.ai.WALK_TIME_MU, self.ai.WALK_TIME_SIGMA)) self.sprite.direction = self.choose_a_backside_direction(self.sprite.direction) def send_actions(self): if self.view_sensor_timer.is_begin(): if self.view_sensor_timer.exceed(): self.view_sensor_timer.begin() return (cfg.EnemyAction.LOOKOUT, cfg.EnemyAction.WALK) else: return (cfg.EnemyAction.WALK, ) else: self.view_sensor_timer.begin() return (cfg.EnemyAction.WALK, ) def check_conditions(self): sp = self.sprite if sp.brain.target is not None: sp.set_emotion(cfg.SpriteEmotion.ALERT) if sp.attacker.chance(sp.brain.target): #print "patrol to attack" return cfg.SpriteState.OFFENCE if happen(self.ai.PATROL_TO_CHASE_PROB): #print "patrol to chase" return cfg.SpriteState.CHASE return cfg.SpriteState.DEFENCE if sp.brain.interrupt: sp.brain.interrupt = False return cfg.SpriteState.STAY if self.enter_timer.exceed(): return cfg.SpriteState.STAY def exit(self): self.enter_timer.clear()
class SpriteStay(State): def __init__(self, sprite, ai): super(SpriteStay, self).__init__(cfg.SpriteState.STAY) self.sprite = sprite self.ai = ai self.enter_timer = Timer() self.view_sensor_timer = Timer(ai.VIEW_SENSOR_TICK) def enter(self, last_state): self.enter_timer.begin(gauss(self.ai.STAY_TIME_MU, self.ai.STAY_TIME_SIGMA)) # turn for a random direction if the last state is the same "stay" if last_state and last_state.id == cfg.SpriteState.STAY \ and happen(self.ai.STAY_CHANGE_DIRECTION_PROB): self.sprite.direction = choice(cfg.Direction.ALL) # a random direction from "all" if happen(self.ai.EMOTION_SILENT_PROB): self.sprite.set_emotion(cfg.SpriteEmotion.SILENT) def send_actions(self): if self.view_sensor_timer.is_begin(): if self.view_sensor_timer.exceed(): self.view_sensor_timer.begin() return (cfg.EnemyAction.LOOKOUT, cfg.EnemyAction.STAND) else: return (cfg.EnemyAction.STAND, ) else: self.view_sensor_timer.begin() return (cfg.EnemyAction.STAND, ) def check_conditions(self): sp = self.sprite if sp.brain.target is not None: sp.set_emotion(cfg.SpriteEmotion.ALERT) # discover a target if happen(self.ai.STAY_TO_OFFENCE_PROB) and sp.attacker.chance(sp.brain.target): #print "to attack" return cfg.SpriteState.OFFENCE if happen(self.ai.STAY_TO_CHASE_PROB): return cfg.SpriteState.CHASE return cfg.SpriteState.DEFENCE if self.enter_timer.exceed(): if happen(self.ai.STAY_TO_PATROL_PROB): #print "stay to patrol" return cfg.SpriteState.PATROL else: return cfg.SpriteState.STAY def exit(self): self.enter_timer.clear()
class GameDirector(object): def __init__(self, chapter, hero, enemy_list): self.chapter = chapter self.win_cond = sfg.Chapter.WIN_CONDITION.get(self.chapter, sfg.Chapter.WIN_CONDITION_NONE) self.hero = hero self.enemy_list = enemy_list if self.win_cond == sfg.Chapter.WIN_CONDITION_BOSS_DIE: self.boss = None for em in self.enemy_list: if hasattr(em.setting, "IS_BOSS"): self.boss = em break self.status = cfg.GameStatus.INIT self.win_panel = gen_panel(battle_images, sfg.GameStatus.HERO_WIN_PANEL_IMAGE_KEY, sfg.GameStatus.HERO_WIN_PANEL_RECT) self.lose_panel = gen_panel(battle_images, sfg.GameStatus.HERO_LOSE_PANEL_IMAGE_KEY, sfg.GameStatus.HERO_LOSE_PANEL_RECT) self.chapter_score_icon = gen_panel(battle_images, sfg.GameStatus.CHAPTER_SCORE_ICON_IMAGE_KEY, sfg.GameStatus.CHAPTER_SCORE_ICON_RECT, sfg.GameStatus.CHAPTER_SCORE_ICON_SCALE_SIZE) self.bonus_icon = gen_panel(battle_images, sfg.GameStatus.BONUS_ICON_IMAGE_KEY, sfg.GameStatus.BONUS_ICON_RECT) self.chapter_score_line = gen_panel(battle_images, sfg.GameStatus.CHAPTER_SCORE_LINE_IMAGE_KEY, sfg.GameStatus.CHAPTER_SCORE_LINE_RECT, sfg.GameStatus.CHAPTER_SCORE_LINE_SCALE_SIZE) self.numbers1 = gen_numbers(battle_images, sfg.GameStatus.NUMBER_IMAGE_KEY1, sfg.GameStatus.NUMBER_RECT1, sfg.GameStatus.NUMBER_SIZE1) self.begin_timer = Timer() self.hero_status = HeroStatus(hero) self.achievement = Achievement(hero, enemy_list) self.menu = Menu(sfg.Menu.PAUSE) self.mask = sfg.Screen.DEFAULT_SURFACE bg_box.play("%s%s" % (sfg.Music.CHAPTER_KEY_PREFIX, chapter)) def update(self, passed_seconds): if self.status == cfg.GameStatus.IN_PROGRESS: if self.hero.hp_status == cfg.HpStatus.DIE: # hero is dead, game over self.status = cfg.GameStatus.HERO_LOSE return if self.win_cond != sfg.Chapter.WIN_CONDITION_NONE and len(self.enemy_list) == 0: # all enemies are gone, hero win self.status = cfg.GameStatus.HERO_WIN self.achievement.cal_chapter_score() return elif self.status == cfg.GameStatus.HERO_WIN: self.achievement.chapter_score.update(passed_seconds) if self.win_cond == sfg.Chapter.WIN_CONDITION_BOSS_DIE \ and self.boss.hp_status == cfg.HpStatus.DIE: # in an chapter that boss die you win, kill all enemy when boss die for em in self.enemy_list: if em.hp > 0: em.hp = 0 em.hp_status = cfg.HpStatus.DIE for em in self.enemy_list: if em.hp_status == cfg.HpStatus.DIE: can_be_removed = em.animation.dead_tick() if can_be_removed: em.hp_status = cfg.HpStatus.VANISH # kill the sprite from sprite groups containing it, but not chaning its internal status em.kill() # achievement calculation here self.achievement.update(passed_seconds) def draw_count_down_number(self, camera, timer, persist_time): left_time = persist_time - timer.passed_time() number_to_draw = self.numbers1[int(left_time)+1] camera.screen.blit(number_to_draw, sfg.GameStatus.BEGIN_NUMBER_BLIT_POS) def draw(self, camera): # hero status draw here self.hero_status.draw(camera) # achievement draw here self.achievement.draw(camera) if self.status == cfg.GameStatus.INIT: if not self.begin_timer.is_begin(): self.begin_timer.begin(sfg.GameStatus.INIT_PERSIST_TIME) if self.begin_timer.exceed(): # game begin, change the status self.status = cfg.GameStatus.IN_PROGRESS else: # count down for game begin, draw corresponding count-down numbers self.draw_count_down_number(camera, self.begin_timer, sfg.GameStatus.INIT_PERSIST_TIME) elif self.status in cfg.GameStatus.STATUS_WITH_MASK: self.mask.fill(sfg.Stuff.MASK_ALPHA_128) camera.screen.blit(self.mask, (0, 0)) if self.status == cfg.GameStatus.HERO_WIN: camera.screen.blit(self.achievement.kill_icon, sfg.GameStatus.CHAPTER_KILL_ICON_BLIT_POS) camera.screen.blit(self.achievement.n_hit_icon, sfg.GameStatus.CHAPTER_N_HIT_ICON_BLIT_POS) camera.screen.blit(self.achievement.n_kill_icon, sfg.GameStatus.CHAPTER_N_KILL_ICON_BLIT_POS) camera.screen.blit(self.chapter_score_line, sfg.GameStatus.CHAPTER_SCORE_LINE_BLIT_POS) camera.screen.blit(self.chapter_score_icon, sfg.GameStatus.CHAPTER_SCORE_ICON_BLIT_POS) camera.screen.blit(self.win_panel, sfg.GameStatus.HERO_WIN_BLIT_POS) camera.screen.blit(self.bonus_icon, sfg.GameStatus.BONUS_ICON_BLIT_POS1) camera.screen.blit(self.bonus_icon, sfg.GameStatus.BONUS_ICON_BLIT_POS2) screen_draw_y_symmetric(camera, sfg.GameStatus.CHAPTER_NEXT, sfg.GameStatus.CHAPTER_NEXT_BLIT_Y) self.achievement.kill_score.draw(camera, sfg.GameStatus.CHAPTER_KILL_BLIT_POS) self.achievement.n_hit_score.draw(camera, sfg.GameStatus.CHAPTER_N_HIT_BLIT_POS) self.achievement.n_kill_score.draw(camera, sfg.GameStatus.CHAPTER_N_KILL_BLIT_POS) self.achievement.chapter_score.draw(camera) if bg_box.current_playing != sfg.Music.HERO_WIN_KEY: bg_box.play(sfg.Music.HERO_WIN_KEY, 0) elif self.status == cfg.GameStatus.HERO_LOSE: camera.screen.blit(self.lose_panel, sfg.GameStatus.HERO_LOSE_BLIT_POS) screen_draw_y_symmetric(camera, sfg.GameStatus.CHAPTER_AGAIN, sfg.GameStatus.CHAPTER_AGAIN_BLIT_Y) if bg_box.current_playing != sfg.Music.HERO_LOSE_KEY: bg_box.play(sfg.Music.HERO_LOSE_KEY, 0) elif self.status == cfg.GameStatus.PAUSE: self.menu.draw(camera.screen)
class SpriteChase(State): def __init__(self, sprite, ai, waypoints): super(SpriteChase, self).__init__(cfg.SpriteState.CHASE) self.sprite = sprite self.ai = ai self.pathfinder = pathfinding.Astar(sprite, waypoints) self.steerer = Steerer(sprite) self.enter_timer = Timer() self.target_move_threshold = sfg.WayPoint.STEP_WIDTH * 4 def enter(self, last_state): sp = self.sprite if last_state and last_state.id in (cfg.SpriteState.STAY, cfg.SpriteState.PATROL): # discover hero right now, record the time for action delay self.enter_timer.begin(self.ai.CHASE_GO_DELAY_TIME) sp.direction = cal_face_direct(sp.pos.as_tuple(), sp.brain.target.pos.as_tuple()) sp.brain.destination = sp.brain.target.pos.copy() path = self.pathfinder.find(sp.brain.destination.as_tuple(), sfg.WayPoint.STEP_WIDTH * 2) self.steerer.init(path) def send_actions(self): if self.enter_timer.is_begin() and not self.enter_timer.exceed(): # delay chase action for a more real effect return (cfg.EnemyAction.STAND, ) if self.steerer.is_ok: self.steerer.run() if not self.steerer.is_end: return (cfg.EnemyAction.STEER, ) return (cfg.EnemyAction.STAND, ) def check_conditions(self): sp = self.sprite if happen(self.ai.CHASE_TO_OFFENCE_PROB) and sp.attacker.chance(sp.brain.target): #print "to attack" return cfg.SpriteState.OFFENCE if (not self.steerer.is_ok) or happen(self.ai.CHASE_TO_DEFENCE_PROB): return cfg.SpriteState.DEFENCE distance_to_target = sp.pos.get_distance_to(sp.brain.target.pos) if distance_to_target <= self.ai.CHASE_RANGE: target_move = sp.brain.destination.get_distance_to(sp.brain.target.pos) if target_move > self.target_move_threshold or self.steerer.is_end: #print "chase to chase" return cfg.SpriteState.CHASE else: # lose target #print "lose target" sp.brain.target = None sp.set_emotion(cfg.SpriteEmotion.CHAOS) return cfg.SpriteState.STAY def exit(self): self.sprite.brain.destination = None self.enter_timer.clear()
class EnemyAnimator(SpriteAnimator): def __init__(self, sprite): super(EnemyAnimator, self).__init__(sprite) self.die_image = None self.dead_timer = Timer(sfg.Enemy.DEAD_TICK) self.hp_bar = pygame.Surface(sfg.SpriteStatus.ENEMY_HP_BAR_SIZE).convert_alpha() def dead_tick(self): if not self.dead_timer.is_begin(): self.die_image = self.image.copy() self.dead_timer.begin() else: if self.dead_timer.exceed(): self.image = None return True pass_time = self.dead_timer.passed_time() # in 1 blink unit, one half show image, another half hide it # make it like a blink effect dead_blink_unit = float(sfg.Enemy.DEAD_TICK) / sfg.Enemy.DEAD_BLINK_TIMES if pass_time % dead_blink_unit < dead_blink_unit * 0.5: self.image = self.die_image else: self.image = None return False def draw_hp_bar(self, camera): # fill color to hp_bar according to the sprite hp sp = self.sprite self.hp_bar.fill(sfg.SpriteStatus.SPRITE_BAR_BG_COLOR) r = self.hp_bar.get_rect() r.width *= float(sp.hp) / sp.setting.HP hp_color = sfg.SpriteStatus.SPRITE_HP_COLORS[sp.hp_status] self.hp_bar.fill(hp_color, r) # adjust hp_bar position relative to screen r = self.hp_bar.get_rect() r.center = (sp.pos.x, sp.pos.y * 0.5 - sp.setting.HEIGHT) r.left -= camera.rect.left r.top -= camera.rect.top if sp.status.get(cfg.SpriteStatus.IN_AIR) is not None: r.top -= sp.status[cfg.SpriteStatus.IN_AIR]["height"] camera.screen.blit(self.hp_bar, r) def draw_with_height(self, camera, height): image_blit_pos = (self.rect.x - camera.rect.x, self.rect.y - camera.rect.y - height) camera.screen.blit(self.image, image_blit_pos) def draw(self, camera): sp = self.sprite if sp.status.get(cfg.SpriteStatus.AMBUSH) is not None: # this enemy is in ambush status, draw it according the corresponding status stage if sp.status[cfg.SpriteStatus.AMBUSH]["status"] == cfg.Ambush.STATUS_INIT: # don't draw it because hero doesn't enter this ambush return elif sp.status[cfg.SpriteStatus.AMBUSH]["status"] == cfg.Ambush.STATUS_ENTER: # hero enter the ambush, draw corresponding episode if sp.status[cfg.SpriteStatus.AMBUSH]["type"] == cfg.Ambush.APPEAR_TYPE_TOP_DOWN: self.draw_with_height(camera, sp.status[cfg.SpriteStatus.AMBUSH]["height"]) return if sp.status.get(cfg.SpriteStatus.INVISIBLE) is not None: return super(EnemyAnimator, self).draw(camera) if sp.hp_status in cfg.HpStatus.ALIVE: self.draw_hp_bar(camera)