From 5556d5f772ee943f9d70abdc5be2b13e6526036f Mon Sep 17 00:00:00 2001 From: Eriks Karls Date: Tue, 7 Jan 2020 16:28:42 +0200 Subject: [PATCH 1/3] Created method for current products on sale. Updated inventory to also include products on sale --- erepublik/citizen.py | 59 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/erepublik/citizen.py b/erepublik/citizen.py index 988f536..d58052a 100644 --- a/erepublik/citizen.py +++ b/erepublik/citizen.py @@ -1,6 +1,5 @@ import re import sys -import warnings from collections import defaultdict from datetime import datetime, timedelta from itertools import product @@ -505,7 +504,21 @@ class Citizen(CitizenAPI): if kind not in final_items: final_items[kind] = {} - icon = item['icon'] if item['icon'] else "//www.erepublik.net/images/modules/manager/tab_storage.png" + if item['icon']: + icon = item['icon'] + else: + if item['type'] == 'damageBoosters': + icon = "/images/modules/pvp/damage_boosters/damage_booster.png" + elif item['type'] == 'aircraftDamageBoosters': + icon = "/images/modules/pvp/damage_boosters/air_damage_booster.png" + elif item['type'] == 'prestigePointsBoosters': + icon = "/images/modules/pvp/prestige_points_boosters/prestige_booster.png" + elif item['type'] == 'speedBoosters': + icon = "/images/modules/pvp/speed_boosters/speed_booster.png" + elif item['type'] == 'catchupBoosters': + icon = "/images/modules/pvp/ghost_boosters/icon_booster_30_60.png" + else: + icon = "//www.erepublik.net/images/modules/manager/tab_storage.png" data = dict(kind=kind, quality=item.get('quality', 0), amount=item.get('amount', 0), durability=item.get('duration', 0), icon=icon, name=name) if item.get('type') in ('damageBoosters', "aircraftDamageBoosters"): @@ -533,9 +546,22 @@ class Citizen(CitizenAPI): icon=icon) ) + offers = {} + for offer in self._get_economy_my_market_offers().json(): + kind = self.get_industry_name(offer['industryId']) + data = dict(quality=offer.get('quality', 0), amount=offer.get('amount', 0), icon=offer.get('icon'), + kind=kind, name=kind) + data = {data['quality']: data} + + if kind not in offers: + offers[kind] = {} + + offers[kind].update(data) + self.inventory.update({"used": j.get("inventoryStatus").get("usedStorage"), "total": j.get("inventoryStatus").get("totalStorage")}) - inventory = dict(items=dict(active=active_items, final=final_items, raw=raw_materials), status=self.inventory) + inventory = dict(items=dict(active=active_items, final=final_items, + raw=raw_materials, offers=offers), status=self.inventory) self.food["total"] = sum([self.food[q] * FOOD_ENERGY[q] for q in FOOD_ENERGY]) return inventory @@ -1668,6 +1694,14 @@ class Citizen(CitizenAPI): "\n".join(["{}: {}".format(k, v) for k, v in kinds.items()]), kind )) + def get_my_market_offers(self) -> List[Dict[str, Union[int, float, str]]]: + ret = [] + for offer in self._get_economy_my_market_offers().json(): + line = offer.copy() + line.pop('icon', None) + ret.append(line) + return ret + def post_market_offer(self, industry: int, quality: int, amount: int, price: float) -> Response: if industry not in self.available_industries.values(): self.write_log(f"Trying to sell unsupported industry {industry}") @@ -1745,7 +1779,8 @@ class Citizen(CitizenAPI): @property def factories(self) -> Dict[int, str]: """Returns factory industries as dict(id: name) - :return: dict + :return: Factory id:name dict + ":rtype: Dict[int, str] """ return {1: "Food", 2: "Weapons", 4: "House", 23: "Aircraft", 7: "FRM q1", 8: "FRM q2", 9: "FRM q3", 10: "FRM q4", 11: "FRM q5", @@ -1754,13 +1789,25 @@ class Citizen(CitizenAPI): 24: "ARM q1", 25: "ARM q2", 26: "ARM q3", 27: "ARM q4", 28: "ARM q5", } def get_industry_id(self, industry_name: str) -> int: - """ - Returns industry id + """Returns industry id + :type industry_name: str :return: int """ return self.available_industries.get(industry_name, 0) + def get_industry_name(self, industry_id: int) -> str: + """Returns industry name from industry ID + + :type industry_id: int + :return: industry name + :rtype: str + """ + for iname, iid in self.available_industries.items(): + if iid == industry_id: + return iname + return "" + def buy_tg_contract(self) -> Response: ret = self._post_main_buy_gold_items('gold', "TrainingContract2", 1) self.reporter.report_action("BUY_TG_CONTRACT", ret.json()) From d9305214eb3ff41c618b832df91f7000a79c880c Mon Sep 17 00:00:00 2001 From: Eriks Karls Date: Tue, 7 Jan 2020 19:55:31 +0200 Subject: [PATCH 2/3] Representation of Citizen class --- erepublik/citizen.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erepublik/citizen.py b/erepublik/citizen.py index d58052a..e7ff99a 100644 --- a/erepublik/citizen.py +++ b/erepublik/citizen.py @@ -111,6 +111,10 @@ class Citizen(CitizenAPI): def __str__(self) -> str: return f"Citizen {self.name}" + def __repr__(self): + return self.__str__() + + def __dict__(self): ret = super().__dict__.copy() ret.pop('stop_threads', None) From 71d204843d20bad3d943f2a376c9a75de77dd53e Mon Sep 17 00:00:00 2001 From: Eriks Karls Date: Thu, 9 Jan 2020 12:03:11 +0200 Subject: [PATCH 3/3] Python 3.8, isort, requirement update --- .editorconfig | 5 ++ .gitignore | 2 +- erepublik/citizen.py | 11 ++-- erepublik/classes.py | 6 +-- erepublik/utils.py | 15 +++++- examples/battle_launcher.py | 96 +++++++++++++++++++++++++++++++++ examples/eat_work_train.py | 102 ++++++++++++++++++++++++++++++++++++ requirements.txt | 16 ++++++ requirements_dev.txt | 14 ----- setup.py | 3 +- 10 files changed, 244 insertions(+), 26 deletions(-) create mode 100644 examples/battle_launcher.py create mode 100644 examples/eat_work_train.py create mode 100644 requirements.txt delete mode 100644 requirements_dev.txt diff --git a/.editorconfig b/.editorconfig index d4a2c44..0dbdbac 100644 --- a/.editorconfig +++ b/.editorconfig @@ -19,3 +19,8 @@ insert_final_newline = false [Makefile] indent_style = tab + +[*.py] +line_length=120 +multi_line_output=0 +balanced_wrapping=True diff --git a/.gitignore b/.gitignore index 30097d7..e314fbb 100644 --- a/.gitignore +++ b/.gitignore @@ -85,7 +85,7 @@ celerybeat-schedule # virtualenv .venv -venv/ +venv*/ ENV/ # Spyder project settings diff --git a/erepublik/citizen.py b/erepublik/citizen.py index e7ff99a..f3efba9 100644 --- a/erepublik/citizen.py +++ b/erepublik/citizen.py @@ -3,17 +3,16 @@ import sys from collections import defaultdict from datetime import datetime, timedelta from itertools import product -from json import loads, dumps +from json import dumps, loads from threading import Event from time import sleep -from typing import Dict, List, Tuple, Any, Union, Set +from typing import Any, Dict, List, Set, Tuple, Union -from requests import Response, RequestException +from requests import RequestException, Response -from erepublik.classes import (CitizenAPI, Battle, Reporter, Config, Energy, Details, Politics, MyCompanies, - TelegramBot, ErepublikException, BattleDivision, MyJSONEncoder) +from erepublik.classes import (Battle, BattleDivision, CitizenAPI, Config, Details, Energy, ErepublikException, + MyCompanies, MyJSONEncoder, Politics, Reporter, TelegramBot) from erepublik.utils import * -from erepublik.utils import process_warning class Citizen(CitizenAPI): diff --git a/erepublik/classes.py b/erepublik/classes.py index c5d6f31..2625b11 100644 --- a/erepublik/classes.py +++ b/erepublik/classes.py @@ -4,9 +4,9 @@ import hashlib import random import threading import time -from collections import deque, defaultdict -from json import JSONDecodeError, loads, JSONEncoder -from typing import Any, Dict, List, Union, Mapping, Iterable, Tuple +from collections import defaultdict, deque +from json import JSONDecodeError, JSONEncoder, loads +from typing import Any, Dict, Iterable, List, Mapping, Tuple, Union from requests import Response, Session, post diff --git a/erepublik/utils.py b/erepublik/utils.py index 06dbcea..e3114c8 100644 --- a/erepublik/utils.py +++ b/erepublik/utils.py @@ -8,7 +8,7 @@ import time import traceback import unicodedata from pathlib import Path -from typing import Union, Any, List, NoReturn, Mapping, Optional +from typing import Any, List, Mapping, NoReturn, Optional, Union import pytz import requests @@ -19,6 +19,10 @@ __all__ = ["FOOD_ENERGY", "COMMIT_ID", "COUNTRIES", "erep_tz", 'COUNTRY_LINK', "write_silent_log", "write_interactive_log", "get_file", "write_file", "send_email", "normalize_html_json", "process_error", "process_warning", 'report_promo', 'calculate_hit'] +if not sys.version_info >= (3, 7): + raise AssertionError('This script requires Python version 3.7 and higher\n' + 'But Your version is v{}.{}.{}'.format(*sys.version_info)) + FOOD_ENERGY = dict(q1=2, q2=4, q3=6, q4=8, q5=10, q6=12, q7=20) COMMIT_ID = "7b92e19" @@ -113,6 +117,15 @@ def localize_dt(dt: Union[datetime.date, datetime.datetime]) -> datetime.datetim def good_timedelta(dt: datetime.datetime, td: datetime.timedelta) -> datetime.datetime: + """Normalize timezone aware datetime object after timedelta to correct jumps over DST switches + + :param dt: Timezone aware datetime object + :type dt: datetime.datetime + :param td: timedelta object + :type td: datetime.timedelta + :return: datetime object with correct timezone when jumped over DST + :rtype: datetime.datetime + """ return erep_tz.normalize(dt + td) diff --git a/examples/battle_launcher.py b/examples/battle_launcher.py new file mode 100644 index 0000000..cf82aa8 --- /dev/null +++ b/examples/battle_launcher.py @@ -0,0 +1,96 @@ +import threading +from datetime import timedelta + +from erepublik import Citizen, utils + +CONFIG = { + 'email': 'player@email.com', + 'password': 'Pa$5w0rd!', + 'interactive': True, + 'fight': True, + 'debug': True, + 'start_battles': { + 121672: {"auto_attack": False, "regions": [661]}, + 125530: {"auto_attack": False, "regions": [259]}, + 125226: {"auto_attack": True, "regions": [549]}, + 124559: {"auto_attack": True, "regions": [176]} + } +} + + +def _battle_launcher(player: Citizen): + """Launch battles. Check every 5th minute (0,5,10...45,50,55) if any battle could be started on specified regions + and after launching wait for 90 minutes before starting next attack so that all battles aren't launched at the same + time. If player is allowed to fight, do 100 hits on the first round in players division. + + :param player: Logged in Citizen instance + ":type player: Citizen + """ + global CONFIG + finished_war_ids = {*[]} + war_data = CONFIG.get('start_battles', {}) + war_ids = {int(war_id) for war_id in war_data.keys()} + next_attack_time = player.now + next_attack_time = next_attack_time.replace(minute=next_attack_time.minute // 5 * 5, second=0) + while not player.stop_threads.is_set(): + try: + attacked = False + player.update_war_info() + running_wars = {b.war_id for b in player.all_battles.values()} + for war_id in war_ids - finished_war_ids - running_wars: + war = war_data[str(war_id)] + war_regions = set(war.get('regions')) + auto_attack = war.get('auto_attack') + + status = player.get_war_status(war_id) + if status.get('ended', False): + CONFIG['start_battles'].pop(str(war_id), None) + finished_war_ids.add(war_id) + continue + elif not status.get('can_attack'): + continue + + if auto_attack or (player.now.hour > 20 or player.now.hour < 2): + for reg in war_regions: + if attacked: + break + if reg in status.get('regions', {}).keys(): + player.launch_attack(war_id, reg, status.get('regions', {}).get(reg)) + attacked = True + hits = 100 + if player.energy.food_fights >= hits and player.config.fight: + for _ in range(120): + player.update_war_info() + battle_id = player.get_war_status(war_id).get("battle_id") + if battle_id is not None and battle_id in player.all_battles: + player.fight(battle_id, player.details.citizenship, hits) + break + player.sleep(1) + if attacked: + break + if attacked: + break + war_ids -= finished_war_ids + if attacked: + next_attack_time = utils.good_timedelta(next_attack_time, timedelta(hours=1, minutes=30)) + else: + next_attack_time = utils.good_timedelta(next_attack_time, timedelta(minutes=5)) + player.stop_threads.wait(utils.get_sleep_seconds(next_attack_time)) + except: + player.report_error("Task error: start_battles") + + +def main(): + player = Citizen(email=CONFIG['email'], password=CONFIG['password'], auto_login=False) + player.config.interactive = CONFIG['interactive'] + player.config.fight = CONFIG['fight'] + player.set_debug(CONFIG.get('debug', False)) + player.login() + if CONFIG.get('start_battles'): + name = "{}-start_battles-{}".format(player.name, threading.active_count() - 1) + state_thread = threading.Thread(target=_battle_launcher, args=(player,), name=name) + state_thread.start() + + +if __name__ == "__main__": + main() diff --git a/examples/eat_work_train.py b/examples/eat_work_train.py new file mode 100644 index 0000000..5572423 --- /dev/null +++ b/examples/eat_work_train.py @@ -0,0 +1,102 @@ +from datetime import timedelta + +from erepublik import Citizen, utils + +CONFIG = { + 'email': 'player@email.com', + 'password': 'Pa$5w0rd!', + 'interactive': True, + 'debug': True +} + + +def main(): + player = Citizen(email=CONFIG['email'], password=CONFIG['password'], auto_login=False) + player.config.interactive = CONFIG['interactive'] + player.config.fight = CONFIG['fight'] + player.set_debug(CONFIG.get('debug', False)) + player.login() + now = player.now.replace(second=0, microsecond=0) + dt_max = now.replace(year=9999) + tasks = { + 'eat': now, + } + if player.config.work: + tasks.update({'work': now}) + if player.config.train: + tasks.update({'train': now}) + if player.config.ot: + tasks.update({'ot': now}) + if player.config.wam: + tasks.update({'wam': now.replace(hour=14, minute=0)}) + while True: + player.update_all() + if tasks.get('work', dt_max) <= now: + player.write_log("Doing task: work") + player.update_citizen_info() + player.work() + if player.config.ot: + tasks['ot'] = now + player.collect_daily_task() + next_time = utils.good_timedelta(now.replace(hour=0, minute=0, second=0), timedelta(days=1)) + tasks.update({'work': next_time}) + + if tasks.get('train', dt_max) <= now: + player.write_log("Doing task: train") + player.update_citizen_info() + player.train() + player.collect_daily_task() + next_time = utils.good_timedelta(now.replace(hour=0, minute=0, second=0), timedelta(days=1)) + tasks.update({'train': next_time}) + + if tasks.get('wam', dt_max) <= now: + player.write_log("Doing task: Work as manager") + success = player.work_wam() + player.eat() + if success: + next_time = utils.good_timedelta(now.replace(hour=14, minute=0, second=0, microsecond=0), + timedelta(days=1)) + else: + next_time = utils.good_timedelta(now.replace(second=0, microsecond=0), timedelta(minutes=30)) + + tasks.update({'wam': next_time}) + + if tasks.get('eat', dt_max) <= now: + player.write_log("Doing task: eat") + player.eat() + + if player.energy.food_fights > player.energy.limit // 10: + next_minutes = 12 + else: + next_minutes = (player.energy.limit - 5 * player.energy.interval) // player.energy.interval * 6 + + next_time = player.energy.reference_time + timedelta(minutes=next_minutes) + tasks.update({'eat': next_time}) + + if tasks.get('ot', dt_max) <= now: + player.write_log("Doing task: ot") + if now > player.my_companies.next_ot_time: + player.work_ot() + next_time = now + timedelta(minutes=60) + else: + next_time = player.my_companies.next_ot_time + tasks.update({'ot': next_time}) + + closest_next_time = dt_max + next_tasks = [] + for task, next_time in sorted(tasks.items(), key=lambda s: s[1]): + next_tasks.append("{}: {}".format(next_time.strftime('%F %T'), task)) + if next_time < closest_next_time: + closest_next_time = next_time + sleep_seconds = int(utils.get_sleep_seconds(closest_next_time)) + if sleep_seconds <= 0: + player.write_log(f"Loop detected! Offending task: '{next_tasks[0]}'") + player.write_log("My next Tasks and there time:\n" + "\n".join(sorted(next_tasks))) + player.write_log("Sleeping until (eRep): {} (sleeping for {}s)".format( + closest_next_time.strftime("%F %T"), sleep_seconds)) + seconds_to_sleep = sleep_seconds if sleep_seconds > 0 else 0 + player.sleep(seconds_to_sleep) + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2d7541e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,16 @@ +bumpversion==0.5.3 +coverage==5.0.2 +edx-sphinx-theme==1.5.0 +flake8==3.7.9 +ipython==7.11.1 +isort==4.3.21 +pip==19.3.1 +PyInstaller==3.5 +pytz==2019.3 +requests==2.22.0 +setuptools==44.0.0 +Sphinx==2.3.1 +tox==3.14.3 +twine==3.1.1 +watchdog==0.9.0 +wheel==0.33.6 diff --git a/requirements_dev.txt b/requirements_dev.txt deleted file mode 100644 index 32a56ca..0000000 --- a/requirements_dev.txt +++ /dev/null @@ -1,14 +0,0 @@ -pip==19.1.1 -bumpversion==0.5.3 -wheel==0.33.4 -watchdog==0.9.0 -flake8==3.7.8 -tox==3.13.2 -coverage==4.5.3 -Sphinx==2.2.0 -twine==2.0.0 -ipython -PyInstaller -pytz==2019.1 -requests==2.22.0 -edx-sphinx-theme diff --git a/setup.py b/setup.py index 9c164d5..97fb3fa 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ with open('README.rst') as readme_file: with open('HISTORY.rst') as history_file: history = history_file.read() -requirements = ['pytz>=2019.2', 'requests>=2.22'] +requirements = ['pytz==2019.3', 'requests==2.22.0'] setup_requirements = [] @@ -27,6 +27,7 @@ setup( 'Natural Language :: English', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', ], description="Python package for automated eRepublik playing", entry_points={},