Compare commits

..

28 Commits

Author SHA1 Message Date
47f5142837 Bump version: 0.23.4.6 → 0.23.4.7 2021-01-08 11:09:39 +02:00
a32fd039dd Report details about fighting also on telegram 2021-01-08 11:08:50 +02:00
311e684c0c Add telegram fight reporting API, Fix message duplication on TelegramReporter initialization 2021-01-08 11:07:59 +02:00
52038a86d5 PackBooster icon update 2021-01-07 17:00:59 +02:00
c35a107641 Bump version: 0.23.4.5 → 0.23.4.6 2021-01-07 15:55:02 +02:00
b53b2f0fae Update loop 2021-01-07 15:54:56 +02:00
8da9b6221a Bump version: 0.23.4.4 → 0.23.4.5 2021-01-07 15:36:33 +02:00
caa41c85f6 minor tweaks 2021-01-07 15:36:27 +02:00
da3b5d5768 Update inventory on booster activation 2021-01-07 15:31:46 +02:00
f96d0233f9 CSRF bugfix 2021-01-07 15:31:10 +02:00
c693a5182e Merge branch 'master' of github.com:eeriks/erepublik 2021-01-07 15:14:17 +02:00
0dfba6a9ff Find battle and fight - reuse already calculated hit count 2021-01-07 15:14:11 +02:00
ee3eca9658 Bump version: 0.23.4.3 → 0.23.4.4 2021-01-06 23:37:05 +02:00
0e8680daca bugfix 2021-01-06 23:35:25 +02:00
8e3606f1a3 Doc update 2021-01-05 19:47:43 +02:00
bd0bcc9ac7 Bump version: 0.23.4.2 → 0.23.4.3 2021-01-05 19:35:00 +02:00
c6f2226e64 . 2021-01-05 19:34:43 +02:00
308807d800 isort, pre-commit 2021-01-05 19:29:20 +02:00
91565d840e Added endpoint for collect all WC rewards 2021-01-05 19:18:20 +02:00
b4a9dd88f8 config generator bugdix 2021-01-05 19:17:50 +02:00
8435aa23ba Bump version: 0.23.4.1 → 0.23.4.2 2021-01-05 16:30:49 +02:00
09cd275a69 type bugfix 2021-01-05 16:30:42 +02:00
e23a67231e Bump version: 0.23.4 → 0.23.4.1 2021-01-05 16:19:43 +02:00
6a03d99abf type bugfix 2021-01-05 16:19:27 +02:00
2e344749a6 Bump version: 0.23.3.4 → 0.23.4 2021-01-05 15:50:55 +02:00
5aecefbd9d Protect those precious air boosters and 100% ground boosters 2021-01-05 15:50:38 +02:00
8b9ee5042d Bugfixes and inventory redone 2021-01-05 15:41:51 +02:00
1e93006c3d History updates 2020-12-17 18:18:46 +02:00
19 changed files with 338 additions and 131 deletions

1
.gitignore vendored
View File

@ -102,5 +102,4 @@ ENV/
debug/
log/
docs/
*dump.json

13
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,13 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
default_language_version:
python: python3.7

View File

@ -2,6 +2,25 @@
History
=======
0.23.4 (2021-01-05)
-------------------
* Added expiration data to inventory items
* Inventory is now based on `classes.Inventory`
* Requirement update to make them more flexible regarding versions required
* Restructured inventory
0.23.3 (2020-12-17)
-------------------
* Fixed carpet bombing
* Fixed hits done amount when fighting on ground
* Minor requirement updates
* Minor tweaks to method signatures
* Fixed buy food if unable to work or train because not enough energy and not enough food
* Fixed applications for party presidency and congress if not a party member
* Removed tox
* Updates to github.io config generator
* Fixed `Citizen.concurrency_available` stuck in unset state if exception is being raised while doing concurrency task
0.23.2 (2020-12-01)
-------------------
* Added concurrency checks to guard against simultaneous fighting/wam'ing/traveling
@ -17,7 +36,7 @@ History
0.23.0 (2020-11-26)
-------------------
* ***0.23 - last supported version for Python 3.7.***
* ***0.23 - last officially supported version for Python 3.7.***
* Added `Config.maverick` switch, to allow/deny automated fighting in non native divisions if the player has MaverickPack
* Added `CitizenMedia.get_article(article_id:int)` method to get article data
* Added `CitizenMedia.delete_article(article_id:int)` method to delete article

View File

@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -76,7 +76,6 @@ servedocs: docs ## compile the docs watching for changes
release: dist ## package and upload a release
twine upload dist/*
clean
dist: clean ## builds source and wheel package
python setup.py sdist

View File

@ -34,4 +34,3 @@ This package was created with Cookiecutter_ and the `audreyr/cookiecutter-pypack
.. _Cookiecutter: https://github.com/audreyr/cookiecutter
.. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage

61
docs/erepublik.rst Normal file
View File

@ -0,0 +1,61 @@
erepublik package
=================
Submodules
----------
erepublik.access\_points module
-------------------------------
.. automodule:: erepublik.access_points
:members:
:undoc-members:
:show-inheritance:
erepublik.citizen module
------------------------
.. automodule:: erepublik.citizen
:members:
:undoc-members:
:show-inheritance:
erepublik.classes module
------------------------
.. automodule:: erepublik.classes
:members:
:undoc-members:
:show-inheritance:
erepublik.constants module
--------------------------
.. automodule:: erepublik.constants
:members:
:undoc-members:
:show-inheritance:
erepublik.types module
----------------------
.. automodule:: erepublik.types
:members:
:undoc-members:
:show-inheritance:
erepublik.utils module
----------------------
.. automodule:: erepublik.utils
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: erepublik
:members:
:undoc-members:
:show-inheritance:

View File

@ -393,7 +393,7 @@
}
config.air = air.checked;
config.ground = ground.cehcked;
config.ground = ground.checked;
config.boosters = boosters.checked;
config.continuous_fighting = continuous_fighting.checked;
config.next_energy = next_energy.checked;
@ -402,10 +402,9 @@
config.travel_to_fight = travel_to_fight.checked;
config.epic_hunt = epic_hunt.checked;
config.epic_hunt_ebs = config.epic_hunt ? epic_hunt_ebs.checked : config.epic_hunt;
config.maverick = false;
// Advanced
let telegram = document.getElementById('telegram'); // Generated
config.telegram = telegram.checked;
let telegram_chat_id = document.getElementById('telegram_chat_id'); // Generated

7
docs/modules.rst Normal file
View File

@ -0,0 +1,7 @@
erepublik
=========
.. toctree::
:maxdepth: 4
erepublik

View File

@ -7,4 +7,3 @@ To use eRepublik script in a project::
from erepublik import Citizen
player = Citizen('email@domain.com', 'password')
player.update_all()

View File

@ -4,7 +4,7 @@
__author__ = """Eriks Karls"""
__email__ = 'eriks@72.lv'
__version__ = '0.23.3.4'
__version__ = '0.23.4.7'
from erepublik import classes, constants, utils
from erepublik.citizen import Citizen

View File

@ -305,11 +305,11 @@ class ErepublikEconomyAPI(CitizenBaseAPI):
return self.post(f"{self.url}/main/buyGoldItems", data=data)
def _post_economy_activate_booster(self, quality: int, duration: int, kind: str) -> Response:
data = dict(type=kind, quality=quality, duration=duration, fromInventory=True)
data = dict(type=kind, quality=quality, duration=duration, fromInventory=True, _token=self.token)
return self.post(f"{self.url}/economy/activateBooster", data=data)
def _post_economy_activate_house(self, quality: int) -> Response:
data = {"action": "activate", "quality": quality, "type": "house", "_token": self.token}
data = dict(action="activate", quality=quality, type="house", _token=self.token)
return self.post(f"{self.url}/economy/activateHouse", data=data)
def _post_economy_donate_items_action(self, citizen_id: int, amount: int, industry: int,
@ -595,6 +595,10 @@ class ErepublikProfileAPI(CitizenBaseAPI):
data = dict(_token=self.token, rewardId=reward_id)
return self.post(f"{self.url}/main/weekly-challenge-collect-reward", data=data)
def _post_main_weekly_challenge_collect_all(self, max_reward_id: int) -> Response:
data = dict(_token=self.token, maxRewardId=max_reward_id)
return self.post(f"{self.url}/main/weekly-challenge-collect-all", data=data)
def _post_main_profile_update(self, action: str, params: str):
data = {"action": action, "params": params, "_token": self.token}
return self.post(f"{self.url}/main/profile-update", data=data)

View File

@ -2,7 +2,7 @@ import re
import sys
import warnings
import weakref
from datetime import datetime, timedelta
from datetime import datetime, time, timedelta
from decimal import Decimal
from itertools import product
from threading import Event
@ -11,7 +11,7 @@ from typing import Any, Dict, List, NoReturn, Optional, Set, Tuple, Union
from requests import HTTPError, RequestException, Response
from . import access_points, classes, constants, utils
from . import access_points, classes, constants, types, utils
from .classes import OfferItem
@ -20,9 +20,7 @@ class BaseCitizen(access_points.CitizenAPI):
_last_inventory_update: datetime = constants.min_datetime
promos: Dict[str, datetime] = None
inventory: Dict[str, Dict[str, Dict[int, Dict[str, Union[str, int, float]]]]]
inventory_status: Dict[str, int]
boosters: Dict[int, Dict[int, int]] = {50: {}, 100: {}}
_inventory: classes.Inventory
ot_points: int = 0
food: Dict[str, int] = {"q1": 0, "q2": 0, "q3": 0, "q4": 0, "q5": 0, "q6": 0, "q7": 0, "total": 0}
@ -65,8 +63,7 @@ class BaseCitizen(access_points.CitizenAPI):
self.config.email = email
self.config.password = password
self.inventory = {}
self.inventory_status = dict(used=0, total=0)
self._inventory = classes.Inventory()
self.wheel_of_fortune = False
def get_csrf_token(self):
@ -241,33 +238,42 @@ class BaseCitizen(access_points.CitizenAPI):
def update_inventory(self):
"""
Updates class properties and returns structured inventory.
Return structure: {status: {used: int, total: int}, items: {active/final/raw: {item_token:{quality: data}}}
If item kind is damageBoosters or aircraftDamageBoosters then kind is renamed to kind+quality and duration is
used as quality.
Updates citizen inventory
"""
self._update_inventory_data(self._get_economy_inventory_items().json())
def get_inventory(self, force: bool = False):
@property
def inventory(self) -> classes.Inventory:
return self.get_inventory()
def get_inventory(self, force: bool = False) -> classes.Inventory:
if utils.good_timedelta(self._last_inventory_update, timedelta(minutes=2)) < self.now or force:
self.update_inventory()
return self.inventory
return self._inventory
def _update_inventory_data(self, inv_data: Dict[str, Any]):
if not isinstance(inv_data, dict):
raise TypeError("Parameter `inv_data` must be dict not '{type(data)}'!")
def _expire_value_to_python(_expire_value: str) -> Dict[str, Union[int, datetime]]:
_data = re.search(
r'((?P<amount>\d+) item\(s\) )?[eE]xpires? on Day (?P<eday>\d,\d{3}), (?P<time>\d\d:\d\d)',
_expire_value).groupdict()
eday = utils.date_from_eday(int(_data['eday'].replace(',', '')))
dt = constants.erep_tz.localize(datetime.combine(eday, time(*[int(_) for _ in _data['time'].split(':')])))
return {'amount': _data.get('amount'), 'expiration': dt}
status = inv_data.get("inventoryStatus", {})
if status:
self.inventory_status.clear()
self.inventory_status.update(used=status.get("usedStorage"), total=status.get("totalStorage"))
self._inventory.used = status.get("usedStorage")
self._inventory.total = status.get("totalStorage")
data = inv_data.get('inventoryItems', {})
if not data:
return
self._last_inventory_update = self.now
self.food.update({"q1": 0, "q2": 0, "q3": 0, "q4": 0, "q5": 0, "q6": 0, "q7": 0})
self.eb_small = self.eb_double = self.eb_normal = 0
active_items: Dict[str, Dict[int, Dict[str, Union[str, int]]]] = {}
active_items: types.InvFinal = {}
if data.get("activeEnhancements", {}).get("items", {}):
for item_data in data.get("activeEnhancements", {}).get("items", {}).values():
if item_data.get('token'):
@ -278,29 +284,35 @@ class BaseCitizen(access_points.CitizenAPI):
kind = constants.INDUSTRIES[constants.INDUSTRIES[kind]]
if kind not in active_items:
active_items[kind] = {}
icon = item_data['icon'] if item_data[
'icon'] else "//www.erepublik.net/images/modules/manager/tab_storage.png"
item_data = dict(name=item_data.get("name"), time_left=item_data['active']['time_left'], icon=icon,
kind=kind,
quality=item_data.get("quality", 0))
expiration_info = []
if item_data.get('attributes').get('expirationInfo'):
expire_info = item_data.get('attributes').get('expirationInfo')
expiration_info = [_expire_value_to_python(v) for v in expire_info['value']]
if not item_data.get('icon') and item_data.get('isPackBooster'):
item_data['icon'] = f"//www.erepublik.com/images/icons/boosters/52px/{item_data.get('type')}.png"
icon = item_data['icon'] if item_data['icon'] else "//www.erepublik.net/images/modules/manager/tab_storage.png"
inv_item: types.InvFinalItem = dict(
name=item_data.get("name"), time_left=item_data['active']['time_left'], icon=icon,
kind=kind, expiration=expiration_info, quality=item_data.get("quality", 0)
)
if item_data.get('isPackBooster'):
active_items[kind].update({0: item_data})
active_items[kind].update({0: inv_item})
else:
active_items[kind].update({item_data.get("quality"): item_data})
active_items[kind].update({inv_item.get("quality"): inv_item})
final_items: Dict[str, Dict[int, Dict[str, Union[str, int]]]] = {}
final_items: types.InvFinal = {}
boosters: types.InvBooster = {}
if data.get("finalProducts", {}).get("items", {}):
for item_data in data.get("finalProducts", {}).get("items", {}).values():
is_booster: bool = False
name = item_data['name']
if item_data.get('type'):
if item_data.get('type') in ['damageBoosters', "aircraftDamageBoosters"]:
kind = f"{item_data['type']}{item_data['quality']}"
if item_data['quality'] == 5:
self.boosters[50].update({item_data['duration']: item_data['amount']})
elif item_data['quality'] == 10:
self.boosters[100].update({item_data['duration']: item_data['amount']})
# in ['damageBoosters', "aircraftDamageBoosters", 'prestigePointsBoosters']
if item_data.get('isBooster'):
is_booster = True
kind = item_data['type']
delta = item_data['duration']
if delta // 3600:
@ -337,8 +349,14 @@ class BaseCitizen(access_points.CitizenAPI):
if constants.INDUSTRIES[kind]:
kind = constants.INDUSTRIES[constants.INDUSTRIES[kind]]
if kind not in final_items:
final_items[kind] = {}
if is_booster:
if kind not in boosters:
boosters[kind] = {}
if item_data.get('quality', 0) not in boosters[kind]:
boosters[kind][item_data['quality']] = {}
else:
if kind not in final_items:
final_items[kind] = {}
if item_data['icon']:
icon = item_data['icon']
@ -355,10 +373,23 @@ class BaseCitizen(access_points.CitizenAPI):
icon = "/images/modules/pvp/ghost_boosters/icon_booster_30_60.png"
else:
icon = "//www.erepublik.net/images/modules/manager/tab_storage.png"
_item_data = dict(kind=kind, quality=item_data.get('quality', 0), amount=item_data.get('amount', 0),
durability=item_data.get('duration', 0), icon=icon, name=name)
if item_data.get('type') in ('damageBoosters', "aircraftDamageBoosters"):
_item_data = {_item_data['durability']: _item_data}
expiration_info = []
if item_data.get('attributes'):
if item_data.get('attributes').get('expirationInfo'):
expire_info = item_data.get('attributes').get('expirationInfo')
expiration_info = [_expire_value_to_python(v) for v in expire_info['value']]
elif item_data.get('attributes').get('expiration'):
_exp = item_data.get('attributes').get('expiration')
exp_dt = (utils.date_from_eday(int(_exp['value'].replace(',', ''))))
expiration_info = [{'amount': item_data.get('amount'), 'expiration': exp_dt}]
_inv_item: Dict[int, types.InvFinalItem]
inv_item: types.InvFinalItem = dict(
kind=kind, quality=item_data.get('quality', 0), icon=icon, expiration=expiration_info,
amount=item_data.get('amount'), durability=item_data.get('duration', 0), name=name
)
if is_booster:
_inv_item = {inv_item['durability']: inv_item}
else:
if item_data.get('type') == 'bomb':
firepower = 0
@ -367,11 +398,14 @@ class BaseCitizen(access_points.CitizenAPI):
except AttributeError:
pass
finally:
_item_data.update(fire_power=firepower)
_item_data = {_item_data['quality']: _item_data}
final_items[kind].update(_item_data)
inv_item.update(fire_power=firepower)
_inv_item = {inv_item['quality']: inv_item}
if is_booster:
boosters[kind][inv_item['quality']].update(_inv_item)
else:
final_items[kind].update(_inv_item)
raw_materials: Dict[str, Dict[int, Dict[str, Union[str, int]]]] = {}
raw_materials: types.InvRaw = {}
if data.get("rawMaterials", {}).get("items", {}):
for item_data in data.get("rawMaterials", {}).get("items", {}).values():
if item_data['isPartial']:
@ -401,8 +435,11 @@ class BaseCitizen(access_points.CitizenAPI):
offers[kind] = {}
offers[kind].update(offer_data)
self.inventory.clear()
self.inventory.update(active=active_items, final=final_items, raw=raw_materials, offers=offers)
self._inventory.active = active_items
self._inventory.final = final_items
self._inventory.boosters = boosters
self._inventory.raw = raw_materials
self._inventory.offers = offers
self.food["total"] = sum([self.food[q] * constants.FOOD_ENERGY[q] for q in constants.FOOD_ENERGY])
def write_log(self, *args, **kwargs):
@ -946,13 +983,13 @@ class CitizenCompanies(BaseCitizen):
raw_factories = wam_holding.get_wam_companies(raw_factory=True)
fin_factories = wam_holding.get_wam_companies(raw_factory=False)
free_inventory = self.inventory_status["total"] - self.inventory_status["used"]
free_inventory = self.inventory.total - self.inventory.used
wam_list = raw_factories + fin_factories
wam_list = wam_list[:self.energy.food_fights]
if int(free_inventory * 0.75) < self.my_companies.get_needed_inventory_usage(wam_list):
self.update_inventory()
free_inventory = self.inventory_status["total"] - self.inventory_status["used"]
free_inventory = self.inventory.total - self.inventory.used
while wam_list and free_inventory < self.my_companies.get_needed_inventory_usage(wam_list):
wam_list.pop(-1)
@ -1020,8 +1057,8 @@ class CitizenEconomy(CitizenTravel):
def check_house_durability(self) -> Dict[int, datetime]:
ret = {}
inv = self.get_inventory()
for house_quality, active_house in inv['active'].get('House', {}).items():
inv = self.inventory
for house_quality, active_house in inv.active.get('House', {}).items():
till = utils.good_timedelta(self.now, timedelta(seconds=active_house['time_left']))
ret.update({house_quality: till})
return ret
@ -1029,8 +1066,8 @@ class CitizenEconomy(CitizenTravel):
def buy_and_activate_house(self, q: int) -> Optional[Dict[int, datetime]]:
original_region = self.details.current_country, self.details.current_region
ok_to_activate = False
inv = self.get_inventory()
if not inv['final'].get('House', {}).get(q, {}):
inv = self.inventory
if not inv.final.get('House', {}).get(q, {}):
countries = [self.details.citizenship, ]
if self.details.current_country != self.details.citizenship:
countries.append(self.details.current_country)
@ -1084,7 +1121,7 @@ class CitizenEconomy(CitizenTravel):
r: Dict[str, Any] = self._post_economy_activate_house(quality).json()
self._update_inventory_data(r)
if r.get("status") and not r.get("error"):
house: Dict[str, Union[int, str]] = self.get_inventory()['active']['House'][quality]
house = self.inventory.active.get('House', {}).get(quality)
time_left = timedelta(seconds=house["time_left"])
active_until = utils.good_timedelta(self.now, time_left)
self._report_action(
@ -1164,12 +1201,13 @@ class CitizenEconomy(CitizenTravel):
self.write_log(f"Trying to sell unsupported industry {industry}")
_inv_qlt = quality if industry in [1, 2, 3, 4, 23] else 0
_kind = 'final' if industry in [1, 2, 4, 23] else 'raw'
inventory = self.get_inventory()
items = inventory[_kind].get(constants.INDUSTRIES[industry], {_inv_qlt: {'amount': 0}})
final_kind = industry in [1, 2, 4, 23]
items = (self.inventory.final if final_kind else self.inventory.raw).get(constants.INDUSTRIES[industry],
{_inv_qlt: {'amount': 0}})
if items[_inv_qlt]['amount'] < amount:
inventory = self.get_inventory(True)
items = inventory[_kind].get(constants.INDUSTRIES[industry], {_inv_qlt: {'amount': 0}})
self.update_inventory()
items = (self.inventory.final if final_kind else self.inventory.raw).get(constants.INDUSTRIES[industry],
{_inv_qlt: {'amount': 0}})
if items[_inv_qlt]['amount'] < amount:
self._report_action("ECONOMY_SELL_PRODUCTS", "Unable to sell! Not enough items in storage!",
kwargs=dict(inventory=items[_inv_qlt], amount=amount))
@ -1735,7 +1773,8 @@ class CitizenMilitary(CitizenTravel):
yield battle, battle_zone, side
def find_battle_and_fight(self):
if self.should_fight()[0]:
count = self.should_fight()[0]
if count:
self.write_log("Checking for battles to fight in...")
for battle, division, side in self.find_battle_to_fight():
@ -1762,7 +1801,7 @@ class CitizenMilitary(CitizenTravel):
if self.change_division(battle, division):
self.set_default_weapon(battle, division)
self.fight(battle, division, side)
self.fight(battle, division, side, count)
self.travel_to_residence()
break
@ -1788,7 +1827,7 @@ class CitizenMilitary(CitizenTravel):
self._report_action("IP_BLACKLISTED", "Fighting is not allowed from restricted IP!")
return 1
if not division.is_air and self.config.boosters:
self.activate_dmg_booster()
self.activate_damage_booster(not division.is_air)
if side is None:
side = battle.defender if self.details.citizenship in battle.defender.allies + [
battle.defender.country] else battle.invader
@ -1814,7 +1853,7 @@ class CitizenMilitary(CitizenTravel):
self.write_log(f"Hits: {total_hits:>4} | Damage: {total_damage}")
ok_to_fight = False
if total_damage:
self.reporter.report_fighting(battle, not side.is_defender, division, total_damage, total_hits)
self.report_fighting(battle, not side.is_defender, division, total_damage, total_hits)
# self.reporter.report_action('FIGHT', dict(battle_id=battle.id, side=side, dmg=total_damage,
# air=battle.has_air, hits=total_hits,
# round=battle.zone_id,
@ -1873,7 +1912,7 @@ class CitizenMilitary(CitizenTravel):
else:
hits = r_json['hits']
if r_json['user']['epicBattle']:
hits /= 1+r_json['user']['epicBattle']
hits /= 1 + r_json['user']['epicBattle']
self.energy.recovered = r_json["details"]["wellness"]
self.details.xp = int(r_json["details"]["points"])
@ -1884,7 +1923,8 @@ class CitizenMilitary(CitizenTravel):
return hits, err, damage
@utils.wait_for_lock
def deploy_bomb(self, battle: classes.Battle, division: classes.BattleDivision, bomb_id: int, inv_side: bool, count: int = 1) -> Optional[int]:
def deploy_bomb(self, battle: classes.Battle, division: classes.BattleDivision, bomb_id: int, inv_side: bool,
count: int = 1) -> Optional[int]:
"""Deploy bombs in a battle for given side.
:param battle: Battle
@ -1979,34 +2019,49 @@ class CitizenMilitary(CitizenTravel):
return utils.calculate_hit(0, rang, True, elite, ne, 0, 20 if weapon else 0)
def activate_dmg_booster(self):
if self.config.boosters:
if not self.get_active_ground_damage_booster():
duration = 0
for length, amount in self.boosters[50].items():
if amount > 2:
duration = length
def activate_damage_booster(self, ground: bool = True) -> int:
kind = 'damageBoosters' if ground else 'aircraftDamageBoosters'
if self.config.boosters and not self.get_active_damage_booster(ground):
booster: Optional[types.InvFinalItem] = None
for quality, data in sorted(self.inventory.boosters.get(kind, {}).items(), key=lambda x: x[0]):
for _duration, _booster in sorted(data.items(), key=lambda y: y[0]):
critical_amount = 2 if quality < 10 and ground else 10
if _booster.get('amount') > critical_amount:
booster = _booster
break
if duration:
self._report_action("MILITARY_BOOSTER", f"Activated 50% {duration / 60}h damage booster")
self._post_economy_activate_booster(5, duration, "damage")
break
if booster:
kind = 'damage' if ground else 'air_damage'
self._report_action("MILITARY_BOOSTER", f"Activated {booster['name']}")
resp = self._post_economy_activate_booster(booster['quality'], booster['durability'], kind).json()
self._update_inventory_data(resp)
return self.get_active_damage_booster(ground)
def get_active_ground_damage_booster(self) -> int:
inventory = self.get_inventory()
def get_active_damage_booster(self, ground: bool = True) -> int:
kind = 'damageBoosters' if ground else 'aircraftDamageBoosters'
boosters = self.inventory.active.get(kind, {})
quality = 0
if inventory['active'].get('damageBoosters', {}).get(10):
quality = 100
elif inventory['active'].get('damageBoosters', {}).get(5):
quality = 50
for q, boost in boosters.items():
if boost['quality'] * 10 > quality:
quality = boost['quality'] * 10
return quality
def activate_battle_effect(self, battle_id: int, kind: str) -> Response:
self._report_action('MILITARY_BOOSTER', f'Activated {kind} booster')
return self._post_main_activate_battle_effect(battle_id, kind, self.details.citizen_id)
def get_active_ground_damage_booster(self) -> int:
return self.get_active_damage_booster(True)
def activate_pp_booster(self, battle_id: int) -> Response:
self._report_action('MILITARY_BOOSTER', 'Activated PrestigePoint booster')
return self._post_military_fight_activate_booster(battle_id, 1, 180, "prestige_points")
def get_active_air_damage_booster(self) -> int:
return self.get_active_damage_booster(False)
def activate_battle_effect(self, battle_id: int, kind: str) -> bool:
self._report_action('MILITARY_BOOSTER', f'Activated {kind} booster')
resp = self._post_main_activate_battle_effect(battle_id, kind, self.details.citizen_id).json()
return not resp.get('error')
def activate_pp_booster(self, pp_item: types.InvFinalItem) -> bool:
self._report_action('MILITARY_BOOSTER', f'Activated {pp_item["name"]}')
resp = self._post_economy_activate_booster(pp_item['quality'], pp_item['durability'], 'prestige_points').json()
self._update_inventory_data(resp)
return pp_item.get('kind') in self.inventory.active
def _rw_choose_side(self, battle: classes.Battle, side: classes.BattleSide) -> Response:
return self._post_main_battlefield_travel(side.id, battle.id)
@ -2167,6 +2222,11 @@ class CitizenMilitary(CitizenTravel):
if division.wall['dom'] == 50 or division.wall['dom'] > 98:
yield division, division.wall['for'] == battle.invader.country.id
def report_fighting(self, battle: classes.Battle, invader: bool, division: classes.BattleDivision, damage: float, hits: int):
self.reporter.report_fighting(battle, invader, division, damage, hits)
if self.config.telegram:
self.telegram.report_fight(battle, invader, division, damage, hits)
class CitizenPolitics(BaseCitizen):
def get_country_parties(self, country: constants.Country = None) -> dict:
@ -2181,7 +2241,8 @@ class CitizenPolitics(BaseCitizen):
self._report_action('POLITIC_PARTY_PRESIDENT', 'Applied for party president elections')
return self._get_candidate_party(self.politics.party_slug)
else:
self._report_action('POLITIC_CONGRESS', 'Unable to apply for party president elections - not a party member')
self._report_action('POLITIC_CONGRESS',
'Unable to apply for party president elections - not a party member')
return None
def candidate_for_congress(self, presentation: str = "") -> Optional[Response]:
@ -2565,17 +2626,21 @@ class Citizen(CitizenAnniversary, CitizenCompanies, CitizenLeaderBoard,
data = self._get_main_weekly_challenge_data().json()
self.details.pp = data.get("player", {}).get("prestigePoints", 0)
self.details.next_pp.clear()
max_collectable_id = data.get('maxRewardId')
should_collect = False
for reward in data.get("rewards", {}).get("normal", {}):
status = reward.get("status", "")
if status == "rewarded":
continue
elif status == "completed":
self._post_main_weekly_challenge_reward(reward.get("id", 0))
should_collect = True
elif reward.get("icon", "") == "energy_booster":
pps = re.search(r"Reach (\d+) Prestige Points to unlock the following reward: \+1 Energy",
reward.get("tooltip", ""))
if pps:
self.details.next_pp.append(int(pps.group(1)))
if should_collect:
self._post_main_weekly_challenge_collect_all(max_collectable_id)
def should_fight(self, silent: bool = True) -> Tuple[int, str, bool]:
count, log_msg, force_fight = super().should_fight()
@ -2618,10 +2683,12 @@ class Citizen(CitizenAnniversary, CitizenCompanies, CitizenLeaderBoard,
else:
start_time = utils.good_timedelta(start_time.replace(minute=0), timedelta(hours=1))
while not self.stop_threads.is_set():
self.update_citizen_info()
start_time = utils.good_timedelta(start_time, timedelta(minutes=30))
self.update_citizen_info()
self.update_weekly_challenge()
self.send_state_update()
self.send_inventory_update()
self.update_companies()
self.send_my_companies_update()
sleep_seconds = (start_time - self.now).total_seconds()
self.stop_threads.wait(sleep_seconds if sleep_seconds > 0 else 0)
@ -2630,13 +2697,13 @@ class Citizen(CitizenAnniversary, CitizenCompanies, CitizenLeaderBoard,
def send_state_update(self):
data = dict(xp=self.details.xp, cc=self.details.cc, gold=self.details.gold, pp=self.details.pp,
inv_total=self.inventory_status['total'], inv=self.inventory_status['used'],
inv_total=self.inventory.total, inv=self.inventory.used,
hp_limit=self.energy.limit,
hp_interval=self.energy.interval, hp_available=self.energy.available, food=self.food['total'], )
self.reporter.send_state_update(**data)
def send_inventory_update(self):
self.reporter.report_action("INVENTORY", json_val=self.get_inventory(True))
self.reporter.report_action("INVENTORY", json_val=self.inventory.as_dict)
def send_my_companies_update(self):
self.reporter.report_action('COMPANIES', json_val=self.my_companies.as_dict)
@ -2774,7 +2841,7 @@ class Citizen(CitizenAnniversary, CitizenCompanies, CitizenLeaderBoard,
for holding in regions.values():
raw_usage = holding.get_wam_raw_usage()
free_storage = self.inventory_status['total'] - self.inventory_status['used']
free_storage = self.inventory.total - self.inventory.used
if (raw_usage['frm'] + raw_usage['wrm']) * 100 > free_storage:
self._report_action('WAM_UNAVAILABLE', 'Not enough storage!')
continue

View File

@ -7,11 +7,12 @@ from typing import Any, Dict, Generator, Iterable, List, NamedTuple, NoReturn, T
from requests import Response, Session, post
from . import constants, utils
from . import constants, types, utils
__all__ = ['Battle', 'BattleDivision', 'BattleSide', 'Company', 'Config', 'Details', 'Energy', 'ErepublikException',
'ErepublikNetworkException', 'EnergyToFight',
'Holding', 'MyCompanies', 'MyJSONEncoder', 'OfferItem', 'Politics', 'Reporter', 'TelegramReporter']
'Holding', 'MyCompanies', 'MyJSONEncoder', 'OfferItem', 'Politics', 'Reporter', 'TelegramReporter',
'Inventory']
class ErepublikException(Exception):
@ -396,11 +397,11 @@ class Config:
self.spin_wheel_of_fortune = False
@property
def as_dict(self):
def as_dict(self) -> Dict[str, Union[bool, int, str, List[str]]]:
return dict(email=self.email, work=self.work, train=self.train, wam=self.wam, ot=self.ot,
auto_sell=self.auto_sell, auto_sell_all=self.auto_sell_all, employees=self.employees,
fight=self.fight, air=self.air, ground=self.ground, all_in=self.all_in,
next_energy=self.next_energy, boosters=self.boosters, travel_to_fight=self.travel_to_fight,
next_energy=self.next_energy, travel_to_fight=self.travel_to_fight,
always_travel=self.always_travel, epic_hunt=self.epic_hunt, epic_hunt_ebs=self.epic_hunt_ebs,
rw_def_side=self.rw_def_side, interactive=self.interactive, maverick=self.maverick,
continuous_fighting=self.continuous_fighting, auto_buy_raw=self.auto_buy_raw,
@ -454,7 +455,7 @@ class Energy:
return self.recovered + self.recoverable
@property
def as_dict(self):
def as_dict(self) -> Dict[str, Union[int, datetime.datetime, bool]]:
return dict(limit=self.limit, interval=self.interval, recoverable=self.recoverable, recovered=self.recovered,
reference_time=self.reference_time, food_fights=self.food_fights,
is_recoverable_full=self.is_recoverable_full, is_recovered_full=self.is_recovered_full,
@ -509,7 +510,7 @@ class Details:
return next_level_up - self.xp
@property
def as_dict(self):
def as_dict(self) -> Dict[str, Union[int, float, str, constants.Country, bool]]:
return dict(xp=self.xp, cc=self.cc, pp=self.pp, pin=self.pin, gold=self.gold, next_pp=self.next_pp,
citizen_id=self.citizen_id, citizenship=self.citizenship, current_region=self.current_region,
current_country=self.current_country, residence_region=self.residence_region,
@ -528,7 +529,7 @@ class Politics:
is_country_president: bool = False
@property
def as_dict(self):
def as_dict(self) -> Dict[str, Union[bool, int, str]]:
return dict(is_party_member=self.is_party_member, party_id=self.party_id, party_slug=self.party_slug,
is_party_president=self.is_party_president, is_congressman=self.is_congressman,
is_country_president=self.is_country_president)
@ -566,7 +567,7 @@ class Reporter:
return self.citizen.details.citizen_id
@property
def as_dict(self):
def as_dict(self) -> Dict[str, Union[bool, int, str, List[Dict[Any, Any]]]]:
return dict(name=self.name, email=self.email, citizen_id=self.citizen_id, key=self.key, allowed=self.allowed,
queue=self.__to_update)
@ -730,7 +731,7 @@ class BattleSide:
return self.country.iso
@property
def as_dict(self):
def as_dict(self) -> Dict[str, Union[int, constants.Country, bool, List[constants.Country]]]:
return dict(points=self.points, country=self.country, is_defender=self.is_defender, allies=self.allies,
deployed=self.deployed)
@ -973,7 +974,7 @@ class TelegramReporter:
self._last_time = utils.good_timedelta(utils.now(), datetime.timedelta(minutes=-5))
self._last_full_energy_report = utils.good_timedelta(utils.now(), datetime.timedelta(minutes=-30))
if self.__queue:
self.send_message("\n\n\n\n".join(self.__queue))
self.send_message("Telegram initialized")
def send_message(self, message: str) -> bool:
self.__queue.append(message)
@ -1001,13 +1002,19 @@ class TelegramReporter:
new_line = '\n' if multiple else ''
self.send_message(f"New award: {new_line}*{msg}*")
def report_fight(self, battle: "Battle", invader: bool, division: "BattleDivision", damage: float, hits: int):
side_txt = (battle.invader if invader else battle.defender).country.iso
self.send_message(f"*Fight report*:\n{int(damage):,d} dmg ({hits} hits) in"
f" [battle {battle.id} for {battle.region_name[:16]}]({battle.link}) in d{division.div} on "
f"{side_txt} side")
def __send_messages(self):
while self._next_time > utils.now():
if self.__thread_stopper.is_set():
break
self.__thread_stopper.wait(utils.get_sleep_seconds(self._next_time))
message = "\n\n\n\n".join(self.__queue)
message = "\n\n\n".join(self.__queue)
if self.player_name:
message = f"Player *{self.player_name}*\n" + message
response = post(self.api_url, json=dict(chat_id=self.chat_id, text=message, parse_mode="Markdown"))
@ -1024,3 +1031,27 @@ class OfferItem(NamedTuple):
amount: int = 0
offer_id: int = 0
citizen_id: int = 0
class Inventory:
final: types.InvFinal
active: types.InvFinal
boosters: types.InvBooster
raw: types.InvRaw
market: types.InvRaw
used: int
total: int
def __init__(self):
self.active = {}
self.final = {}
self.boosters = {}
self.raw = {}
self.offers = {}
self.used = 0
self.total = 0
@property
def as_dict(self) -> Dict[str, Union[types.InvFinal, types.InvRaw, int]]:
return dict(active=self.active, final=self.final, boosters=self.boosters, raw=self.raw, offers=self.offers,
total=self.total, used=self.used)

7
erepublik/types.py Normal file
View File

@ -0,0 +1,7 @@
from datetime import datetime
from typing import Dict, List, Union
InvFinalItem = Dict[str, Union[str, int, List[Dict[str, Union[int, datetime]]]]]
InvBooster = Dict[str, Dict[int, Dict[int, InvFinalItem]]]
InvFinal = Dict[str, Dict[int, InvFinalItem]]
InvRaw = Dict[str, Dict[int, Dict[str, Union[str, int]]]]

View File

@ -10,7 +10,7 @@ import unicodedata
import warnings
from decimal import Decimal
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
from typing import Any, Dict, List, Union
import requests
@ -64,7 +64,9 @@ def good_timedelta(dt: datetime.datetime, td: datetime.timedelta) -> datetime.da
return constants.erep_tz.normalize(dt + td)
def eday_from_date(date: Union[datetime.date, datetime.datetime] = now()) -> int:
def eday_from_date(date: Union[datetime.date, datetime.datetime] = None) -> int:
if date is None:
date = now()
if isinstance(date, datetime.date):
date = datetime.datetime.combine(date, datetime.time(0, 0, 0))
return (date - datetime.datetime(2007, 11, 20, 0, 0, 0)).days
@ -253,7 +255,7 @@ def caught_error(e: Exception):
def process_error(log_info: str, name: str, exc_info: tuple, citizen=None, commit_id: str = None,
interactive: Optional[bool] = None):
interactive: bool = None):
"""
Process error logging and email sending to developer
:param interactive: Should print interactively
@ -377,6 +379,7 @@ def get_final_hit_dmg(base_dmg: Union[Decimal, float, str], rang: int,
dmg = dmg * 11 / 10
return Decimal(dmg)
# def _clear_up_battle_memory(battle):
# del battle.invader._battle, battle.defender._battle
# for div_id, division in battle.div.items():
@ -404,4 +407,5 @@ def wait_for_lock(function):
raise e
instance.concurrency_available.set()
return ret
return wrapper

View File

@ -1,18 +1,19 @@
bump2version==1.0.1
coverage==5.3
edx-sphinx-theme==1.5.0
coverage==5.3.1
edx-sphinx-theme==1.6.0
flake8==3.8.4
ipython>=7.19.0
isort==5.6.4
isort==5.7.0
pip==20.3.3
PyInstaller==4.1
pytz==2020.4
pytest==6.2.1
responses==0.12.1
setuptools==51.0.0
Sphinx==3.3.1
requests>=2.24.0,<2.26.0
PySocks==1.7.1
twine==3.2.0
wheel==0.36.2
pre-commit==2.9.3
pur==5.3.0
PyInstaller==4.1
PySocks==1.7.1
pytest==6.2.1
pytz>=2020.5
requests>=2.25.1
responses==0.12.1
setuptools==51.1.1
Sphinx==3.4.2
twine==3.3.0
wheel==0.36.2

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.23.3.4
current_version = 0.23.4.7
commit = True
tag = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\.?(?P<dev>\d+)?
@ -37,4 +37,3 @@ warn_unused_configs = True
[isort]
multi_line_output = 2
line_length = 120
not_skip = __init__.py

View File

@ -12,7 +12,7 @@ with open('HISTORY.rst') as history_file:
history = history_file.read()
requirements = [
'pytz==2020.4',
'pytz>=2020.0',
'requests>=2.24.0,<2.26.0',
'PySocks==1.7.1'
]
@ -50,6 +50,6 @@ setup(
test_suite='tests',
tests_require=test_requirements,
url='https://github.com/eeriks/erepublik/',
version='0.23.3.4',
version='0.23.4.7',
zip_safe=False,
)