Beispiel #1
0
class ChromaTest(unittest.TestCase):

    def setUp(self):
        logging.basicConfig(level=logging.DEBUG)
        conf = MockConf(dbstring="sqlite://")
        self.db = DB(conf)
        self.db.create_all()
        self.sess = self.db.session()
        self.sess.add_all(Region.create_from_json(TEST_LANDS))

        self.sess.commit()
        # Create some users
        self.alice = self.create_user("alice", 0)
        self.bob = self.create_user("bob", 1)

    def create_user(self, name, team):
        newbie = User(name=name, team=team, loyalists=100, leader=True)
        self.sess.add(newbie)
        cap = Region.capital_for(team, self.sess)
        newbie.region = cap
        self.sess.commit()
        return newbie

    def get_region(self, name):
        name = name.lower()
        region = self.sess.query(Region).filter_by(name=name).first()
        return region
Beispiel #2
0
class Bot(object):
    def __init__(self, config, reddit):
        self.config = config
        self.reddit = reddit
        self.db = DB(config)
        self.db.create_all()
        self.session = self.db.session()

    @failable
    def check_battles(self):
        session = self.session
        battles = session.query(Battle).all()
        for battle in battles:
            post = self.reddit.get_submission(
                comment_limit=None,
                submission_id=name_to_id(battle.submission_id))
            if post:
                self.process_post_for_battle(post, battle, session)

    @failable
    def check_hq(self):
        hq = self.reddit.get_subreddit(self.config.headquarters)
        submissions = hq.get_new()
        for submission in submissions:
            if "[recruitment]" in submission.title.lower():
                self.recruit_from_post(submission)
                break  # Only recruit from the first one

    @failable
    def check_messages(self):
        unread = reddit.get_unread(True, True)
        session = self.session
        for comment in unread:
            # Only PMs, we deal with comment replies in process_post_for_battle
            if not comment.was_comment:
                seen = (session.query(Processed).
                    filter_by(id36=comment.name).
                    count())
                if seen:
                    continue
                player = self.find_player(comment, session)
                if player:
                    cmds = extract_command(comment.body)
                    if not cmds:
                        cmds = [comment.body]
                    context = Context(player, self.config, session,
                                      comment, self.reddit)
                    for cmd in cmds:
                        self.command(cmd, context)
                session.add(Processed(id36=comment.name))
                session.commit()
            comment.mark_as_read()

    @failable
    def command(self, text, context):
        text = text.lower()
        logging.info("Processing command: '%s' by %s" %
                     (text, context.player.name))
        try:
            parsed = parse(text)
            parsed.execute(context)
        except ParseException as pe:
            result = (
                "I'm sorry, I couldn't understand your command:"
                "\n\n"
                "> %s\n"
                "\nThe parsing error is below:\n\n"
                "    %s") % (text, pe)
            context.reply(result)

    def find_player(self, comment, session):
        if comment.author:  # Some messages (mod invites) don't have authors
            player = session.query(User).filter_by(
            name=comment.author.name.lower()).first()
            if not player and getattr(comment, 'was_comment', None):
                comment.reply(Command.FAIL_NOT_PLAYER %
                                  self.config.headquarters)
            return player
        return None

    @failable
    def generate_markdown_report(self, loop_start):
        """
        Separate from the others as this logs to a sidebar rather than
        a file
        """
        s = self.session

        land_report = StatusCommand.lands_status_for(s, self.config)
        hq = self.reddit.get_subreddit(self.config.headquarters)

        cur = now()
        elapsed = (cur - loop_start) + self.config["bot"]["sleep"]
        version_str = version(self.config)

        bot_report = ("Bot Status:\n\n"
                      "* Last run at %s\n\n"
                      "* Seconds per Frame: %d\n\n"
                      "* Version: %s") % (timestr(cur), elapsed, version_str)

        report = "%s\n\n%s" % (land_report, bot_report)

        # This is apparently not immediately done, or there's some caching.
        # Keep an eye on it.
        hq.update_settings(description=report)

    def generate_reports(self, loop_start):
        logging.info("Generating reports")
        self.generate_markdown_report(loop_start)
        rdir = self.config["bot"].get("report_dir")
        if not rdir:
            return
        s = self.session
        regions = s.query(Region).all()
        with open(os.path.join(rdir, "report.txt"), 'w') as url:
            urldict = {}
            for r in regions:
                if r.owner is not None:
                    owner = r.owner
                else:
                    owner = -1
                urldict[r.name] = owner
            url.write(urlencode(urldict))

        with open(os.path.join(rdir, "report.json"), 'w') as j:
            jdict = {}
            jdict['regions'] = {}
            for r in regions:
                rdict = {}
                rdict['name'] = r.name
                rdict['srname'] = r.srname
                if r.owner is not None:
                    rdict['owner'] = r.owner
                else:
                    rdict['owner'] = -1

                if r.battle:
                    if r.battle.has_started():
                        rdict['battle'] = 'underway'
                    else:
                        rdict['battle'] = 'preparing'
                else:
                    rdict['battle'] = 'none'
                jdict['regions'][r.name] = rdict

            users = {}
            for u in s.query(User).all():
                udict = {}
                udict['team'] = u.team
                udict['leader'] = u.leader
                users[u.name] = udict
            jdict['users'] = users
            j.write(json.dumps(jdict, sort_keys=True, indent=4))

    def process_post_for_battle(self, post, battle, sess):
        p = sess.query(Processed).filter_by(battle=battle).all()
        seen = [entry.id36 for entry in p]

        replaced = post.replace_more_comments(limit=None, threshold=0)
        if replaced:
            logging.info("Comments that went un-replaced: %s" % replaced)
        flat_comments = praw.helpers.flatten_tree(
            post.comments)

        for comment in flat_comments:
            if comment.name in seen:
                continue
            if not comment.author:  # Deleted comments don't have an author
                continue
            if comment.author.name.lower() == self.config.username.lower():
                continue
            cmds = extract_command(comment.body)
            if cmds:
                player = self.find_player(comment, sess)
                if player:
                    context = Context(player, self.config, sess,
                                          comment, self.reddit)
                    for cmd in cmds:
                        self.command(cmd, context)
            sess.add(Processed(id36=comment.name, battle=battle))
            sess.commit()

    @failable
    def recruit_from_post(self, post):
        post.replace_more_comments(threshold=0)
        flat_comments = praw.helpers.flatten_tree(post.comments)
        for comment in flat_comments:
            self.recruit_from_comment(comment)

    @failable
    def recruit_from_comment(self, comment):
        session = self.session
        if not comment.author:  # Deleted comments don't have an author
            return
        name = comment.author.name.lower()
        if name == self.config.username.lower():
            return

        # Is this author already one of us?
        found = session.query(User).filter_by(
            name=name).first()
        if not found:
            # Getting the author ID triggers a lookup on the userpage.  In the
            # case of banned users, this will 404.  @failable would normally
            # catch that just fine, but I want to check it here so that in the
            # future, I can do things like e.g. add to an 'ignored' list and
            # save myself the lookup
            try:
                author_id = comment.author.id
            except NotFound:
                logging.warn("Ignored banned user %s" % name)
                return

            team = 0
            assignment = self.config['game']['assignment']
            if assignment == 'uid':
                base10_id = base36decode(author_id)
                team = base10_id % 2
            elif assignment == "random":
                team = random.randint(0, 1)
            is_leader = name in self.config["game"]["leaders"]
            newbie = User(name=name,
                          team=team,
                          loyalists=100,
                          leader=is_leader)
            session.add(newbie)

            cap = Region.capital_for(newbie.team, session)
            if not cap:
                logging.fatal("Could not find capital for %d" %
                              newbie.team)
            newbie.region = cap

            session.commit()
            logging.info("Created combatant %s", newbie)

            reply = ("Welcome to Chroma!  You are now a %s "
                     "in the %s army, commanding a force of loyalists "
                     "%d people strong. You are currently encamped at %s"
            ) % (newbie.rank,
                 num_to_team(newbie.team, self.config),
                 newbie.loyalists,
                 cap.markdown())
            comment.reply(reply)
        else:
            #logging.info("Already registered %s", comment.author.name)
            pass

    @failable
    def update_skirmish_summaries(self, skirmishes):
        c = Context(player=None,
                    config=self.config,
                    session=self.session,
                    comment=None,
                    reddit=self.reddit)
        for s in skirmishes:
            SkirmishCommand.update_summary(c, s)

    @failable
    def update_game(self):
        session = self.session
        MarchingOrder.update_all(session, self.config)

        results = Region.update_all(session, self.config)
        to_add = []
        for newternal in results['new_eternal']:
            title = "The Eternal Battle Rages On"
            post = InvadeCommand.post_invasion(title, newternal, self.reddit)
            if post:
                newternal.submission_id = post.name
                to_add.append(newternal)
            else:
                logging.warn("Couldn't submit eternal battle thread")
                session.rollback()
        if to_add:
            session.add_all(to_add)
            session.commit()

        results = Battle.update_all(session, self.config)

        for ready in results['begin']:
            ready.display_ends = (ready.begins +
                self.config["game"]["battle_time"])

            # Actual ending is within battle_lockout of the end
            chooserange = self.config["game"]["battle_lockout"]
            chosen = random.randint(0, chooserange)
            ready.ends = ready.display_ends - (chooserange / 2) + chosen

            text = ("War is now at your doorstep!  Mobilize your armies! "
                    "The battle has begun now, and will end at %s.\n\n"
                    "> Enter your commands in this thread, prefixed with "
                    "'>'") % ready.ends_str()
            post = self.reddit.get_submission(
                submission_id=name_to_id(ready.submission_id))
            post.edit(text)
            session.commit()

        self.update_skirmish_summaries(results['skirmish_ended'])

        for done in results['ended']:
            report = ["The battle is complete...\n"]
            report += done.report(self.config)

            report.append("")

            if done.old_buffs:
                report.append("Buffs in effect for Team %s\n" %
                              num_to_team(done.old_owner, self.config))
                for buff in done.old_buffs:
                    report.append("  * %s" % buff.name)
                report.append("")

            team0_name = num_to_team(0, self.config)
            team1_name = num_to_team(1, self.config)

            # Homeland buffs?
            if done.homeland_buffs:
                report.append("Homeland buffs in effect:")
                for team in range(0, 2):
                    name = num_to_team(team, self.config)
                    report.append("%s: %d%%" %
                                  (name, done.homeland_buffs[team]))

            report.append(("## Final Score:  Team %s: %d "
                           "Team %s: %d") % (team0_name, done.score0,
                                             team1_name, done.score1))
            if done.victor is not None:
                report.append("\n# The Victor:  Team %s" %
                              num_to_team(done.victor, self.config))
            else:
                report.append("# TIE")

            text = "\n".join(report)
            post = self.reddit.get_submission(
                submission_id=name_to_id(done.submission_id))
            post.edit(text)

            # Update all the skirmish summaries
            self.update_skirmish_summaries(done.toplevel_skirmishes())

            session.delete(done)
            session.commit()
        db.Buff.update_all(session)

    @failable
    def login(self):
        reddit.login(c.username, c.password)
        return True

    def run(self):
        logging.info("Bot started up")
        if self.config.bot.get("verbose_logging"):
            logging.info("Verbose logging enabled")
        logged_in = self.login()
        while(logged_in):
            loop_start = now()
            self.config.refresh()
            logging.info("Checking headquarters")
            self.check_hq()
            logging.info("Checking Messages")
            self.check_messages()
            logging.info("Checking Battles")
            self.check_battles()
            logging.info("Updating game state")
            self.update_game()
            # generate_reports logs itself
            self.generate_reports(loop_start)
            logging.info("Sleeping")
            time.sleep(self.config["bot"]["sleep"])
        logging.fatal("Unable to log into bot; shutting down")