├── __init__.py ├── base ├── __init__.py ├── client │ ├── __init__.py │ ├── generals_api.py │ ├── constants.py │ ├── tile.py │ ├── map.py │ ├── bot_cmds.py │ └── generals.py ├── bot_moves.py ├── bot_base.py └── viewer.py ├── requirements.txt ├── Dockerfile ├── License.txt ├── startup.py ├── .gitignore ├── README.md ├── bot_blob.py ├── bot_test.py ├── bot_control.py ├── tools └── playerstats.py └── bot_path_collect.py /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /base/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /base/client/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | websocket-client 2 | pygame 3 | requests 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6.1 2 | 3 | WORKDIR /usr/src/app 4 | ADD requirements.txt /usr/src/app/requirements.txt 5 | RUN pip install -U websocket-client 6 | 7 | ADD base/ /usr/src/app/base/ 8 | ADD *.py /usr/src/app/ 9 | 10 | CMD ["python", "bot_blob.py", "--no-ui"] 11 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Harris Christiansen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /base/client/generals_api.py: -------------------------------------------------------------------------------- 1 | ''' 2 | @ Harris Christiansen (code@HarrisChristiansen.com) 3 | Generals.io Automated Client - https://github.com/harrischristiansen/generals-bot 4 | Generals.io Web API Requests 5 | ''' 6 | 7 | import requests 8 | 9 | ######################### Public Methods ######################### 10 | 11 | _list_top = None 12 | def list_top(): 13 | global _list_top 14 | if _list_top == None: 15 | _list_top = _get_list_maps("http://generals.io/api/maps/lists/top") 16 | return _list_top 17 | 18 | _list_hot = None 19 | def list_hot(): 20 | global _list_hot 21 | if _list_hot == None: 22 | _list_hot = _get_list_maps("http://generals.io/api/maps/lists/hot") 23 | return _list_hot 24 | 25 | def list_both(): 26 | maps = list_top() 27 | maps.extend(list_hot()) 28 | return maps 29 | 30 | def list_search(query): 31 | return _get_list_maps("http://generals.io/api/maps/search?q="+query) 32 | 33 | ######################### Private Methods ######################### 34 | 35 | def _get_list_maps(url): 36 | data = _get_url(url) 37 | maps = [] 38 | for custommap in data: 39 | if _is_valid_name(custommap['title']): 40 | maps.append(custommap['title']) 41 | return maps 42 | 43 | def _get_url(url): 44 | return requests.get(url).json() 45 | 46 | def _is_valid_name(name): 47 | return all(ord(c) < 128 for c in name) 48 | -------------------------------------------------------------------------------- /startup.py: -------------------------------------------------------------------------------- 1 | ''' 2 | @ Harris Christiansen (code@HarrisChristiansen.com) 3 | Generals.io Automated Client - https://github.com/harrischristiansen/generals-bot 4 | Startup: Initiate Bots with command line arguments 5 | ''' 6 | import os 7 | import argparse 8 | from base import bot_base 9 | 10 | def startup(moveMethod, moveEvent=None, botName="PurdueBot"): 11 | parser = argparse.ArgumentParser() 12 | parser.add_argument('-name', metavar='str', type=str, default=os.environ.get('GENERALS_BOT_NAME', botName), help='Name of Bot') 13 | parser.add_argument('-g', '--gameType', metavar='str', type=str, choices=["private","1v1","ffa"], default=os.environ.get('GENERALS_BOT_MODE', 'private'), help='Game Type: private, 1v1, or ffa') 14 | parser.add_argument('-r', '--roomID', metavar='str', type=str, default=os.environ.get("GENERALS_BOT_ROOM_ID", "PurdueBot"), help='Private Room ID (optional)') 15 | parser.add_argument('-c', '--command', metavar='str', type=str, default="", help='Initial Setup Command (optional)') 16 | parser.add_argument('--no-ui', action='store_false', help="Hide UI (no game viewer)") 17 | parser.add_argument('--public', action='store_true', help="Run on public (not bot) server") 18 | args = vars(parser.parse_args()) 19 | 20 | if moveMethod == None: 21 | raise ValueError("A move method must be supplied upon startup") 22 | 23 | bot_base.GeneralsBot(moveMethod, moveEvent=moveEvent, name=args['name'], gameType=args['gameType'], privateRoomID=args['roomID'], showGameViewer=args['no_ui'], public_server=args['public'], start_msg_cmd=args['command']) 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | games/game_* 2 | 3 | # Mac DS Store 4 | .DS_Store 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | env/ 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *,cover 51 | .hypothesis/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # IPython Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | 93 | # Rope project settings 94 | .ropeproject 95 | 96 | # Wildcard Ignore 97 | *_ignore.* 98 | 99 | # PyBuilder 100 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Generals.io - Automated Client 2 | 3 | ![Generals.IO Gameplay Image](http://files.harrischristiansen.com/0r0y0C1t2r26/generals.png "Generals.IO Gameplay Image") 4 | 5 | ## Synopsis 6 | 7 | [Generals.io](http://generals.io) is a multiplayer web game where the goal is to protect your general and capture the enemy generals. 8 | 9 | This is a collection of various automated clients (bots) for playing [Generals.io](http://generals.io). The project includes a toolkit for creating bots, as well as a UI viewer for watching live games. 10 | 11 | Project available on [GitHub](https://github.com/harrischristiansen/generals-bot). 12 | 13 | ## Setup 14 | 15 | - [ ] Python3 (https://www.python.org/downloads/) 16 | - [ ] Install Dependencies: `pip3 install -r requirements.txt` 17 | - [ ] NPM Forever: `npm install -g forever` (optional) 18 | 19 | ## Usage 20 | 21 | - [ ] Blob Bot: `python3 bot_blob.py [-name] [-g gameType] [-r roomID]` 22 | - [ ] Path Bot: `python3 bot_path_collect.py [-name] [-g gameType] [-r roomID]` 23 | 24 | - [ ] Run Forever: `forever start -c python3 bot_blob.py -name BotName -g ffa` 25 | 26 | ## Features 27 | 28 | ### Bots 29 | - [X] bot_blob.py 30 | - [X] move_toward: Run largest army to nearest priority target 31 | - [X] move_outward: Move Border Armies Outward 32 | - [ ] bot_path_collect.py 33 | - [X] Primary Path Routine: Run path from largest city to primary target 34 | - [ ] Continue running after reaching primary target 35 | - [X] Collect Troops Routine (Run largest army toward nearest path tile) 36 | - [X] Move Border Armies Outward 37 | - [ ] Proximity Targeting 38 | 39 | ### Sample Code 40 | - [ ] samples/nearest.py: Run largest army to nearest priority target 41 | 42 | ## Contributors 43 | 44 | @harrischristiansen [HarrisChristiansen.com](http://www.harrischristiansen.com) (code@harrischristiansen.com) 45 | -------------------------------------------------------------------------------- /bot_blob.py: -------------------------------------------------------------------------------- 1 | ''' 2 | @ Harris Christiansen (code@HarrisChristiansen.com) 3 | Generals.io Automated Client - https://github.com/harrischristiansen/generals-bot 4 | Bot_blob: Creates a blob of troops. 5 | ''' 6 | 7 | import logging 8 | from base import bot_moves 9 | 10 | # Show all logging 11 | logging.basicConfig(level=logging.DEBUG) 12 | 13 | ######################### Move Making ######################### 14 | 15 | _bot = None 16 | _map = None 17 | def make_move(currentBot, currentMap): 18 | global _bot, _map 19 | _bot = currentBot 20 | _map = currentMap 21 | 22 | if move_priority(): 23 | return 24 | 25 | if _map.turn % 3 == 0: 26 | if move_outward(): 27 | return True 28 | if not move_toward(): 29 | move_outward() 30 | return 31 | 32 | def place_move(source, dest): 33 | _bot.place_move(source, dest, move_half=bot_moves.should_move_half(_map, source, dest)) 34 | 35 | ######################### Move Priority ######################### 36 | 37 | def move_priority(): 38 | (source, dest) = bot_moves.move_priority(_map) 39 | if source and dest: 40 | place_move(source, dest) 41 | return True 42 | return False 43 | 44 | ######################### Move Outward ######################### 45 | 46 | def move_outward(): 47 | (source, dest) = bot_moves.move_outward(_map) 48 | if source and dest: 49 | place_move(source, dest) 50 | return True 51 | return False 52 | 53 | ######################### Move Toward ######################### 54 | 55 | def move_toward(): 56 | _map.path = bot_moves.path_proximity_target(_map) 57 | (move_from, move_to) = bot_moves.move_path(_map.path) 58 | if move_from and move_to: 59 | place_move(move_from, move_to) 60 | return True 61 | return False 62 | 63 | ######################### Main ######################### 64 | 65 | # Start Game 66 | import startup 67 | if __name__ == '__main__': 68 | startup.startup(make_move, botName="PurdueBot-B2") 69 | -------------------------------------------------------------------------------- /bot_test.py: -------------------------------------------------------------------------------- 1 | ''' 2 | @ Harris Christiansen (code@HarrisChristiansen.com) 3 | Generals.io Automated Client - https://github.com/harrischristiansen/generals-bot 4 | bot_test: Used for testing various move methods 5 | ''' 6 | 7 | import logging 8 | import time 9 | from base import bot_moves 10 | 11 | PRINT_TIMING = False 12 | PRINT_MOVES = False 13 | # Show all logging 14 | logging.basicConfig(level=logging.DEBUG) 15 | 16 | ######################### Move Making ######################### 17 | 18 | _bot = None 19 | _map = None 20 | def make_move(currentBot, currentMap): 21 | global _bot, _map 22 | _bot = currentBot 23 | _map = currentMap 24 | 25 | if _map.turn < 24 and currentBot._gameType != "private": 26 | return 27 | 28 | start_time = time.time() 29 | 30 | if not move_priority(): 31 | if _map.turn < 42 or not move_outward(): 32 | move_toward() 33 | 34 | if PRINT_TIMING: 35 | move_time = time.time() - start_time 36 | logging.info("Move (%d) took: %1.7fs" % move_time) 37 | 38 | def place_move(source, dest): 39 | if PRINT_MOVES: 40 | logging.info("Move: %s -> %s" % (source, dest)) 41 | _bot.place_move(source, dest) 42 | 43 | ######################### Move Priority ######################### 44 | 45 | def move_priority(): 46 | (source, dest) = bot_moves.move_priority(_map) 47 | if source and dest: 48 | place_move(source, dest) 49 | return True 50 | return False 51 | 52 | ######################### Move Outward ######################### 53 | 54 | def move_outward(): 55 | (source, dest) = bot_moves.move_outward(_map) 56 | if source and dest: 57 | place_move(source, dest) 58 | return True 59 | return False 60 | 61 | ######################### Move Toward ######################### 62 | 63 | def move_toward(): 64 | _map.path = bot_moves.path_proximity_target(_map) 65 | (move_from, move_to) = bot_moves.move_path(_map.path) 66 | if move_from and move_to: 67 | place_move(move_from, move_to) 68 | return True 69 | return False 70 | 71 | ######################### Main ######################### 72 | 73 | # Start Game 74 | import startup 75 | if __name__ == '__main__': 76 | startup.startup(make_move, botName="PurdueBot-T") 77 | -------------------------------------------------------------------------------- /bot_control.py: -------------------------------------------------------------------------------- 1 | ''' 2 | @ Harris Christiansen (code@HarrisChristiansen.com) 3 | Generals.io Automated Client - https://github.com/harrischristiansen/generals-bot 4 | Bot_control: Create a human controlled bot 5 | ''' 6 | 7 | import logging 8 | from base import bot_moves 9 | 10 | # Set logging level 11 | logging.basicConfig(level=logging.INFO) 12 | 13 | ######################### Move Making ######################### 14 | 15 | nextMove = [] 16 | last_manual = 0 17 | 18 | _bot = None 19 | _map = None 20 | def make_move(currentBot, currentMap): 21 | global _bot, _map, last_manual 22 | _bot = currentBot 23 | _map = currentMap 24 | 25 | if not move_priority(): 26 | if not move_manual(): 27 | last_manual += 1 28 | if not move_outward(): 29 | if last_manual > 5: 30 | move_toward() 31 | else: 32 | last_manual = 0 33 | return 34 | 35 | def place_move(source, dest): 36 | _bot.place_move(source, dest, move_half=bot_moves.should_move_half(_map, source, dest)) 37 | 38 | ######################### Manual Control ######################### 39 | 40 | def add_next_move(source_xy, dest_xy): 41 | if _map == None: 42 | return False 43 | 44 | source = _map.grid[source_xy[1]][source_xy[0]] 45 | dest = _map.grid[dest_xy[1]][dest_xy[0]] 46 | 47 | move = (source, dest) 48 | nextMove.append(move) 49 | _bot._path = [t[1] for t in nextMove] 50 | 51 | def move_manual(): 52 | global nextMove, last_manual 53 | if len(nextMove) == 0: 54 | return False 55 | 56 | (source, dest) = nextMove.pop(0) 57 | if source and dest: 58 | place_move(source, dest) 59 | return True 60 | return False 61 | 62 | ######################### Move Priority ######################### 63 | 64 | def move_priority(): 65 | (source, dest) = bot_moves.move_priority(_map) 66 | if source and dest: 67 | place_move(source, dest) 68 | return True 69 | return False 70 | 71 | ######################### Move Outward ######################### 72 | 73 | def move_outward(): 74 | (source, dest) = bot_moves.move_outward(_map) 75 | if source and dest: 76 | place_move(source, dest) 77 | return True 78 | return False 79 | 80 | ######################### Move Toward ######################### 81 | 82 | def move_toward(): 83 | path = bot_moves.path_proximity_target(_map) 84 | (move_from, move_to) = bot_moves.move_path(path) 85 | if move_from and move_to: 86 | place_move(move_from, move_to) 87 | return True 88 | return False 89 | 90 | ######################### Main ######################### 91 | 92 | # Start Game 93 | import startup 94 | if __name__ == '__main__': 95 | startup.startup(make_move, moveEvent=add_next_move, botName="PurdueBot-H") 96 | -------------------------------------------------------------------------------- /base/client/constants.py: -------------------------------------------------------------------------------- 1 | ''' 2 | @ Harris Christiansen (code@HarrisChristiansen.com) 3 | Generals.io Automated Client - https://github.com/harrischristiansen/generals-bot 4 | Constants: Constants used throughout the code 5 | ''' 6 | 7 | SHOULD_DIRTY_MAP_ON_MOVE = True 8 | 9 | ENDPOINT_BOT = "://botws.generals.io/socket.io/?EIO=4" 10 | ENDPOINT_PUBLIC = "://ws.generals.io/socket.io/?EIO=4" 11 | BOT_KEY = "sd09fjd203i0ejwi" 12 | 13 | BANNED_PLAYERS = [ 14 | "hanwi4", 15 | ''' 16 | "lilBlakey", 17 | "hunterjacksoncarr@" 18 | "ExpiredCat", 19 | "hunter2.0", 20 | "okloveme", 21 | "hunterjacksoncarr@", 22 | "creeded", 23 | "Centro2", 24 | "hunter4.0", 25 | ''' 26 | ] 27 | 28 | BANNED_CHAT_PLAYERS = [ 29 | "UYHS4J", 30 | ] 31 | 32 | REPLAY_URLS = { 33 | 'na': "http://generals.io/replays/", 34 | 'eu': "http://eu.generals.io/replays/", 35 | 'bot': "http://bot.generals.io/replays/", 36 | } 37 | 38 | START_KEYWORDS = ["start", "go", "force", "play", "ready", "rdy"] 39 | HELLO_KEYWORDS = ["hi", "hello", "hey", "sup", "myssix"] 40 | HELP_KEYWORDS = ["help", "config", "change"] 41 | 42 | GENERALS_MAPS = [ 43 | "KILL A KING", 44 | "Plots", 45 | "Speed", 46 | "Experiment G", 47 | "WIN or LOSE", 48 | "The Inquisitor", 49 | "Kingdom of Branches", 50 | "Hidden 1", 51 | ] 52 | 53 | DIRECTIONS = [(1, 0), (-1, 0), (0, 1), (0, -1)] 54 | 55 | TILE_EMPTY = -1 56 | TILE_MOUNTAIN = -2 57 | TILE_FOG = -3 58 | TILE_OBSTACLE = -4 59 | 60 | # Opponent Type Definitions 61 | OPP_EMPTY = 0 62 | OPP_ARMY = 1 63 | OPP_CITY = 2 64 | OPP_GENERAL = 3 65 | 66 | MAX_NUM_TEAMS = 8 67 | 68 | 69 | PRE_HELP_TEXT = [ 70 | "| Hi, I am Myssix - a generals.io bot", 71 | "| ======= Available Commands =======", 72 | "| start: send force start", 73 | "| speed 4: set game play speed [1, 2, 3, 4]", 74 | "| map [top, hot]: set a random map (optionally from the top or hot list)", 75 | "| map Map Name: set map by name", 76 | "| team 1: join a team [1 - 8]", 77 | "| normal: set map to default (no map)", 78 | "| swamp 0.5: set swamp value for normal map", 79 | "| Code available at: git.io/myssix", 80 | ] 81 | GAME_HELP_TEXT = [ 82 | "| ======= Available Commands =======", 83 | "| team: request not to be attacked", 84 | "| unteam: cancel team", 85 | "| pause: pause army movement", 86 | "| unpause: unpause army movement", 87 | "| Code available at: git.io/myssix", 88 | ] 89 | HELLO_TEXT = [ 90 | " Hi, I am Myssix - a generals.io bot", 91 | " Say 'go' to start, or 'help' for a list of additional commands", 92 | " Code available at: git.io/myssix", 93 | ] 94 | GAME_HELLO_TEXT = [ 95 | " Hi, I am Myssix - a generals.io bot", 96 | " Say 'help' for available commands - Code available at: git.io/myssix", 97 | ] -------------------------------------------------------------------------------- /tools/playerstats.py: -------------------------------------------------------------------------------- 1 | ''' 2 | @ Harris Christiansen (code@HarrisChristiansen.com) 3 | Generals.io Automated Client - https://github.com/harrischristiansen/generals-bot 4 | Generals.io Player Replay Statistics 5 | ''' 6 | 7 | import requests 8 | import lzstring 9 | 10 | NUM_REPLAYS_TO_USE = 4000 11 | 12 | URL_REPLAYS_FOR_USER = "https://generals.io/api/replaysForUsername?u=" 13 | URL_REPLAY = "https://generalsio-replays-na.s3.amazonaws.com/" 14 | COUNT_BY = 200 15 | 16 | ######################### Public Methods ######################### 17 | 18 | def mapstats(playername): 19 | replays = _get_list_replays(playername, NUM_REPLAYS_TO_USE) 20 | 21 | maps = {} 22 | for replay in replays: 23 | if replay['type'] == "custom": 24 | mapName = _get_map_name(replay['id']) 25 | 26 | def opponentstats(playername, mingames=0): 27 | replays = _get_list_replays(playername, NUM_REPLAYS_TO_USE) 28 | 29 | opponents = {} 30 | for replay in replays: 31 | if replay['type'] == "classic": 32 | didBeatPlayer = True 33 | for opponent in replay['ranking']: 34 | name = opponent['name'] 35 | if not _is_valid_name(name): 36 | continue 37 | if name != playername: 38 | if name not in opponents: 39 | opponents[name] = {"games":1, "wins":(1 if didBeatPlayer else 0)} 40 | else: 41 | opponents[name]['games'] += 1 42 | if didBeatPlayer: 43 | opponents[name]['wins'] += 1 44 | else: 45 | didBeatPlayer = False 46 | 47 | opponents_selected = {} 48 | for (name, opponent) in opponents.items(): 49 | opponents[name]['winPercent'] = opponent['wins'] / opponent['games'] 50 | if opponent['games'] > mingames: 51 | opponents_selected[name] = opponents[name] 52 | 53 | opponents_sorted = sorted(opponents_selected.items(), key=lambda x:x[1]['winPercent'], reverse=True) 54 | return opponents_sorted 55 | 56 | 57 | ######################### Private Methods ######################### 58 | 59 | def _get_list_replays(playername, count): 60 | replays = [] 61 | for offset in range(0, count, COUNT_BY): 62 | data = _get_json_url(URL_REPLAYS_FOR_USER+playername+"&offset="+str(offset)+"&count="+str(COUNT_BY)) 63 | if len(data) > 0: 64 | replays.extend(data) 65 | return replays 66 | 67 | def _get_map_name(replay_id): 68 | data = _get_url(URL_REPLAY+replay_id+".gior") 69 | lz = lzstring.LZString() 70 | #return bytes(data.text, "utf-8") 71 | return lz.decompress(data.content) 72 | return list(data.text) 73 | 74 | def _get_json_url(url): 75 | return _get_url(url).json() 76 | 77 | def _get_url(url): 78 | return requests.get(url) 79 | 80 | def _is_valid_name(name): 81 | return all(ord(c) < 128 for c in name) 82 | 83 | #print(_get_map_name("HY43dQdab")) 84 | print(opponentstats("myssix", 10)) 85 | -------------------------------------------------------------------------------- /base/bot_moves.py: -------------------------------------------------------------------------------- 1 | ''' 2 | @ Harris Christiansen (code@HarrisChristiansen.com) 3 | Generals.io Automated Client - https://github.com/harrischristiansen/generals-bot 4 | Generals Bot: Common Move Logic 5 | ''' 6 | import logging 7 | import random 8 | 9 | from base import bot_base 10 | from .client.constants import * 11 | 12 | ######################### Move Priority Capture ######################### 13 | 14 | def move_priority(gamemap): 15 | priority_move = (False, False) 16 | generals_and_cities = [t for t in gamemap.generals if t is not None] 17 | generals_and_cities.extend(gamemap.cities) 18 | 19 | for tile in generals_and_cities: 20 | if not tile.shouldNotAttack(): 21 | for neighbor in tile.neighbors(): 22 | if neighbor.isSelf() and neighbor.army > max(1, tile.army + 1): 23 | if priority_move[0] == False or priority_move[0].army < neighbor.army: 24 | priority_move = (neighbor, tile) 25 | if priority_move[0] != False: 26 | #logging.info("Priority Move from %s -> %s" % (priority_move[0], priority_move[1])) # TODO: Note, priority moves are repeatedly sent, indiating move making is sending repeated moves 27 | break 28 | return priority_move 29 | 30 | ######################### Move Outward ######################### 31 | 32 | def move_outward(gamemap, path=[]): 33 | move_swamp = (False, False) 34 | 35 | for source in gamemap.tiles[gamemap.player_index]: # Check Each Owned Tile 36 | if source.army >= 2 and source not in path: # Find One With Armies 37 | target = source.neighbor_to_attack(path) 38 | if target: 39 | if not target.isSwamp: 40 | return (source, target) 41 | move_swamp = (source, target) 42 | 43 | return move_swamp 44 | 45 | 46 | ######################### Move Path Forward ######################### 47 | 48 | def move_path(path): 49 | if len(path) < 2: 50 | return (False, False) 51 | 52 | source = path[0] 53 | target = path[-1] 54 | 55 | if target.tile == source.tile: 56 | return _move_path_largest(path) 57 | 58 | move_capture = _move_path_capture(path) 59 | 60 | if not target.isGeneral and move_capture[1] != target: 61 | return _move_path_largest(path) 62 | 63 | return move_capture 64 | 65 | def _move_path_largest(path): 66 | largest = path[0] 67 | largest_index = 0 68 | for i, tile in enumerate(path): 69 | if tile == path[-1]: 70 | break 71 | if tile.tile == path[0].tile and tile > largest: 72 | largest = tile 73 | largest_index = i 74 | 75 | dest = path[largest_index+1] 76 | return (largest, dest) 77 | 78 | 79 | def _move_path_capture(path): 80 | source = path[0] 81 | capture_army = 0 82 | for i, tile in reversed(list(enumerate(path))): 83 | if tile.tile == source.tile: 84 | capture_army += (tile.army - 1) 85 | else: 86 | capture_army -= tile.army 87 | 88 | if capture_army > 0 and i+1 < len(path) and path[i].army > 1: 89 | return (path[i], path[i+1]) 90 | 91 | return _move_path_largest(path) 92 | 93 | ######################### Move Path Forward ######################### 94 | 95 | def should_move_half(gamemap, source, dest=None): 96 | if dest != None and dest.isCity: 97 | return False 98 | 99 | if gamemap.turn > 250: 100 | if source.isGeneral: 101 | return random.choice([True, True, True, False]) 102 | elif source.isCity: 103 | if gamemap.turn - source.turn_captured < 16: 104 | return True 105 | return random.choice([False, False, False, True]) 106 | return False 107 | 108 | ######################### Proximity Targeting - Pathfinding ######################### 109 | 110 | def path_proximity_target(gamemap): 111 | # Find path from largest tile to closest target 112 | source = gamemap.find_largest_tile(includeGeneral=0.5) 113 | target = source.nearest_target_tile() 114 | path = source.path_to(target) 115 | #logging.info("Proximity %s -> %s via %s" % (source, target, path)) 116 | 117 | if not gamemap.canStepPath(path): 118 | path = path_gather(gamemap) 119 | #logging.info("Proximity FAILED, using path %s" % path) 120 | return path 121 | 122 | def path_gather(gamemap, elsoDo=[]): 123 | target = gamemap.find_largest_tile() 124 | source = gamemap.find_largest_tile(notInPath=[target], includeGeneral=0.5) 125 | if source and target and source != target: 126 | return source.path_to(target) 127 | return elsoDo 128 | 129 | ######################### Helpers ######################### 130 | 131 | def _shuffle(seq): 132 | shuffled = list(seq) 133 | random.shuffle(shuffled) 134 | return iter(shuffled) 135 | -------------------------------------------------------------------------------- /base/bot_base.py: -------------------------------------------------------------------------------- 1 | ''' 2 | @ Harris Christiansen (code@HarrisChristiansen.com) 3 | Generals.io Automated Client - https://github.com/harrischristiansen/generals-bot 4 | Generals Bot: Base Bot Class 5 | ''' 6 | 7 | import logging 8 | import os 9 | from queue import Queue 10 | import random 11 | import threading 12 | import time 13 | 14 | from .client import generals 15 | from .client.constants import * 16 | from .viewer import GeneralsViewer 17 | 18 | class GeneralsBot(object): 19 | def __init__(self, moveMethod, moveEvent=None, name="PurdueBot", gameType="private", privateRoomID=None, showGameViewer=True, public_server=False, start_msg_cmd=""): 20 | # Save Config 21 | self._moveMethod = moveMethod 22 | self._name = name 23 | self._gameType = gameType 24 | self._privateRoomID = privateRoomID 25 | self._public_server = public_server 26 | self._start_msg_cmd = start_msg_cmd 27 | 28 | # ----- Start Game ----- 29 | self._running = True 30 | self._move_event = threading.Event() 31 | _create_thread(self._start_game_thread) 32 | _create_thread(self._start_chat_thread) 33 | _create_thread(self._start_moves_thread) 34 | _create_thread(self._start_timelimitCounter, daemonThread = False) 35 | 36 | # Start Game Viewer 37 | if showGameViewer: 38 | window_title = "%s (%s)" % (self._name, self._gameType) 39 | if self._privateRoomID != None: 40 | window_title = "%s (%s - %s)" % (self._name, self._gameType, self._privateRoomID) 41 | self._viewer = GeneralsViewer(window_title, moveEvent=moveEvent) 42 | self._viewer.mainViewerLoop() # Consumes Main Thread 43 | self._exit_game() 44 | 45 | while self._running: 46 | time.sleep(10) 47 | 48 | self._exit_game() 49 | 50 | ######################### Handle Updates From Server ######################### 51 | 52 | def _start_game_thread(self): 53 | # Create Game 54 | self._game = generals.Generals(self._name, self._name, self._gameType, gameid=self._privateRoomID, public_server=self._public_server, start_command=self._start_msg_cmd) 55 | 56 | # Start Receiving Updates 57 | for gamemap in self._game.get_updates(): 58 | self._set_update(gamemap) 59 | 60 | if not gamemap.complete: 61 | self._move_event.set() # Permit another move 62 | 63 | self._exit_game() 64 | 65 | def _set_update(self, gamemap): 66 | self._map = gamemap 67 | selfDir = dir(self) 68 | 69 | # Update GeneralsViewer Grid 70 | if '_viewer' in selfDir: 71 | if '_moves_realized' in selfDir: 72 | self._map.bottomText = "Realized: "+str(self._moves_realized) 73 | viewer = self._viewer.updateGrid(gamemap) 74 | 75 | # Handle Game Complete 76 | if gamemap.complete and not self._has_completed: 77 | logging.info("!!!! Game Complete. Result = " + str(gamemap.result) + " !!!!") 78 | if '_moves_realized' in selfDir: 79 | logging.info("Moves: %d, Realized: %d" % (self._map.turn, self._moves_realized)) 80 | _create_thread(self._exit_game) 81 | self._has_completed = gamemap.complete 82 | 83 | ######################### Game Exit / Timelimit ######################### 84 | 85 | def _start_timelimitCounter(self): 86 | time.sleep(60 * 25) 87 | self._exit_game() 88 | 89 | def _exit_game(self): 90 | time.sleep(0.15) 91 | if self._map != None and not self._map.exit_on_game_over: 92 | time.sleep(100) 93 | self._running = False 94 | os._exit(0) # End Program 95 | 96 | ######################### Move Generation ######################### 97 | 98 | def _start_moves_thread(self): 99 | self._moves_realized = 0 100 | while self._running: 101 | self._move_event.wait() 102 | self._make_move() 103 | self._move_event.clear() 104 | self._moves_realized += 1 105 | 106 | def _make_move(self): 107 | self._moveMethod(self, self._map) 108 | 109 | ######################### Chat Messages ######################### 110 | 111 | def _start_chat_thread(self): 112 | # Send Chat Messages 113 | while self._running: 114 | msg = str(input('Send Msg:')) 115 | self._game.send_chat(msg) 116 | time.sleep(0.7) 117 | return 118 | 119 | ######################### Move Making ######################### 120 | 121 | def place_move(self, source, dest, move_half=False): 122 | if self._map.isValidPosition(dest.x, dest.y): 123 | self._game.move(source.y, source.x, dest.y, dest.x, move_half) 124 | if SHOULD_DIRTY_MAP_ON_MOVE: 125 | self._update_map_dirty(source, dest, move_half) 126 | return True 127 | return False 128 | 129 | def _update_map_dirty(self, source, dest, move_half): 130 | army = source.army if not move_half else source.army/2 131 | source.update(self._map, source.tile, 1) 132 | 133 | if dest.isOnTeam(): # Moved Internal Tile 134 | dest_army = army - 1 + dest.army 135 | dest.update(self._map, source.tile, dest_army, isDirty=True) 136 | return True 137 | 138 | elif army > dest.army+1: # Captured Tile 139 | dest_army = army - 1 - dest.army 140 | dest.update(self._map, source.tile, dest_army, isCity=dest.isGeneral, isDirty=True) 141 | return True 142 | return False 143 | 144 | ######################### Global Helpers ######################### 145 | 146 | def _create_thread(f, daemonThread = True): 147 | t = threading.Thread(target=f) 148 | t.daemon = daemonThread 149 | t.start() 150 | -------------------------------------------------------------------------------- /bot_path_collect.py: -------------------------------------------------------------------------------- 1 | ''' 2 | @ Harris Christiansen (code@HarrisChristiansen.com) 3 | Generals.io Automated Client - https://github.com/harrischristiansen/generals-bot 4 | Path Collect Both: Collects troops along a path, and attacks outward using path. 5 | ''' 6 | 7 | import logging 8 | import random 9 | from base import bot_base, bot_moves 10 | 11 | # Show all logging 12 | logging.basicConfig(level=logging.INFO) 13 | 14 | ######################### Move Making ######################### 15 | 16 | _bot = None 17 | _map = None 18 | def make_move(currentBot, currentMap): 19 | global _bot, _map 20 | _bot = currentBot 21 | _map = currentMap 22 | 23 | # Make Move 24 | if _map.turn % 8 == 0: 25 | if move_collect_to_path(): 26 | return 27 | if _map.turn % 2 == 0: 28 | if make_primary_move(): 29 | return 30 | if not move_outward(): 31 | if not move_collect_to_path(): 32 | make_primary_move() 33 | return 34 | 35 | def place_move(source, dest): 36 | _bot.place_move(source, dest, move_half=bot_moves.should_move_half(_map, source, dest)) 37 | 38 | ######################### Primary Move Making ######################### 39 | 40 | def make_primary_move(): 41 | update_primary_target() 42 | if len(_map.path) > 1: 43 | return move_primary_path_forward() 44 | elif _target != None: 45 | new_primary_path() 46 | return False 47 | 48 | ######################### Primary Targeting ######################### 49 | 50 | _target = None 51 | _path_position = 0 52 | def update_primary_target(): 53 | global _target 54 | movesLeft = 100 55 | pathLength = len(_map.path) 56 | if pathLength > 2: 57 | movesLeft = pathLength - _path_position - 1 58 | 59 | if _target != None: # Refresh Target Tile State 60 | _target = _map.grid[_target.y][_target.x] 61 | if movesLeft <= 2: # Make target appear smaller to avoid un-targeting # TEMP-FIX 62 | _target.army = _target.army / 5 63 | newTarget = _map.find_primary_target(_target) 64 | 65 | if _target != newTarget: 66 | _target = newTarget 67 | new_primary_path(restoreOldPosition=True) 68 | 69 | ######################### Primary Path ######################### 70 | 71 | def move_primary_path_forward(): 72 | global _path_position 73 | try: 74 | source = _map.path[_path_position] 75 | except IndexError: 76 | #logging.debug("Invalid Current Path Position") 77 | return new_primary_path() 78 | 79 | if source.tile != _map.player_index or source.army < 2: # Out of Army, Restart Path 80 | #logging.debug("Path Error: Out of Army (%d,%d)" % (source.tile, source.army)) 81 | return new_primary_path() 82 | 83 | try: 84 | dest = _map.path[_path_position+1] # Determine Destination 85 | if dest.tile == _map.player_index or source.army > (dest.army + 1): 86 | place_move(source, dest) 87 | else: 88 | #logging.debug("Path Error: Out of Army To Attack (%d,%d,%d,%d)" % (dest.x,dest.y,source.army,dest.army)) 89 | return new_primary_path() 90 | except IndexError: 91 | #logging.debug("Path Error: Target Destination Out Of List Bounds") 92 | return new_primary_path(restoreOldPosition=True) 93 | 94 | _path_position += 1 95 | return True 96 | 97 | def new_primary_path(restoreOldPosition=False): 98 | global _bot, _path_position, _target 99 | 100 | # Store Old Tile 101 | old_tile = None 102 | if _path_position > 0 and len(_map.path) > 0: # Store old path position 103 | old_tile = _map.path[_path_position] 104 | _path_position = 0 105 | 106 | # Determine Source and Path 107 | source = _map.find_city() 108 | if source == None: 109 | source = _map.find_largest_tile(includeGeneral=True) 110 | _map.path = source.path_to(_target) # Find new path to target 111 | 112 | # Restore Old Tile 113 | if restoreOldPosition and old_tile != None: 114 | for i, tile in enumerate(_map.path): 115 | if (tile.x, tile.y) == (old_tile.x, old_tile.y): 116 | _path_position = i 117 | return True 118 | 119 | return False 120 | 121 | ######################### Move Outward ######################### 122 | 123 | def move_outward(): 124 | (source, dest) = bot_moves.move_outward(_map, _map.path) 125 | if source: 126 | place_move(source, dest) 127 | return True 128 | return False 129 | 130 | ######################### Collect To Path ######################### 131 | 132 | def find_collect_path(): 133 | # Find Largest Tile 134 | source = _map.find_largest_tile(notInPath=_map.path, includeGeneral=0.33) 135 | if (source == None or source.army < 4): 136 | _map.collect_path = [] 137 | return _map.collect_path 138 | 139 | # Determine Target Tile 140 | dest = None 141 | if source.army > 40: 142 | dest = source.nearest_target_tile() 143 | if dest == None: 144 | dest = source.nearest_tile_in_path(_map.path) 145 | 146 | # Return Path 147 | return source.path_to(dest) 148 | 149 | def move_collect_to_path(): 150 | # Update Path 151 | _map.collect_path = find_collect_path() 152 | 153 | # Perform Move 154 | (move_from, move_to) = bot_moves.move_path(_map.collect_path) 155 | if move_from != None: 156 | place_move(move_from, move_to) 157 | return True 158 | 159 | return False 160 | 161 | ######################### Main ######################### 162 | 163 | # Start Game 164 | import startup 165 | if __name__ == '__main__': 166 | startup.startup(make_move, botName="PurdueBot-P2") 167 | -------------------------------------------------------------------------------- /base/client/tile.py: -------------------------------------------------------------------------------- 1 | ''' 2 | @ Harris Christiansen (code@HarrisChristiansen.com) 3 | Generals.io Automated Client - https://github.com/harrischristiansen/generals-bot 4 | Tile: Objects for representing Generals IO Tiles 5 | ''' 6 | 7 | from queue import Queue 8 | import time 9 | import logging 10 | 11 | from .constants import * 12 | 13 | class Tile(object): 14 | def __init__(self, gamemap, x, y): 15 | # Public Properties 16 | self.x = x # Integer X Coordinate 17 | self.y = y # Integer Y Coordinate 18 | self.tile = TILE_FOG # Integer Tile Type (TILE_OBSTACLE, TILE_FOG, TILE_MOUNTAIN, TILE_EMPTY, or player_ID) 19 | self.turn_captured = 0 # Integer Turn Tile Last Captured 20 | self.turn_held = 0 # Integer Last Turn Held 21 | self.army = 0 # Integer Army Count 22 | self.isCity = False # Boolean isCity 23 | self.isSwamp = False # Boolean isSwamp 24 | self.isGeneral = False # Boolean isGeneral 25 | 26 | # Private Properties 27 | self._map = gamemap # Pointer to Map Object 28 | self._general_index = -1 # Player Index if tile is a general 29 | self._dirtyUpdateTime = 0 # Last time Tile was updated by bot, not server 30 | 31 | def __repr__(self): 32 | return "(%2d,%2d)[%2d,%3d]" % (self.x, self.y, self.tile, self.army) 33 | 34 | '''def __eq__(self, other): 35 | return (other != None and self.x==other.x and self.y==other.y)''' 36 | 37 | def __lt__(self, other): 38 | return self.army < other.army 39 | 40 | def setNeighbors(self, gamemap): 41 | self._map = gamemap 42 | self._setNeighbors() 43 | 44 | def setIsSwamp(self, isSwamp): 45 | self.isSwamp = isSwamp 46 | 47 | def update(self, gamemap, tile, army, isCity=False, isGeneral=False, isDirty=False): 48 | self._map = gamemap 49 | 50 | if (isDirty): 51 | self._dirtyUpdateTime = time.time() 52 | 53 | if self.tile < 0 or tile >= TILE_MOUNTAIN or (tile < TILE_MOUNTAIN and self.isSelf()): # Tile should be updated 54 | if (tile >= 0 or self.tile >= 0) and self.tile != tile: # Remember Discovered Tiles 55 | self.turn_captured = gamemap.turn 56 | if self.tile >= 0: 57 | gamemap.tiles[self.tile].remove(self) 58 | if tile >= 0: 59 | gamemap.tiles[tile].append(self) 60 | if tile == gamemap.player_index: 61 | self.turn_held = gamemap.turn 62 | self.tile = tile 63 | if self.army == 0 or army > 0 or tile >= TILE_MOUNTAIN or self.isSwamp: # Remember Discovered Armies 64 | self.army = army 65 | 66 | if isCity: 67 | self.isCity = True 68 | self.isGeneral = False 69 | if self not in gamemap.cities: 70 | gamemap.cities.append(self) 71 | if self._general_index != -1 and self._general_index < 8: 72 | gamemap.generals[self._general_index] = None 73 | self._general_index = -1 74 | elif isGeneral: 75 | self.isGeneral = True 76 | gamemap.generals[tile] = self 77 | self._general_index = self.tile 78 | 79 | ################################ Tile Properties ################################ 80 | 81 | def isDirty(self): 82 | return (time.time() - self._dirtyUpdateTime) < 0.6 83 | 84 | def distance_to(self, dest): 85 | if dest != None: 86 | return abs(self.x - dest.x) + abs(self.y - dest.y) 87 | return 0 88 | 89 | def neighbors(self, includeSwamps=False, includeCities=True): 90 | neighbors = [] 91 | for tile in self._neighbors: 92 | if (tile.tile != TILE_OBSTACLE or tile.isCity or tile.isGeneral) and tile.tile != TILE_MOUNTAIN and (includeSwamps or not tile.isSwamp) and (includeCities or not tile.isCity): 93 | neighbors.append(tile) 94 | return neighbors 95 | 96 | def isValidTarget(self): # Check tile to verify reachability 97 | if self.tile < TILE_EMPTY: 98 | return False 99 | for tile in self.neighbors(includeSwamps=True): 100 | if tile.turn_held > 0: 101 | return True 102 | return False 103 | 104 | def isEmpty(self): 105 | return self.tile == TILE_EMPTY 106 | 107 | def isSelf(self): 108 | return self.tile == self._map.player_index 109 | 110 | def isOnTeam(self): 111 | if self.isSelf(): 112 | return True 113 | return False 114 | 115 | def shouldNotAttack(self): # DEPRECATED: Use Tile.shouldAttack 116 | return not self.shouldAttack() 117 | 118 | def shouldAttack(self): 119 | if not self.isValidTarget(): 120 | return False 121 | if self.isOnTeam(): 122 | return False 123 | if self.tile in self._map.do_not_attack_players: 124 | return False 125 | if self.isDirty(): 126 | return False 127 | return True 128 | 129 | ################################ Select Neighboring Tile ################################ 130 | 131 | def neighbor_to_attack(self, path=[]): 132 | if not self.isSelf(): 133 | return None 134 | 135 | target = None 136 | for neighbor in self.neighbors(includeSwamps=True): 137 | if (neighbor.shouldAttack() and self.army > neighbor.army + 1) or neighbor in path: # Move into caputurable target Tiles 138 | if not neighbor.isSwamp: 139 | if target == None: 140 | target = neighbor 141 | elif neighbor.isCity and (not target.isCity or target.army > neighbor.army): 142 | target = neighbor 143 | elif not neighbor.isEmpty and neighbor.army <= 1 and target.isEmpty: # Special case, prioritize opponents with 1 army over empty tiles 144 | target = neighbor 145 | elif target.army > neighbor.army and not target.isCity: 146 | if neighbor.isEmpty: 147 | if target.army > 1: 148 | target = neighbor 149 | else: 150 | target = neighbor 151 | elif neighbor.turn_held == 0: # Move into swamps that we have never held before 152 | target = neighbor 153 | 154 | return target 155 | 156 | 157 | ################################ Select Distant Tile ################################ 158 | 159 | def nearest_tile_in_path(self, path): 160 | dest = None 161 | dest_distance = 9999 162 | for tile in path: 163 | distance = self.distance_to(tile) 164 | if distance < tile_distance: 165 | dest = tile 166 | dest_distance = distance 167 | 168 | return dest 169 | 170 | def nearest_target_tile(self): 171 | if not self.isSelf(): 172 | return None 173 | 174 | max_target_army = self.army * 4 + 14 175 | 176 | dest = None 177 | dest_distance = 9999 178 | for x in range(self._map.cols): # Check Each Square 179 | for y in range(self._map.rows): 180 | tile = self._map.grid[y][x] 181 | if not tile.isValidTarget() or tile.shouldNotAttack() or tile.army > max_target_army: # Non Target Tiles 182 | continue 183 | 184 | distance = self.distance_to(tile) 185 | if tile.isGeneral: # Generals appear closer 186 | distance = distance * 0.09 187 | elif tile.isCity: # Cities vary distance based on size, but appear closer 188 | distance = distance * sorted((0.17, (tile.army / (3.2*self.army)), 20))[1] 189 | 190 | if tile.tile == TILE_EMPTY: # Empties appear further away 191 | if tile.isCity: 192 | distance = distance * 1.6 193 | else: 194 | distance = distance * 4.3 195 | 196 | if tile.army > self.army: # Larger targets appear further away 197 | distance = distance * (1.6*tile.army/self.army) 198 | 199 | if tile.isSwamp: # Swamps appear further away 200 | distance = distance * 10 201 | if tile.turn_held > 0: # Swamps which have been held appear even further away 202 | distance = distance * 3 203 | 204 | if distance < dest_distance: # ----- Set nearest target ----- 205 | dest = tile 206 | dest_distance = distance 207 | 208 | return dest 209 | 210 | ################################ Pathfinding ################################ 211 | 212 | def path_to(self, dest, includeCities=False): 213 | if dest == None: 214 | return [] 215 | 216 | frontier = Queue() 217 | frontier.put(self) 218 | came_from = {} 219 | came_from[self] = None 220 | army_count = {} 221 | army_count[self] = self.army 222 | 223 | while not frontier.empty(): 224 | current = frontier.get() 225 | 226 | if current == dest: # Found Destination 227 | break 228 | 229 | for next in current.neighbors(includeSwamps=True, includeCities=includeCities): 230 | if next not in came_from and (next.isOnTeam() or next == dest or next.army < army_count[current]): 231 | #priority = self.distance(next, dest) 232 | frontier.put(next) 233 | came_from[next] = current 234 | if next.isOnTeam(): 235 | army_count[next] = army_count[current] + (next.army - 1) 236 | else: 237 | army_count[next] = army_count[current] - (next.army + 1) 238 | 239 | if dest not in came_from: # Did not find dest 240 | if includeCities: 241 | return [] 242 | else: 243 | return self.path_to(dest, includeCities=True) 244 | 245 | # Create Path List 246 | path = _path_reconstruct(came_from, dest) 247 | 248 | return path 249 | 250 | ################################ PRIVATE FUNCTIONS ################################ 251 | 252 | def _setNeighbors(self): 253 | x = self.x 254 | y = self.y 255 | 256 | neighbors = [] 257 | for dy, dx in DIRECTIONS: 258 | if self._map.isValidPosition(x+dx, y+dy): 259 | tile = self._map.grid[y+dy][x+dx] 260 | neighbors.append(tile) 261 | 262 | self._neighbors = neighbors 263 | return neighbors 264 | 265 | def _path_reconstruct(came_from, dest): 266 | current = dest 267 | path = [current] 268 | try: 269 | while came_from[current] != None: 270 | current = came_from[current] 271 | path.append(current) 272 | except KeyError: 273 | None 274 | path.reverse() 275 | 276 | return path -------------------------------------------------------------------------------- /base/viewer.py: -------------------------------------------------------------------------------- 1 | ''' 2 | @ Harris Christiansen (code@HarrisChristiansen.com) 3 | Generals.io Automated Client - https://github.com/harrischristiansen/generals-bot 4 | Game Viewer 5 | ''' 6 | 7 | import logging 8 | import pygame 9 | import threading 10 | import time 11 | 12 | from .client.constants import * 13 | 14 | # Color Definitions 15 | BLACK = (0,0,0) 16 | GRAY_DARK = (110,110,110) 17 | GRAY = (160,160,160) 18 | WHITE = (255,255,255) 19 | GOLD = (217, 163, 0) 20 | PLAYER_COLORS = [(255,0,0), (0,0,255), (0,128,0), (128,0,128), (0,128,128), (0,70,0), (128,0,0), (255,165,0), (30,250,30)] 21 | 22 | # Table Properies 23 | CELL_WIDTH = 20 24 | CELL_HEIGHT = 20 25 | CELL_MARGIN = 5 26 | SCORES_ROW_HEIGHT = 28 27 | ACTIONBAR_ROW_HEIGHT = 25 28 | TOGGLE_GRID_BTN_WIDTH = 75 29 | TOGGLE_EXIT_BTN_WIDTH = 65 30 | ABOVE_GRID_HEIGHT = ACTIONBAR_ROW_HEIGHT 31 | 32 | class GeneralsViewer(object): 33 | def __init__(self, name=None, moveEvent=None): 34 | self._runPygame = True 35 | self._name = name 36 | self._moveEvent = moveEvent # self._moveEvent([source_x, source_y], [target_x, target_y]) 37 | self._receivedUpdate = False 38 | self._showGrid = True 39 | self._clicked = None 40 | 41 | def mainViewerLoop(self): 42 | while not self._receivedUpdate: # Wait for first update 43 | time.sleep(0.5) 44 | 45 | self._initViewier() 46 | 47 | while self._runPygame: 48 | for event in pygame.event.get(): # User did something 49 | if event.type == pygame.QUIT: # User clicked quit 50 | self._runPygame = False # Flag done 51 | elif event.type == pygame.MOUSEBUTTONDOWN: # Mouse Click 52 | self._handleClick(pygame.mouse.get_pos()) 53 | elif event.type == pygame.KEYDOWN: # Key Press Down 54 | self._handleKeypress(event.key) 55 | 56 | if self._receivedUpdate: 57 | self._drawViewer() 58 | self._receivedUpdate = False 59 | 60 | time.sleep(0.2) 61 | 62 | pygame.quit() # Done. Quit pygame. 63 | 64 | ''' ======================== Call to update viewer with new map state ======================== ''' 65 | 66 | def updateGrid(self, update): 67 | updateDir = dir(update) 68 | self._map = update 69 | self._scores = sorted(update.scores, key=lambda general: general['total'], reverse=True) # Sort Scores 70 | self._receivedUpdate = True 71 | if "bottomText" in updateDir: 72 | self._bottomText = update.bottomText 73 | if "path" in updateDir: 74 | self._path = [(path.x, path.y) for path in update.path] 75 | else: 76 | self._path = [] 77 | if "collect_path" in updateDir: 78 | self._collect_path = [(path.x, path.y) for path in update.collect_path] 79 | else: 80 | self._collect_path = None 81 | 82 | return self 83 | 84 | ''' ======================== PRIVATE METHODS - Viewer Init - PRIVATE METHODS ======================== ''' 85 | 86 | def _initViewier(self): 87 | pygame.init() 88 | 89 | # Set Window Size 90 | self._grid_height = self._map.rows * (CELL_HEIGHT + CELL_MARGIN) + CELL_MARGIN 91 | window_height = ACTIONBAR_ROW_HEIGHT + self._grid_height + SCORES_ROW_HEIGHT 92 | window_width = self._map.cols * (CELL_WIDTH + CELL_MARGIN) + CELL_MARGIN 93 | self._window_size = [window_width, window_height] 94 | self._screen = pygame.display.set_mode(self._window_size) 95 | 96 | window_title = "Generals IO Bot" 97 | if self._name != None: 98 | window_title += " - " + str(self._name) 99 | pygame.display.set_caption(window_title) 100 | self._font = pygame.font.SysFont('Arial', CELL_HEIGHT-10) 101 | self._fontLrg = pygame.font.SysFont('Arial', CELL_HEIGHT) 102 | self._bottomText = "" 103 | 104 | self._clock = pygame.time.Clock() 105 | 106 | ''' ======================== Handle Clicks ======================== ''' 107 | 108 | def _handleClick(self, pos): 109 | if pos[1] < ABOVE_GRID_HEIGHT: 110 | if pos[0] < TOGGLE_GRID_BTN_WIDTH: # Toggle Grid 111 | self._toggleGrid() 112 | elif pos[0] < TOGGLE_GRID_BTN_WIDTH+TOGGLE_EXIT_BTN_WIDTH: # Toggle Exit on Game Over 113 | self._map.exit_on_game_over = not self._map.exit_on_game_over 114 | self._receivedUpdate = True 115 | elif self._showGrid and pos[1] > ABOVE_GRID_HEIGHT and pos[1] < (self._window_size[1] - SCORES_ROW_HEIGHT): # Click inside Grid 116 | column = pos[0] // (CELL_WIDTH + CELL_MARGIN) 117 | row = (pos[1] - ABOVE_GRID_HEIGHT) // (CELL_HEIGHT + CELL_MARGIN) 118 | self._clicked = (column, row) 119 | logging.debug("Click %s, Grid Coordinates: %s" % (pos, self._clicked)) 120 | 121 | def _toggleGrid(self): 122 | self._showGrid = not self._showGrid 123 | window_height = ACTIONBAR_ROW_HEIGHT + SCORES_ROW_HEIGHT 124 | if self._showGrid: 125 | window_height += self._grid_height 126 | self._window_size[1] = window_height 127 | self._screen = pygame.display.set_mode(self._window_size) 128 | 129 | ''' ======================== Handle Keypresses ======================== ''' 130 | 131 | def _handleKeypress(self, key): 132 | if self._clicked == None or self._moveEvent == None: 133 | return False 134 | column = self._clicked[0] 135 | row = self._clicked[1] 136 | 137 | target = None 138 | if key == pygame.K_LEFT: 139 | if column > 0: 140 | target = (column-1, row) 141 | elif key == pygame.K_RIGHT: 142 | if column < self._map.cols - 1: 143 | target = (column+1, row) 144 | elif key == pygame.K_UP: 145 | if row > 0: 146 | target = (column, row-1) 147 | elif key == pygame.K_DOWN: 148 | if row < self._map.rows - 1: 149 | target = (column, row+1) 150 | 151 | if target != None: 152 | self._moveEvent(self._clicked, target) 153 | self._clicked = target 154 | 155 | ''' ======================== Viewer Drawing ======================== ''' 156 | 157 | def _drawViewer(self): 158 | self._screen.fill(BLACK) # Set BG Color 159 | self._drawActionbar() 160 | if self._showGrid: 161 | self._drawGrid() 162 | self._drawScores() 163 | 164 | self._clock.tick(60) # Limit to 60 FPS 165 | pygame.display.flip() # update screen with new drawing 166 | 167 | def _drawActionbar(self): 168 | # Toggle Grid Button 169 | pygame.draw.rect(self._screen, (0,80,0), [0, 0, TOGGLE_GRID_BTN_WIDTH, ACTIONBAR_ROW_HEIGHT]) 170 | self._screen.blit(self._font.render("Toggle Grid", True, WHITE), (10, 5)) 171 | 172 | # Toggle Exit on Game Over Button 173 | pygame.draw.rect(self._screen, (0,100,0) if self._map.exit_on_game_over else (90,0,0), [TOGGLE_GRID_BTN_WIDTH, 0, TOGGLE_EXIT_BTN_WIDTH, ACTIONBAR_ROW_HEIGHT]) 174 | self._screen.blit(self._font.render("Auto Quit", True, WHITE), (TOGGLE_GRID_BTN_WIDTH+10, 5)) 175 | 176 | # Info Text 177 | self._screen.blit(self._fontLrg.render("Turn: %d" % self._map.turn, True, WHITE), (self._window_size[0]-200, 5)) 178 | self._screen.blit(self._font.render("%s" % self._bottomText, True, WHITE), (self._window_size[0]-90, 12)) 179 | 180 | def _drawScores(self): 181 | pos_top = self._window_size[1]-SCORES_ROW_HEIGHT 182 | score_width = self._window_size[0] / len(self._scores) 183 | for i, score in enumerate(self._scores): 184 | score_color = PLAYER_COLORS[int(score['i'])] 185 | if score['dead'] == True: 186 | score_color = GRAY_DARK 187 | pygame.draw.rect(self._screen, score_color, [score_width*i, pos_top, score_width, SCORES_ROW_HEIGHT]) 188 | self._screen.blit(self._font.render(self._map.usernames[int(score['i'])], True, WHITE), (score_width*i+3, pos_top+1)) 189 | self._screen.blit(self._font.render(str(score['total'])+" on "+str(score['tiles']), True, WHITE), (score_width*i+3, pos_top+1+self._font.get_height())) 190 | 191 | def _drawGrid(self): 192 | for row in range(self._map.rows): 193 | for column in range(self._map.cols): 194 | tile = self._map.grid[row][column] 195 | # Determine BG Color 196 | color = WHITE 197 | color_font = WHITE 198 | if tile.tile == TILE_MOUNTAIN: # Mountain 199 | color = BLACK 200 | elif tile.tile == TILE_FOG: # Fog 201 | color = GRAY 202 | elif tile.tile == TILE_OBSTACLE: # Obstacle 203 | color = GRAY_DARK 204 | elif tile.tile >= 0: # Player 205 | color = PLAYER_COLORS[tile.tile] 206 | else: 207 | color_font = BLACK 208 | 209 | pos_left = (CELL_MARGIN + CELL_WIDTH) * column + CELL_MARGIN 210 | pos_top = (CELL_MARGIN + CELL_HEIGHT) * row + CELL_MARGIN + ABOVE_GRID_HEIGHT 211 | if tile.isCity or tile.isGeneral: # City/General 212 | # Draw Circle 213 | pos_left_circle = int(pos_left + (CELL_WIDTH/2)) 214 | pos_top_circle = int(pos_top + (CELL_HEIGHT/2)) 215 | if tile in self._map.generals: 216 | pygame.draw.rect(self._screen, GOLD, [pos_left, pos_top, CELL_WIDTH, CELL_HEIGHT]) 217 | pygame.draw.circle(self._screen, color, [pos_left_circle, pos_top_circle], int(CELL_WIDTH/2)) 218 | else: 219 | # Draw Rect 220 | pygame.draw.rect(self._screen, color, [pos_left, pos_top, CELL_WIDTH, CELL_HEIGHT]) 221 | 222 | # Draw Text Value 223 | if tile.army != 0: # Don't draw on empty tiles 224 | textVal = str(tile.army) 225 | self._screen.blit(self._font.render(textVal, True, color_font), (pos_left, pos_top+2)) 226 | 227 | # Draw Swamps 228 | if tile.isSwamp: 229 | self._screen.blit(self._font.render("±", True, color_font), (pos_left+9, pos_top+7)) 230 | 231 | # Draw Path 232 | if self._path != None and (column,row) in self._path: 233 | self._screen.blit(self._fontLrg.render("*", True, color_font), (pos_left+5, pos_top-3)) 234 | if self._collect_path != None and (column,row) in self._collect_path: 235 | self._screen.blit(self._fontLrg.render("*", True, PLAYER_COLORS[8]), (pos_left+6, pos_top+6)) 236 | -------------------------------------------------------------------------------- /base/client/map.py: -------------------------------------------------------------------------------- 1 | ''' 2 | @ Harris Christiansen (code@HarrisChristiansen.com) 3 | Generals.io Automated Client - https://github.com/harrischristiansen/generals-bot 4 | Map: Objects for representing Generals IO Map 5 | ''' 6 | 7 | import logging 8 | 9 | from .constants import * 10 | from .tile import Tile 11 | 12 | class Map(object): 13 | def __init__(self, start_data, data): 14 | # Start Data 15 | self._start_data = start_data 16 | self.player_index = start_data['playerIndex'] # Integer Player Index 17 | self.usernames = start_data['usernames'] # List of String Usernames 18 | self.replay_url = REPLAY_URLS["na"] + start_data['replay_id'] # String Replay URL # TODO: Use Client Region 19 | 20 | # Map Properties 21 | self._applyUpdateDiff(data) 22 | self.rows = self.rows # Integer Number Grid Rows 23 | self.cols = self.cols # Integer Number Grid Cols 24 | self.grid = [[Tile(self,x,y) for x in range(self.cols)] for y in range(self.rows)] # 2D List of Tile Objects 25 | self._setNeighbors() 26 | self.swamps = [(c // self.cols, c % self.cols) for c in start_data['swamps']] # List [(y,x)] of swamps 27 | self._setSwamps() 28 | self.turn = data['turn'] # Integer Turn # (1 turn / 0.5 seconds) 29 | self.tiles = [[] for x in range(12)] # List of 8 (+ extra) Players Tiles 30 | self.cities = [] # List of City Tiles 31 | self.generals = [None for x in range(12)] # List of 8 (+ extra) Generals (None if not found) 32 | self._setGenerals() 33 | self.stars = [] # List of Player Star Ratings 34 | self.scores = self._getScores(data) # List of Player Scores 35 | self.complete = False # Boolean Game Complete 36 | self.result = False # Boolean Game Result (True = Won) 37 | 38 | # Public/Shared Components 39 | self.path = [] 40 | self.collect_path = [] 41 | 42 | # Public Options 43 | self.exit_on_game_over = True # Controls if bot exits after game over 44 | self.do_not_attack_players = [] # List of player IDs not to attack 45 | 46 | ################################ Game Updates ################################ 47 | 48 | def update(self, data): 49 | if self.complete: # Game Over - Ignore Empty Board Updates 50 | return self 51 | 52 | self._applyUpdateDiff(data) 53 | self.scores = self._getScores(data) 54 | self.turn = data['turn'] 55 | 56 | for x in range(self.cols): # Update Each Tile 57 | for y in range(self.rows): 58 | tile_type = self._tile_grid[y][x] 59 | army_count = self._army_grid[y][x] 60 | isCity = (y,x) in self._visible_cities 61 | isGeneral = (y,x) in self._visible_generals 62 | self.grid[y][x].update(self, tile_type, army_count, isCity, isGeneral) 63 | 64 | return self 65 | 66 | def updateResult(self, result): 67 | self.complete = True 68 | self.result = result == "game_won" 69 | return self 70 | 71 | ################################ Map Search/Selection ################################ 72 | 73 | def find_city(self, ofType=None, notOfType=None, notInPath=[], findLargest=True, includeGeneral=False): # ofType = Integer, notOfType = Integer, notInPath = [Tile], findLargest = Boolean 74 | if ofType == None and notOfType == None: 75 | ofType = self.player_index 76 | 77 | found_city = None 78 | for city in self.cities: # Check Each City 79 | if city.tile == ofType or (notOfType != None and city.tile != notOfType): 80 | if city in notInPath: 81 | continue 82 | if found_city == None: 83 | found_city = city 84 | elif (findLargest and found_city.army < city.army) or (not findLargest and city.army < found_city.army): 85 | found_city = city 86 | 87 | if includeGeneral: 88 | general = self.generals[ofType] 89 | if found_city == None: 90 | return general 91 | if general != None and ((findLargest and general.army > found_city.army) or (not findLargest and general.army < found_city.army)): 92 | return general 93 | 94 | return found_city 95 | 96 | def find_largest_tile(self, ofType=None, notInPath=[], includeGeneral=False): # ofType = Integer, notInPath = [Tile], includeGeneral = False|True|Int Acceptable Largest|0.1->0.9 Ratio 97 | if ofType == None: 98 | ofType = self.player_index 99 | general = self.generals[ofType] 100 | if general == None: 101 | logging.error("ERROR: find_largest_tile encountered general=None for player %d with list %s" % (ofType, self.generals)) 102 | 103 | largest = None 104 | for tile in self.tiles[ofType]: # Check each ofType tile 105 | if largest == None or largest.army < tile.army: # New largest 106 | if not tile.isGeneral and tile not in notInPath: # Exclude general and path 107 | largest = tile 108 | 109 | if includeGeneral > 0 and general != None and general not in notInPath: # Handle includeGeneral 110 | if includeGeneral < 1: 111 | includeGeneral = general.army * includeGeneral 112 | if includeGeneral < 6: 113 | includeGeneral = 6 114 | if largest == None: 115 | largest = general 116 | elif includeGeneral == True and largest.army < general.army: 117 | largest = general 118 | elif includeGeneral > True and largest.army < general.army and largest.army <= includeGeneral: 119 | largest = general 120 | 121 | return largest 122 | 123 | def find_primary_target(self, target=None): 124 | target_type = OPP_EMPTY - 1 125 | if target != None and target.shouldNotAttack(): # Acquired Target 126 | target = None 127 | if target != None: # Determine Previous Target Type 128 | target_type = OPP_EMPTY 129 | if target.isGeneral: 130 | target_type = OPP_GENERAL 131 | elif target.isCity: 132 | target_type = OPP_CITY 133 | elif target.army > 0: 134 | target_type = OPP_ARMY 135 | 136 | # Determine Max Target Size 137 | largest = self.find_largest_tile(includeGeneral=True) 138 | max_target_size = largest.army * 1.25 139 | 140 | for x in _shuffle(range(self.cols)): # Check Each Tile 141 | for y in _shuffle(range(self.rows)): 142 | source = self.grid[y][x] 143 | if not source.isValidTarget() or source.tile == self.player_index: # Don't target invalid tiles 144 | continue 145 | 146 | if target_type <= OPP_GENERAL: # Search for Generals 147 | if source.tile >= 0 and source.isGeneral and source.army < max_target_size: 148 | return source 149 | 150 | if target_type <= OPP_CITY: # Search for Smallest Cities 151 | if source.isCity and source.army < max_target_size: 152 | if target_type < OPP_CITY or source.army < target.army: 153 | target = source 154 | target_type = OPP_CITY 155 | 156 | if target_type <= OPP_ARMY: # Search for Largest Opponent Armies 157 | if source.tile >= 0 and (target == None or source.army > target.army) and not source.isCity: 158 | target = source 159 | target_type = OPP_ARMY 160 | 161 | if target_type < OPP_EMPTY: # Search for Empty Squares 162 | if source.tile == TILE_EMPTY and source.army < largest.army: 163 | target = source 164 | target_type = OPP_EMPTY 165 | 166 | return target 167 | 168 | ################################ Validators ################################ 169 | 170 | def isValidPosition(self, x, y): 171 | return 0 <= y < self.rows and 0 <= x < self.cols and self._tile_grid[y][x] != TILE_MOUNTAIN 172 | 173 | def canCompletePath(self, path): 174 | if len(path) < 2: 175 | return False 176 | 177 | army_total = 0 178 | for tile in path: # Verify can obtain every tile in path 179 | if tile.isSwamp: 180 | army_total -= 1 181 | 182 | if tile.tile == self.player_index: 183 | army_total += tile.army - 1 184 | elif tile.army + 1 > army_total: # Cannot obtain tile 185 | return False 186 | return True 187 | 188 | def canStepPath(self, path): 189 | if len(path) < 2: 190 | return False 191 | 192 | army_total = 0 193 | for tile in path: # Verify can obtain at least one tile in path 194 | if tile.isSwamp: 195 | army_total -= 1 196 | 197 | if tile.tile == self.player_index: 198 | army_total += tile.army - 1 199 | else: 200 | if tile.army + 1 > army_total: # Cannot obtain tile 201 | return False 202 | return True 203 | return True 204 | 205 | ################################ PRIVATE FUNCTIONS ################################ 206 | 207 | def _getScores(self, data): 208 | scores = {s['i']: s for s in data['scores']} 209 | scores = [scores[i] for i in range(len(scores))] 210 | 211 | if 'stars' in data: 212 | self.stars[:] = data['stars'] 213 | 214 | return scores 215 | 216 | def _applyUpdateDiff(self, data): 217 | if not '_map_private' in dir(self): 218 | self._map_private = [] 219 | self._cities_private = [] 220 | _apply_diff(self._map_private, data['map_diff']) 221 | _apply_diff(self._cities_private, data['cities_diff']) 222 | 223 | # Get Number Rows + Columns 224 | self.rows, self.cols = self._map_private[1], self._map_private[0] 225 | 226 | # Create Updated Tile Grid 227 | self._tile_grid = [[self._map_private[2 + self.cols*self.rows + y*self.cols + x] for x in range(self.cols)] for y in range(self.rows)] 228 | # Create Updated Army Grid 229 | self._army_grid = [[self._map_private[2 + y*self.cols + x] for x in range(self.cols)] for y in range(self.rows)] 230 | 231 | # Update Visible Cities 232 | self._visible_cities = [(c // self.cols, c % self.cols) for c in self._cities_private] # returns [(y,x)] 233 | 234 | # Update Visible Generals 235 | self._visible_generals = [(-1, -1) if g == -1 else (g // self.cols, g % self.cols) for g in data['generals']] # returns [(y,x)] 236 | 237 | def _setNeighbors(self): 238 | for x in range(self.cols): 239 | for y in range(self.rows): 240 | self.grid[y][x].setNeighbors(self) 241 | 242 | def _setSwamps(self): 243 | for (y,x) in self.swamps: 244 | self.grid[y][x].setIsSwamp(True) 245 | 246 | def _setGenerals(self): 247 | for i, general in enumerate(self._visible_generals): 248 | if general[0] != -1: 249 | self.generals[i] = self.grid[general[0]][general[1]] 250 | 251 | def _apply_diff(cache, diff): 252 | i = 0 253 | a = 0 254 | while i < len(diff) - 1: 255 | 256 | # offset and length 257 | a += diff[i] 258 | n = diff[i+1] 259 | 260 | cache[a:a+n] = diff[i+2:i+2+n] 261 | a += n 262 | i += n + 2 263 | 264 | if i == len(diff) - 1: 265 | cache[:] = cache[:a+diff[i]] 266 | i += 1 267 | 268 | assert i == len(diff) 269 | 270 | def _shuffle(seq): 271 | shuffled = list(seq) 272 | random.shuffle(shuffled) 273 | return iter(shuffled) -------------------------------------------------------------------------------- /base/client/bot_cmds.py: -------------------------------------------------------------------------------- 1 | ''' 2 | @ Harris Christiansen (code@HarrisChristiansen.com) 3 | Generals.io Automated Client - https://github.com/harrischristiansen/generals-bot 4 | Generals.io Bot Commands 5 | ''' 6 | 7 | import random 8 | import threading 9 | import time 10 | 11 | from .constants import * 12 | from . import generals_api 13 | 14 | class BotCommands(object): 15 | def __init__(self, bot): 16 | self._bot = bot 17 | self._permitted_username = "" 18 | 19 | def setMap(self, gamemap): 20 | self._map = gamemap 21 | 22 | ######################### Bot Commands ######################### 23 | 24 | def _get_command(self, msg, isFromChat, username): 25 | msg_lower = msg.lower() 26 | command = msg.split(' ') 27 | command_len = len(command) 28 | if command_len == 1: 29 | command = command[0].split(':') # Handle : delimiters 30 | base_command = command[0].lower() 31 | arg_command = " ".join(command[1:]) 32 | 33 | if command_len > 1 and "_map" in dir(self) and "usernames" in dir(self._map): # Handle directed commands (ex: myssix pause) 34 | if base_command == self._map.usernames[self._map.player_index].lower(): 35 | command = command[1:] 36 | command_len = len(command) 37 | base_command = command[0].lower() 38 | arg_command = " ".join(command[1:]) 39 | 40 | return (msg, msg_lower, command, command_len, base_command, arg_command, isFromChat, username) 41 | 42 | def handle_command(self, msg, isFromChat=False, username=""): 43 | commandlist = self._get_command(msg, isFromChat, username) 44 | 45 | if self._handleStartCommand(commandlist): 46 | return True 47 | 48 | if self._handleChatCommand(commandlist): 49 | return True 50 | 51 | if self._handleUnrestrictedCommand(commandlist): 52 | return True 53 | 54 | if self._handleRestrictedCommand(commandlist): 55 | return True 56 | 57 | return False 58 | 59 | def _handleStartCommand(self, commandlist): 60 | (msg, msg_lower, command, command_len, base_command, arg_command, isFromChat, username) = commandlist 61 | 62 | if len(msg) < 12 and any(k in msg_lower for k in START_KEYWORDS): 63 | self._bot.send_forcestart(delay=0) 64 | self._bot.isPaused = False 65 | return True 66 | if len(msg) < 2: 67 | return True 68 | 69 | return False 70 | 71 | def _handleChatCommand(self, commandlist): 72 | (msg, msg_lower, command, command_len, base_command, arg_command, isFromChat, username) = commandlist 73 | 74 | if self._handlePlayerCommand(msg, username): 75 | return True 76 | if base_command.startswith(tuple(HELP_KEYWORDS)): 77 | self._print_command_help(isFromChat) 78 | return True 79 | if base_command.startswith(tuple(HELLO_KEYWORDS)): 80 | self._print_command_hello() 81 | return True 82 | 83 | return False 84 | 85 | def _handleUnrestrictedCommand(self, commandlist): 86 | (msg, msg_lower, command, command_len, base_command, arg_command, isFromChat, username) = commandlist 87 | 88 | if "setup" in base_command: 89 | self._bot.set_game_speed(4) 90 | self._set_game_map() 91 | self._bot.set_game_public() 92 | return True 93 | if "speed" in base_command and command_len >= 2 and command[1][0].isdigit(): 94 | self._bot.set_game_speed(command[1][0]) 95 | return True 96 | if "public" in base_command: 97 | self._bot.set_game_public() 98 | return True 99 | 100 | return False 101 | 102 | def _handleRestrictedCommand(self, commandlist): 103 | (msg, msg_lower, command, command_len, base_command, arg_command, isFromChat, username) = commandlist 104 | 105 | if username in BANNED_CHAT_PLAYERS: 106 | return False 107 | 108 | if self._permitted_username != "" and self._permitted_username != username: # Only allow permitted user 109 | return False 110 | 111 | if self._handleSetupCommand(commandlist): 112 | return True 113 | 114 | if self._handleGameCommand(commandlist): 115 | return True 116 | 117 | return False 118 | 119 | def _handleSetupCommand(self, commandlist): 120 | (msg, msg_lower, command, command_len, base_command, arg_command, isFromChat, username) = commandlist 121 | 122 | if "map" in base_command: 123 | if command_len >= 2: 124 | self._set_game_map(arg_command) 125 | else: 126 | self._set_game_map() 127 | return True 128 | elif "normal" in base_command: 129 | self._set_normal_map() 130 | return True 131 | elif "maxsize" in base_command: 132 | self._bot.set_normal_map(width=1.0, height=1.0) 133 | return True 134 | elif "mincity" in base_command: 135 | self._bot.set_normal_map(city=0.0) 136 | return True 137 | elif "maxcity" in base_command: 138 | self._bot.set_normal_map(city=1.0) 139 | return True 140 | elif "minmountain" in base_command: 141 | self._bot.set_normal_map(mountain=0.0) 142 | return True 143 | elif "maxmountain" in base_command: 144 | self._bot.set_normal_map(mountain=1.0) 145 | return True 146 | elif "maxswamp" in base_command: 147 | self._bot.set_normal_map(swamp=1.0) 148 | return True 149 | elif "maxall" in base_command: 150 | self._bot.set_normal_map(1.0, 1.0, 1.0, 1.0, 1.0) 151 | return True 152 | elif "width" in base_command: 153 | if command_len == 2: 154 | try: 155 | self._bot.set_normal_map(width=float(arg_command)) 156 | return True 157 | except ValueError: 158 | None 159 | self._bot.set_normal_map(width=1.0) 160 | return True 161 | elif "height" in base_command: 162 | if command_len == 2: 163 | try: 164 | self._bot.set_normal_map(height=float(arg_command)) 165 | return True 166 | except ValueError: 167 | None 168 | self._bot.set_normal_map(height=1.0) 169 | return True 170 | elif "city" in base_command: 171 | if command_len == 2: 172 | try: 173 | self._bot.set_normal_map(city=float(arg_command)) 174 | return True 175 | except ValueError: 176 | None 177 | self._bot.set_normal_map(city=1.0) 178 | return True 179 | elif "mountain" in base_command: 180 | if command_len == 2: 181 | try: 182 | self._bot.set_normal_map(mountain=float(arg_command)) 183 | return True 184 | except ValueError: 185 | None 186 | self._bot.set_normal_map(mountain=1.0) 187 | return True 188 | elif "swamp" in base_command: 189 | if command_len == 2: 190 | try: 191 | self._set_swamp_map(float(arg_command)) 192 | return True 193 | except ValueError: 194 | None 195 | self._set_swamp_map() 196 | return True 197 | elif isFromChat and len(msg) < 12 and "map" in msg_lower: 198 | self._set_game_map() 199 | return True 200 | 201 | return False 202 | 203 | def _handleGameCommand(self, commandlist): 204 | (msg, msg_lower, command, command_len, base_command, arg_command, isFromChat, username) = commandlist 205 | 206 | if "take" in base_command and username != "": 207 | self._permitted_username = username 208 | elif "team" in base_command: 209 | if command_len >= 2: 210 | if len(command[1]) == 1: 211 | self._bot.set_game_team(command[1]) 212 | else: 213 | return self._add_teammate(arg_command) 214 | elif base_command in ["unteamall"]: 215 | self._remove_all_teammates() 216 | elif base_command in ["unteam", "cancelteam"]: 217 | self._remove_teammate(username) 218 | elif base_command in ["noteam"]: 219 | _spawn(self._start_avoiding_team) 220 | else: 221 | return self._add_teammate(username) 222 | return True 223 | elif "bye!" in base_command: 224 | if "_map" in dir(self): 225 | #self._map.exit_on_game_over = False # Wait 2 minutes before exiting 226 | self._bot.send_surrender() 227 | return True 228 | 229 | elif "unpause" in base_command: 230 | self._bot.isPaused = False 231 | return True 232 | elif "pause" in base_command: 233 | self._bot.isPaused = True 234 | return True 235 | 236 | return False 237 | 238 | ######################### Sending Messages ######################### 239 | 240 | def _print_command_help(self, isFromChat=False): 241 | if isFromChat: 242 | self._bot.sent_hello = True 243 | for txt in GAME_HELP_TEXT if "_map" in dir(self) else PRE_HELP_TEXT: 244 | self._bot.send_chat(txt) 245 | time.sleep(0.34) 246 | else: 247 | print("\n".join(GAME_HELP_TEXT if "_map" in dir(self) else PRE_HELP_TEXT)) 248 | 249 | def _print_command_hello(self): 250 | if "sent_hello" in dir(self._bot): 251 | return True 252 | self._bot.sent_hello = True 253 | 254 | for txt in GAME_HELLO_TEXT if "_map" in dir(self) else HELLO_TEXT: 255 | self._bot.send_chat(txt) 256 | time.sleep(0.34) 257 | 258 | ######################### Teammates ######################### 259 | 260 | def _add_teammate(self, username): 261 | if "_map" in dir(self) and "usernames" in dir(self._map): 262 | if username != "" and username != self._map.usernames[self._map.player_index] and username in self._map.usernames: 263 | self._map.do_not_attack_players.append(self._map.usernames.index(username)) 264 | return True 265 | return False 266 | 267 | def _remove_teammate(self, username): 268 | if "_map" in dir(self) and "usernames" in dir(self._map): 269 | if username != "" and username != self._map.usernames[self._map.player_index]: 270 | if self._map.usernames.index(username) in self._map.do_not_attack_players: 271 | self._map.do_not_attack_players.remove(self._map.usernames.index(username)) 272 | return True 273 | return False 274 | 275 | def _remove_all_teammates(self): 276 | self._map.do_not_attack_players = [] 277 | return True 278 | 279 | def _start_avoiding_team(self): 280 | while True: 281 | if not "teams" in dir(self._bot): 282 | time.sleep(0.1) 283 | continue 284 | for i, members in self._bot.teams.items(): 285 | if self._bot.username in members: 286 | if len(members) > 1: # More than 1 person on bots team 287 | for team in range(1, MAX_NUM_TEAMS+1): 288 | if not team in self._bot.teams: 289 | self._bot.set_game_team(team) 290 | break 291 | 292 | time.sleep(0.1) 293 | 294 | ######################### Set Custom Gamemap ######################### 295 | 296 | def _set_game_map(self, mapname=""): 297 | if len(mapname) > 1: 298 | maplower = mapname.lower() 299 | if maplower in ["win", "good"]: 300 | self._bot.set_game_map(random.choice(GENERALS_MAPS)) 301 | elif maplower == "top": 302 | self._bot.set_game_map(random.choice(generals_api.list_top())) 303 | elif maplower == "hot": 304 | self._bot.set_game_map(random.choice(generals_api.list_hot())) 305 | else: 306 | maps = generals_api.list_search(mapname) 307 | if mapname in maps: 308 | self._bot.set_game_map(mapname) 309 | elif len(maps) >= 1: 310 | self._bot.set_game_map(maps[0]) 311 | self._bot.send_chat("I could not find "+mapname+", so I set the map to "+maps[0]+" (Note: names are case sensitive)") 312 | else: 313 | self._bot.send_chat("Could not find map named "+mapname+" (Note: names are case sensitive)") 314 | else: 315 | self._bot.set_game_map(random.choice(generals_api.list_both())) 316 | 317 | def _set_normal_map(self): 318 | width = round(random.uniform(0, 1), 2) 319 | height = round(random.uniform(0, 1), 2) 320 | city = round(random.uniform(0, 1), 2) 321 | mountain = round(random.uniform(0, 1), 2) 322 | self._bot.set_normal_map(width, height, city, mountain) 323 | 324 | def _set_swamp_map(self, swamp=-1): 325 | if swamp == -1: 326 | swamp = round(random.uniform(0, 1), 2) 327 | if swamp >= 0 and swamp <= 1: 328 | self._bot.set_normal_map(swamp=swamp) 329 | 330 | ######################### Player Requested Commands ######################### 331 | 332 | def _handlePlayerCommand(self, msg, username): 333 | if "boomer" in username.lower(): 334 | self._bot.send_chat("Okay Boomer <3") 335 | return True 336 | 337 | if "hitler" in username.lower(): 338 | self._bot.send_chat("I dont like your name :(") 339 | return True 340 | 341 | return False 342 | 343 | def _spawn(f): 344 | t = threading.Thread(target=f) 345 | t.daemon = True 346 | t.start() 347 | -------------------------------------------------------------------------------- /base/client/generals.py: -------------------------------------------------------------------------------- 1 | ''' 2 | @ Harris Christiansen (code@HarrisChristiansen.com) 3 | Generals.io Automated Client - https://github.com/harrischristiansen/generals-bot 4 | Generals.io Web Socket Communication 5 | ''' 6 | 7 | import certifi 8 | import logging 9 | import json 10 | import ssl 11 | import threading 12 | import time 13 | import requests 14 | from websocket import create_connection, WebSocketConnectionClosedException 15 | 16 | from .constants import * 17 | from . import bot_cmds 18 | from . import map 19 | 20 | class Generals(object): 21 | def __init__(self, userid, username, mode="1v1", gameid=None, public_server=False, start_command=""): 22 | self._should_forcestart = mode == "private" or mode == "team" 23 | self.userid = userid 24 | self.username = username 25 | self.gamemode = mode 26 | self.roomid = gameid 27 | self.public_server = public_server 28 | self._start_msg_cmd = start_command 29 | self.isPaused = False 30 | self._seen_update = False 31 | self._move_id = 1 32 | self._start_data = {} 33 | self._stars = [] 34 | self._cities = [] 35 | self._messagesToSave = [] 36 | self._numberPlayers = 0 37 | 38 | self._connect_and_join(userid, username, mode, gameid, self._should_forcestart) 39 | _spawn(self._send_start_msg_cmd) 40 | 41 | def close(self): 42 | with self._lock: 43 | self._ws.close() 44 | 45 | ######################### Get updates from server ######################### 46 | 47 | def get_updates(self): 48 | while True: 49 | try: 50 | msg = self._ws.recv() 51 | except WebSocketConnectionClosedException: 52 | logging.info("Connection Closed") 53 | break 54 | 55 | # logging.info("Received message type: {}".format(msg)) 56 | 57 | if not msg.strip(): 58 | continue 59 | 60 | # ignore heartbeats and connection acks 61 | if msg in {"2", "3", "40"}: 62 | continue 63 | 64 | # remove numeric prefix 65 | while msg and msg[0].isdigit(): 66 | msg = msg[1:] 67 | 68 | if msg == "probe": 69 | continue 70 | 71 | msg = json.loads(msg) 72 | if not isinstance(msg, list): 73 | continue 74 | 75 | if msg[0] == "error_user_id": 76 | logging.info("Exit: User already in game queue") 77 | return 78 | elif msg[0] == "queue_update": 79 | self._log_queue_update(msg[1]) 80 | elif msg[0] == "pre_game_start": 81 | logging.info("pre_game_start") 82 | elif msg[0] == "game_start": 83 | self._messagesToSave.append(msg) 84 | self._start_data = msg[1] 85 | elif msg[0] == "game_update": 86 | #self._messagesToSave.append(msg) 87 | yield self._make_update(msg[1]) 88 | elif msg[0] in ["game_won", "game_lost"]: 89 | yield self._make_result(msg[0], msg[1]) 90 | elif msg[0] == "chat_message": 91 | self._handle_chat(msg[2]) 92 | elif msg[0] == "error_queue_full": 93 | self.changeToNewRoom() 94 | elif msg[0] == "error_set_username": 95 | None 96 | elif msg[0] == "game_over": 97 | None 98 | elif msg[0] == "notify": 99 | None 100 | else: 101 | logging.info("Unknown message type: {}".format(msg)) 102 | 103 | ######################### Make Moves ######################### 104 | 105 | def move(self, y1, x1, y2, x2, move_half=False): 106 | if not self._seen_update: 107 | raise ValueError("Cannot move before first map seen") 108 | 109 | if self.isPaused: 110 | return False 111 | 112 | cols = self._map.cols 113 | a = y1 * cols + x1 114 | b = y2 * cols + x2 115 | self._send(["attack", a, b, move_half, self._move_id]) 116 | self._move_id += 1 117 | 118 | ######################### Send Chat Messages ######################### 119 | 120 | def send_chat(self, msg): 121 | if self.handle_command(msg): 122 | return 123 | 124 | try: 125 | if self._seen_update: 126 | self._send(["chat_message", self._start_data['chat_room'], msg, None, ""]) 127 | elif self._gameid != None: 128 | self._send(["chat_message", "chat_custom_queue_"+self._gameid, msg, None, ""]) 129 | except WebSocketConnectionClosedException: 130 | pass 131 | 132 | ######################### PRIVATE FUNCTIONS - PRIVATE FUNCTIONS ######################### 133 | 134 | ######################### Server -> Client ######################### 135 | 136 | def _log_queue_update(self, msg): 137 | if 'queueTimeLeft' in msg: 138 | logging.info("Queue (%ds) %s/%s" % (msg['queueTimeLeft'], str(len(msg['numForce'])), str(msg['numPlayers']))) 139 | return 140 | 141 | self.teams = {} 142 | if "teams" in msg: 143 | for i in range(len(msg['teams'])): 144 | if msg['teams'][i] not in self.teams: 145 | self.teams[msg['teams'][i]] = [] 146 | self.teams[msg['teams'][i]].append(msg['usernames'][i]) 147 | 148 | if 'map_title' in msg: 149 | mapname = msg['map_title'] 150 | if mapname and len(mapname) > 1: 151 | logging.info("Queue [%s] %d/%d %s" % (mapname, msg['numForce'], msg['numPlayers'], self.teams)) 152 | return 153 | 154 | logging.info("Queue %s/%s %s" % (str(len(msg['numForce'])), str(msg['numPlayers']), self.teams)) 155 | 156 | numberPlayers = msg['numPlayers'] 157 | if self._numberPlayers != numberPlayers: 158 | self._numberPlayers = numberPlayers 159 | if self._should_forcestart: 160 | _spawn(self.send_forcestart) 161 | 162 | if "usernames" in msg: 163 | for username in BANNED_PLAYERS: 164 | for userInMatch in msg['usernames']: 165 | if userInMatch != None: 166 | if userInMatch.lower().find(username.lower()) != -1: 167 | logging.info("Found banned player: %s" % username) 168 | self.changeToNewRoom() 169 | 170 | def _make_update(self, data): 171 | if not self._seen_update: 172 | self._seen_update = True 173 | self._map = map.Map(self._start_data, data) 174 | self._bot_cmds().setMap(self._map) 175 | logging.info("Joined Game: %s - %s" % (self._map.replay_url, self._map.usernames)) 176 | return self._map 177 | 178 | return self._map.update(data) 179 | 180 | def _make_result(self, update, data): 181 | self._saveMessagesToDisk() 182 | return self._map.updateResult(update) 183 | 184 | def _handle_chat(self, chat_msg): 185 | if "username" in chat_msg: 186 | self.handle_command(chat_msg["text"], from_chat=True, username=chat_msg["username"]) 187 | logging.info("From %s: %s" % (chat_msg["username"], chat_msg["text"])) 188 | else: 189 | logging.info("Message: %s" % chat_msg["text"]) 190 | 191 | def handle_command(self, msg, from_chat=False, username=""): 192 | return self._bot_cmds().handle_command(msg, from_chat, username) 193 | def _bot_cmds(self): 194 | if not "_commands" in dir(self): 195 | self._commands = bot_cmds.BotCommands(self) 196 | return self._commands 197 | 198 | ######################### Client -> Server ######################### 199 | 200 | def _endpointWS(self): 201 | return "wss" + (ENDPOINT_BOT if not self.public_server else ENDPOINT_PUBLIC) + "&transport=websocket" 202 | 203 | def _endpointRequests(self): 204 | return "https" + (ENDPOINT_BOT if not self.public_server else ENDPOINT_PUBLIC) + "&transport=polling" 205 | 206 | def _getSID(self): 207 | request = requests.get(self._endpointRequests() + "&t=ObyKmaZ") 208 | result = request.text 209 | while result and result[0].isdigit(): 210 | result = result[1:] 211 | 212 | msg = json.loads(result) 213 | sid = msg["sid"] 214 | self._gio_sessionID = sid 215 | _spawn(self._verifySID) 216 | return sid 217 | 218 | def _verifySID(self): 219 | sid = self._gio_sessionID 220 | checkOne = requests.post(self._endpointRequests() + "&t=ObyKmbC&sid=" + sid, data="40") 221 | # checkTwo = requests.get(self._endpointRequests() + "&t=ObyKmbC.0&sid=" + sid) 222 | # logging.debug("Check two: %s" % checkTwo.text) 223 | 224 | def _connect_and_join(self, userid, username, mode, gameid, force_start): 225 | endpoint = self._endpointWS() + "&sid=" + self._getSID() 226 | logging.debug("Creating connection with endpoint %s: %s" % (endpoint, certifi.where())) 227 | # ssl_context = ssl.create_default_context(cafile=certifi.where()) 228 | ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) 229 | ssl_context.load_verify_locations(certifi.where()) 230 | # ctx.load_cert_chain(certfile=certifi.where()) 231 | # self._ws = create_connection(endpoint, ssl=ssl_context) 232 | self._ws = create_connection(endpoint, sslopt={"cert_reqs": ssl.CERT_NONE}) 233 | self._lock = threading.RLock() 234 | self._ws.send("2probe") 235 | self._ws.send("5") 236 | _spawn(self._start_sending_heartbeat) 237 | # logging.debug("Setting Username: %s" % username) 238 | # self._send(["set_username", userid, username, BOT_KEY]) 239 | 240 | logging.info("Joining game") 241 | self._gameid = None 242 | if mode == "private": 243 | self._gameid = gameid 244 | if gameid is None: 245 | raise ValueError("Gameid must be provided for private games") 246 | self._send(["join_private", gameid, userid, BOT_KEY]) 247 | elif mode == "1v1": 248 | self._send(["join_1v1", userid, BOT_KEY]) 249 | elif mode == "team": 250 | self._send(["join_team", userid, BOT_KEY]) 251 | elif mode == "ffa": 252 | self._send(["play", userid, BOT_KEY]) 253 | else: 254 | raise ValueError("Invalid mode") 255 | 256 | def _start_sending_heartbeat(self): 257 | while True: 258 | try: 259 | with self._lock: 260 | self._ws.send("3") 261 | except WebSocketConnectionClosedException: 262 | logging.info("Connection Closed - heartbeat") 263 | break 264 | time.sleep(19) 265 | 266 | def send_forcestart(self, delay=10): 267 | time.sleep(delay) 268 | self._send(["set_force_start", self._gameid, True]) 269 | logging.info("Sent force start") 270 | 271 | def set_game_speed(self, speed="1"): 272 | speed = int(speed) 273 | if speed in [1, 2, 3, 4]: 274 | self._send(["set_custom_options", self._gameid, {"game_speed":speed}]) 275 | 276 | def set_game_team(self, team="1"): 277 | team = int(team) 278 | if team in range(1, MAX_NUM_TEAMS+1): 279 | self._send(["set_custom_team", self._gameid, team]) 280 | 281 | def set_game_public(self): 282 | self._send(["make_custom_public", self._gameid]) 283 | 284 | def set_game_map(self, mapname=""): 285 | if len(mapname) > 1: 286 | self._send(["set_custom_options", self._gameid, {"map":mapname}]) 287 | 288 | def set_normal_map(self, width=-1, height=-1, city=-1, mountain=-1, swamp=-1): 289 | self._send(["set_custom_options", self._gameid, {"map":None}]) 290 | if width >= 0 and width <=1: 291 | self._send(["set_custom_options", self._gameid, {"width":width}]) 292 | if height >= 0 and height <=1: 293 | self._send(["set_custom_options", self._gameid, {"height":height}]) 294 | if city >= 0 and city <=1: 295 | self._send(["set_custom_options", self._gameid, {"city_density":city}]) 296 | if mountain >= 0 and mountain <=1: 297 | self._send(["set_custom_options", self._gameid, {"mountain_density":mountain}]) 298 | if swamp >= 0 and swamp <=1: 299 | self._send(["set_custom_options", self._gameid, {"swamp_density":swamp}]) 300 | 301 | def send_surrender(self): 302 | self._send(["surrender"]) 303 | 304 | def _send(self, msg, prefix="42"): 305 | try: 306 | with self._lock: 307 | self._ws.send(prefix + json.dumps(msg)) 308 | except WebSocketConnectionClosedException: 309 | pass 310 | 311 | ######################### Game Replay ######################### 312 | 313 | def _saveMessagesToDisk(self): 314 | fileName = "game_" + self._map.replay_url + ".txt" 315 | fileName = fileName.replace("/", ".") 316 | fileName = fileName.replace(":", "") 317 | 318 | with open("games/"+fileName, 'w+') as file: 319 | file.write(str(self._messagesToSave)) 320 | 321 | ######################### Change Rooms ######################### 322 | 323 | 324 | def _send_start_msg_cmd(self): 325 | time.sleep(0.2) 326 | for cmd in self._start_msg_cmd.split("\\n"): 327 | self.handle_command(cmd) 328 | 329 | def changeToNewRoom(self): 330 | self.close() 331 | self.roomid = self.roomid + "x" 332 | self._connect_and_join(self.userid, self.username, self.gamemode, self.roomid, self._should_forcestart) 333 | _spawn(self._send_start_msg_cmd) 334 | 335 | 336 | 337 | def _spawn(f): 338 | t = threading.Thread(target=f) 339 | t.daemon = True 340 | t.start() 341 | --------------------------------------------------------------------------------