geniuslib 5.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- geniuslib/__init__.py +186 -0
- geniuslib/__main__.py +8 -0
- geniuslib/abc.py +274 -0
- geniuslib/battlelog.py +458 -0
- geniuslib/battlelog_analytics.py +672 -0
- geniuslib/buildings.py +510 -0
- geniuslib/characters.py +144 -0
- geniuslib/clans.py +289 -0
- geniuslib/cli.py +239 -0
- geniuslib/client.py +2924 -0
- geniuslib/comparer.py +122 -0
- geniuslib/constants.py +331 -0
- geniuslib/cosmetics.py +198 -0
- geniuslib/entry_logs.py +260 -0
- geniuslib/enums.py +186 -0
- geniuslib/errors.py +139 -0
- geniuslib/events.py +1153 -0
- geniuslib/events.pyi +232 -0
- geniuslib/exporter.py +248 -0
- geniuslib/ext/discordlinks/__init__.py +279 -0
- geniuslib/ext/triggers/__init__.py +15 -0
- geniuslib/ext/triggers/cron.py +215 -0
- geniuslib/ext/triggers/triggers.py +503 -0
- geniuslib/formatters.py +151 -0
- geniuslib/game_data.py +687 -0
- geniuslib/hero.py +414 -0
- geniuslib/http.py +707 -0
- geniuslib/iterators.py +217 -0
- geniuslib/middleware.py +197 -0
- geniuslib/miscmodels.py +918 -0
- geniuslib/player_clan.py +41 -0
- geniuslib/players.py +758 -0
- geniuslib/raid.py +502 -0
- geniuslib/raid_analytics.py +130 -0
- geniuslib/spell.py +126 -0
- geniuslib/static/__init__.py +0 -0
- geniuslib/static/static_data.json +63983 -0
- geniuslib/static/update_static.py +1481 -0
- geniuslib/troop.py +192 -0
- geniuslib/upgrade_tracker.py +510 -0
- geniuslib/utils.py +856 -0
- geniuslib/war_analytics.py +218 -0
- geniuslib/war_attack.py +105 -0
- geniuslib/war_clans.py +199 -0
- geniuslib/war_members.py +143 -0
- geniuslib/wars.py +538 -0
- geniuslib-5.5.0.dist-info/METADATA +375 -0
- geniuslib-5.5.0.dist-info/RECORD +51 -0
- geniuslib-5.5.0.dist-info/WHEEL +5 -0
- geniuslib-5.5.0.dist-info/licenses/LICENSE +21 -0
- geniuslib-5.5.0.dist-info/top_level.txt +1 -0
geniuslib/__init__.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# GeniusLib - Clash of Clans API wrapper
|
|
2
|
+
# Based on coc.py v4.0.0 (MIT License, copyright (c) 2019-2020 mathsman5133)
|
|
3
|
+
# (c) 2026 AkumaHalls / ClashGenius
|
|
4
|
+
|
|
5
|
+
__version__ = "5.5.0"
|
|
6
|
+
|
|
7
|
+
from .abc import BasePlayer, BaseClan
|
|
8
|
+
from .battlelog import (
|
|
9
|
+
BattleLogEntry,
|
|
10
|
+
BattleLogResource,
|
|
11
|
+
LeagueHistoryEntry,
|
|
12
|
+
LeagueTierGroup,
|
|
13
|
+
LeagueTierGroupBattleLogEntry,
|
|
14
|
+
LeagueTierGroupMember,
|
|
15
|
+
)
|
|
16
|
+
from .buildings import (
|
|
17
|
+
Building,
|
|
18
|
+
GearUp,
|
|
19
|
+
MergeRequirement,
|
|
20
|
+
SeasonalDefenseModule,
|
|
21
|
+
SeasonalDefense,
|
|
22
|
+
Supercharge,
|
|
23
|
+
TownhallUnlock,
|
|
24
|
+
TownhallWeapon,
|
|
25
|
+
Trap,
|
|
26
|
+
)
|
|
27
|
+
from .characters import Guardian, Helper
|
|
28
|
+
from .clans import RankedClan, Clan
|
|
29
|
+
from .client import Client
|
|
30
|
+
from .constants import *
|
|
31
|
+
from .cosmetics import Skin, Scenery, Obstacle, Decoration, ClanCapitalHousePart
|
|
32
|
+
from .events import PlayerEvents, ClanEvents, WarEvents, EventsClient, ClientEvents
|
|
33
|
+
from .enums import (
|
|
34
|
+
PlayerHouseElementType,
|
|
35
|
+
Resource,
|
|
36
|
+
Role,
|
|
37
|
+
WarRound,
|
|
38
|
+
WarState,
|
|
39
|
+
BattleModifier,
|
|
40
|
+
WarResult,
|
|
41
|
+
ProductionBuildingType,
|
|
42
|
+
BuildingType,
|
|
43
|
+
VillageType,
|
|
44
|
+
SceneryType,
|
|
45
|
+
EquipmentRarity,
|
|
46
|
+
SkinTier
|
|
47
|
+
)
|
|
48
|
+
from .errors import (
|
|
49
|
+
ClashOfClansException,
|
|
50
|
+
HTTPException,
|
|
51
|
+
NotFound,
|
|
52
|
+
InvalidArgument,
|
|
53
|
+
InvalidCredentials,
|
|
54
|
+
LoginError,
|
|
55
|
+
Forbidden,
|
|
56
|
+
Maintenance,
|
|
57
|
+
GatewayError,
|
|
58
|
+
PrivateWarLog,
|
|
59
|
+
)
|
|
60
|
+
from .game_data import AccountData, Upgrade, Boosts, ArmyRecipe, HeroLoadout, StaticData
|
|
61
|
+
from .hero import Equipment, Hero, Pet
|
|
62
|
+
from .http import BasicThrottler, BatchThrottler, HTTPClient
|
|
63
|
+
from .iterators import (
|
|
64
|
+
ClanIterator,
|
|
65
|
+
ClanWarIterator,
|
|
66
|
+
PlayerIterator,
|
|
67
|
+
LeagueWarIterator,
|
|
68
|
+
CurrentWarIterator,
|
|
69
|
+
SeasonIterator,
|
|
70
|
+
)
|
|
71
|
+
from .miscmodels import (
|
|
72
|
+
Achievement,
|
|
73
|
+
Badge,
|
|
74
|
+
BaseLeague,
|
|
75
|
+
CapitalDistrict,
|
|
76
|
+
ChatLanguage,
|
|
77
|
+
GoldPassSeason,
|
|
78
|
+
Icon,
|
|
79
|
+
Label,
|
|
80
|
+
League,
|
|
81
|
+
LeagueGroupClan,
|
|
82
|
+
LeagueGroupInfo,
|
|
83
|
+
LegendStatistics,
|
|
84
|
+
LoadGameData,
|
|
85
|
+
Location,
|
|
86
|
+
PlayerHouseElement,
|
|
87
|
+
Season,
|
|
88
|
+
Timestamp,
|
|
89
|
+
TimeDelta,
|
|
90
|
+
TID,
|
|
91
|
+
Translation
|
|
92
|
+
)
|
|
93
|
+
from .players import Player, ClanMember, RankedPlayer
|
|
94
|
+
from .player_clan import PlayerClan
|
|
95
|
+
from .raid import RaidClan, RaidMember, RaidLogEntry, RaidDistrict, RaidAttack
|
|
96
|
+
from .spell import Spell
|
|
97
|
+
from .troop import Troop
|
|
98
|
+
from .war_clans import WarClan, ClanWarLeagueClan
|
|
99
|
+
from .war_attack import WarAttack
|
|
100
|
+
from .war_members import ClanWarLeagueClanMember, ClanWarMember
|
|
101
|
+
from .wars import ClanWar, ClanWarLogEntry, ClanWarLeagueGroup, ExtendedCWLGroup
|
|
102
|
+
from .war_analytics import (
|
|
103
|
+
new_stars,
|
|
104
|
+
previous_best_attack,
|
|
105
|
+
best_attack_on,
|
|
106
|
+
best_defense_on,
|
|
107
|
+
total_attack_stars,
|
|
108
|
+
total_attack_destruction,
|
|
109
|
+
get_cleanup_attacks,
|
|
110
|
+
count_missed_attacks,
|
|
111
|
+
get_attack_order,
|
|
112
|
+
get_war_result,
|
|
113
|
+
)
|
|
114
|
+
from .raid_analytics import (
|
|
115
|
+
total_member_attack_stars,
|
|
116
|
+
total_member_destruction,
|
|
117
|
+
best_raid_attack,
|
|
118
|
+
average_attack_destruction,
|
|
119
|
+
count_missed_raid_attacks,
|
|
120
|
+
get_inactive_raid_members,
|
|
121
|
+
member_raid_contribution,
|
|
122
|
+
district_attack_breakdown,
|
|
123
|
+
get_raid_cleanup_attacks,
|
|
124
|
+
get_wasted_attacks,
|
|
125
|
+
clan_offensive_stats,
|
|
126
|
+
clan_defensive_stats,
|
|
127
|
+
raid_summary,
|
|
128
|
+
)
|
|
129
|
+
from .battlelog_analytics import (
|
|
130
|
+
battle_win_rate,
|
|
131
|
+
battle_attack_stats,
|
|
132
|
+
battle_defense_stats,
|
|
133
|
+
battle_loot_summary,
|
|
134
|
+
battle_streak,
|
|
135
|
+
battle_consistency_score,
|
|
136
|
+
battle_daily_summary,
|
|
137
|
+
battle_period_summary,
|
|
138
|
+
league_history_progression,
|
|
139
|
+
league_season_stats,
|
|
140
|
+
league_tier_distribution,
|
|
141
|
+
tier_group_member_stats,
|
|
142
|
+
tier_group_attack_analysis,
|
|
143
|
+
tier_group_defense_analysis,
|
|
144
|
+
tier_group_mvp,
|
|
145
|
+
decode_army_code,
|
|
146
|
+
)
|
|
147
|
+
from .formatters import (
|
|
148
|
+
town_hall_emoji,
|
|
149
|
+
format_th,
|
|
150
|
+
format_role,
|
|
151
|
+
format_league,
|
|
152
|
+
format_builder_base_league,
|
|
153
|
+
format_trophies,
|
|
154
|
+
format_player_brief,
|
|
155
|
+
format_member_brief,
|
|
156
|
+
format_clan_brief,
|
|
157
|
+
format_clan_detailed,
|
|
158
|
+
format_war_state,
|
|
159
|
+
format_war_result,
|
|
160
|
+
format_war_score,
|
|
161
|
+
format_attack,
|
|
162
|
+
format_percentage,
|
|
163
|
+
format_number,
|
|
164
|
+
format_raid_brief,
|
|
165
|
+
)
|
|
166
|
+
from .middleware import (
|
|
167
|
+
Middleware,
|
|
168
|
+
RequestMiddleware,
|
|
169
|
+
ResponseMiddleware,
|
|
170
|
+
request_logger,
|
|
171
|
+
response_logger,
|
|
172
|
+
timing_header,
|
|
173
|
+
middleware,
|
|
174
|
+
)
|
|
175
|
+
from .upgrade_tracker import (
|
|
176
|
+
UpgradeCost,
|
|
177
|
+
UpgradeSummary,
|
|
178
|
+
estimate_upgrade_cost,
|
|
179
|
+
estimate_upgrade_time,
|
|
180
|
+
get_th_upgrade_summary,
|
|
181
|
+
format_upgrade_summary,
|
|
182
|
+
)
|
|
183
|
+
from .exporter import to_json, to_csv, to_dict, export_players, export_clans
|
|
184
|
+
from .comparer import compare_players, compare_clans
|
|
185
|
+
from . import utils
|
|
186
|
+
from .utils import get_assets_dir, ASSETS_PREFIX
|
geniuslib/__main__.py
ADDED
geniuslib/abc.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# GeniusLib - Clash of Clans API wrapper
|
|
2
|
+
# Based on coc.py (MIT License, copyright (c) 2019-2020 mathsman5133)
|
|
3
|
+
# (c) 2026 AkumaHalls / ClashGenius
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import AsyncIterator, Type, TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from .miscmodels import try_enum, Badge
|
|
9
|
+
from .iterators import PlayerIterator
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .players import Player
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BaseClan:
|
|
16
|
+
"""
|
|
17
|
+
Abstract data class that represents base Clan objects
|
|
18
|
+
|
|
19
|
+
Attributes
|
|
20
|
+
----------
|
|
21
|
+
tag: :class:`str`
|
|
22
|
+
The clan's tag
|
|
23
|
+
name: :class:`str`
|
|
24
|
+
The clan's name
|
|
25
|
+
badge: :class:`Badge`
|
|
26
|
+
The clan's badge
|
|
27
|
+
level: :class:`int`
|
|
28
|
+
The clan's level.
|
|
29
|
+
"""
|
|
30
|
+
__slots__ = ("tag", "name", "_client", "badge", "level", "_response_retry", "_raw_data")
|
|
31
|
+
|
|
32
|
+
def __init__(self, *, data, client, **kwargs):
|
|
33
|
+
self._client = client
|
|
34
|
+
|
|
35
|
+
self._response_retry = data.get("_response_retry")
|
|
36
|
+
self.tag = data.get("tag")
|
|
37
|
+
self.name = data.get("name")
|
|
38
|
+
self.badge = try_enum(Badge, data=data.get("badgeUrls"),
|
|
39
|
+
client=self._client)
|
|
40
|
+
self.level = data.get("clanLevel")
|
|
41
|
+
self._raw_data = data if client and client.raw_attribute else None
|
|
42
|
+
|
|
43
|
+
def __str__(self):
|
|
44
|
+
return self.name
|
|
45
|
+
|
|
46
|
+
def __repr__(self):
|
|
47
|
+
return f"<{self.__class__.__name__} tag={self.tag} name={self.name}>"
|
|
48
|
+
|
|
49
|
+
def __eq__(self, other):
|
|
50
|
+
return isinstance(other, BaseClan) and self.tag == other.tag
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def share_link(self) -> str:
|
|
54
|
+
"""str: A formatted link to open the clan in-game"""
|
|
55
|
+
return f"https://link.clashofclans.com/en?action=OpenClanProfile&tag=%23{self.tag.strip('#')}"
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def members(self):
|
|
59
|
+
# pylint: disable=missing-function-docstring
|
|
60
|
+
return NotImplemented
|
|
61
|
+
|
|
62
|
+
def get_detailed_members(self, cls: Type["Player"] = None,
|
|
63
|
+
load_game_data: bool = None) -> AsyncIterator["Player"]:
|
|
64
|
+
"""Get detailed player information for every player in the clan.
|
|
65
|
+
|
|
66
|
+
This returns a :class:`PlayerIterator` which fetches all player
|
|
67
|
+
tags in the clan in parallel.
|
|
68
|
+
|
|
69
|
+
Example
|
|
70
|
+
---------
|
|
71
|
+
|
|
72
|
+
.. code-block:: python3
|
|
73
|
+
|
|
74
|
+
clan = await client.get_clan(clan_tag)
|
|
75
|
+
|
|
76
|
+
async for player in clan.get_detailed_members():
|
|
77
|
+
print(player.name)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
Yields
|
|
81
|
+
------
|
|
82
|
+
:class:`Player`
|
|
83
|
+
A full player object of a clan member.
|
|
84
|
+
"""
|
|
85
|
+
if self.members is NotImplemented:
|
|
86
|
+
return NotImplemented
|
|
87
|
+
if load_game_data and not isinstance(load_game_data, bool):
|
|
88
|
+
raise TypeError("load_game_data must be either True or False.")
|
|
89
|
+
|
|
90
|
+
return PlayerIterator(self._client,
|
|
91
|
+
(p.tag for p in self.members),
|
|
92
|
+
cls=cls,
|
|
93
|
+
load_game_data=load_game_data,
|
|
94
|
+
members=self.members_dict)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class BasePlayer:
|
|
98
|
+
"""An ABC that implements some common operations on players, regardless of type.
|
|
99
|
+
|
|
100
|
+
Attributes
|
|
101
|
+
----------
|
|
102
|
+
tag: :class:`str`
|
|
103
|
+
The player's tag
|
|
104
|
+
name: :class:`str`
|
|
105
|
+
The player's name
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
__slots__ = ("tag", "name", "_client", "_response_retry", "_raw_data")
|
|
109
|
+
|
|
110
|
+
def __str__(self):
|
|
111
|
+
return self.name
|
|
112
|
+
|
|
113
|
+
def __repr__(self):
|
|
114
|
+
return f"<{self.__class__.__name__} tag={self.tag} name={self.name}>"
|
|
115
|
+
|
|
116
|
+
def __eq__(self, other):
|
|
117
|
+
return isinstance(other, BasePlayer) and self.tag == other.tag
|
|
118
|
+
|
|
119
|
+
def __init__(self, *, data, client, **_):
|
|
120
|
+
self._client = client
|
|
121
|
+
self._response_retry = data.get("_response_retry")
|
|
122
|
+
self._raw_data = data if client and client.raw_attribute else None
|
|
123
|
+
self.tag = data.get("tag")
|
|
124
|
+
self.name = data.get("name")
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def share_link(self) -> str:
|
|
128
|
+
""":class:`str` - A formatted link to open the player in-game"""
|
|
129
|
+
return f"https://link.clashofclans.com/en?action=OpenPlayerProfile&tag=%23{self.tag.strip('#')}"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class BaseDataClass:
|
|
133
|
+
"""Base class for simple data classes with standard repr.
|
|
134
|
+
|
|
135
|
+
This class provides a default __repr__ implementation that displays
|
|
136
|
+
the name and id attributes if they exist.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
__slots__ = ()
|
|
140
|
+
|
|
141
|
+
def __repr__(self):
|
|
142
|
+
attrs = [
|
|
143
|
+
("name", getattr(self, "name", "Unknown")),
|
|
144
|
+
("id", getattr(self, "id", None)),
|
|
145
|
+
]
|
|
146
|
+
# Filter out None values
|
|
147
|
+
attrs = [(k, v) for k, v in attrs if v is not None]
|
|
148
|
+
return "<%s %s>" % (
|
|
149
|
+
self.__class__.__name__, " ".join("%s=%r" % t for t in attrs))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class LevelManager:
|
|
153
|
+
"""Handles level property and level data loading.
|
|
154
|
+
|
|
155
|
+
This class manages the level state and triggers level-specific data loading
|
|
156
|
+
when the level changes. Subclasses must implement _load_level_data().
|
|
157
|
+
|
|
158
|
+
Attributes
|
|
159
|
+
----------
|
|
160
|
+
level: :class:`int`
|
|
161
|
+
The current level of the unit.
|
|
162
|
+
max_level: :class:`int`
|
|
163
|
+
The maximum level this unit can reach.
|
|
164
|
+
is_max: :class:`bool`
|
|
165
|
+
Whether the unit is at its maximum level.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
__slots__ = (
|
|
169
|
+
"_level",
|
|
170
|
+
"_static_data",
|
|
171
|
+
"max_level",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def __init__(self, initial_level: int, static_data: dict | None):
|
|
175
|
+
|
|
176
|
+
#if the level is set to 0, initalize it as the lowest available level
|
|
177
|
+
if initial_level == 0 and static_data:
|
|
178
|
+
min_level = 1
|
|
179
|
+
if static_data.get("levels"):
|
|
180
|
+
min_level = static_data["levels"][0].get("level")
|
|
181
|
+
initial_level = min_level
|
|
182
|
+
|
|
183
|
+
self._level: int = initial_level
|
|
184
|
+
self._static_data = static_data
|
|
185
|
+
|
|
186
|
+
self.max_level: int = len(static_data["levels"]) if static_data else 0
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def level(self) -> int:
|
|
190
|
+
""":class:`int`: The current level of the unit."""
|
|
191
|
+
return self._level
|
|
192
|
+
|
|
193
|
+
@level.setter
|
|
194
|
+
def level(self, value: int):
|
|
195
|
+
if not self._static_data:
|
|
196
|
+
self._level = value
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
if value < 1:
|
|
200
|
+
raise ValueError(f"Level must be greater than 1")
|
|
201
|
+
|
|
202
|
+
self._level = value
|
|
203
|
+
self._load_level_data()
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def is_max(self) -> bool:
|
|
207
|
+
""":class:`bool`: Whether the unit is at its maximum level."""
|
|
208
|
+
return self.max_level == self.level
|
|
209
|
+
|
|
210
|
+
def _load_level_data(self):
|
|
211
|
+
raise NotImplementedError("Subclasses must implement _load_level_data()")
|
|
212
|
+
|
|
213
|
+
def __getattribute__(self, name):
|
|
214
|
+
try:
|
|
215
|
+
return object.__getattribute__(self, name)
|
|
216
|
+
except AttributeError:
|
|
217
|
+
if name == "_raw_data":
|
|
218
|
+
raise AttributeError(
|
|
219
|
+
f"'{self.__class__.__name__}' object has no attribute '{name}'. "
|
|
220
|
+
f"This attribute requires client raw_attribute to be enabled and set to True."
|
|
221
|
+
f"Additionally must be an API data filled unit."
|
|
222
|
+
)
|
|
223
|
+
else:
|
|
224
|
+
raise AttributeError(
|
|
225
|
+
f"'{self.__class__.__name__}' object has no attribute '{name}'. "
|
|
226
|
+
f"This attribute requires static game data to be loaded. "
|
|
227
|
+
f"Use load_game_data=True when fetching this object."
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def __repr__(self):
|
|
232
|
+
attrs = [
|
|
233
|
+
("name", getattr(self, "name", "Unknown")),
|
|
234
|
+
("level", self.level),
|
|
235
|
+
]
|
|
236
|
+
if hasattr(self, "id"):
|
|
237
|
+
attrs.insert(1, ("id", self.id))
|
|
238
|
+
return "<%s %s>" % (
|
|
239
|
+
self.__class__.__name__, " ".join("%s=%r" % t for t in attrs))
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class LeveledUnit(LevelManager):
|
|
243
|
+
"""Base class for units with level-dependent data and additional utilities.
|
|
244
|
+
|
|
245
|
+
Extends LevelManager with townhall-specific level queries.
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
__slots__ = ()
|
|
249
|
+
|
|
250
|
+
def get_max_level_for_townhall(self, townhall: int) -> int | None:
|
|
251
|
+
"""Get the maximum level for this unit at a given townhall level.
|
|
252
|
+
|
|
253
|
+
Parameters
|
|
254
|
+
----------
|
|
255
|
+
townhall: :class:`int`
|
|
256
|
+
The townhall level to check.
|
|
257
|
+
|
|
258
|
+
Returns
|
|
259
|
+
-------
|
|
260
|
+
Optional[:class:`int`]
|
|
261
|
+
The maximum level this unit can reach at the given townhall level,
|
|
262
|
+
or ``None`` if no static data is available.
|
|
263
|
+
"""
|
|
264
|
+
if not self._static_data or not self._static_data["levels"]:
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
max_level = None
|
|
268
|
+
for level_data in self._static_data.get("levels", []):
|
|
269
|
+
if level_data.get("required_townhall", 0) <= townhall:
|
|
270
|
+
max_level = level_data.get("level")
|
|
271
|
+
else:
|
|
272
|
+
break
|
|
273
|
+
|
|
274
|
+
return max_level
|