├── .gitignore ├── LICENSE ├── README.md ├── attack.py ├── bot.py ├── config.ini ├── config.py ├── ogame.ini ├── planet.py ├── requirements.txt ├── sim.py ├── smsapigateway.py ├── transport_manager.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Rafał Furmański 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ogame-bot 2 | ========= 3 | 4 | Intelligent BOT playing in Ogame 5 | 6 | Features 7 | ======== 8 | 9 | * building buildings 10 | * transporting resources 11 | * SMS notifications about incoming attack(s) 12 | * sending messages to attacker 13 | * fleet save 14 | * farming (hell yeah!) 15 | * sending expeditions 16 | * and many more! 17 | 18 | Usage 19 | ===== 20 | 21 | Adjust your credentials and other settings in `config.ini` file. 22 | 23 | mkvirtualenv ogame 24 | pip install -r requirements.txt 25 | python bot.py 26 | 27 | -------------------------------------------------------------------------------- /attack.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from datetime import datetime 3 | from random import randint 4 | 5 | from config import options 6 | 7 | attack_options = options['attack'] 8 | 9 | 10 | class Attack(object): 11 | 12 | def __init__(self, planet, attack_id, arrivalTime, coordsOrigin, destCoords, 13 | detailsFleet, player, message_url): 14 | 15 | self.planet = planet 16 | self.id = attack_id 17 | self.arrivalTime = arrivalTime 18 | self.coordsOrigin = coordsOrigin 19 | self.destCoords = destCoords 20 | self.detailsFleet = detailsFleet 21 | self.player = player 22 | self.message_url = message_url 23 | self.noticed_time = datetime.now() 24 | self.message_sent = False 25 | self.sms_sent = False 26 | 27 | def _parse_time(self, time_str): 28 | d = datetime.now() 29 | dd = datetime.strptime(time_str, '%H:%M:%S') 30 | 31 | gd = datetime(d.year, d.month, d.day, dd.hour, dd.minute, dd.second) 32 | return gd 33 | 34 | def is_dangerous(self): 35 | return self.detailsFleet > int(attack_options['max_ships']) 36 | 37 | def get_random_message(self): 38 | messages = attack_options['messages'].split(',') 39 | return messages[randint(0, len(messages) - 1)] 40 | 41 | def get_sms_text(self): 42 | return u'%s %s %s %s' % (self.planet, self.arrivalTime, self.player, 43 | self.detailsFleet) 44 | 45 | def __str__(self): 46 | return 'Attack id: %s to: %s. Agressor: %s, ships: %s. Time:%s' % \ 47 | (self.id, self.destCoords, self.player, self.detailsFleet, 48 | self.arrivalTime) 49 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import socket 3 | import random 4 | from BeautifulSoup import BeautifulSoup 5 | from logging.handlers import RotatingFileHandler 6 | import time 7 | import logging 8 | import mechanize 9 | import os 10 | import re 11 | import sys 12 | from random import randint 13 | from datetime import datetime, timedelta 14 | from utils import * 15 | from urllib import urlencode 16 | from planet import Planet, Moon 17 | from attack import Attack 18 | from transport_manager import TransportManager 19 | from config import options 20 | from sim import Sim 21 | socket.setdefaulttimeout(float(options['general']['timeout'])) 22 | 23 | 24 | class Bot(object): 25 | 26 | BASE_URL = 'https://pl.ogame.gameforge.com/' 27 | LOGIN_URL = 'https://pl.ogame.gameforge.com/main/login' 28 | HEADERS = [('User-agent', 'Mozilla/5.0 (Windows NT 6.2; WOW64)\ 29 | AppleWebKit/537.15 (KHTML, like Gecko) Chrome/24.0.1295.0 Safari/537.15')] 30 | RE_BUILD_REQUEST = re.compile(r"sendBuildRequest\(\'(.*)\', null, 1\)") 31 | RE_SERVER_TIME = re.compile(r"var serverTime=new Date\((.*)\);var localTime") 32 | 33 | #ship -> ship id on the page 34 | SHIPS = { 35 | 'lm': '204', 36 | 'hm': '205', 37 | 'cr': '206', 38 | 'ow': '207', 39 | 'pn': '215', 40 | 'bb': '211', 41 | 'ns': '213', 42 | 'gs': '214', 43 | 'lt': '202', 44 | 'dt': '203', 45 | 'cs': '208', 46 | 'rc': '209', 47 | 'ss': '210' 48 | } 49 | 50 | # mission ids 51 | MISSIONS = { 52 | 'attack': '1', 53 | 'transport': '3', 54 | 'station': '4', 55 | 'expedition': '15', 56 | 'collect' : '8' 57 | } 58 | 59 | TARGETS = { 60 | 'planet' : '1', 61 | 'moon' : '3', 62 | 'debris' : '2' 63 | } 64 | 65 | SPEEDS = { 66 | 100: '10', 67 | 90: '9', 68 | 80: '8', 69 | 70: '7', 70 | 60: '6', 71 | 50: '5', 72 | 40: '4', 73 | 30: '3', 74 | 20: '2', 75 | 10: '1' 76 | } 77 | 78 | def __init__(self, username=None, password=None, uni='69'): 79 | self.uni = uni 80 | self.username = username 81 | self.password = password 82 | self.logged_in = False 83 | 84 | self._prepare_logger() 85 | self._prepare_browser() 86 | farms = options['farming']['farms'] 87 | self.farm_no = randint(0, len(farms)-1) if farms else 0 88 | 89 | self.MAIN_URL = 'https://s%s-pl.ogame.gameforge.com/game/index.php' % self.uni 90 | self.PAGES = { 91 | 'main': self.MAIN_URL + '?page=overview', 92 | 'resources': self.MAIN_URL + '?page=resources', 93 | 'station': self.MAIN_URL + '?page=station', 94 | 'research': self.MAIN_URL + '?page=research', 95 | 'shipyard': self.MAIN_URL + '?page=shipyard', 96 | 'defense': self.MAIN_URL + '?page=defense', 97 | 'fleet': self.MAIN_URL + '?page=fleet1', 98 | 'galaxy': self.MAIN_URL + '?page=galaxy', 99 | 'galaxyCnt': self.MAIN_URL + '?page=galaxyContent', 100 | 'events': self.MAIN_URL + '?page=eventList', 101 | } 102 | self.planets = [] 103 | self.moons = [] 104 | self.active_attacks = [] 105 | 106 | self.fleet_slots = 0 107 | self.active_fleets = 0 108 | 109 | self.server_time = self.local_time = datetime.now() 110 | self.time_diff = 0 111 | self.emergency_sms_sent = False 112 | self.transport_manager = TransportManager() 113 | self.sim = Sim() 114 | 115 | def _get_url(self, page, planet=None): 116 | url = self.PAGES[page] 117 | if planet is not None: 118 | url += '&cp=%s' % planet.id 119 | return url 120 | 121 | def _prepare_logger(self): 122 | self.logger = logging.getLogger("mechanize") 123 | fh = RotatingFileHandler('bot.log', maxBytes=100000, backupCount=5) 124 | sh = logging.StreamHandler() 125 | fmt = logging.Formatter(fmt='%(asctime)s %(levelname)s %(message)s', 126 | datefmt='%m-%d, %H:%M:%S') 127 | fh.setFormatter(fmt) 128 | sh.setFormatter(fmt) 129 | self.logger.setLevel(logging.INFO) 130 | self.logger.addHandler(fh) 131 | self.logger.addHandler(sh) 132 | 133 | def _prepare_browser(self): 134 | self.br = mechanize.Browser() 135 | self.br.set_handle_equiv(True) 136 | self.br.set_handle_redirect(True) 137 | self.br.set_handle_referer(True) 138 | self.br.set_handle_robots(False) 139 | self.br.addheaders = self.HEADERS 140 | 141 | def _parse_build_url(self, js): 142 | """ 143 | convert: `sendBuildRequest('url', null, 1)`; into: `url` 144 | """ 145 | return self.RE_BUILD_REQUEST.findall(js)[0] 146 | 147 | def _parse_server_time(self, content): 148 | return self.RE_SERVER_TIME.findall(content)[0] 149 | 150 | def get_mother(self): 151 | for p in self.planets: 152 | if p.mother: 153 | return p 154 | return p[0] if self.planets else None 155 | 156 | def get_closest_planet(self, p): 157 | def min_dist(p, d): 158 | return d 159 | _, d, _ = p.split(":") 160 | return sorted([(planet, planet.get_distance(p)) for planet in self.planets], 161 | key=lambda x: x[1])[0][0] 162 | 163 | 164 | def find_planet(self, name=None, coords=None, id=None, is_moon=None): 165 | if is_moon: 166 | planets = self.moons 167 | else: 168 | planets = self.planets 169 | for p in planets: 170 | if name == p.name or coords == p.coords or id == p.id: 171 | return p 172 | 173 | def get_safe_planet(self, planet): 174 | ''' 175 | Get first planet which is not under attack and isn't `planet` 176 | ''' 177 | unsafe_planets = [a.planet for a in self.active_attacks] 178 | for p in self.planets: 179 | if not p in unsafe_planets and p != planet: 180 | return p 181 | # no safe planets! go to mother 182 | return self.planets[0] 183 | 184 | def login(self, username=None, password=None): 185 | username = username or self.username 186 | password = password or self.password 187 | 188 | try: 189 | resp = self.br.open(self.MAIN_URL, timeout=10) 190 | soup = BeautifulSoup(resp) 191 | except: 192 | return False 193 | 194 | alert = soup.find(id='attack_alert') 195 | 196 | # no redirect on main page == user logged in 197 | if resp.geturl() != self.BASE_URL and alert: 198 | self.logged_in = True 199 | self.logger.info('Logged as: %s' % username) 200 | return True 201 | 202 | self.logger.info('Logging in..') 203 | self.br.select_form(name='loginForm') 204 | self.br.form['uni'] = ['s%s-pl.ogame.gameforge.com' % self.uni] 205 | self.br.form['login'] = username 206 | self.br.form['pass'] = password 207 | self.br.submit() 208 | 209 | if self.br.geturl().startswith(self.MAIN_URL): 210 | self.logged_in = True 211 | self.logger.info('Logged as: %s' % username) 212 | return True 213 | else: 214 | self.logged_in = False 215 | self.logger.error('Login failed!') 216 | return False 217 | 218 | def calc_time(self, resp): 219 | try: 220 | y, mo, d, h, mi, sec = map(int, self._parse_server_time(resp).split(',')) 221 | except: 222 | self.logger.error('Exception while calculating time') 223 | else: 224 | self.local_time = n = datetime.now() 225 | self.server_time = datetime(n.year, n.month, n.day, h, mi, sec) 226 | self.time_diff = self.server_time - self.local_time 227 | 228 | self.logger.info('Server time: %s, local time: %s' % \ 229 | (self.server_time, self.local_time)) 230 | 231 | def fetch_planets(self): 232 | self.logger.info('Fetching planets..') 233 | resp = self.br.open(self.PAGES['main']).read() 234 | 235 | self.calc_time(resp) 236 | 237 | soup = BeautifulSoup(resp) 238 | self.planets = [] 239 | self.moons = [] 240 | 241 | try: 242 | for i, c in enumerate(soup.findAll('a', 'planetlink')): 243 | name = c.find('span', 'planet-name').text 244 | coords = c.find('span', 'planet-koords').text[1:-1] 245 | url = c.get('href') 246 | p_id = int(c.parent.get('id').split('-')[1]) 247 | construct_mode = len(c.parent.findAll('a', 'constructionIcon')) != 0 248 | p = Planet(p_id, name, coords, url, construct_mode) 249 | if i == 0: 250 | p.mother = True 251 | self.planets.append(p) 252 | 253 | #check if planet has moon 254 | moon = c.parent.find('a', 'moonlink') 255 | if moon and 'moonlink' in moon['class']: 256 | url = moon.get('href') 257 | m_id = url.split('cp=')[1] 258 | m = Moon(m_id, coords, url) 259 | self.moons.append(m) 260 | except: 261 | self.logger.exception('Exception while fetching planets') 262 | else: 263 | self.check_attacks(soup) 264 | 265 | def handle_planets(self): 266 | self.fetch_planets() 267 | 268 | for p in iter(self.planets): 269 | self.update_planet_info(p) 270 | self.update_planet_fleet(p) 271 | for m in iter(self.moons): 272 | self.update_planet_info(m) 273 | self.update_planet_fleet(m) 274 | 275 | def update_planet_fleet(self, planet): 276 | resp = self.br.open(self._get_url('fleet', planet)) 277 | soup = BeautifulSoup(resp) 278 | ships = {} 279 | for k, v in self.SHIPS.iteritems(): 280 | available = 0 281 | try: 282 | s = soup.find(id='button' + v) 283 | available = int(s.find('span', 'textlabel').nextSibling.replace('.', '')) 284 | except: 285 | available = 0 286 | ships[k] = available 287 | 288 | #self.logger.info('Updating %s fleet' % planet) 289 | #self.logger.info('%s' % fleet) 290 | planet.ships = ships 291 | 292 | def update_planet_info(self, planet): 293 | in_construction_mode = False 294 | resp = self.br.open(self._get_url('resources', planet)) 295 | soup = BeautifulSoup(resp) 296 | 297 | try: 298 | metal = int(soup.find(id='resources_metal').text.replace('.','')) 299 | planet.resources['metal'] = metal 300 | crystal = int(soup.find(id='resources_crystal').text.replace('.','')) 301 | planet.resources['crystal'] = crystal 302 | deuterium = int(soup.find(id='resources_deuterium').text.replace('.','')) 303 | planet.resources['deuterium'] = deuterium 304 | energy = int(soup.find(id='resources_energy').text.replace('.','')) 305 | planet.resources['energy'] = energy 306 | except: 307 | self.logger.exception('Exception while updating resources info') 308 | else: 309 | self.logger.info('Updating resources info for %s:' % planet) 310 | s = 'metal - %(metal)s, crystal - %(crystal)s, deuterium - %(deuterium)s' 311 | self.logger.info(s % planet.resources) 312 | if planet.is_moon(): 313 | return 314 | try: 315 | buildingList = soup.find(id='building') 316 | buildings = ('metalMine', 'crystalMine', 'deuteriumMine', 'solarPlant', 317 | 'fusionPlant', 'solarSatellite' 318 | ) 319 | for building, b in zip(buildings, buildingList.findAll('li')): 320 | can_build = 'on' in b.get('class') 321 | fb = b.find('a', 'fastBuild') 322 | build_url = fb.get('onclick') if fb else '' 323 | if build_url: 324 | build_url = self._parse_build_url(build_url) 325 | try: 326 | level = int(b.find('span', 'textlabel').nextSibling) 327 | except AttributeError: 328 | try: 329 | level = int(b.find('span', 'level').text) 330 | except: 331 | pass 332 | suff_energy = planet.resources['energy'] - self.sim.upgrade_energy_cost(building, level+1) > 0 333 | res = dict( 334 | level=level, 335 | can_build=can_build, 336 | build_url=build_url, 337 | sufficient_energy=suff_energy 338 | ) 339 | 340 | planet.buildings[building] = res 341 | 342 | if buildingList.find('div', 'construction'): 343 | in_construction_mode = True 344 | except: 345 | self.logger.exception('Exception while updating buildings info') 346 | return False 347 | else: 348 | self.logger.info('%s buildings were updated' % planet) 349 | if not in_construction_mode: 350 | text, url = planet.get_mine_to_upgrade() 351 | if url: 352 | self.logger.info('Building upgrade on %s: %s'% (planet, text)) 353 | self.br.open(url) 354 | planet.in_construction_mode = True 355 | #let now transport manager to clear building queue 356 | self.transport_manager.update_building(planet) 357 | else: 358 | self.logger.info('Building queue is not empty') 359 | return True 360 | 361 | def transport_resources(self): 362 | tasks = self.transport_manager.find_dest_planet(self.planets) 363 | if tasks is None: 364 | return False 365 | self.logger.info(self.transport_manager.get_summary()) 366 | for task in iter(tasks): 367 | self.logger.info('Transport attempt from: %s, to: %s with resources %s' \ 368 | % (task['from'], task['where'], task['resources'])) 369 | result = self.send_fleet( 370 | task['from'], 371 | task['where'].coords, 372 | fleet=task['from'].get_fleet_for_resources(task['resources']), 373 | resources=task['resources'], 374 | mission='transport' 375 | ) 376 | if result: 377 | self.transport_manager.update_sent_resources(task['resources']) 378 | self.logger.info('Resources sent: %s, resources needed: %s' \ 379 | % (task['resources'], self.transport_manager.get_resources_needed())) 380 | 381 | return True 382 | 383 | def build_defense(self, planet): 384 | """ 385 | Build defense for all resources on the planet 386 | 1. plasma 387 | 2. gauss 388 | 3. heavy cannon 389 | 4. light cannon 390 | 5. rocket launcher 391 | """ 392 | url = self._get_url('defense', planet) 393 | resp = self.br.open(url) 394 | for t in ('406', '404', '403', '402', '401'): 395 | self.br.select_form(name='form') 396 | self.br.form.new_control('text','menge',{'value':'100'}) 397 | self.br.form.fixup() 398 | self.br['menge'] = '100' 399 | 400 | self.br.form.new_control('text','type',{'value':t}) 401 | self.br.form.fixup() 402 | self.br['type'] = t 403 | 404 | self.br.form.new_control('text','modus',{'value':'1'}) 405 | self.br.form.fixup() 406 | self.br['modus'] = '1' 407 | 408 | self.br.submit() 409 | 410 | def get_player_status(self, destination, origin_planet=None): 411 | if not destination: 412 | return 413 | 414 | status = {} 415 | origin_planet = origin_planet or self.get_closest_planet(destination) 416 | galaxy, system, position = destination.split(':') 417 | 418 | url = self._get_url('galaxyCnt', origin_planet) 419 | data = urlencode({'galaxy': galaxy, 'system': system}) 420 | resp = self.br.open(url, data=data) 421 | soup = BeautifulSoup(resp) 422 | 423 | soup.find(id='galaxytable') 424 | planets = soup.findAll('tr', {'class': 'row'}) 425 | target_planet = planets[int(position)-1] 426 | name_el = target_planet.find('td', 'playername') 427 | status['name'] = name_el.find('span').text 428 | 429 | status['inactive'] = 'inactive' in name_el.get('class', '') 430 | return status 431 | 432 | def find_inactive_nearby(self, planet, radius=15): 433 | 434 | self.logger.info("Searching idlers near %s in radius %s" 435 | % (planet, radius)) 436 | 437 | nearby_systems = planet.get_nearby_systems(radius) 438 | idlers = [] 439 | 440 | for system in nearby_systems: 441 | galaxy, system = system.split(":") 442 | url = self._get_url('galaxyCnt', planet) 443 | data = urlencode({'galaxy': galaxy, 'system': system}) 444 | resp = self.br.open(url, data=data) 445 | soup = BeautifulSoup(resp) 446 | 447 | galaxy_el = soup.find(id='galaxytable') 448 | planets = galaxy_el.findAll('tr', {'class': 'row'}) 449 | for pl in planets: 450 | name_el = pl.find('td', 'playername') 451 | debris_el = pl.find('td', 'debris') 452 | inactive = 'inactive' in name_el.get('class', '') 453 | debris_not_found = 'js_no_action' in debris_el.get('class', '') 454 | if not inactive or not debris_not_found: 455 | continue 456 | position = pl.find('td', 'position').text 457 | coords = "%s:%s:%s" % (galaxy, system, position) 458 | player_id = name_el.find('a').get('rel') 459 | 460 | player_info = soup.find(id=player_id) 461 | rank_el = player_info.find('li', 'rank') 462 | 463 | if not rank_el: 464 | continue 465 | 466 | rank = int(rank_el.find('a').text) 467 | if rank > 4000 or rank < 900: 468 | continue 469 | 470 | idlers.append(coords) 471 | time.sleep(2) 472 | 473 | return idlers 474 | 475 | def find_inactives(self): 476 | 477 | inactives = [] 478 | for p in self.planets: 479 | try: 480 | idlers = self.find_inactive_nearby(p) 481 | self.logger.info(" ".join(idlers)) 482 | inactives.extend(idlers) 483 | except Exception as e: 484 | self.logger.exception(e) 485 | continue 486 | time.sleep(5) 487 | 488 | self.logger.info(" ".join(inactives)) 489 | self.inactives = list(set(inactives)) 490 | self.logger.info(inactives) 491 | 492 | def send_fleet(self, origin_planet, destination, fleet={}, resources={}, 493 | mission='attack', target='planet', speed=None): 494 | if origin_planet.coords == destination: 495 | self.logger.error('Cannot send fleet to the same planet') 496 | return False 497 | self.logger.info('Sending fleet from %s to %s (%s)' \ 498 | % (origin_planet, destination, mission)) 499 | resp = self.br.open(self._get_url('fleet', origin_planet)) 500 | try: 501 | try: 502 | self.br.select_form(name='shipsChosen') 503 | except mechanize.FormNotFoundError: 504 | self.logger.info('No available ships on the planet') 505 | return False 506 | 507 | soup = BeautifulSoup(resp) 508 | for ship, num in fleet.iteritems(): 509 | s = soup.find(id='button' + self.SHIPS[ship]) 510 | num = int(num) 511 | try: 512 | available = int(s.find('span', 'textlabel').nextSibling.replace('.', '')) 513 | except: 514 | available = 0 515 | if available < num and mission in ('attack', 'expedition'): 516 | self.logger.info('No available ships to send') 517 | return False 518 | if num > 0: 519 | self.br.form['am' + self.SHIPS[ship]] = str(num) 520 | 521 | self.br.submit() 522 | 523 | try: 524 | self.br.select_form(name='details') 525 | except mechanize.FormNotFoundError: 526 | self.logger.info('No available ships on the planet') 527 | return False 528 | 529 | galaxy, system, position = destination.split(':') 530 | self.br['galaxy'] = galaxy 531 | self.br['system'] = system 532 | self.br['position'] = position 533 | self.br.form.find_control("type").readonly = False 534 | self.br['type'] = self.TARGETS[target] 535 | self.br.form.find_control("speed").readonly = False 536 | if speed: 537 | self.br['speed'] = self.SPEEDS[speed] 538 | self.br.submit() 539 | 540 | self.br.select_form(name='sendForm') 541 | self.br.form.find_control("mission").readonly = False 542 | self.br.form['mission'] = self.MISSIONS[mission] 543 | if 'metal' in resources: 544 | self.br.form['metal'] = str(resources['metal']) 545 | if 'crystal' in resources: 546 | self.br.form['crystal'] = str(resources['crystal']) 547 | if 'deuterium' in resources: 548 | self.br.form['deuterium'] = str(resources['deuterium']) 549 | self.br.submit() 550 | except Exception as e: 551 | self.logger.exception(e) 552 | return False 553 | else: 554 | if mission == 'attack': 555 | self.farm_no += 1 556 | return True 557 | 558 | def send_message(self, url, player, subject, message): 559 | self.logger.info('Sending message to %s: %s' % (player, message)) 560 | self.br.open(url) 561 | self.br.select_form(nr=0) 562 | self.br.form['betreff'] = subject 563 | self.br.form['text'] = message 564 | self.br.submit() 565 | 566 | def send_sms(self, msg): 567 | from smsapigateway import SMSAPIGateway 568 | try: 569 | SMSAPIGateway().send(msg) 570 | except Exception as e: 571 | self.logger.exception(str(e)) 572 | 573 | def handle_attacks(self): 574 | attack_opts = options['attack'] 575 | send_sms = bool(options['sms']['send_sms']) 576 | 577 | for a in self.active_attacks: 578 | if a.is_dangerous(): 579 | self.logger.info('Handling attack: %s' % a) 580 | if not a.planet.is_moon(): 581 | self.build_defense(a.planet) 582 | if send_sms and not a.sms_sent: 583 | self.send_sms(a.get_sms_text()) 584 | a.sms_sent = True 585 | if send_sms and not a.message_sent: 586 | self.send_message(a.message_url, a.player, attack_opts['message_topic'], 587 | a.get_random_message()) 588 | a.message_sent = True 589 | self.fleet_save(a.planet) 590 | 591 | def check_attacks(self, soup): 592 | alert = soup.find(id='attack_alert') 593 | if not alert: 594 | self.logger.exception('Check attack failed') 595 | return 596 | if 'noAttack' in alert.get('class', ''): 597 | self.logger.info('No attacks') 598 | self.active_attacks = [] 599 | else: 600 | self.logger.info('ATTACK!') 601 | resp = self.br.open(self.PAGES['events']) 602 | soup = BeautifulSoup(resp) 603 | hostile = False 604 | try: 605 | for tr in soup.findAll('tr'): 606 | countDown = tr.find('td', 'countDown') 607 | if countDown and 'hostile' in countDown.get('class', ''): 608 | hostile = True 609 | # First: check if attack was noticed 610 | if tr.get('id'): 611 | attack_id = tr.get('id').split('-')[1] 612 | elif countDown.get('id'): 613 | attack_id = countDown.get('id').split('-')[2] 614 | if not attack_id or attack_id in [a.id for a in self.active_attacks]: 615 | continue 616 | try: 617 | # Attack first discovered: save attack info 618 | arrivalTime = tr.find('td', 'arrivalTime').text.split(' ')[0] 619 | coordsOrigin = tr.find('td', 'coordsOrigin') 620 | if coordsOrigin: 621 | if coordsOrigin.find('a'): 622 | coordsOrigin = coordsOrigin.find('a').text.strip()[1:-1] 623 | destCoords = tr.find('td', 'destCoords') 624 | if destCoords: 625 | destCoords = destCoords.find('a').text.strip()[1:-1] 626 | originFleet = tr.find('td', 'originFleet') 627 | detailsFleet = int(tr.find('td', 'detailsFleet').span.text.replace('.', '')) 628 | player_info = originFleet.find('a') 629 | message_url = player_info.get('href') 630 | player = player_info.get('data-player-name') 631 | is_moon = False # TODO! 632 | planet = self.find_planet(coords=destCoords, is_moon=is_moon) 633 | a = Attack(planet, attack_id, arrivalTime, coordsOrigin, 634 | destCoords, detailsFleet, player, message_url) 635 | 636 | self.active_attacks.append(a) 637 | except Exception as e: 638 | self.logger.exception(e) 639 | self.send_sms('ATTACKEROR') 640 | if not hostile: 641 | self.active_attacks = [] 642 | except Exception as e: 643 | self.logger.exception(e) 644 | 645 | def fleet_save(self, p): 646 | if not p.has_ships(): 647 | return 648 | fleet = p.ships 649 | #recyclers are staying! 650 | #fleet['rc'] = 0 651 | self.logger.info('Making fleet save from %s' % p) 652 | self.send_fleet(p, 653 | self.get_safe_planet(p).coords, 654 | fleet=fleet, 655 | mission='station', 656 | speed=10, 657 | resources={'metal': p.resources['metal']+500, 658 | 'crystal': p.resources['crystal']+500, 659 | 'deuterium': p.resources['deuterium']+500}) 660 | 661 | def collect_debris(self, p): 662 | if not p.has_ships(): 663 | return 664 | self.logger.info('Collecting debris from %s using %s recyclers' % (p, p.ships['rc'])) 665 | self.send_fleet(p, 666 | p.coords, 667 | fleet={'rc':p.ships['rc']}, 668 | mission='collect', 669 | target='debris') 670 | 671 | def send_expedition(self): 672 | expedition = options['expedition'] 673 | planets = expedition['planets'].split(' ') 674 | random.shuffle(planets) 675 | for coords in planets[:3]: 676 | planet = self.find_planet(coords=coords) 677 | if planet: 678 | galaxy, system, position = planet.coords.split(':') 679 | expedition_coords = '%s:%s:16' % (galaxy, system) 680 | self.send_fleet(planet, expedition_coords, 681 | fleet={expedition['ships_kind']:expedition['ships_number']}, 682 | mission='expedition') 683 | 684 | def farm(self): 685 | farms = options['farming']['farms'].split(' ') 686 | ships_kind = options['farming']['ships_kind'] 687 | ships_number = options['farming']['ships_number'] 688 | 689 | l = len(farms) 690 | if l == 0 or not farms[0]: 691 | return 692 | farm = farms[self.farm_no%l] 693 | if not self.get_player_status(farm)['inactive']: 694 | self.farm_no += 1 695 | self.logger.error('farm %s seems not to be inactive!', farm) 696 | return 697 | self.send_fleet( 698 | self.get_closest_planet(farm), 699 | farm, 700 | fleet={ships_kind:ships_number} 701 | ) 702 | 703 | def sleep(self): 704 | sleep_options = options['general'] 705 | sleep_time = randint(0, int(sleep_options['seed']))+int(sleep_options['check_interval']) 706 | self.logger.info('Sleeping for %s secs' % sleep_time) 707 | if self.active_attacks: 708 | sleep_time = 60 709 | time.sleep(sleep_time) 710 | 711 | def stop(self): 712 | self.logger.info('Stopping bot') 713 | os.unlink(self.pidfile) 714 | 715 | def start(self): 716 | self.logger.info('Starting bot') 717 | self.pid = str(os.getpid()) 718 | self.pidfile = 'bot.pid' 719 | file(self.pidfile, 'w').write(self.pid) 720 | 721 | #main loop 722 | while True: 723 | if self.login(): 724 | try: 725 | self.handle_planets() 726 | #self.find_inactives() 727 | if not self.active_attacks: 728 | if True or not self.transport_resources(): 729 | self.send_expedition() 730 | self.farm() 731 | self.farm() 732 | else: 733 | self.handle_attacks() 734 | 735 | except Exception as e: 736 | self.logger.exception(e) 737 | #self.stop() 738 | #return 739 | else: 740 | self.logger.error('Login failed!') 741 | #self.stop() 742 | #return 743 | self.sleep() 744 | 745 | if __name__ == "__main__": 746 | credentials = options['credentials'] 747 | bot = Bot(credentials['username'], credentials['password'], credentials['uni']) 748 | bot.start() 749 | -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [credentials] 2 | uni = 127 3 | username = username 4 | password = password 5 | 6 | [general] 7 | seed = 300 8 | check_interval = 400 9 | timeout = 10 10 | 11 | [building] 12 | min_energy_level = 10 13 | max_fusion_plant_level = 0 14 | levels_diff = 2,3 15 | 16 | [attack] 17 | max_ships = 25 18 | messages = :),Pozdro,No i po co te nerwy?,Nie tym razem,bu,nie spie!,ahoj!,czas uciekac 19 | message_topic = 'hej!' 20 | 21 | [fleet] 22 | 23 | [sms] 24 | send_sms = 1 25 | 26 | [farming] 27 | farms = 1:1:8 1:1:10 1:1:12 1:2:5 1:2:9 1:2:11 1:2:12 1:4:10 1:5:5 1:5:9 1:5:10 1:5:12 1:7:5 1:7:8 1:8:12 1:10:4 1:10:7 1:10:8 1:10:10 1:11:7 1:11:8 1:13:12 1:17:8 1:18:5 1:19:3 1:19:4 1:19:5 1:21:4 1:21:5 1:21:8 1:23:3 1:23:8 1:23:10 1:23:12 1:36:6 1:36:8 1:37:5 1:37:7 1:37:8 1:37:12 1:38:9 1:39:4 1:39:10 1:42:6 1:42:10 1:43:5 1:44:4 1:44:6 1:44:7 1:44:8 1:44:9 1:44:10 1:46:7 1:46:10 1:49:6 1:49:9 1:49:12 1:50:5 1:50:10 1:50:11 1:50:12 1:51:5 1:52:4 1:52:10 1:53:12 1:55:6 1:55:10 1:56:10 1:56:11 1:57:4 1:57:5 1:57:6 1:57:11 1:59:7 1:60:6 1:60:10 1:60:12 2:1:3 2:1:7 2:2:7 2:2:10 2:3:9 2:4:4 2:4:9 2:4:11 2:4:12 2:5:4 2:5:5 2:5:9 2:5:10 2:6:6 2:6:7 2:6:11 2:6:12 2:8:7 2:8:8 2:9:5 2:9:11 2:10:4 2:10:5 2:11:5 2:11:6 2:11:8 2:11:9 2:11:11 2:12:7 2:12:9 2:12:10 2:13:4 2:14:4 2:14:11 2:15:7 2:15:10 2:16:4 2:16:5 2:16:7 2:16:8 2:16:11 2:17:9 2:17:12 2:18:4 2:18:7 2:19:6 2:19:9 2:19:10 2:19:12 2:20:8 2:20:9 2:21:8 2:22:4 2:22:5 2:22:7 2:22:12 2:23:7 2:23:12 1:262:12 1:263:4 1:264:4 1:264:8 1:265:8 1:265:9 1:266:5 1:267:5 1:268:4 1:268:7 1:268:8 1:270:7 1:270:8 1:270:9 1:270:11 1:271:6 1:271:13 1:272:5 1:272:7 1:272:8 1:272:12 1:272:13 1:273:11 1:274:4 1:274:5 1:274:7 1:274:9 1:274:10 1:275:4 1:275:9 1:275:10 1:276:4 1:276:6 1:276:9 1:278:4 1:279:10 1:282:8 1:282:9 1:283:7 1:283:8 1:283:9 1:284:9 1:285:3 1:285:6 1:285:7 1:285:8 1:285:11 1:286:6 1:286:8 1:286:9 1:286:11 1:287:8 1:288:4 1:288:9 1:289:9 1:290:8 1:291:4 1:291:5 1:291:8 1:291:10 1:291:11 2:356:9 2:357:5 2:357:6 2:358:9 2:358:12 2:359:8 2:360:9 2:360:12 2:361:10 2:364:3 2:364:5 2:364:7 2:364:12 2:365:4 2:365:6 2:366:9 2:366:10 2:366:11 2:366:12 2:367:4 2:367:5 2:368:6 2:368:7 2:369:5 2:369:6 2:369:7 2:369:10 2:369:12 2:371:12 2:372:5 2:373:10 2:374:10 2:374:11 2:375:7 2:375:11 2:375:12 2:376:4 2:378:6 2:378:10 2:379:4 2:379:7 2:379:8 2:379:9 2:379:10 2:380:6 2:381:5 2:381:8 2:381:12 2:382:5 2:382:10 2:382:12 2:383:8 2:383:11 2:384:4 2:384:6 2:384:8 2:385:11 3:63:7 3:63:9 3:63:13 3:64:9 3:65:3 3:65:6 3:65:10 3:65:11 3:66:9 3:66:10 3:67:4 3:68:6 3:68:12 3:69:7 3:70:7 3:70:9 3:70:12 3:71:10 3:72:4 3:72:6 3:72:7 3:72:8 3:72:10 3:72:11 3:73:6 3:74:10 3:74:13 3:75:12 3:76:5 3:76:6 3:76:9 3:76:10 3:77:5 3:79:6 3:79:8 3:79:10 3:79:11 3:79:12 3:80:5 3:81:9 3:82:6 3:82:11 3:82:12 3:83:8 3:84:7 3:84:8 3:85:9 3:86:5 3:87:3 3:87:4 3:87:7 3:88:3 3:88:11 3:88:12 3:88:13 3:88:14 3:89:9 3:91:4 1:100:8 1:102:11 1:103:4 1:103:5 1:103:10 1:103:11 1:104:4 1:104:6 1:104:7 1:104:12 1:106:5 1:106:6 1:106:10 1:107:10 1:108:6 1:109:4 1:109:9 1:109:10 1:110:5 1:110:11 1:111:6 1:113:11 1:113:12 1:114:12 1:116:3 1:116:8 1:116:11 1:117:12 1:118:4 1:118:5 1:119:11 1:120:5 1:120:7 1:122:7 1:122:8 1:122:9 1:122:11 1:123:5 1:123:12 1:125:4 1:126:6 1:127:4 1:127:5 1:127:7 1:127:12 1:128:5 1:129:5 1:129:11 28 | 29 | ships_kind = dt 30 | ships_number = 2 31 | 32 | [expedition] 33 | planets = 1:9:4 1:50:7 1:277:8 2:9:7 2:371:9 34 | ships_number = 100 35 | ships_kind = dt 36 | 37 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import ConfigParser 2 | import watchdog 3 | from watchdog.observers import Observer 4 | 5 | global options 6 | 7 | class Options(watchdog.events.FileSystemEventHandler): 8 | CONFIG_FILE = 'config.ini' 9 | REQUIRED_SECTIONS = ( 10 | 'credentials', 11 | 'general', 12 | 'building', 13 | 'attack', 14 | 'fleet', 15 | 'sms', 16 | 'farming', 17 | 'expedition' 18 | ) 19 | 20 | def __init__(self, *args, **kwargs): 21 | 22 | self._config = ConfigParser.RawConfigParser() 23 | self.reload_config() 24 | self._config_valid = False 25 | self.observer = Observer() 26 | self.observer.schedule(self, path='.', recursive=False) 27 | self.observer.start() 28 | 29 | def __del__(self): 30 | self.observer.stop() 31 | self.observer.join() 32 | 33 | def on_any_event(self, event): 34 | if self.CONFIG_FILE in event.src_path: 35 | self.reload_config() 36 | 37 | def reload_config(self): 38 | print 'config reloaded' 39 | self._config.read(self.CONFIG_FILE) 40 | self.check_config() 41 | 42 | def check_config(self): 43 | self._config_valid = False 44 | 45 | try: 46 | for section in self.REQUIRED_SECTIONS: 47 | self._config.items(section) 48 | self._config_valid = True 49 | except ConfigParser.NoSectionError: 50 | self._config_valid = False 51 | 52 | def __getitem__(self, section): 53 | 54 | return dict(self._config.items(section)) 55 | 56 | @property 57 | def valid(self): 58 | return self._is_config_valid 59 | 60 | 61 | options = Options() 62 | -------------------------------------------------------------------------------- /ogame.ini: -------------------------------------------------------------------------------- 1 | # /etc/supervisor/conf.d/ogame.ini 2 | [program:ogame] 3 | directory=/var/www/ogame/ 4 | command=/root/.virtualenvs/ogame/bin/python bot.py 5 | autostart=true 6 | autorestart=true 7 | redirect_stderr=true 8 | -------------------------------------------------------------------------------- /planet.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from config import options 3 | 4 | 5 | class Planet(object): 6 | def __init__(self, id, name, coords, url, in_construction_mode=False): 7 | self.id = id 8 | self.name = name 9 | self.url = url 10 | self.coords = coords 11 | self.mother = False 12 | self.galaxy, self.system, self.position = map(int, self.coords.split(":")) 13 | self.in_construction_mode = in_construction_mode 14 | self.mines = ( 15 | 'metalMine', 16 | 'crystalMine', 17 | 'deuteriumMine' 18 | ) 19 | self.resources = { 20 | 'metal': 0, 21 | 'crystal': 0, 22 | 'deuterium':0, 23 | 'energy':0 24 | } 25 | 26 | self.buildings = { 27 | 'metalMine': { 28 | 'level': 0, 29 | 'buildUrl':'', 30 | 'can_build': False, 31 | 'sufficient_energy': False 32 | }, 33 | 'crystalMine': { 34 | 'level': 0, 35 | 'buildUrl':'', 36 | 'can_build': False, 37 | 'sufficient_energy': False 38 | }, 39 | 'deuteriumMine': { 40 | 'level': 0, 41 | 'buildUrl':'', 42 | 'can_build': False, 43 | 'sufficient_energy': False 44 | }, 45 | 'solarPlant': { 46 | 'level': 0, 47 | 'buildUrl':'', 48 | 'can_build': False, 49 | 'sufficient_energy': True 50 | }, 51 | 'fusionPlant': { 52 | 'level': 0, 53 | 'buildUrl':'', 54 | 'can_build': False, 55 | 'sufficient_energy': True 56 | }, 57 | 'solarSatellite': { 58 | 'level': 0, 59 | 'buildUrl':'', 60 | 'can_build': False, 61 | 'sufficient_energy': True 62 | }, 63 | } 64 | 65 | self.ships = { 66 | 'lm': 0, 67 | 'hm': 0, 68 | 'cr': 0, 69 | 'ow': 0, 70 | 'pn': 0, 71 | 'bb': 0, 72 | 'ns': 0, 73 | 'gs': 0, 74 | 'lt': 0, 75 | 'dt': 0, 76 | 'cs': 0, 77 | 'rc': 0, 78 | 'ss': 0 79 | } 80 | 81 | def __str__(self): 82 | return self.name 83 | 84 | def __eq__(self, other): 85 | return self.id == other.id 86 | 87 | def get_mine_to_upgrade(self): 88 | build_options = options['building'] 89 | min_energy_level = int(build_options['min_energy_level']) 90 | levels_diff = map(int, build_options['levels_diff'].split(',')) 91 | max_fusion_lvl = int(build_options['max_fusion_plant_level']) 92 | 93 | b = self.buildings 94 | build_power_plant = self.resources['energy'] <= 0 95 | 96 | mine_levels = [0, 0, 0] 97 | 98 | for i, mine in enumerate(self.mines): 99 | mine_levels[i] = b[mine]['level'] 100 | 101 | proposed_levels = [ 102 | b['metalMine']['level'], 103 | b['metalMine']['level'] - levels_diff[0], 104 | b['metalMine']['level'] - levels_diff[0] - levels_diff[1] 105 | ] 106 | proposed_levels = [0 if l < 0 else l for l in proposed_levels] 107 | if proposed_levels == mine_levels or (mine_levels[1] >= proposed_levels[1] and mine_levels[2] >= proposed_levels[2]): 108 | proposed_levels[0] += 1 109 | 110 | num_suff_energy = 0 111 | for i in xrange(3): 112 | building = self.mines[i] 113 | if b[building]['sufficient_energy']: 114 | num_suff_energy += 1 115 | if b[building]['can_build'] and proposed_levels[i] > b[building]['level']: 116 | if b[building]['sufficient_energy']: 117 | return building, b[building]['build_url'] 118 | else: 119 | build_power_plant = True 120 | 121 | if build_power_plant or num_suff_energy == 0: 122 | if b['solarPlant']['can_build']: 123 | return u'Solar plant', b['solarPlant']['build_url'] 124 | elif b['fusionPlant']['can_build'] and \ 125 | b['fusionPlant']['level'] < max_fusion_lvl: 126 | return u'Fusion plant', b['fusionPlant']['build_url'] 127 | else: 128 | return None, None 129 | else: 130 | return None, None 131 | 132 | def is_moon(self): 133 | return False 134 | 135 | def has_ships(self): 136 | ''' 137 | Return true if any ship is stationing on the planet 138 | ''' 139 | return any(self.ships.values()) 140 | 141 | def get_distance(self, planet): 142 | """ 143 | Return distance to planet `planet` 144 | """ 145 | try: 146 | g, s, p = map(int, planet.split(":")) 147 | except Exception: 148 | return 100000 149 | d = 0 150 | d += (abs(g - self.galaxy) * 100) 151 | d += (abs(s - self.system) * 10) 152 | d += (abs(p - self.position)) 153 | 154 | return d 155 | 156 | def get_fleet_for_resources(self, r): 157 | total = sum([r.get('metal', 0), r.get('crystal', 0), r.get('deuterium', 0)]) 158 | to_send = 0 159 | ships = {'dt': 0, 'lt': 0} 160 | for kind in ('dt', 'lt'): 161 | if self.ships[kind] > 0: 162 | for i in xrange(self.ships[kind]): 163 | to_send += (25000 if kind == 'dt' else 5000) 164 | ships[kind] += 1 165 | if to_send > total: 166 | return ships 167 | return ships 168 | 169 | def get_nearby_systems(self, radius): 170 | g, s, p = map(int, self.coords.split(":")) 171 | start_system = max(1, s-radius) 172 | end_system = min(499, s+radius) 173 | systems = [] 174 | for system in xrange(start_system, end_system): 175 | systems.append("%s:%s" % (g, system)) 176 | 177 | return systems 178 | 179 | class Moon(Planet): 180 | 181 | def __init__(self, id, coords, url): 182 | super(Moon, self).__init__(id, 'Moon', coords, url) 183 | 184 | def get_mine_to_upgrade(self): 185 | return None, None 186 | 187 | def is_moon(self): 188 | return True 189 | 190 | def __str__(self): 191 | return '[%s] %s' % (self.coords, self.name) 192 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mechanize 2 | BeautifulSoup 3 | watchdog -------------------------------------------------------------------------------- /sim.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | class Sim(object): 4 | FIRST_COST = { 5 | 'metalMine': { 6 | 'metal': 60, 7 | 'crystal': 15, 8 | 'deuterium': 0, 9 | }, 10 | 'crystalMine': { 11 | 'metal': 48, 12 | 'crystal': 24, 13 | 'deuterium': 0 14 | }, 15 | 'deuteriumMine':{ 16 | 'metal': 225, 17 | 'crystal': 75, 18 | 'deuterium': 0 19 | }, 20 | 'solarPlant': { 21 | 'metal': 75, 22 | 'crystal': 30, 23 | 'deuterium': 0 24 | }, 25 | 'fusionPlant': { 26 | 'metal': 900, 27 | 'crystal': 360, 28 | 'deuterium': 180 29 | } 30 | } 31 | FACTORS = { 32 | 'metalMine': 1.5, 33 | 'crystalMine': 1.6, 34 | 'deuteriumMine': 1.5, 35 | 'solarPlant': 1.5, 36 | 'fusionPlant': 1.8 37 | } 38 | 39 | ENERGY_COST_FACTORS = { 40 | 'metalMine': 10, 41 | 'crystalMine': 10, 42 | 'deuteriumMine': 20 43 | } 44 | 45 | def _calc_building_cost(self, what, level): 46 | assert what in ('metalMine', 'crystalMine', 'deuteriumMine', 'solarPlant', 'fusionPlant') 47 | return { 48 | 'metal': int(self.FIRST_COST[what]['metal'] * (self.FACTORS[what] ** (level - 1))), 49 | 'crystal': int(self.FIRST_COST[what]['crystal'] * (self.FACTORS[what] ** (level - 1))), 50 | 'deuterium': int(self.FIRST_COST[what]['deuterium'] * (self.FACTORS[what] ** (level - 1))), 51 | } 52 | 53 | def _calc_energy_cost(self, what, level): 54 | return math.floor(self.ENERGY_COST_FACTORS[what] * level * 1.1 ** level) + 1 55 | 56 | def upgrade_energy_cost(self, what, to_level): 57 | try: 58 | return self._calc_energy_cost(what, to_level) - self._calc_energy_cost(what, to_level-1) 59 | except KeyError: 60 | return -10000000 61 | 62 | def cost_solar_plant(self, level): 63 | return self._calc_building_cost('solarPlant', level) 64 | 65 | def cost_metal_mine(self, level): 66 | return self._calc_building_cost('metalMine', level) 67 | 68 | def cost_crystal_mine(self, level): 69 | return self._calc_building_cost('crystalMine', level) 70 | 71 | def cost_deuterium_mine(self, level): 72 | return self._calc_building_cost('deuteriumMine', level) 73 | 74 | def get_cost(self, what, level): 75 | return self._calc_building_cost(what, level) 76 | 77 | def get_total_transport_capacity(self, ships): 78 | return (ships['lt'] * 5000) + (ships['dt'] * 25000) 79 | 80 | #test 81 | if __name__ == "__main__": 82 | s = Sim() 83 | -------------------------------------------------------------------------------- /smsapigateway.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import urllib2 3 | from urlparse import urlparse 4 | from urllib import quote 5 | 6 | class SMSAPIGateway(object): 7 | PASS_MD5 = '85d9ee56e9927912119fbc89de2eb22e' 8 | USERNAME = 'username' 9 | URL = 'https://ssl.smsapi.pl/sms.do?' 10 | TO = 'PHONE_NUMBER' 11 | 12 | def send(self, msg): 13 | url = '%susername=%s&password=%s&message=%s&to=%s&eco=1&encoding=utf-8' % \ 14 | (self.URL, self.USERNAME, self.PASS_MD5, msg, self.TO) 15 | url = quote(url, safe='/:?&=') 16 | try: 17 | print urllib2.urlopen(url).read() 18 | except Exception, e: 19 | print e 20 | 21 | if __name__ == "__main__": 22 | SMSAPIGateway().send('tesfg zażółć gęślą jażźń') 23 | -------------------------------------------------------------------------------- /transport_manager.py: -------------------------------------------------------------------------------- 1 | from sim import Sim 2 | 3 | class TransportManager(object): 4 | def __init__(self, planets=[]): 5 | self.planets = planets 6 | self.sim = Sim() 7 | self.dest_planet = None 8 | self.building = None 9 | self.resources_needed = { 10 | 'metal': 0, 11 | 'crystal': 0, 12 | 'deuterium': 0 13 | } 14 | self.resources_sent = self.resources_needed 15 | self.building_queue = set() 16 | 17 | def find_solar_to_upgrade(self): 18 | planets = filter(lambda x: not x.in_construction_mode and not (x in self.building_queue), self.planets) 19 | for p in iter(planets): 20 | if p.resources['energy'] < 0: 21 | return p, 'solarPlant' 22 | return None, None 23 | 24 | def find_planet_to_upgrade(self): 25 | # check mine levels and pick one to upgrade 26 | mm, cm, dm, sp = [], [], [], [] 27 | planets = filter(lambda x: not x.in_construction_mode and not (x in self.building_queue), self.planets) 28 | if not planets: 29 | return None, None 30 | for p in iter(planets): 31 | mm.append(p.buildings['metalMine']['level']) 32 | cm.append(p.buildings['crystalMine']['level']) 33 | dm.append(p.buildings['deuteriumMine']['level']) 34 | sp.append(p.buildings['solarPlant']['level']) 35 | 36 | # search biggest difference 37 | worst_mm = min(mm) 38 | mm_diff = max(mm) - worst_mm 39 | worst_cm = min(cm) 40 | cm_diff = max(cm) - worst_cm 41 | worst_dm = min(dm) 42 | dm_diff = max(dm) - worst_dm 43 | worst_sp = min(sp) 44 | sp_diff = max(sp) - worst_sp 45 | worst_planets = { 46 | 'metalMine' : planets[mm.index(worst_mm)], 47 | 'crystalMine': planets[cm.index(worst_cm)], 48 | 'deuteriumMine': planets[dm.index(worst_dm)], 49 | 'solarPlant' : planets[sp.index(worst_sp)] 50 | } 51 | 52 | diff = { 53 | 'metalMine': mm_diff, 54 | 'crystalMine': cm_diff, 55 | 'deuteriumMine': dm_diff, 56 | 'solarPlant': sp_diff 57 | } 58 | 59 | sorted_diff = sorted(diff, key=diff.get, reverse=True) 60 | #we now what to build, but where? 61 | what = sorted_diff[0] 62 | 63 | #print 'planet to upgrade', worst_planets[what], what 64 | return worst_planets[what], what 65 | 66 | def calc_resources_needed(self): 67 | level = self.dest_planet.buildings[self.building]['level'] 68 | cost = self.sim.get_cost(self.building, level+1) 69 | available = self.dest_planet.resources 70 | #print 'level: ', level 71 | #print 'cost: ', cost 72 | #print 'available: ', available 73 | res = {} 74 | for resType in ('metal', 'crystal', 'deuterium'): 75 | res[resType] = cost[resType] - available[resType] 76 | if res[resType] < 0: 77 | res[resType] = 0 78 | return res 79 | 80 | def get_resources_available_to_send(self, planet, need_to_send): 81 | total = self.sim.get_total_transport_capacity(planet.ships) 82 | will_be_sent = {'metal': 0, 'crystal': 0, 'deuterium': 0} 83 | for resourceType in ('metal', 'crystal', 'deuterium'): 84 | if need_to_send[resourceType] <= 0: 85 | continue 86 | if need_to_send[resourceType] <= total: 87 | if need_to_send[resourceType] <= planet.resources[resourceType]: 88 | total -= need_to_send[resourceType] 89 | will_be_sent[resourceType] = need_to_send[resourceType] 90 | else: 91 | will_be_sent[resourceType] = planet.resources[resourceType] 92 | total-= planet.resources[resourceType] 93 | else: 94 | return will_be_sent 95 | return will_be_sent 96 | 97 | def process_dest_planet(self): 98 | self.resources_needed = self.calc_resources_needed() 99 | planets = filter(lambda x: x != self.dest_planet, self.planets) 100 | if self.enough_resources_to_build(): 101 | self.building_queue.add(self.dest_planet) 102 | #sort planets by resources 103 | planets.sort(key=lambda p: sum([p.resources['metal'], p.resources['crystal']]), reverse=True) 104 | res = [] 105 | need_to_send = {} 106 | send = False 107 | for resourceType in ('metal', 'crystal', 'deuterium'): 108 | need_to_send[resourceType] = self.resources_needed[resourceType] - self.resources_sent[resourceType] 109 | if need_to_send[resourceType] > 0: 110 | send = True 111 | if not send: 112 | return 113 | for p in iter(planets): 114 | task = {'from': p, 'where': self.dest_planet, 'resources': {}} 115 | task['resources'] = self.get_resources_available_to_send(p, need_to_send) 116 | if task['resources']['metal'] + task['resources']['crystal'] < 50000: 117 | continue 118 | res.append(task) 119 | for resourceType in ('metal', 'crystal', 'deuterium'): 120 | need_to_send[resourceType] -= task['resources'][resourceType] 121 | return res 122 | return res 123 | else: 124 | return None 125 | 126 | 127 | def enough_resources_to_build(self): 128 | res = { 129 | 'metal': 0, 130 | 'crystal': 0, 131 | 'deuterium': 0 132 | } 133 | 134 | for p in iter(self.planets): 135 | r = p.resources 136 | res['metal'] += r['metal'] 137 | res['crystal'] += r['crystal'] 138 | res['deuterium'] += r['deuterium'] 139 | #print 'available: ', res 140 | #print 'needed: ', self.resources_needed 141 | #print 'sent: ', self.resources_sent 142 | for resType in ('metal', 'crystal', 'deuterium'): 143 | if res[resType] < (self.resources_needed[resType] - self.resources_sent[resType]): 144 | return False 145 | 146 | return True 147 | 148 | def reset(self): 149 | self.dest_planet = None 150 | self.building = None 151 | self.resources_needed = { 152 | 'metal': 0, 153 | 'crystal': 0, 154 | 'deuterium': 0 155 | } 156 | self.resources_sent = self.resources_needed 157 | 158 | 159 | def update_sent_resources(self, resources): 160 | all_sent = True 161 | for resType in ('metal', 'crystal', 'deuterium'): 162 | self.resources_sent[resType] += resources[resType] 163 | if self.resources_sent[resType] < self.resources_needed[resType]: 164 | all_sent = False 165 | 166 | if all_sent: 167 | self.building_queue.add(self.dest_planet) 168 | self.reset() 169 | 170 | def get_resources_needed(self): 171 | return self.resources_needed 172 | 173 | def get_summary(self): 174 | summ = '\nDest planet: %s\n' 175 | summ += 'Building: %s\n' 176 | summ += 'Resources needed: %s\n' 177 | summ += 'Resources sent: %s\n' 178 | summ = summ % (self.dest_planet, self.building, 179 | self.resources_needed, self.resources_sent) 180 | return summ 181 | 182 | 183 | def update_building(self, planet): 184 | if planet in self.building_queue: 185 | self.building_queue.remove(planet) 186 | self.reset() 187 | 188 | def find_dest_planet(self, planets): 189 | self.planets = planets 190 | if len(self.planets) < 2: 191 | return None 192 | 193 | if self.dest_planet is not None: 194 | return self.process_dest_planet() 195 | 196 | # check if solar plant needs upgrade somewhere 197 | p, building = self.find_solar_to_upgrade() 198 | if not p: 199 | p, building = self.find_planet_to_upgrade() 200 | if p: 201 | self.dest_planet = p 202 | self.building = building 203 | return self.process_dest_planet() 204 | 205 | # nothing to do 206 | return None 207 | 208 | 209 | 210 | 211 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # HELPER FUNCTIONS 4 | def login_required(fn): 5 | from functools import wraps 6 | @wraps(fn) 7 | def wrapper(self): 8 | if not self.logged_in: 9 | self.login() 10 | return fn(self) 11 | return wrapper 12 | 13 | def load_sms_gateway(name): 14 | mod = __import__(name) 15 | components = name.split('.') 16 | for comp in components[1:]: 17 | mod = getattr(mod, comp) 18 | return mod --------------------------------------------------------------------------------