class Thread(GameEvent): forum_id = db.Column(db.Integer, db.ForeignKey('forum.id'), nullable=False) posts = db.relationship('Post', backref='thread') title = db.Column(db.String(32)) author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) author = db.relationship("User") def __init__(self, forum_id, title, author_id): self.forum_id = forum_id self.title = title self.author_id = author_id def get_post_count(self): return len(self.posts) def get_most_recent_post(self): return Post.query.filter_by(thread_id=self.id).order_by( desc('time_created')).first() def get_author(self): return self.author.username def get_votes(self): """Return sum of all votes on all posts.""" return sum([ Upvote.query.filter_by(post_id=post.id, vote=1).count() for post in self.posts ]) def get_views(self): """Return sum of all views on all posts.""" raise NotImplementedError("We currently don't have this feature.")
class GameState(Base): __abstract__ = True id = db.Column(db.Integer, primary_key=True, autoincrement=True, nullable=False) time_created = db.Column(db.DateTime, default=datetime.utcnow) time_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class Base(object): @declared_attr def __tablename__(cls): return to_var_name(cls.__name__) # pluralize? __table_args__ = {'mysql_engine': 'InnoDB'} id = db.Column(db.Integer, primary_key=True)
class Session(GameEvent): time_logged_out = db.Column(db.DateTime) user_id = db.Column(db.Integer) county_day = db.Column(db.Integer) minutes = db.Column(db.Integer) def __init__(self, user_id, county_day): self.user_id = user_id self.county_day = county_day self.seconds = None self.valid = True def set_minutes(self): if self.time_logged_out == self.time_created: self.minutes = 0 else: self.minutes = (self.time_logged_out - self.time_created).seconds // 60
class BlacklistToken(GameState): """Token model for storing JWT tokens.""" token = db.Column(db.String(500), unique=True, nullable=False) blacklisted_on = db.Column(db.DateTime, nullable=False) def __init__(self, token): self.token = token self.blacklisted_on = datetime.datetime.utcnow() @staticmethod def check_blacklist(auth_token): """Check whether an auth token has been blacklisted.""" res = BlacklistToken.query.filter_by(token=str(auth_token)).first() if res: return True else: return False def __repr__(self): return f'<id: token: {self.token}'
class Post(GameEvent): thread_id = db.Column(db.Integer, db.ForeignKey('thread.id'), nullable=False) parent_post_id = db.Column(db.Integer) # 0 if this is the parent title = db.Column(db.String(32)) content = db.Column(db.String(5000)) author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) votes = db.Column(db.Integer) author = db.relationship("User") def __init__(self, thread_id, author_id, title, content, parent_post_id): self.thread_id = thread_id self.author_id = author_id self.title = title self.content = content self.parent_post_id = parent_post_id self.votes = 0 def get_reply_count(self): replies = Post.query.filter_by(parent_post_id=self.id).all() return len(replies) def get_replies(self): return Post.query.filter_by(parent_post_id=self.id).all() def get_most_recent_reply(self): post = Post.query.filter_by(parent_post_id=self.id).order_by( desc('time_created')).first() if post is None: return self return post def get_author(self): return self.author.username def get_votes(self): return Upvote.query.filter_by(post_id=self.id, vote=1).count() def get_vote_status(self, user_id): """Return 1 for voted and 0 for not voted. If no vote exists then vote is 0. This uses integers rather than booleans because it makes math easier. """ vote = Upvote.query.filter_by(post_id=self.id, user_id=user_id).first() try: return vote.vote except AttributeError: return 0
class DAU(GameEvent): # User data user_id = db.Column(db.Integer) county_id = db.Column(db.Integer) account_age_in_days = db.Column(db.Integer) sessions = db.Column(db.Integer) minutes_played = db.Column(db.Integer) ads_watched = db.Column(db.Integer) # Game data world_day = db.Column(db.Integer) county_day = db.Column(db.Integer) score = db.Column(db.Integer) land = db.Column(db.Integer) population = db.Column(db.Integer) happiness = db.Column(db.Integer) healthiness = db.Column(db.Integer) # Resources gold = db.Column(db.Integer) wood = db.Column(db.Integer) iron = db.Column(db.Integer) stone = db.Column(db.Integer) research = db.Column(db.Integer) mana = db.Column(db.Integer) grain = db.Column(db.Integer) lifetime_gold = db.Column(db.Integer) lifetime_wood = db.Column(db.Integer) lifetime_iron = db.Column(db.Integer) lifetime_stone = db.Column(db.Integer) lifetime_research = db.Column(db.Integer) lifetime_mana = db.Column(db.Integer) # Choices production_choice = db.Column(db.Integer) research_choice = db.Column(db.String(128)) rations = db.Column(db.Float) taxes = db.Column(db.Integer) technologies = db.Column(db.Integer) # Military data peasant = db.Column(db.Integer) soldier = db.Column(db.Integer) archer = db.Column(db.Integer) besieger = db.Column(db.Integer) summon = db.Column(db.Integer) elite = db.Column(db.Integer) monster = db.Column(db.Integer) maximum_offense = db.Column(db.Integer) maximum_defence = db.Column(db.Integer) # Building Data house = db.Column(db.Integer) field = db.Column(db.Integer) pasture = db.Column(db.Integer) mill = db.Column(db.Integer) mine = db.Column(db.Integer) fort = db.Column(db.Integer) stables = db.Column(db.Integer) bank = db.Column(db.Integer) tavern = db.Column(db.Integer) tower = db.Column(db.Integer) lab = db.Column(db.Integer) arcane = db.Column(db.Integer) quarry = db.Column(db.Integer) lair = db.Column(db.Integer) def __init__(self, user_id): self.user_id = user_id self.update_self(user_id) self.sessions = self.get_sessions(user_id) self.minutes_played = self.get_minutes_played(user_id) self.ads_watched = 0 def update_self(self, user_id): user = User.query.get(user_id) county = user.county infrastructure = county.infrastructure self.world_day = county.kingdom.world.day self.county_day = county.day self.account_age_in_days = (datetime.utcnow() - user.time_created).days self.county_id = county.id self.land = county.land self.population = county.population self.gold = county.gold self.wood = county.wood self.iron = county.iron self.stone = county.stone self.research = county.research self.mana = county.mana self.grain = county.grain_stores self.lifetime_gold = county.lifetime_gold self.lifetime_wood = county.lifetime_wood self.lifetime_iron = county.lifetime_iron self.lifetime_stone = county.lifetime_stone self.lifetime_research = county.lifetime_research self.lifetime_mana = county.lifetime_mana self.happiness = county.happiness self.healthiness = county.healthiness self.production_choice = county.production_choice try: self.research_choice = county.research_choice.name except AttributeError: self.research_choice = None self.rations = county.rations self.taxes = county.tax_rate self.technologies = len(county.technologies) self.peasant = county.armies['peasant'].total self.soldier = county.armies['soldier'].total self.archer = county.armies['archer'].total self.besieger = county.armies['besieger'].total self.summon = county.armies['summon'].total self.elite = county.armies['elite'].total self.monster = county.armies['monster'].total self.maximum_offense = county.get_offensive_strength(scoreboard=True) self.maximum_defence = county.get_defensive_strength() self.house = infrastructure.buildings['house'].total self.field = infrastructure.buildings['field'].total self.pasture = infrastructure.buildings['pasture'].total self.mill = infrastructure.buildings['mill'].total self.mine = infrastructure.buildings['mine'].total self.fort = infrastructure.buildings['fort'].total self.stables = infrastructure.buildings['stables'].total self.bank = infrastructure.buildings['bank'].total self.tavern = infrastructure.buildings['tavern'].total self.tower = infrastructure.buildings['tower'].total self.lab = infrastructure.buildings['lab'].total self.arcane = infrastructure.buildings['arcane'].total self.quarry = infrastructure.buildings['quarry'].total self.lair = infrastructure.buildings['lair'].total @staticmethod def get_sessions(user_id): time_cutoff = datetime.utcnow() - timedelta(hours=1) return Session.query.filter_by(user_id=user_id).filter( Session.time_logged_out > time_cutoff).count() @staticmethod def get_minutes_played(user_id): time_cutoff = datetime.utcnow() - timedelta(hours=1) sessions = Session.query.filter_by(user_id=user_id).filter( Session.time_logged_out > time_cutoff).all() return sum(session.minutes for session in sessions if session.minutes is not None)
class Achievement(GameEvent): user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete="CASCADE"), nullable=False) name = db.Column(db.String(64)) # Unique name for mapping purposes display_title = db.Column( db.String(64)) # The name of the achievement the user sees category = db.Column( db.String(64)) # A way to group it for coding purposes sub_category = db.Column( db.String(64)) # A way to identify it from its group description = db.Column(db.String(64)) # For the user to read current_tier = db.Column(db.Integer) # What level they are on. Starts at 0 maximum_tier = db.Column(db.Integer) # The highest level they can reach points_rewarded = db.Column(db.Integer) # Points rewarded at each level tier1 = db.Column(db.Integer) tier2 = db.Column(db.Integer) tier3 = db.Column(db.Integer) tier4 = db.Column(db.Integer) tier5 = db.Column(db.Integer) def __init__(self, name, display_title=None, category=None, sub_category=None, description=None, points_rewarded=0, maximum_tier=0, tier1=None, tier2=None, tier3=None, tier4=None, tier5=None): self.name = name self.display_title = display_title self.category = category self.sub_category = sub_category self.description = description self.current_tier = 0 self.maximum_tier = maximum_tier self.points_rewarded = points_rewarded self.tier1 = tier1 self.tier2 = tier2 self.tier3 = tier3 self.tier4 = tier4 self.tier5 = tier5 def get_earned_required_amount_message(self): if self.current_tier == 0: return "You have made no progress on this achievement." return f"Last reward at {getattr(self, 'tier' + str(self.current_tier))} {self.name}." def get_next_required_amount_message(self): if self.current_tier == self.maximum_tier: return "You have unlocked all tiers." return f"Next reward at {getattr(self, 'tier' + str(self.current_tier + 1))} {self.name}."
class Army(GameState): # These would work better as integers but I'd have to modify a lot of code. PEASANT = 0 SOLDIER = 1 ARCHER = 2 BESIEGER = 3 ELITE = 4 MONSTER = 5 SUMMON = 6 TYPES = dict(peasant=PEASANT, soldier=SOLDIER, archer=ARCHER, besieger=BESIEGER, elite=ELITE, monster=MONSTER, summon=SUMMON) county_id = db.Column(db.Integer, db.ForeignKey('county.id', ondelete="CASCADE"), nullable=False) name = db.Column(db.String(64)) class_name = db.Column(db.String(64)) class_name_plural = db.Column(db.String(64)) total = db.Column(db.Integer) traveling = db.Column(db.Integer) currently_training = db.Column(db.Integer) trainable_per_day = db.Column(db.Integer) gold = db.Column(db.Integer) wood = db.Column(db.Integer) iron = db.Column(db.Integer) upkeep = db.Column(db.Integer) category = db.Column(db.String(32)) attack = db.Column(db.Integer) defence = db.Column(db.Integer) _health = db.Column(db.Integer) armour_type = db.Column(db.String(32)) description = db.Column(db.String(128)) ability = db.Column(db.String(32)) ability_description = db.Column(db.String(128)) @property def type(self): """Return an int representing the unit type. This will later allow comparison with constants such as: if unit.type == unit.BESIEGER: do_something_to_siege_units() This helps prevent typos. """ return self.TYPES[self.name] @property def key(self): return self.name def __init__(self, name, class_name, class_name_plural, total, trainable_per_day, gold, wood, iron, upkeep, category, attack, defence, health, armour_type, description, ability, ability_description): self.name = name self.class_name = class_name self.class_name_plural = class_name_plural self.total = total self.traveling = 0 self.currently_training = 0 self.trainable_per_day = trainable_per_day # Number than can train per game-day self.gold = gold self.wood = wood self.iron = iron self.upkeep = upkeep self.category = category self.attack = attack self.defence = defence self.health = health self.armour_type = armour_type self.description = description self.ability = ability self.ability_description = ability_description @property def available(self): return self.total - self.traveling @property def health(self): """Health can't go below 0.""" return max(self._health, 1) @health.setter def health(self, value): self._health = value
class World(GameState): kingdoms = db.relationship('Kingdom', backref='world') age = db.Column(db.Integer) # How many 'reset events' day = db.Column(db.Integer) # How many in-game days have passed this age season = db.Column(db.String(16)) # The current season def __init__(self): self.age = 1 self.day = 0 self.season = seasons[0] def advance_day(self): if self.day >= 0: for county in County.query.all(): if county.user.is_bot: county.temporary_bot_tweaks() else: county.advance_day() for kingdom in Kingdom.query.all(): kingdom.advance_day() self.day += 1 if self.day % 36 == 0: # Every 36 game days we advance the game season season_index = seasons.index(self.season) + 1 if season_index == len(seasons): season_index = 0 self.season = seasons[season_index] def advance_analytics(self): if self.day < 0: return users = User.query.filter_by(is_bot=False).filter( User.county != None).all() for user in users: # First check and set their retention user_age = (datetime.utcnow() - user.time_created).days if user.get_last_logout() and user.get_last_logout().date( ) == datetime.today().date(): retention = 1 else: retention = 0 user.day1_retention = 1 if user_age == 1: user.day1_retention = retention elif user_age == 3: user.day3_retention = retention elif user_age == 7: user.day7_retention = retention # If they are playing this age, create a DAU for them if user.county: dau_event = DAU(user.id) dau_event.save() self.export_data_to_csv() def advance_age(self): kingdoms = Kingdom.query.all() counties = County.query.all() users = User.query.all() for user in users: if user.county: user.ages_completed += 1 for kingdom in kingdoms: kingdom.leader = 0 kingdom.wars_total_ta = 0 kingdom.wars_won_ta = 0 kingdom.approval_rating = Kingdom.BASE_APPROVAL_RATING kingdom.save() winning_kingdoms = [ sorted(kingdoms, key=lambda x: x.wars_won_ta, reverse=True)[0], sorted(kingdoms, key=lambda x: x.total_land_of_top_three_counties, reverse=True)[0] ] winning_county = sorted(counties, key=lambda x: x.land, reverse=True)[0] for kingdom in winning_kingdoms: for county in kingdom.counties: county.user.gems += 1 winning_county.user.gems += 1 tables = [ 'DAU', 'army', 'building', 'casting', 'chatroom', 'diplomacy', 'economy', 'espionage', 'expedition', 'infiltration', 'infrastructure', 'magic', 'message', 'military', 'notification', 'preferences', 'scientist', 'session', 'technology_to_technology', 'technology', 'trade', 'transaction', 'wizardry', 'county' ] table_mixers.drop_then_rebuild_tables(db, tables) self.age += 1 self.day = -24 def export_data_to_csv(self): all_tables = [] table_name_reference = [] tables = db.metadata.tables table_names = db.engine.table_names() for name in table_names: # Each table is a list inside the list new_table = [] table_name_reference.append(name) if name not in {}: # Each row will be a list inside the table list, inside the all_tables list table = tables[name] header_row = [] for column in table.columns: header_row.append(column.name) new_table.append(header_row) for row in db.session.query(table).all(): normal_row = [] for value in row: normal_row.append(value) new_table.append(normal_row) all_tables.append(new_table) # We have a list of smaller lists. Each smaller list should be a csv file (each one is a separate sql table) current_path = f"export/age-{self.age}" if not os.path.exists(current_path): os.makedirs(current_path) for index, table in enumerate(all_tables): if len(table) < 2: continue headers = table.pop(0) name = table_name_reference[index] filename = f"{current_path}/{name}_table.csv" df = DataFrame(table) df.to_csv(filename, header=headers) def __repr__(self): return '<World %r (%r)>' % ('id:', self.id)
class GameEvent(Base): __abstract__ = True id = db.Column(db.Integer, primary_key=True) time_created = db.Column(db.DateTime, default=datetime.utcnow)