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={},