Example #1
0
class CardString(models.Model):
    id = models.AutoField(primary_key=True, serialize=False, verbose_name="ID")
    card = models.ForeignKey("Card",
                             to_field="dbf_id",
                             db_column="card_dbf_id",
                             related_name="strings",
                             on_delete=models.CASCADE)
    locale = IntEnumField(enum=enums.Locale, db_index=True)
    game_tag = IntEnumField(enum=enums.GameTag, db_index=True)
    value = models.TextField(blank=True)

    def __str__(self):
        return self.value
Example #2
0
class Archetype(models.Model):
    """
	Archetypes cluster decks with minor card variations that all share the same strategy
	into a common group.

	E.g. 'Freeze Mage', 'Miracle Rogue', 'Pirate Warrior', 'Zoolock', 'Control Priest'
	"""

    id = models.BigAutoField(primary_key=True)
    objects = ArchetypeManager()
    name = models.CharField(max_length=250, blank=True)
    player_class = IntEnumField(enum=enums.CardClass,
                                default=enums.CardClass.INVALID)

    def get_canonical_decks(self,
                            format=enums.FormatType.FT_STANDARD,
                            as_of=None):
        if as_of is None:
            canonicals = self.canonical_decks.filter(format=format, ).order_by(
                "-created").prefetch_related("deck__includes").all()
        else:
            canonicals = self.canonical_decks.filter(
                format=format, created__lte=as_of).order_by(
                    "-created").prefetch_related("deck__includes").all()

        if canonicals:
            return [c.deck for c in canonicals]
        else:
            return None

    def __str__(self):
        return self.name
Example #3
0
class CanonicalDeck(models.Model):
	"""
	The CanonicalDeck for an Archetype is the list of cards that is most commonly
	associated with that Archetype.

	The canonical deck for an Archetype may evolve incrementally over time and is likely to
	evolve more rapidly when new card sets are first released.
	"""

	id = models.BigAutoField(primary_key=True)
	archetype = models.ForeignKey(
		Archetype,
		related_name="canonical_decks",
		on_delete=models.CASCADE
	)
	deck = models.ForeignKey(
		Deck,
		related_name="canonical_for_archetypes",
		on_delete=models.PROTECT
	)
	created = models.DateTimeField(auto_now_add=True)
	format = IntEnumField(enum=enums.FormatType, default=enums.FormatType.FT_STANDARD)

	class Meta:
		db_table = "cards_canonicaldeck"
Example #4
0
class PegasusAccount(models.Model):
	id = models.BigAutoField(primary_key=True)

	account_hi = models.BigIntegerField(
		"Account Hi",
		help_text="The region value from account hilo"
	)
	account_lo = models.BigIntegerField(
		"Account Lo",
		help_text="The account ID value from account hilo"
	)
	region = IntEnumField(enum=BnetRegion)
	battletag = models.CharField(max_length=64, blank=True)

	user = models.ForeignKey(
		settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True
	)
	created = models.DateTimeField(auto_now_add=True)
	modified = models.DateTimeField(auto_now=True)

	class Meta:
		unique_together = ("account_hi", "account_lo")

	def __str__(self):
		region = REGIONS.get(self.region, "Dev. region")
		return "%s - %s" % (self.battletag, region)
Example #5
0
class CardTag(models.Model):
    id = models.AutoField(primary_key=True, serialize=False, verbose_name="ID")
    card = models.ForeignKey("Card",
                             to_field="dbf_id",
                             db_column="card_dbf_id",
                             related_name="tags",
                             on_delete=models.CASCADE)
    game_tag = IntEnumField(enum=enums.GameTag, db_index=True)
    value = models.PositiveIntegerField()

    def __str__(self):
        return f"{str(self.card)}.{self.game_tag.name}={str(self.value)}"
Example #6
0
class Pack(models.Model):
    id = models.BigAutoField(primary_key=True)
    booster_type = IntEnumField(enum=Booster)
    date = models.DateTimeField()
    cards = models.ManyToManyField(Card, through="packs.PackCard")
    user = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
    account_hi = models.BigIntegerField()
    account_lo = models.BigIntegerField()

    def __str__(self):
        cards = self.cards.all()
        if not cards:
            return "(Empty pack)"
        return ", ".join(str(card) for card in cards)
Example #7
0
class Scenario(models.Model):
    id = models.AutoField(primary_key=True, serialize=False, verbose_name="ID")
    note_desc = models.CharField(max_length=64)
    players = models.PositiveSmallIntegerField()
    player1_hero_card_id = models.IntegerField(null=True)
    player2_hero_card_id = models.IntegerField(null=True)
    is_tutorial = models.BooleanField(default=False)
    is_expert = models.BooleanField(default=False)
    is_coop = models.BooleanField(default=False)
    adventure = models.ForeignKey(Adventure,
                                  models.SET_NULL,
                                  null=True,
                                  blank=True)
    wing = models.ForeignKey(Wing, models.SET_NULL, null=True, blank=True)
    sort_order = models.PositiveIntegerField(default=0)
    mode = IntEnumField(enum=AdventureMode, default=0)
    client_player2_hero_card_id = models.IntegerField(null=True)
    name = models.CharField(max_length=64)
    description = models.TextField()
    opponent_name = models.CharField(max_length=64)
    completed_description = models.TextField()
    player1_deck_id = models.IntegerField(null=True)

    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    build = models.PositiveIntegerField()

    dbf_columns = [
        "ID",
        "NOTE_DESC",
        "PLAYERS",
        "PLAYER1_HERO_CARD_ID",
        "IS_TUTORIAL",
        "IS_EXPERT",
        "IS_COOP",
        "ADVENTURE_ID",
        "WING_ID",
        "SORT_ORDER",
        ("MODE_ID", "mode"),
        "CLIENT_PLAYER2_HERO_CARD_ID",
        "NAME",
        "DESCRIPTION",
        "OPPONENT_NAME",
        "COMPLETED_DESCRIPTION",
        "PLAYER1_DECK_ID",
    ]

    def __str__(self):
        return self.name or self.note_desc
Example #8
0
class Signature(models.Model):
	id = models.AutoField(primary_key=True)
	archetype = models.ForeignKey(
		Archetype,
		on_delete=models.CASCADE
	)
	format = IntEnumField(enum=enums.FormatType, default=enums.FormatType.FT_STANDARD)
	as_of = models.DateTimeField()

	def distance(self, deck):
		dist = 0
		card_counts = {i.card: i.count for i in deck.includes.all()}
		for component in self.components.all():
			if component.card in card_counts:
				dist += (card_counts[component.card] * component.weight)
		return dist
Example #9
0
class Pack(models.Model):
    id = models.BigAutoField(primary_key=True)
    booster_type = IntEnumField(enum=Booster)
    date = models.DateTimeField()
    cards = models.ManyToManyField(Card, through="packs.PackCard")
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
                             on_delete=models.SET_NULL,
                             null=True,
                             blank=True,
                             related_name="packs")
    blizzard_account = models.ForeignKey("accounts.BlizzardAccount",
                                         on_delete=models.CASCADE,
                                         related_name="packs")

    def __str__(self):
        cards = self.cards.all()
        if not cards:
            return "(Empty pack)"
        return ", ".join(str(card) for card in cards)
Example #10
0
class ArchetypeName(models.Model):
    id = models.BigAutoField(primary_key=True)
    objects = ArchetypeNameManager()
    name = models.CharField(max_length=250, blank=False)
    contributing_user = models.ForeignKey(settings.AUTH_USER_MODEL,
                                          on_delete=models.SET_NULL,
                                          blank=True,
                                          null=True)
    created = models.DateTimeField(default=timezone.now)
    player_class = IntEnumField(enum=enums.CardClass,
                                default=enums.CardClass.INVALID)

    class Meta:
        # If only name had unique=True, users could submit wrong feedback (e.g. Zoolock on a
        # Warrior Deck) and the "Zoolock" name would be marked as WARRIOR for ever.
        unique_together = ("name", "player_class")

    def __str__(self):
        return self.name
Example #11
0
class GameReplay(models.Model):
    """
	Represents a replay as captured from the point of view of a single
	packet stream sent to a Hearthstone client.

	Replays can be uploaded by either of the players or by any number
	of spectators who watched the match. It is possible
	that the same game could be uploaded from multiple points of view.
	When this happens each GameReplay will point
	to the same GlobalGame record via the global_game foreign key.

	It is possible that different uploads of the same game will have
	different information in them.
	For example:
	- If Player A and Player B are Real ID Friends and Player C is
	Battle.net friends with just Player B, then when Player C spectates
	a match between Players A and B, his uploaded replay will show the
	BattleTag as the name of Player A. However if Player B uploads a
	replay of the same match, his replay will show the real name for
	Player A.

	- Likewise, if Player C either starts spectating the game after it has
	already begun or stops spectating before it ends, then his uploaded
	replay will have fewer turns of gameplay then Player B's replay.
	"""
    class Meta:
        # ordering = ("global_game", )
        # Ordering on global_game causes nasty inner joins.
        # We order by descending ID instead for now, until we have an upload_date.
        ordering = ("-id", )

        # Replays are unique to a perspective on the game (global_game):
        # - client_handle: the *same* client cant be used for multiple replays
        # - reconnecting: We do not currently unify games where the player reconnects
        # - friendly_player_id: Unique across a client_handle and a spectator_mode
        unique_together = ("global_game", "client_handle",
                           "friendly_player_id", "spectator_mode",
                           "reconnecting")

    id = models.BigAutoField(primary_key=True)
    shortid = ShortUUIDField("Short ID")
    upload_token = models.ForeignKey(AuthToken,
                                     on_delete=models.SET_NULL,
                                     null=True,
                                     blank=True,
                                     related_name="replays")
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
                             on_delete=models.SET_NULL,
                             null=True,
                             blank=True,
                             related_name="replays")
    global_game = models.ForeignKey(
        GlobalGame,
        on_delete=models.CASCADE,
        related_name="replays",
        help_text="References the single global game that this replay shows.")

    # The "friendly player" is the player whose cards are at the bottom of the
    # screen when watching a game. For spectators this is determined by which
    # player they started spectating first (if they spectate both).
    friendly_player_id = PlayerIDField(
        "Friendly PlayerID",
        null=True,
        help_text="PlayerID of the friendly player (1 or 2)",
    )

    # When serving replay data in the security context of the user who uploaded this replay
    # this deck list should be used for the opponent, so as to avoid revealing unplayed cards
    # in the opponent's deck, which we might know in the case where multiple uploads were
    # unified. If the other uploader has set their replay visibility to private, then we cannot
    # leak the cards in their deck, via the globalgameplayer.
    opponent_revealed_deck = models.ForeignKey(
        "decks.Deck",
        on_delete=models.PROTECT,
        null=True,
        help_text="As much as is known of the opponent's starting deck list.")

    # This is useful to know because replays that are spectating both players
    # will have more data then those from a single player.
    # For example, they will have access to the cards that are in each players hand.
    # This is detectable from the raw logs, although we currently intend to have
    # the client uploading the replay provide it.
    spectator_mode = models.BooleanField(default=False)
    spectator_password = models.CharField("Spectator Password",
                                          max_length=16,
                                          blank=True)
    client_handle = models.IntegerField(null=True, blank=True)
    aurora_password = models.CharField(max_length=16, blank=True)

    build = models.PositiveIntegerField("Hearthstone Build",
                                        null=True,
                                        blank=True)

    replay_xml = models.FileField(upload_to=generate_upload_path)
    hsreplay_version = models.CharField(
        "HSReplay library version",
        max_length=16,
        help_text="The HSReplay library version used to generate the replay",
    )
    hslog_version = models.CharField(
        "hslog library version",
        max_length=24,
        help_text="The python-hearthstone library version at processing",
        null=True,  # TODO: Remove this once the table is clean of NULLs
    )
    upload_ip = models.GenericIPAddressField(null=True,
                                             help_text="Uploader IP address")
    user_agent = models.CharField(max_length=100,
                                  null=True,
                                  help_text="Uploader User-Agent")

    # The fields below capture the preferences of the user who uploaded it.
    is_deleted = models.BooleanField(
        "Soft deleted",
        default=False,
        help_text="Indicates user request to delete the upload")

    won = models.NullBooleanField()
    disconnected = models.BooleanField(default=False)
    reconnecting = models.BooleanField(
        "Is reconnecting",
        default=False,
        help_text="Whether the player is reconnecting to an existing game",
    )
    resumable = models.NullBooleanField()

    visibility = IntEnumField(enum=Visibility, default=Visibility.Public)
    hide_player_names = models.BooleanField(default=False)

    views = models.PositiveIntegerField(default=0)
    objects = GameReplayManager()

    def __str__(self):
        return str(self.global_game)

    @property
    def pretty_name(self):
        return self.build_pretty_name()

    @property
    def pretty_name_spoilerfree(self):
        return self.build_pretty_name(spoilers=False)

    @property
    def upload_event_admin_url(self):
        from hsreplaynet.uploads.models import UploadEvent
        # These get garbage collected periodically
        # So this will not exist for old games
        upload_event = UploadEvent.objects.filter(shortid=self.shortid)
        if upload_event.count() > 0:
            return upload_event.first().get_admin_url()
        else:
            return None

    def build_pretty_name(self, spoilers=True):
        players = self.global_game.players.values_list("player_id",
                                                       "final_state", "name")
        if len(players) != 2:
            return "Broken game (%i players)" % (len(players))
        if players[0][0] == self.friendly_player_id:
            friendly, opponent = players
        else:
            opponent, friendly = players
        if spoilers:
            if self.disconnected:
                state = "Disconnected"
            elif self.won:
                state = "Won"
            elif friendly[1] == opponent[1]:
                state = "Tied"
            else:
                state = "Lost"
            return "%s (%s) vs. %s" % (friendly[2], state, opponent[2])
        return "%s vs. %s" % (friendly[2], opponent[2])

    def get_absolute_url(self):
        return reverse("games_replay_view", kwargs={"id": self.shortid})

    def generate_description(self):
        tpl = "Watch a game of Hearthstone between %s (%s) and %s (%s) in your browser."
        players = self.global_game.players.all()
        if len(players) != 2:
            return ""
        player1, player2 = players[0], players[1]
        return tpl % (player1, player1.hero.card_class.name.capitalize(),
                      player2, player2.hero.card_class.name.capitalize())

    def player(self, number):
        for player in self.global_game.players.all():
            if player.player_id == number:
                return player

    @property
    def friendly_player(self):
        return self.global_game.players.get(player_id=self.friendly_player_id)

    @property
    def friendly_deck(self):
        return self.friendly_player.deck_list

    @property
    def opposing_player(self):
        return self.global_game.players.exclude(
            player_id=self.friendly_player_id).get()

    @property
    def opposing_deck(self):
        return self.opponent_revealed_deck

    @property
    def region(self):
        return self.friendly_player.pegasus_account.region

    def serialize(self):
        from .processing import get_replay_url
        from .serializers import GameReplaySerializer

        s = GameReplaySerializer(self)
        serialized = s.data
        serialized["url"] = get_replay_url(self.shortid)

        return serialized
Example #12
0
class GlobalGame(models.Model):
    """
	Represents a globally unique game (e.g. from the server's POV).

	The fields on this object represent information that is public
	to all players and spectators. When the same game is uploaded
	by multiple players or spectators they will all share a
	reference to a single global game.

	When a replay or raw log file is uploaded the server first checks
	for the existence of a GlobalGame record. It looks for any games
	that occured on the same region where both players have matching
	battle_net_ids and where the match start timestamp is within +/- 1
	minute from the timestamp on the upload.
	The +/- range on the match start timestamp is to account for
	potential clock drift between the computer that generated this
	replay and the computer that uploaded the earlier record which
	first created the GlobalGame record. If no existing GlobalGame
	record is found, then one is created.
	"""

    id = models.BigAutoField(primary_key=True)

    # We believe game_id is not monotonically increasing as it appears
    # to roll over and reset periodically.
    game_handle = models.IntegerField(
        "Game handle",
        null=True,
        blank=True,
        help_text="Game ID on the Battle.net server")
    server_address = models.GenericIPAddressField(null=True, blank=True)
    server_port = models.IntegerField(null=True, blank=True)
    server_version = models.IntegerField(null=True, blank=True)

    build = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Hearthstone build number the game was played on.")

    match_start = models.DateTimeField(null=True, db_index=True)
    match_end = models.DateTimeField(null=True)

    game_type = IntEnumField("Game type",
                             enum=BnetGameType,
                             null=True,
                             blank=True)
    format = IntEnumField("Format type",
                          enum=FormatType,
                          default=FormatType.FT_UNKNOWN)

    # ladder_season is nullable since not all games are ladder games
    ladder_season = models.IntegerField(
        "Ladder season",
        null=True,
        blank=True,
        help_text="The season as calculated from the match start timestamp.")

    # Nullable, since not all replays are TBs.
    # Will currently have no way to calculate this so it will always be null for now.
    brawl_season = models.IntegerField(
        "Tavern Brawl season",
        default=0,
        help_text=
        "The brawl season which increments every time the brawl changes.")

    # Nullable, We currently have no way to discover this.
    scenario_id = models.IntegerField(
        "Scenario ID",
        null=True,
        blank=True,
        db_index=True,
        help_text="ID from DBF/SCENARIO.xml or Scenario cache",
    )

    # The following basic stats are globally visible to all
    num_turns = models.IntegerField(null=True, blank=True)
    num_entities = models.IntegerField(null=True, blank=True)

    tainted_decks = models.NullBooleanField()

    digest = models.CharField(
        max_length=40,
        unique=True,
        null=True,
        help_text=
        "SHA1 of str(game_handle), str(server_address), str(lo1), str(lo2)")
    loaded_into_redshift = models.DateTimeField(null=True)

    class Meta:
        ordering = ("-match_start", )

    @property
    def exclude_from_statistics(self):
        return any(
            map(lambda r: r.user and r.user.exclude_from_statistics,
                self.replays.all()))

    def __str__(self):
        return " vs ".join(str(p) for p in self.players.all())

    @property
    def duration(self):
        return self.match_end - self.match_start

    @property
    def is_ranked(self):
        return self.game_type in (
            BnetGameType.BGT_RANKED_WILD,
            BnetGameType.BGT_RANKED_STANDARD,
        )

    @property
    def is_casual(self):
        return self.game_type in (
            BnetGameType.BGT_CASUAL_WILD,
            BnetGameType.BGT_CASUAL_STANDARD,
        )

    @property
    def is_tavern_brawl(self):
        return self.game_type in (
            BnetGameType.BGT_TAVERNBRAWL_PVP,
            BnetGameType.BGT_TAVERNBRAWL_1P_VERSUS_AI,
            BnetGameType.BGT_TAVERNBRAWL_2P_COOP,
        )

    @property
    def num_own_turns(self):
        return ceil(self.num_turns / 2)

    @property
    def format_friendly_name(self):
        if self.is_ranked:
            if self.format == FormatType.FT_STANDARD:
                return "Ranked - Standard"
            elif self.format == FormatType.FT_WILD:
                return "Ranked - Wild"
            return "Ranked"
        elif self.is_casual:
            if self.format == FormatType.FT_STANDARD:
                return "Casual - Standard"
            elif self.format == FormatType.FT_WILD:
                return "Casual - Wild"
            return "Casual"
        elif self.is_tavern_brawl:
            return "Tavern Brawl"
        elif self.game_type == BnetGameType.BGT_ARENA:
            return "Arena"
        elif self.game_type == BnetGameType.BGT_FRIENDS:
            return "Friendly Match"
        return ""

    def acquire_redshift_lock(self):
        if not self.id:
            raise RuntimeError("Cannot claim lock on unsaved instances.")
        return acquire_redshift_lock([self.id])

    def release_redshift_lock(self):
        if not self.id:
            raise RuntimeError("Cannot release lock on unsaved instances.")
        return release_redshift_lock([self.id])
Example #13
0
class GlobalGamePlayer(models.Model):
    id = models.BigAutoField(primary_key=True)
    game = models.ForeignKey(GlobalGame,
                             on_delete=models.CASCADE,
                             related_name="players")

    name = models.CharField("Player name",
                            blank=True,
                            max_length=64,
                            db_index=True)
    real_name = models.CharField("Real name", blank=True, max_length=64)

    player_id = PlayerIDField(blank=True)
    pegasus_account = models.ForeignKey("accounts.BlizzardAccount",
                                        on_delete=models.PROTECT)
    is_ai = models.BooleanField(
        "Is AI",
        default=False,
        help_text="Whether the player is an AI.",
    )
    is_first = models.BooleanField(
        "Is first player",
        help_text="Whether the player is the first player",
    )

    hero = models.ForeignKey(Card, on_delete=models.PROTECT)
    hero_premium = models.BooleanField(
        "Hero Premium",
        default=False,
        help_text="Whether the player's initial hero is golden.")

    final_state = IntEnumField(
        "Final State",
        enum=PlayState,
        default=PlayState.INVALID,
    )
    extra_turns = models.SmallIntegerField(
        null=True,
        help_text="Extra turns taken by the player this game (Time Warp).")

    deck_list = models.ForeignKey(
        "decks.Deck",
        on_delete=models.PROTECT,
        help_text="As much as is known of the player's starting deck list.")

    # Game type metadata

    rank = models.SmallIntegerField(
        "Rank",
        null=True,
        blank=True,
        help_text="1 through 25, or 0 for legend.",
    )
    legend_rank = models.PositiveIntegerField(null=True, blank=True)
    stars = models.PositiveSmallIntegerField(null=True, blank=True)
    wins = models.PositiveIntegerField(
        "Wins",
        null=True,
        blank=True,
        help_text=
        "Number of wins in the current game mode (eg. ladder season, arena key...)",
    )
    losses = models.PositiveIntegerField(
        "Losses",
        null=True,
        blank=True,
        help_text="Number of losses in the current game mode (current season)",
    )
    # Legacy deck id, mistakenly defined as 32-bit
    deck_id_legacy = models.IntegerField("Deck ID (OLD)",
                                         null=True,
                                         blank=True)
    deck_id = models.BigIntegerField("Deck ID", null=True, blank=True)
    cardback_id = models.IntegerField("Cardback ID", null=True, blank=True)

    class Meta:
        unique_together = ("game", "player_id")

    def __str__(self):
        return self.name or self.real_name

    @property
    def won(self):
        return self.final_state in (PlayState.WINNING, PlayState.WON)

    @property
    def opponent(self):
        opponent_id = 2 if self.player_id == 1 else 2
        try:
            player = GlobalGamePlayer.objects.get(game=self.game_id,
                                                  player_id=opponent_id)
        except GlobalGamePlayer.DoesNotExist:
            player = None
        return player

    @property
    def hero_class_name(self):
        return self.hero.card_class.name
Example #14
0
class Scenario(models.Model):
    note_desc = models.CharField(max_length=64)
    players = models.PositiveSmallIntegerField()
    player1_hero_card_id = models.IntegerField(null=True)
    player2_hero_card_id = models.IntegerField(null=True)
    is_tutorial = models.BooleanField(default=False)
    is_expert = models.BooleanField(default=False)
    is_coop = models.BooleanField(default=False)
    adventure = models.ForeignKey(Adventure,
                                  models.SET_NULL,
                                  null=True,
                                  blank=True)
    wing = models.ForeignKey(Wing, models.SET_NULL, null=True, blank=True)
    sort_order = models.PositiveIntegerField(default=0)
    mode = IntEnumField(enum=AdventureMode, default=0)
    client_player2_hero_card_id = models.IntegerField(null=True)
    name = models.CharField(max_length=64)
    description = models.TextField()
    opponent_name = models.CharField(max_length=64)
    completed_description = models.TextField()
    player1_deck_id = models.IntegerField(null=True)

    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    build = models.PositiveIntegerField()

    dbf_columns = [
        "ID",
        "NOTE_DESC",
        "PLAYERS",
        "PLAYER1_HERO_CARD_ID",
        "IS_TUTORIAL",
        "IS_EXPERT",
        "IS_COOP",
        "ADVENTURE_ID",
        "WING_ID",
        "SORT_ORDER",
        ("MODE_ID", "mode"),
        "CLIENT_PLAYER2_HERO_CARD_ID",
        "NAME",
        "DESCRIPTION",
        "OPPONENT_NAME",
        "COMPLETED_DESCRIPTION",
        "PLAYER1_DECK_ID",
    ]

    def __str__(self):
        return self.name or self.note_desc

    @staticmethod
    def ai_deck_list(scenario_id):
        """Return the AIs card list as determined across all games played."""
        deck = defaultdict(int)

        # Only examine this many games to make it perform faster
        sample_size = 20
        replays = GameReplay.objects.filter(
            global_game__scenario_id=scenario_id)[:sample_size]
        for replay in replays:
            for include in replay.opposing_player.deck_list.includes.all():
                card = include.card
                current_count = deck[card]
                if include.count > current_count:
                    deck[card] = include.count

        alpha_sorted = sorted(deck.keys(), key=lambda c: c.name)
        mana_sorted = sorted(alpha_sorted, key=lambda c: c.cost)

        result = []
        for card in mana_sorted:
            for i in range(0, deck[card]):
                result.append(card)

        return result

    @staticmethod
    def winning_decks(scenario_id):
        """
		Returns a list like:
		[
			{
				"deck": <Deck>,
				"num_wins": 23,
				# These are sorted in fastest order
				"fastest_wins": [<Replay>, <Replay>, ...]
			},
			{
				"deck": <Deck>,
				"num_wins": 19,
				# These are sorted in fastest order
				"fastest_wins": [<Replay>, <Replay>, ...]
			},
			...
		]

		The top level list elements are sorted by the deck with the most wins,
		and the "fastest_wins" element is sorted in order of the wins which
		took the least number of turns.
		"""
        complete_replays = []
        for replay in GameReplay.objects.filter(
                global_game__scenario_id=scenario_id).all():
            if replay.friendly_player.final_state == PlayState.WON:
                if replay.friendly_player.deck_list.size == 30:
                    complete_replays.append(replay)

        all_decks = defaultdict(dict)

        # Sort all the examples by match start so that the first example
        # of a win is selected if there are many.
        for replay in sorted(complete_replays,
                             key=lambda r: r.global_game.match_start):

            current_winning_deck = all_decks[replay.friendly_player.deck_list]
            if "num_wins" in current_winning_deck:
                current_winning_deck["num_wins"] += 1
            else:
                current_winning_deck["num_wins"] = 1

            if "fastest_wins" in current_winning_deck:
                fastest_wins = current_winning_deck["fastest_wins"]
            else:
                fastest_wins = defaultdict(list)
                current_winning_deck["fastest_wins"] = fastest_wins

            fastest_wins[replay.global_game.num_turns].append(replay)

        result = []

        for deck, meta in sorted(all_decks.items(),
                                 key=lambda t: t[1]["num_wins"],
                                 reverse=True):
            current_result = {
                "deck": deck,
                "num_wins": meta["num_wins"],
                "fastest_wins": []
            }

            for turns, replays in sorted(meta["fastest_wins"].items(),
                                         key=lambda t: t[0]):
                current_result["fastest_wins"].extend(replays)

            result.append(current_result)

        return result
Example #15
0
class Webhook(models.Model):
    uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
    endpoint = models.ForeignKey(WebhookEndpoint,
                                 null=True,
                                 on_delete=models.SET_NULL,
                                 related_name="webhooks")
    url = models.URLField()
    event = models.ForeignKey(Event,
                              null=True,
                              on_delete=models.SET_NULL,
                              related_name="webhooks")
    payload = JSONField(encoder=DjangoJSONEncoder)
    status = IntEnumField(enum=WebhookStatus, default=WebhookStatus.UNKNOWN)

    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    def __str__(self):
        return "%s -> %s" % (self.event, self.url)

    def schedule_delivery(self):
        """
		Schedule the webhook for delivery.

		On ENV_AWS, this schedules a Lambda trigger.
		Otherwise, triggers immediately.
		"""
        self.status = WebhookStatus.PENDING
        self.save()

        if settings.WEBHOOKS["USE_LAMBDA"]:
            from hsreplaynet.utils.aws.clients import LAMBDA
            LAMBDA.invoke(
                FunctionName="trigger_webhook",
                InvocationType="Event",
                Payload=json.dumps({"webhook": str(self.pk)}),
            )
        else:
            self.deliver()

    def deliver(self):
        if not self.endpoint:
            raise ForbiddenWebhookDelivery(
                "Cannot deliver a webhook with no endpoint.")

        if self.status != WebhookStatus.PENDING:
            raise ForbiddenWebhookDelivery("Not triggering for status %r" %
                                           (self.status))

        self.status = WebhookStatus.IN_PROGRESS
        self.save()

        secret = str(self.endpoint.secret).encode("utf-8")
        body = json.dumps(self.payload, cls=DjangoJSONEncoder).encode("utf-8")
        signature = generate_signature(secret, body)
        default_headers = {
            "content-type": WEBHOOK_CONTENT_TYPE,
            "user-agent": WEBHOOK_USER_AGENT,
            "x-webhook-signature": signature,
        }
        session = Session()
        request = session.prepare_request(
            Request("POST", self.url, headers=default_headers, data=body))

        delivery = WebhookDelivery(webhook=self,
                                   url=request.url,
                                   request_headers=dict(request.headers),
                                   request_body=body)
        begin = time.time()
        try:
            response = session.send(request,
                                    allow_redirects=False,
                                    timeout=self.endpoint.timeout)
        except Exception as e:
            delivery.success = False
            delivery.error = str(e)
            delivery.traceback = traceback.format_exc()
            delivery.response_headers = {}
            delivery.response_body = ""
            self.status = WebhookStatus.ERROR
        else:
            delivery.success = 200 <= response.status_code <= 299
            delivery.response_status = response.status_code
            delivery.response_headers = dict(response.headers)
            delivery.response_body = response.text[:RESPONSE_BODY_MAX_SIZE]
            self.status = WebhookStatus.SUCCESS

        delivery.completed_time = int((time.time() - begin) * 1000)
        delivery.save()
        self.save()
Example #16
0
class ClusterSetSnapshot(models.Model, ClusterSet):
    id = models.AutoField(primary_key=True)
    objects = ClusterSetManager()
    as_of = models.DateTimeField(default=timezone.now)
    game_format = IntEnumField(enum=enums.FormatType,
                               default=enums.FormatType.FT_STANDARD)
    live_in_production = models.BooleanField(default=False)
    promoted_on = models.DateTimeField(null=True)
    latest = models.BooleanField(default=False)
    training_run_id = models.IntegerField(null=True, blank=True)

    CLASS_CLUSTER_FACTORY = ClassClusterSnapshot
    CLUSTER_FACTORY = ClusterSnapshot

    class Meta:
        get_latest_by = "as_of"

    def __str__(self):
        return ClusterSet.__str__(self)

    @property
    def class_clusters(self):
        if not hasattr(self, "_class_clusters"):
            self._class_clusters = list(self.classclustersnapshot_set.all())
        return self._class_clusters

    @class_clusters.setter
    def class_clusters(self, cc):
        self._class_clusters = cc
        for class_cluster in cc:
            class_cluster.cluster_set = self

    @property
    def cluster_set_key_prefix(self):
        template = "models/{game_format}/{cluster_set_id}/{run_id}/"
        return template.format(
            game_format=self.game_format.name,
            cluster_set_id=self.id,
            run_id=self.training_run_id,
        )

    def update_all_signatures(self):
        for class_cluster in self.class_clusters:
            class_cluster.update_cluster_signatures()
            for cluster in class_cluster.clusters:
                cluster.save()

    def update_archetype_signatures(self, force=False):
        if force or all(c.neural_network_ready() for c in self.class_clusters):
            with transaction.atomic():
                ClusterSetSnapshot.objects.filter(
                    live_in_production=True).update(live_in_production=False)
                self.live_in_production = True
                self.promoted_on = now()
                self.save()
                self.synchronize_deck_archetype_assignments()
        else:
            msg = "Cannot promote to live=True because the neural network is not ready"
            raise RuntimeError(msg)

    def synchronize_deck_archetype_assignments(self):
        for_update = collections.defaultdict(list)

        for class_cluster in self.class_clusters:
            for cluster in class_cluster.clusters:
                if cluster.external_id and cluster.external_id != -1:
                    for data_point in cluster.data_points:
                        digest = Deck.objects.get_digest_from_shortid(
                            data_point["shortid"])
                        for_update[cluster.external_id].append(digest)

        for external_id, digests in for_update.items():
            deck_ids = Deck.objects.filter(digest__in=digests).values_list(
                "id", flat=True)
            Deck.objects.bulk_update_to_archetype(deck_ids, external_id)

    def train_neural_network(self,
                             num_examples=1000000,
                             max_dropped_cards=15,
                             stratified=False,
                             min_cards_for_determination=5,
                             batch_size=1000,
                             num_epochs=20,
                             base_layer_size=64,
                             hidden_layer_size=64,
                             num_hidden_layers=2,
                             working_dir=None,
                             upload_to_s3=False,
                             included_classes=None):
        start_ts = time.time()
        run_id = int(start_ts)
        self.training_run_id = run_id
        self.save()

        if working_dir:
            training_dir = working_dir
        else:
            training_dir = os.path.join(settings.BUILD_DIR, "models",
                                        str(run_id))

        if not os.path.exists(training_dir):
            os.mkdir(training_dir)

        summary_path = os.path.join(training_dir, "summary.txt")
        with open(summary_path, "w") as summary:
            summary.write("Game Format: %s\n" % self.game_format.name)
            summary.write("Cluster Set As Of: %s\n" % self.as_of.isoformat())
            summary.write("Training Run: %i\n\n" % run_id)

            summary.write("Num Examples: %i\n" % num_examples)
            summary.write("Max Dropped Cards: %i\n" % max_dropped_cards)
            summary.write("Stratified: %s\n" % str(stratified))
            summary.write("Min Cards For Determination: %i\n" %
                          min_cards_for_determination)
            summary.write("Batch Size: %i\n" % batch_size)
            summary.write("Num Epochs: %i\n" % num_epochs)
            summary.write("Base Layer Size: %i\n" % base_layer_size)
            summary.write("Hidden Layer Size: %i\n" % hidden_layer_size)
            summary.write("Num Hidden Layers: %i\n\n" % num_hidden_layers)

            for class_cluster in self.class_clusters:
                player_class_name = class_cluster.player_class.name

                if included_classes and player_class_name not in included_classes:
                    continue

                print("\nInitiating training for %s" %
                      class_cluster.player_class.name)
                training_start = time.time()
                accuracy = class_cluster.train_neural_network(
                    num_examples=num_examples,
                    max_dropped_cards=max_dropped_cards,
                    stratified=stratified,
                    min_cards_for_determination=min_cards_for_determination,
                    batch_size=batch_size,
                    num_epochs=num_epochs,
                    base_layer_size=base_layer_size,
                    hidden_layer_size=hidden_layer_size,
                    num_hidden_layers=num_hidden_layers,
                    working_dir=training_dir,
                    upload_to_s3=upload_to_s3)
                training_stop = time.time()
                duration = int(training_stop - training_start)
                print("Duration: %s seconds" % duration)
                print("Accuracy: %s" % round(accuracy, 4))

                summary.write("%s Duration: %i seconds\n" %
                              (player_class_name, duration))
                summary.write("%s Accuracy: %s\n\n" %
                              (player_class_name, round(accuracy, 4)))

            end_ts = time.time()
            full_duration = end_ts - start_ts
            duration_mins = int(full_duration / 60)
            duration_secs = int(full_duration % 60)
            summary.write("Full Duration: %i min(s) %i seconds\n" %
                          (duration_mins, duration_secs))

        if upload_to_s3 and os.path.exists(summary_path):
            with open(summary_path, "rb") as summary:
                S3.put_object(Bucket=settings.KERAS_MODELS_BUCKET,
                              Key=self.cluster_set_key_prefix + "summary.txt",
                              Body=summary)
Example #17
0
class ClassClusterSnapshot(models.Model, ClassClusters):
    id = models.AutoField(primary_key=True)
    cluster_set = models.ForeignKey("ClusterSetSnapshot",
                                    on_delete=models.CASCADE)
    player_class = IntEnumField(enum=enums.CardClass,
                                default=enums.CardClass.INVALID)

    def __str__(self):
        return ClassClusters.__str__(self)

    @property
    def clusters(self):
        if not hasattr(self, "_clusters"):
            self._clusters = list(self.clustersnapshot_set.all())
        return self._clusters

    @clusters.setter
    def clusters(self, clusters):
        self._clusters = clusters
        for cluster in clusters:
            cluster.class_cluster = self

    def _fetch_training_data(self,
                             num_examples=1000000,
                             max_dropped_cards=15,
                             stratified=False,
                             min_cards_for_determination=5):
        from hsarchetypes.features import to_neural_net_training_data
        key = (self.id, num_examples, max_dropped_cards, stratified,
               min_cards_for_determination)
        if key not in _TRAINING_DATA_CACHE:
            print("Constructing new training data: %s" % str(key))
            _TRAINING_DATA_CACHE[key] = to_neural_net_training_data(
                self,
                num_examples=num_examples,
                max_dropped_cards=max_dropped_cards,
                stratified=stratified,
                min_cards_for_determination=min_cards_for_determination)
        else:
            print("Serving data from cache: %s" % str(key))

        return _TRAINING_DATA_CACHE[key]

    def train_neural_network(self,
                             num_examples=1000000,
                             max_dropped_cards=15,
                             stratified=False,
                             min_cards_for_determination=5,
                             batch_size=1000,
                             num_epochs=20,
                             base_layer_size=64,
                             hidden_layer_size=64,
                             num_hidden_layers=2,
                             working_dir=None,
                             upload_to_s3=False):
        from hsarchetypes.classification import train_neural_net
        from hsarchetypes.utils import plot_accuracy_graph, plot_loss_graph
        common_prefix_template = "%s_%i_%i_%s"
        values = (
            self.cluster_set.game_format.name,
            self.cluster_set.id,
            self.cluster_set.training_run_id,
            self.player_class.name,
        )
        common_prefix = common_prefix_template % values
        full_model_path = os.path.join(working_dir,
                                       common_prefix + "_model.h5")
        train_x, train_Y = self._fetch_training_data(
            num_examples=num_examples,
            max_dropped_cards=max_dropped_cards,
            stratified=stratified,
            min_cards_for_determination=min_cards_for_determination)
        print("Finished generating training data")
        history = train_neural_net(train_x,
                                   train_Y,
                                   full_model_path,
                                   batch_size=batch_size,
                                   num_epochs=num_epochs,
                                   base_layer_size=base_layer_size,
                                   hidden_layer_size=hidden_layer_size,
                                   num_hidden_layers=num_hidden_layers)
        accuracy = history.history["val_acc"][-1] * 100
        vals = (self.player_class.name, accuracy)
        print("%s accuracy: %.2f%%\n" % vals)

        loss_file_path = os.path.join(working_dir, common_prefix + "_loss.png")
        plot_loss_graph(history, self.player_class.name, loss_file_path)

        accuracy_file_path = os.path.join(working_dir,
                                          common_prefix + "_accuracy.png")
        plot_accuracy_graph(history, self.player_class.name,
                            accuracy_file_path)

        if upload_to_s3:
            # The key structure for models in the bucket is as follows:
            # /models/<game_format>/<cluster_set_id>/<run_id>/<player_class>.h5
            # Which allows for easy listing of all the run_ids for a given snapshot

            # Within each run_id folder we expect:
            # A <player_class>.h5 file for each class
            # A summary.txt
            # A <player_class>_accuracy.png
            # A <player_class>_loss.png

            if os.path.exists(full_model_path):
                with open(full_model_path, "rb") as model:
                    S3.put_object(Bucket=settings.KERAS_MODELS_BUCKET,
                                  Key=self.model_key,
                                  Body=model)

            if os.path.exists(loss_file_path):
                with open(loss_file_path, "rb") as model:
                    S3.put_object(Bucket=settings.KERAS_MODELS_BUCKET,
                                  Key=self.loss_graph_key,
                                  Body=model)

            if os.path.exists(accuracy_file_path):
                with open(accuracy_file_path, "rb") as model:
                    S3.put_object(Bucket=settings.KERAS_MODELS_BUCKET,
                                  Key=self.accuracy_graph_key,
                                  Body=model)

        return accuracy

    def predict_archetype_id(self, deck):
        event = self._to_prediction_event(deck)

        if settings.USE_ARCHETYPE_PREDICTION_LAMBDA or settings.ENV_AWS:
            with influx_timer("callout_to_predict_deck_archetype"):
                response = LAMBDA.invoke(
                    FunctionName="predict_deck_archetype",
                    InvocationType="RequestResponse",  # Synchronous invocation
                    Payload=json.dumps(event),
                )
                if response[
                        "StatusCode"] == 200 and "FunctionError" not in response:
                    result = json.loads(
                        response["Payload"].read().decode("utf8"))
                else:
                    raise RuntimeError(
                        response["Payload"].read().decode("utf8"))
        else:
            from keras_handler import handler
            result = handler(event, None)

        predicted_class = result["predicted_class"]
        id_encoding = self.one_hot_external_ids(inverse=True)
        predicted_archetype_id = id_encoding[predicted_class]

        if predicted_archetype_id == -1:
            return None
        else:
            return predicted_archetype_id

    def _to_prediction_event(self, deck):
        from hsarchetypes.utils import to_prediction_vector_from_dbf_map
        prediction_vector = to_prediction_vector_from_dbf_map(deck.dbf_map())
        return {
            "model_bucket": settings.KERAS_MODELS_BUCKET,
            "model_key": self.model_key,
            "deck_vector": json.dumps(prediction_vector)
        }

    def neural_network_ready(self):
        return s3_object_exists(settings.KERAS_MODELS_BUCKET, self.model_key)

    @property
    def loss_graph_key(self):
        return self.common_key_prefix + "-loss.png"

    @property
    def accuracy_graph_key(self):
        return self.common_key_prefix + "-accuracy.png"

    @property
    def model_key(self):
        return self.common_key_prefix + ".h5"

    @property
    def common_key_prefix(self):
        return self.cluster_set.cluster_set_key_prefix + self.player_class.name
Example #18
0
class Archetype(models.Model):
    """
	Archetypes cluster decks with minor card variations that all share the same strategy
	into a common group.

	E.g. 'Freeze Mage', 'Miracle Rogue', 'Pirate Warrior', 'Zoolock', 'Control Priest'
	"""

    id = models.BigAutoField(primary_key=True)
    objects = ArchetypeManager()
    name = models.CharField(max_length=250, blank=True)
    player_class = IntEnumField(enum=enums.CardClass,
                                default=enums.CardClass.INVALID)
    deleted = models.BooleanField(default=False)

    class Meta:
        db_table = "cards_archetype"

    def __str__(self):
        return self.name

    @property
    def promoted_clusters(self):
        return ClusterSnapshot.objects.filter(
            class_cluster__player_class=self.player_class,
            external_id=self.id).exclude(
                class_cluster__cluster_set__promoted_on=None).order_by(
                    "-class_cluster__cluster_set__promoted_on")

    @property
    def standard_cluster(self):
        return ClusterSnapshot.objects.get_live_cluster_for_archetype(
            enums.FormatType.FT_STANDARD, self)

    @property
    def wild_cluster(self):
        return ClusterSnapshot.objects.get_live_cluster_for_archetype(
            enums.FormatType.FT_WILD, self)

    @property
    def sankey_visualization(self):
        return self.standard_cluster.sankey_visualization()

    @property
    def standard_signature(self):
        return self.get_signature(enums.FormatType.FT_STANDARD)

    @property
    def wild_signature(self):
        return self.get_signature(enums.FormatType.FT_WILD)

    @property
    def standard_ccp_signature(self):
        return self.get_signature(enums.FormatType.FT_STANDARD, True)

    @property
    def wild_ccp_signature(self):
        return self.get_signature(enums.FormatType.FT_WILD, True)

    def get_signature(self, game_format, use_ccp=False):
        cluster = self.promoted_clusters.filter(
            class_cluster__cluster_set__game_format=game_format).first()
        if not cluster:
            return {}
        signature = cluster.ccp_signature if use_ccp else cluster.signature
        return {
            "as_of":
            cluster.class_cluster.cluster_set.as_of,
            "format":
            int(cluster.class_cluster.cluster_set.game_format),
            "components":
            [(int(dbf_id), weight) for dbf_id, weight in signature.items()],
        }

    @property
    def standard_signature_pretty(self):
        cluster = self.standard_cluster
        if cluster:
            return cluster.pretty_signature_string()
        return ""

    @property
    def standard_ccp_signature_pretty(self):
        cluster = self.standard_cluster
        if cluster:
            return cluster.pretty_ccp_signature_string()
        return ""

    @property
    def wild_signature_pretty(self):
        cluster = self.wild_cluster
        if cluster:
            return cluster.pretty_signature_string()
        else:
            return ""

    @property
    def wild_signature_as_of(self):
        cluster = self.wild_cluster
        if cluster:
            return cluster.class_cluster.cluster_set.as_of

    @property
    def standard_signature_as_of(self):
        cluster = self.standard_cluster
        if cluster:
            return cluster.class_cluster.cluster_set.as_of

    def get_absolute_url(self):
        return reverse("archetype_detail",
                       kwargs={
                           "id": self.id,
                           "slug": slugify(self.name)
                       })
Example #19
0
class Feature(models.Model):
    """
	A Feature is any logical chunk of functionality whose visibility should be
	determined at runtime based on the context of the current user. The status of a
	feature and the rules that govern whether it is rendered are determined by the
	FeatureStatus enum that is defined above.

	The read_only flag is a flag that always defaults to False but can be set true
	temporarily when maintenance needs to be done to the database. In order for this to
	work, ALL operations that write to or mutate the database in any way must be wrapped in
	a feature tag or a feature decorator, and they must implement graceful behavior and
	feedback to users when they are operating in read_only mode.

	For version 2 of a released feature, a new feature tag should be created that is named
	appropriately to indicate it is a new version. It's status lifecycle should be
	managed in the usual way for new features, independently of the previous version's
	status.
	"""

    name = models.CharField(max_length=255, null=False, unique=True)
    description = models.TextField(blank=True)
    created = models.DateTimeField(auto_now_add=True)
    status = IntEnumField(enum=FeatureStatus, default=FeatureStatus.OFF)
    read_only = models.BooleanField(default=False)
    rollout_percent = models.PositiveIntegerField(
        default=100,
        validators=[MaxValueValidator(100)],
        help_text=(
            "This may be set to a value lower than 100 to further restrict "
            "the user pool with access to this feature."))

    def __str__(self):
        return self.name

    def is_user_part_of_rollout(self, user) -> bool:
        if user.is_authenticated:
            base = user.pk
        else:
            base = id(user)

        offset = int(self.created.microsecond / 10000)
        return (base + offset) % 100 <= self.rollout_percent

    def enabled_for_user(self, user) -> bool:
        if self.status == FeatureStatus.OFF:
            return False

        if self.status == FeatureStatus.STAFF_ONLY:
            return user.is_superuser or user.is_staff

        if self.status == FeatureStatus.AUTHORIZED_ONLY:
            return user.groups.filter(name=self.authorized_group_name).exists()

        if not self.is_user_part_of_rollout(user):
            return user.groups.filter(name=self.authorized_group_name).exists()

        if self.status == FeatureStatus.LOGGED_IN_USERS:
            return user.is_authenticated

        if self.status == FeatureStatus.PUBLIC:
            return True

        return False

    def add_user_to_authorized_group(self, user) -> bool:
        group = self.authorized_group
        if group not in user.groups.all():
            user.groups.add(self.authorized_group)
            return True
        return False

    def remove_user(self, user) -> bool:
        group = self.authorized_group
        if group in user.groups.all():
            user.groups.remove(self.authorized_group)
            return True
        return False

    @property
    def authorized_group_name(self) -> str:
        return f"feature:{self.name}:preview"

    @property
    def authorized_group(self):
        return Group.objects.get(name=self.authorized_group_name)
Example #20
0
class Card(models.Model):
    card_id = models.CharField(primary_key=True, max_length=50)
    dbf_id = models.IntegerField(null=True, unique=True, db_index=True)

    name = models.CharField(max_length=64)
    description = models.TextField(blank=True)
    flavortext = models.TextField(blank=True)
    how_to_earn = models.TextField(blank=True)
    how_to_earn_golden = models.TextField(blank=True)
    artist = models.CharField(max_length=255, blank=True)

    card_class = IntEnumField(enum=enums.CardClass,
                              default=enums.CardClass.INVALID)
    card_set = IntEnumField(enum=enums.CardSet, default=enums.CardSet.INVALID)
    faction = IntEnumField(enum=enums.Faction, default=enums.Faction.INVALID)
    race = IntEnumField(enum=enums.Race, default=enums.Race.INVALID)
    rarity = IntEnumField(enum=enums.Rarity, default=enums.Rarity.INVALID)
    type = IntEnumField(enum=enums.CardType, default=enums.CardType.INVALID)
    multi_class_group = IntEnumField(
        enum=enums.MultiClassGroup,
        default=enums.MultiClassGroup.INVALID,
    )

    collectible = models.BooleanField(default=False)
    battlecry = models.BooleanField(default=False)
    divine_shield = models.BooleanField(default=False)
    deathrattle = models.BooleanField(default=False)
    elite = models.BooleanField(default=False)
    evil_glow = models.BooleanField(default=False)
    inspire = models.BooleanField(default=False)
    forgetful = models.BooleanField(default=False)
    one_turn_effect = models.BooleanField(default=False)
    poisonous = models.BooleanField(default=False)
    ritual = models.BooleanField(default=False)
    secret = models.BooleanField(default=False)
    taunt = models.BooleanField(default=False)
    topdeck = models.BooleanField(default=False)

    atk = models.IntegerField(default=0)
    health = models.IntegerField(default=0)
    durability = models.IntegerField(default=0)
    cost = models.IntegerField(default=0)
    windfury = models.IntegerField(default=0)

    spare_part = models.BooleanField(default=False)
    overload = models.IntegerField(default=0)
    spell_damage = models.IntegerField(default=0)

    craftable = models.BooleanField(default=False)

    objects = models.Manager()
    includibles = IncludibleCardManager()

    class Meta:
        db_table = "card"

    @property
    def id(self):
        return self.card_id

    @classmethod
    def from_cardxml(cls, card, save=False):
        obj = cls(card_id=card.id)
        obj.update_from_cardxml(card, save=save)
        return obj

    @classmethod
    def get_string_id(cls, dbf_id):
        if not DBF_DB:
            db, _ = cardxml.load_dbf()
            DBF_DB.update(db)
        return DBF_DB[dbf_id].id

    def __str__(self):
        return self.name

    @property
    def slug(self):
        return slugify(self.name)

    def get_absolute_url(self):
        # XXX
        return reverse("card_detail",
                       kwargs={
                           "pk": self.dbf_id,
                           "slug": self.slug
                       })

    def get_card_art_url(self, resolution=256, format="jpg"):
        return "https://art.hearthstonejson.com/v1/%ix/%s.%s" % (
            resolution, self.id, format)

    def get_card_render_url(self,
                            resolution=256,
                            format="png",
                            locale="enUS",
                            build="latest"):
        return "https://art.hearthstonejson.com/v1/render/%s/%s/%ix/%s.%s" % (
            build, locale, resolution, self.id, format)

    def get_tile_url(self, format="png"):
        return "https://art.hearthstonejson.com/v1/tiles/%s.%s" % (self.id,
                                                                   format)

    def localized_name(self, locale) -> str:
        try:
            return self.strings.get(locale=locale,
                                    game_tag=enums.GameTag.CARDNAME).value
        except CardString.DoesNotExist:
            return ""

    def update_from_cardxml(self, cardxml, save=False):
        for k in dir(cardxml):
            if k.startswith("_") or k in ("id", "tags", "strings"):
                continue
            # Transfer all existing CardXML attributes to our model
            if hasattr(self, k):
                setattr(self, k, getattr(cardxml, k))

        if save:
            self.save()

    @property
    def playable(self):
        """Returns whether the card can be played."""
        if self.type in [
                enums.CardType.MINION, enums.CardType.SPELL,
                enums.CardType.WEAPON
        ]:
            return True
        # Heroes can only be played if they are not the basic heroes or hero skins.
        if self.type == enums.CardType.HERO:
            return self.card_set not in [
                enums.CardSet.CORE, enums.CardSet.HERO_SKINS
            ]
        return False

    @property
    def includible(self):
        return self.collectible and self.playable
Example #21
0
class Card(models.Model):
    id = models.CharField(primary_key=True, max_length=50)
    dbf_id = models.IntegerField(null=True, unique=True, db_index=True)
    objects = CardManager()

    name = models.CharField(max_length=50)
    description = models.TextField(blank=True)
    flavortext = models.TextField(blank=True)
    how_to_earn = models.TextField(blank=True)
    how_to_earn_golden = models.TextField(blank=True)
    artist = models.CharField(max_length=255, blank=True)

    card_class = IntEnumField(enum=enums.CardClass,
                              default=enums.CardClass.INVALID)
    card_set = IntEnumField(enum=enums.CardSet, default=enums.CardSet.INVALID)
    faction = IntEnumField(enum=enums.Faction, default=enums.Faction.INVALID)
    race = IntEnumField(enum=enums.Race, default=enums.Race.INVALID)
    rarity = IntEnumField(enum=enums.Rarity, default=enums.Rarity.INVALID)
    type = IntEnumField(enum=enums.CardType, default=enums.CardType.INVALID)

    collectible = models.BooleanField(default=False)
    battlecry = models.BooleanField(default=False)
    divine_shield = models.BooleanField(default=False)
    deathrattle = models.BooleanField(default=False)
    elite = models.BooleanField(default=False)
    evil_glow = models.BooleanField(default=False)
    inspire = models.BooleanField(default=False)
    forgetful = models.BooleanField(default=False)
    one_turn_effect = models.BooleanField(default=False)
    poisonous = models.BooleanField(default=False)
    ritual = models.BooleanField(default=False)
    secret = models.BooleanField(default=False)
    taunt = models.BooleanField(default=False)
    topdeck = models.BooleanField(default=False)

    atk = models.IntegerField(default=0)
    health = models.IntegerField(default=0)
    durability = models.IntegerField(default=0)
    cost = models.IntegerField(default=0)
    windfury = models.IntegerField(default=0)

    spare_part = models.BooleanField(default=False)
    overload = models.IntegerField(default=0)
    spell_damage = models.IntegerField(default=0)

    craftable = models.BooleanField(default=False)

    class Meta:
        db_table = "card"

    @classmethod
    def from_cardxml(cls, card, save=False):
        obj = cls(id=card.id)
        obj.update_from_cardxml(card, save=save)
        return obj

    def __str__(self):
        return self.name

    @property
    def slug(self):
        return slugify(self.name)

    def get_absolute_url(self):
        return reverse("card_detail",
                       kwargs={
                           "pk": self.dbf_id,
                           "slug": self.slug
                       })

    def update_from_cardxml(self, cardxml, save=False):
        for k in dir(cardxml):
            if k.startswith("_"):
                continue
            # Transfer all existing CardXML attributes to our model
            if hasattr(self, k):
                setattr(self, k, getattr(cardxml, k))

        if save:
            self.save()
Example #22
0
class SampleModel(models.Model):
    int_field = IntEnumField(enum=SampleEnum)
    null_field = IntEnumField(enum=SampleEnum, null=True)
Example #23
0
class Feature(models.Model):
    """
	A Feature is any logical chunk of functionality whose visibility should be
	determined at runtime based on the context of the current user. The status of a
	feature and the rules that govern whether it is rendered are determined by the
	FeatureStatus enum that is defined above.

	The read_only flag is a flag that always defaults to False but can be set true
	temporarily when maintenance needs to be done to the database. In order for this to
	work, ALL operations that write to or mutate the database in any way must be wrapped in
	a feature tag or a feature decorator, and they must implement graceful behavior and
	feedback to users when they are operating in read_only mode.

	For version 2 of a released feature, a new feature tag should be created that is named
	appropriately to indicate it is a new version. It's status lifecycle should be
	managed in the usual way for new features, independently of the previous version's
	status.
	"""

    name = models.CharField(max_length=255, null=False, unique=True)
    description = models.TextField(blank=True)
    created = models.DateTimeField(auto_now_add=True)
    status = IntEnumField(enum=FeatureStatus, default=FeatureStatus.OFF)
    read_only = models.BooleanField(default=False)

    def __str__(self):
        return self.name

    def enabled_for_user(self, user):
        if self.status == FeatureStatus.OFF:
            return False

        if user.is_staff:
            # Staff can always see everything except OFF
            return True

        if self.status == FeatureStatus.STAFF_ONLY:
            # If the user is staff we will have already returned True
            return False

        if self.status == FeatureStatus.AUTHORIZED_ONLY:
            return user.groups.filter(name=self.authorized_group_name).exists()

        if self.status == FeatureStatus.LOGGED_IN_USERS:
            return user.is_authenticated

        if self.status == FeatureStatus.PUBLIC:
            return True

    def add_user_to_authorized_group(self, user):
        group = self.authorized_group
        if group not in user.groups.all():
            user.groups.add(self.authorized_group)
            return True

    @property
    def authorized_group_name(self):
        return "feature:%s:preview" % self.name

    @property
    def authorized_group(self):
        return Group.objects.get(name=self.authorized_group_name)