예제 #1
0
파일: start.py 프로젝트: hasegaw/IkaLog
    def _init_scene(self, debug=False):
        self.election_period = 5 * 1000  # msec

        self.stage_matchers = MultiClassIkaMatcher()
        self.rule_matchers = MultiClassIkaMatcher()

        for stage_id in stages.keys():
            stage = IkaMatcher(
                self.mapname_left, self.mapname_top, self.mapname_width, self.mapname_height,
                img_file='stage_%s.png' % stage_id,
                threshold=0.95,
                orig_threshold=0.30,
                bg_method=matcher.MM_NOT_WHITE(),
                fg_method=matcher.MM_WHITE(),
                label='stage:%s' % stage_id,
                call_plugins=self._call_plugins,
                debug=debug,
            )
            setattr(stage, 'id_', stage_id)
            self.stage_matchers.add_mask(stage)

        for rule_id in rules.keys():
            rule = IkaMatcher(
                self.rulename_left, self.rulename_top, self.rulename_width, self.rulename_height,
                img_file='rule_%s.png' % rule_id,
                threshold=0.95,
                orig_threshold=0.30,
                bg_method=matcher.MM_NOT_WHITE(),
                fg_method=matcher.MM_WHITE(),
                label='rule:%s' % rule_id,
                call_plugins=self._call_plugins,
                debug=debug,
            )
            setattr(rule, 'id_', rule_id)
            self.rule_matchers.add_mask(rule)
예제 #2
0
    def _init_scene(self, debug=False):
        #
        # To gather mask data, enable this.
        #
        self.write_samples = False

        # Load mask files.
        self._masks = MultiClassIkaMatcher()
        for special_weapon in special_weapons.keys():
            try:
                mask = IkaMatcher(
                    0, 0, 150, 24,
                    img_file='special_%s.png' % special_weapon,
                    threshold=0.90,
                    orig_threshold=0.20,
                    bg_method=matcher.MM_NOT_WHITE(),
                    fg_method=matcher.MM_WHITE(),
                    label='special/%s' % special_weapon,
                    call_plugins=self._call_plugins,
                    debug=debug,
                )
                mask._id = special_weapon
                self._masks.add_mask(mask)
            except:
                IkaUtils.dprint('%s: Failed to load mask for %s' %
                                (self, special_weapon))
                pass
예제 #3
0
    def reset(self):
        super(GameRankedBattleEvents, self).reset()

        self._last_event_msec = - 100 * 1000
        self._last_mask_matched = None
        self._last_mask_triggered_msec = - 100 * 1000
        self._masks_active = {}
        self._masks_active2 = MultiClassIkaMatcher()
예제 #4
0
    def _init_scene(self, debug=False):
        self.election_period = 5 * 1000  # msec

        self.stage_matchers = MultiClassIkaMatcher()
        self.rule_matchers = MultiClassIkaMatcher()

        for stage_id in stages.keys():
            stage = IkaMatcher(
                self.mapname_left,
                self.mapname_top,
                self.mapname_width,
                self.mapname_height,
                img_file='stage_%s.png' % stage_id,
                threshold=0.95,
                orig_threshold=0.30,
                bg_method=matcher.MM_NOT_WHITE(),
                fg_method=matcher.MM_WHITE(),
                label='stage:%s' % stage_id,
                call_plugins=self._call_plugins,
                debug=debug,
            )
            setattr(stage, 'id_', stage_id)
            self.stage_matchers.add_mask(stage)

        for rule_id in rules.keys():
            rule = IkaMatcher(
                self.rulename_left,
                self.rulename_top,
                self.rulename_width,
                self.rulename_height,
                img_file='rule_%s.png' % rule_id,
                threshold=0.95,
                orig_threshold=0.30,
                bg_method=matcher.MM_NOT_WHITE(),
                fg_method=matcher.MM_WHITE(),
                label='rule:%s' % rule_id,
                call_plugins=self._call_plugins,
                debug=debug,
            )
            setattr(rule, 'id_', rule_id)
            self.rule_matchers.add_mask(rule)
예제 #5
0
    def on_game_start(self, context):
        rule_id = context['game']['rule']
        masks_active = self._masks_ranked.copy()
        if rule_id == 'area':
            masks_active.update(self._masks_splatzone)

        elif rule_id == 'hoko':
            masks_active.update(self._masks_rainmaker)

        elif rule_id == 'yagura':
            masks_active.update(self._masks_towercontrol)

        else:
            masks_active = {}

        self._masks_active = masks_active

        # Initialize Multi-Class IkaMatcher
        self._masks_active2 = MultiClassIkaMatcher()
        for mask in masks_active.keys():
            self._masks_active2.add_mask(mask)
예제 #6
0
class GameSpecialWeapon(StatefulScene):

    # Called per Engine's reset.
    def reset(self):
        super(GameSpecialWeapon, self).reset()
        self.img_last_special = None

    def _match_phase1(self, context, img_special, img_last_special):
        #
        # Phase 1
        #
        # Crop the area special weapon message supposed to be appeared.
        # Compare with last frame, and check if it is (almost) same with
        # the last frame.
        #

        img_special_diff = abs(img_special - img_last_special)
        matched = bool(np.average(img_special_diff) < 90)
        return matched

    def _is_my_special_weapon(self, context, img_special_bgr):
        img = img_special_bgr[:, :150]

        img_s = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)[:, :, 2]
        img_s[matcher.MM_WHITE()(img) > 127] = 127

        img_s_hist = cv2.calcHist(img_s[:, :], [0], None, [5], [0, 256])
        img_s_hist_black = float(np.amax(img_s_hist[0:1]))
        img_s_hist_non_black = float(np.amax(img_s_hist[3:4]))
        return img_s_hist_black < img_s_hist_non_black

    def _state_default(self, context):
        if not self.is_another_scene_matched(context, 'GameTimerIcon'):
            return False

        frame = context['engine']['frame']
        if frame is None:
            return False

        # FIXME: this code works with the first special weapon only
        img_special_bgr = frame[260:260 + 24, 1006:1006 + 210, :]
        img_special = np.array(img_special_bgr)
        img_last_special = self.img_last_special
        self.img_last_special = img_special

        if img_last_special is None:
            return False

        if not self._match_phase1(context, img_special, img_last_special):
            return False

        #
        # Phase 2
        #
        # Check inkling image on right side
        img_sp_char = cv2.cvtColor(
            img_special_bgr[:, 150:210], cv2.COLOR_BGR2GRAY)

        laplacian_threshold = 60
        img_laplacian = cv2.Laplacian(img_sp_char, cv2.CV_64F)
        img_laplacian_abs = cv2.convertScaleAbs(img_laplacian)
        c_matched = bool(np.average(img_laplacian_abs) > 20)

        if not c_matched:
            return False

        # Phase 3
        # TODO: Background color

        # Phase 4
        # Forground text
        white_filter = matcher.MM_WHITE()
        img_sp_text = white_filter(img_special_bgr[:, 0:150])

        special = self._masks.match_best(img_special_bgr)[1]

        if self.write_samples:
            cv2.imwrite('training/_special_%s.png' %
                        time.time(), 255 - img_sp_text)

        if special is None:
            return False

        context['game']['special_weapon'] = special._id
        context['game']['special_weapon_is_mine'] = \
            self._is_my_special_weapon(context, img_special_bgr)
        self._call_plugins('on_game_special_weapon')

        self._switch_state(self._state_tracking)
        return True

    def _state_tracking(self, context):
        if not self.is_another_scene_matched(context, 'GameTimerIcon'):
            return False

        frame = context['engine']['frame']
        if frame is None:
            return False

        # FIXME
        img_special_bgr = frame[260:260 + 24, 1006:1006 + 210, :]
        img_special = np.array(img_special_bgr)
        img_last_special = self.img_last_special

        if img_last_special is None:
            return False

        special = self._masks.match_best(img_special_bgr)[1]
        if special is not None:
            self._call_plugins(
                'on_mark_rect_in_preview',
                [(1006, 260), (1006 + 210, 260 + 24)]
            )

            if context['game']['special_weapon'] == special._id:
                return True

        if self.matched_in(context, 150):
            return False

        self._switch_state(self._state_default)
        self.img_last_special = None
        return False

    def dump(self, context):
        # Not implemented :\
        pass

    def _analyze(self, context):
        pass

    # Called only once on initialization.
    def _init_scene(self, debug=False):
        #
        # To gather mask data, enable this.
        #
        self.write_samples = False

        # Load mask files.
        self._masks = MultiClassIkaMatcher()
        for special_weapon in special_weapons.keys():
            try:
                mask = IkaMatcher(
                    0, 0, 150, 24,
                    img_file='special_%s.png' % special_weapon,
                    threshold=0.90,
                    orig_threshold=0.20,
                    bg_method=matcher.MM_NOT_WHITE(),
                    fg_method=matcher.MM_WHITE(),
                    label='special/%s' % special_weapon,
                    call_plugins=self._call_plugins,
                    debug=debug,
                )
                mask._id = special_weapon
                self._masks.add_mask(mask)
            except:
                IkaUtils.dprint('%s: Failed to load mask for %s' %
                                (self, special_weapon))
                pass
예제 #7
0
파일: start.py 프로젝트: hasegaw/IkaLog
class GameStart(StatefulScene):

    # 720p サイズでの値
    mapname_width = 430
    mapname_left = 1280 - mapname_width
    mapname_top = 580
    mapname_height = 640 - mapname_top

    rulename_left = 640 - 120
    rulename_right = 640 + 120
    rulename_width = rulename_right - rulename_left
    rulename_top = 250
    rulename_bottom = 310
    rulename_height = rulename_bottom - rulename_top

    def reset(self):
        super(GameStart, self).reset()
        self.stage_votes = []
        self.rule_votes = []

        self._last_event_msec = - 100 * 1000
        self._last_run_msec = - 100 * 1000

    def elect(self, context, votes):
        # Discard too old data.
        election_start = context['engine']['msec'] - self.election_period
        votes = list(filter(lambda e: election_start < e[0], votes))

        # count
        items = {}
        for vote in votes:
            if vote[1] is None:
                continue
            key = vote[1]
            items[key] = items.get(key, 0) + 1

        # return the best key
        sorted_keys = sorted(
            items.keys(), key=lambda x: items[x], reverse=True)
        sorted_keys.extend([None])  # fallback

        return sorted_keys[0]

    def _detect_stage_and_rule(self, context):
        frame = context['engine']['frame']

        stage = None
        rule = None

        best_stage = self.stage_matchers.match_best(frame)
        best_rule = self.rule_matchers.match_best(frame)

        if best_stage[1] is not None:
            stage = best_stage[1].id_
        if best_rule[1] is not None:
            rule = best_rule[1].id_

        return stage, rule

    def _state_default(self, context):
        timer_icon = self.find_scene_object('GameTimerIcon')
        if (timer_icon is not None) and timer_icon.matched_in(context, 3000):
            return False

        frame = context['engine']['frame']

        if frame is None:
            return False

        if self.matched_in(context, 1500, attr='_last_run_msec'):
            return False
        else:
            self._last_run_msec = context['engine']['msec']

        # Get the best matched stat.ink key
        stage, rule = self._detect_stage_and_rule(context)

        if stage or rule:
            self.stage_votes = []
            self.rule_votes = []
            self.stage_votes.append((context['engine']['msec'], stage))
            self.rule_votes.append((context['engine']['msec'], rule))
            self._switch_state(self._state_tracking)
            return True

        return False

    def _state_tracking(self, context):
        frame = context['engine']['frame']

        if frame is None:
            return False

        stage, rule = self._detect_stage_and_rule(context)
        matched = (stage or rule)

        # 画面が続いているならそのまま
        if matched:
            self.stage_votes.append((context['engine']['msec'], stage))
            self.rule_votes.append((context['engine']['msec'], rule))
            return True

        # 1000ms 以内の非マッチはチャタリングとみなす
        if not matched and self.matched_in(context, 1000):
            return False

        # それ以上マッチングしなかった場合 -> シーンを抜けている

        if not self.matched_in(context, 20000, attr='_last_event_msec'):
            context['game']['map'] = self.elect(context, self.stage_votes)
            context['game']['rule'] = self.elect(context, self.rule_votes)

            if not context['game']['start_time']:
                # start_time should be initialized in GameGoSign.
                # This is a fallback in case GameGoSign was skipped.
                context['game']['start_time'] = IkaUtils.getTime(context)
                context['game']['start_offset_msec'] = \
                    context['engine']['msec']

            self._call_plugins('on_game_start')
            self._last_event_msec = context['engine']['msec']

        self._switch_state(self._state_default)
        return False

    def _analyze(self, context):
        pass

    def dump(self, context):
        for v in self.stage_votes:
            if v[1] is None:
                continue
            print('stage', v[0], v[1])

        for v in self.rule_votes:
            if v[1] is None:
                continue
            print('rule', v[0], v[1])

    def _init_scene(self, debug=False):
        self.election_period = 5 * 1000  # msec

        self.stage_matchers = MultiClassIkaMatcher()
        self.rule_matchers = MultiClassIkaMatcher()

        for stage_id in stages.keys():
            stage = IkaMatcher(
                self.mapname_left, self.mapname_top, self.mapname_width, self.mapname_height,
                img_file='stage_%s.png' % stage_id,
                threshold=0.95,
                orig_threshold=0.30,
                bg_method=matcher.MM_NOT_WHITE(),
                fg_method=matcher.MM_WHITE(),
                label='stage:%s' % stage_id,
                call_plugins=self._call_plugins,
                debug=debug,
            )
            setattr(stage, 'id_', stage_id)
            self.stage_matchers.add_mask(stage)

        for rule_id in rules.keys():
            rule = IkaMatcher(
                self.rulename_left, self.rulename_top, self.rulename_width, self.rulename_height,
                img_file='rule_%s.png' % rule_id,
                threshold=0.95,
                orig_threshold=0.30,
                bg_method=matcher.MM_NOT_WHITE(),
                fg_method=matcher.MM_WHITE(),
                label='rule:%s' % rule_id,
                call_plugins=self._call_plugins,
                debug=debug,
            )
            setattr(rule, 'id_', rule_id)
            self.rule_matchers.add_mask(rule)
예제 #8
0
 def on_game_reset(self, context):
     self._masks_active = {}
     self._masks_active2 = MultiClassIkaMatcher()
예제 #9
0
class GameRankedBattleEvents(StatefulScene):

    # Called per Engine's reset.
    def reset(self):
        super(GameRankedBattleEvents, self).reset()

        self._last_event_msec = - 100 * 1000
        self._last_mask_matched = None
        self._last_mask_triggered_msec = - 100 * 1000
        self._masks_active = {}
        self._masks_active2 = MultiClassIkaMatcher()

    def on_game_reset(self, context):
        self._masks_active = {}
        self._masks_active2 = MultiClassIkaMatcher()

    def on_game_start(self, context):
        rule_id = context['game']['rule']
        masks_active = self._masks_ranked.copy()
        if rule_id == 'area':
            masks_active.update(self._masks_splatzone)

        elif rule_id == 'hoko':
            masks_active.update(self._masks_rainmaker)

        elif rule_id == 'yagura':
            masks_active.update(self._masks_towercontrol)

        else:
            masks_active = {}

        self._masks_active = masks_active

        # Initialize Multi-Class IkaMatcher
        self._masks_active2 = MultiClassIkaMatcher()
        for mask in masks_active.keys():
            self._masks_active2.add_mask(mask)

    def _state_triggered(self, context):
        frame = context['engine']['frame']
        if frame is None:
            return False

        most_possible = self._masks_active2.match_best(frame)[1]
        if most_possible is None:
            self._switch_state(self._state_default)

        if most_possible != self._last_mask_matched:
            IkaUtils.dprint('%s: matched %s' % (self, most_possible))
            self._last_mask_matched = most_possible
            # self._switch_state(self._state_pending)
            return True

    def _state_pending(self, context):
        # if self.is_another_scene_matched(context, 'GameTimerIcon'):
        #    return False

        frame = context['engine']['frame']
        if frame is None:
            return False

        most_possible = self._masks_active2.match_best(frame)[1]

        if most_possible is None:
            self._switch_state(self._state_default)

        if most_possible != self._last_mask_matched:
            self._last_mask_matched = most_possible
            return True

        # else: # if most_possbile == self._last_mask_matched:
            # go through

        # not self.matched_in(context, 3000, attr='_last_mask_triggered_msec'):

        if 1:
            event = self._masks_active[most_possible]
            IkaUtils.dprint('%s: trigger an event %s' % (self, event))
            self._call_plugins(event)

        self._last_mask_triggered = most_possible
        self._last_mask_triggered_msec = context['engine']['msec']
        self._switch_state(self._state_triggered)

    def _state_default(self, context):
        # if self.is_another_scene_matched(context, 'GameTimerIcon'):
        #     return False

        frame = context['engine']['frame']
        if frame is None:
            return False

        most_possible = self._masks_active2.match_best(frame)[1]

        if most_possible is None:
            return False

        # IkaUtils.dprint('%s: matched %s' % (self, most_possible))
        self._last_mask_matched = most_possible
        self._switch_state(self._state_pending)
        return True

    def _analyze(self, context):
        pass

    def _load_splatzone_masks(self, debug=False):
        mask_we_got = IkaMatcher(
            473, 177, 273, 36,
            img_file='splatzone_we_got.png',
            threshold=0.9,
            orig_threshold=0.1,
            label='splatzone/we_got',
            bg_method=matcher.MM_NOT_WHITE(),
            fg_method=matcher.MM_WHITE(),
            call_plugins=self._call_plugins,
            debug=debug,
        )

        mask_we_lost = IkaMatcher(
            473, 177, 273, 36,
            img_file='splatzone_we_lost.png',
            threshold=0.9,
            orig_threshold=0.1,
            label='splatzone/we_lost',
            bg_method=matcher.MM_NOT_WHITE(),
            fg_method=matcher.MM_WHITE(),
            call_plugins=self._call_plugins,
            debug=debug,
        )

        mask_they_got = IkaMatcher(
            473, 177, 273, 36,
            img_file='splatzone_they_got.png',
            threshold=0.9,
            orig_threshold=0.1,
            label='splatzone/they_got',
            bg_method=matcher.MM_NOT_WHITE(),
            fg_method=matcher.MM_WHITE(),
            call_plugins=self._call_plugins,
            debug=debug,
        )

        mask_they_lost = IkaMatcher(
            473, 177, 273, 36,
            img_file='splatzone_they_lost.png',
            threshold=0.9,
            orig_threshold=0.1,
            label='splatzone/they_lost',
            bg_method=matcher.MM_NOT_WHITE(),
            fg_method=matcher.MM_WHITE(),
            call_plugins=self._call_plugins,
            debug=debug,
        )

        self._masks_splatzone = {
            mask_we_got:    'on_game_splatzone_we_got',
            mask_we_lost:   'on_game_splatzone_we_lost',
            mask_they_got:  'on_game_splatzone_they_got',
            mask_they_lost: 'on_game_splatzone_they_lost',
        }

    def _load_rainmaker_masks(self, debug=False):
        mask_we_got = IkaMatcher(
            473, 177, 273, 36,
            img_file='rainmaker_we_got.png',
            threshold=0.9,
            orig_threshold=0.1,
            label='rainmaker/we_got',
            bg_method=matcher.MM_NOT_WHITE(),
            fg_method=matcher.MM_WHITE(),
            call_plugins=self._call_plugins,
            debug=debug,
        )

        mask_we_lost = IkaMatcher(
            473, 177, 273, 36,
            img_file='rainmaker_we_lost.png',
            threshold=0.9,
            orig_threshold=0.1,
            label='rainmaker/we_lost',
            bg_method=matcher.MM_NOT_WHITE(),
            fg_method=matcher.MM_WHITE(),
            call_plugins=self._call_plugins,
            debug=debug,
        )

        mask_they_got = IkaMatcher(
            473, 177, 273, 36,
            img_file='rainmaker_they_got.png',
            threshold=0.9,
            orig_threshold=0.1,
            label='rainmaker/they_got',
            bg_method=matcher.MM_NOT_WHITE(),
            fg_method=matcher.MM_WHITE(),
            call_plugins=self._call_plugins,
            debug=debug,
        )

        mask_they_lost = IkaMatcher(
            473, 177, 273, 36,
            img_file='rainmaker_they_lost.png',
            threshold=0.9,
            orig_threshold=0.1,
            label='rainmaker/they_lost',
            bg_method=matcher.MM_NOT_WHITE(),
            fg_method=matcher.MM_WHITE(),
            call_plugins=self._call_plugins,
            debug=debug,
        )

        self._masks_rainmaker = {
            mask_we_got:    'on_game_rainmaker_we_got',
            mask_we_lost:   'on_game_rainmaker_we_lost',
            mask_they_got:  'on_game_rainmaker_they_got',
            mask_they_lost: 'on_game_rainmaker_they_lost',
        }

    def _load_towercontrol_masks(self, debug=False):
        mask_we_took = IkaMatcher(
            473, 177, 273, 36,
            img_file='towercontrol_we_took.png',
            threshold=0.9,
            orig_threshold=0.1,
            label='towercontrol/we_took',
            bg_method=matcher.MM_NOT_WHITE(),
            fg_method=matcher.MM_WHITE(),
            call_plugins=self._call_plugins,
            debug=debug,
        )

        mask_we_lost = IkaMatcher(
            473, 177, 273, 36,
            img_file='towercontrol_we_lost.png',
            threshold=0.9,
            orig_threshold=0.1,
            label='towercontrol/we_lost',
            bg_method=matcher.MM_NOT_WHITE(),
            fg_method=matcher.MM_WHITE(),
            call_plugins=self._call_plugins,
            debug=debug,
        )

        mask_they_took = IkaMatcher(
            473, 177, 273, 36,
            img_file='towercontrol_they_took.png',
            threshold=0.9,
            orig_threshold=0.1,
            label='towercontrol/they_took',
            bg_method=matcher.MM_NOT_WHITE(),
            fg_method=matcher.MM_WHITE(),
            call_plugins=self._call_plugins,
            debug=debug,
        )

        mask_they_lost = IkaMatcher(
            473, 177, 273, 36,
            img_file='towercontrol_they_lost.png',
            threshold=0.9,
            orig_threshold=0.1,
            label='towercontrol/they_lost',
            bg_method=matcher.MM_NOT_WHITE(),
            fg_method=matcher.MM_WHITE(),
            call_plugins=self._call_plugins,
            debug=debug,
        )

        self._masks_towercontrol = {
            mask_we_took:    'on_game_towercontrol_we_took',
            mask_we_lost:   'on_game_towercontrol_we_lost',
            mask_they_took:  'on_game_towercontrol_they_took',
            mask_they_lost: 'on_game_towercontrol_they_lost',
        }

    # Called only once on initialization.
    def _init_scene(self, debug=False):
        self._load_rainmaker_masks(debug=debug)
        self._load_splatzone_masks(debug=debug)
        self._load_towercontrol_masks(debug=debug)

        self.mask_we_lead = IkaMatcher(
            473, 177, 273, 36,
            img_file='ranked_we_lead.png',
            threshold=0.9,
            orig_threshold=0.1,
            label='splatzone/we_lead',
            bg_method=matcher.MM_NOT_WHITE(),
            fg_method=matcher.MM_WHITE(),
            call_plugins=self._call_plugins,
            debug=debug,
        )

        self.mask_they_lead = IkaMatcher(
            473, 177, 273, 36,
            img_file='ranked_they_lead.png',
            threshold=0.9,
            orig_threshold=0.1,
            label='splatzone/they_lead',
            bg_method=matcher.MM_NOT_WHITE(),
            fg_method=matcher.MM_WHITE(),
            call_plugins=self._call_plugins,
            debug=debug,
        )

        self._masks_ranked = {
            self.mask_we_lead:   'on_game_ranked_we_lead',
            self.mask_they_lead: 'on_game_ranked_they_lead',
        }
예제 #10
0
class GameStart(StatefulScene):

    # 720p サイズでの値
    mapname_width = 430
    mapname_left = 1280 - mapname_width
    mapname_top = 580
    mapname_height = 640 - mapname_top

    rulename_left = 640 - 120
    rulename_right = 640 + 120
    rulename_width = rulename_right - rulename_left
    rulename_top = 250
    rulename_bottom = 310
    rulename_height = rulename_bottom - rulename_top

    def reset(self):
        super(GameStart, self).reset()
        self.stage_votes = []
        self.rule_votes = []

        self._last_event_msec = -100 * 1000
        self._last_run_msec = -100 * 1000

    def elect(self, context, votes):
        # Discard too old data.
        election_start = context['engine']['msec'] - self.election_period
        votes = list(filter(lambda e: election_start < e[0], votes))

        # count
        items = {}
        for vote in votes:
            if vote[1] is None:
                continue
            key = vote[1]
            items[key] = items.get(key, 0) + 1

        # return the best key
        sorted_keys = sorted(items.keys(),
                             key=lambda x: items[x],
                             reverse=True)
        sorted_keys.extend([None])  # fallback

        return sorted_keys[0]

    def _detect_stage_and_rule(self, context):
        frame = context['engine']['frame']

        stage = None
        rule = None

        best_stage = self.stage_matchers.match_best(frame)
        best_rule = self.rule_matchers.match_best(frame)

        if best_stage[1] is not None:
            stage = best_stage[1].id_
        if best_rule[1] is not None:
            rule = best_rule[1].id_

        return stage, rule

    def _state_default(self, context):
        timer_icon = self.find_scene_object('GameTimerIcon')
        if (timer_icon is not None) and timer_icon.matched_in(context, 3000):
            return False

        frame = context['engine']['frame']

        if frame is None:
            return False

        if self.matched_in(context, 1500, attr='_last_run_msec'):
            return False
        else:
            self._last_run_msec = context['engine']['msec']

        # Get the best matched stat.ink key
        stage, rule = self._detect_stage_and_rule(context)

        if stage or rule:
            self.stage_votes = []
            self.rule_votes = []
            self.stage_votes.append((context['engine']['msec'], stage))
            self.rule_votes.append((context['engine']['msec'], rule))
            self._switch_state(self._state_tracking)
            return True

        return False

    def _state_tracking(self, context):
        frame = context['engine']['frame']

        if frame is None:
            return False

        stage, rule = self._detect_stage_and_rule(context)
        matched = (stage or rule)

        # 画面が続いているならそのまま
        if matched:
            self.stage_votes.append((context['engine']['msec'], stage))
            self.rule_votes.append((context['engine']['msec'], rule))
            return True

        # 1000ms 以内の非マッチはチャタリングとみなす
        if not matched and self.matched_in(context, 1000):
            return False

        # それ以上マッチングしなかった場合 -> シーンを抜けている

        if not self.matched_in(context, 20000, attr='_last_event_msec'):
            context['game']['map'] = self.elect(context, self.stage_votes)
            context['game']['rule'] = self.elect(context, self.rule_votes)

            if not context['game']['start_time']:
                # start_time should be initialized in GameGoSign.
                # This is a fallback in case GameGoSign was skipped.
                context['game']['start_time'] = IkaUtils.getTime(context)
                context['game']['start_offset_msec'] = \
                    context['engine']['msec']

            self._call_plugins('on_game_start')
            self._last_event_msec = context['engine']['msec']

        self._switch_state(self._state_default)
        return False

    def _analyze(self, context):
        pass

    def dump(self, context):
        for v in self.stage_votes:
            if v[1] is None:
                continue
            print('stage', v[0], v[1])

        for v in self.rule_votes:
            if v[1] is None:
                continue
            print('rule', v[0], v[1])

    def _init_scene(self, debug=False):
        self.election_period = 5 * 1000  # msec

        self.stage_matchers = MultiClassIkaMatcher()
        self.rule_matchers = MultiClassIkaMatcher()

        for stage_id in stages.keys():
            stage = IkaMatcher(
                self.mapname_left,
                self.mapname_top,
                self.mapname_width,
                self.mapname_height,
                img_file='stage_%s.png' % stage_id,
                threshold=0.95,
                orig_threshold=0.30,
                bg_method=matcher.MM_NOT_WHITE(),
                fg_method=matcher.MM_WHITE(),
                label='stage:%s' % stage_id,
                call_plugins=self._call_plugins,
                debug=debug,
            )
            setattr(stage, 'id_', stage_id)
            self.stage_matchers.add_mask(stage)

        for rule_id in rules.keys():
            rule = IkaMatcher(
                self.rulename_left,
                self.rulename_top,
                self.rulename_width,
                self.rulename_height,
                img_file='rule_%s.png' % rule_id,
                threshold=0.95,
                orig_threshold=0.30,
                bg_method=matcher.MM_NOT_WHITE(),
                fg_method=matcher.MM_WHITE(),
                label='rule:%s' % rule_id,
                call_plugins=self._call_plugins,
                debug=debug,
            )
            setattr(rule, 'id_', rule_id)
            self.rule_matchers.add_mask(rule)