-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathGameManager.py
221 lines (208 loc) · 10.2 KB
/
GameManager.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
import importlib
import sys
import time
import selenium.common
from selenium import webdriver
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
import backend.Move
from backend import Constants
from backend.Timer import Timer
from backend.Board import Board
from backend.Leaderboard import Leaderboard
import ruamel.yaml
from inspect import isclass
from backend.Log import dprint
from backend.Chat import Chat
config = ruamel.yaml.YAML().load(open('backend/config.yaml'))
ai_import = config['ai'].split('.')
ai = importlib.import_module('.'.join(ai_import[:-1]))
class_name = [x for x in dir(ai) if isclass(getattr(ai, x))]
if ai_import[-1] not in class_name:
dprint('GameManager', f'error: The referenced class does not exist!'
f' class name: {ai_import[-1]} module: {".".join(ai_import[:-1])}')
sys.exit(1)
class GameManager:
GIO_HOME = 'https:/generals.io'
GIO_CUSTOM_GAME_PREFIX = 'https://generals.io/games/'
GIO_HOME_PLAY_BUTTON_XPATH = "/html/body/div/div/div/center/div[1]/div[2]/button[1]"
GIO_GAME_TURN_COUNTER_XPATH = "/html/body/div/div/div/div[4]"
GIO_GAME_OVER_XPATH = "/html/body/div/div/div/div[3]/center"
GIO_GAME_OVER_TEXT_XPATH = "/html/body/div/div/div/div[3]/center/p/span"
GIO_GAME_TABLE_XPATH = "/html/body/div/div/div/div[2]/table/tbody"
GIO_SEND_KEY_XPATH = "/html/body"
GIO_HOME_1v1_BUTTON_XPATH = "/html/body/div/div/div/center/div[4]/div/center/button[2]"
GIO_HOME_FFA_BUTTON_XPATH = "/html/body/div/div/div/center/div[4]/div/center/button[1]"
GIO_GAME_LEADERBOARD_XPATH = "/html/body/div/div/div/table"
VALID_BROWSER = ['edge', 'chrome', 'firefox', 'safari']
def __init__(self, configuration) -> None:
"""
Initialize the GameManager.
:param configuration: The json configuration from config.yaml
"""
try:
dprint("GameManager.init", 'creating webdriver...')
options = webdriver.ChromeOptions()
options.add_argument('--user-data-dir=./ChromeData')
self.driver = webdriver.Chrome(options=options)
except selenium.common.NoSuchDriverException:
dprint("GameManager.init", "error: could not create driver. this could be because of"
" a non-existent internet connection"
" or the driver package has not been installed."
"if the latter is true, go to https://pypi.org/project/selenium#drivers and "
"download the correct webdriver.")
sys.exit(1)
self.configuration = configuration
self.current_turn = 0
def enter_game(self) -> None:
"""
Join the game according to self.configuration
:return: None
"""
if self.configuration['sign_in_mode']:
dprint("GameManager.enter_game", "# mode active! #to your account,"
" then close the window, stop the program, and change the setting back.")
dprint("GameManager.enter_game", f"Loading {Constants.GIO_HOME}")
self.driver.get(Constants.GIO_HOME)
while True:
time.sleep(1)
continue
if 'custom' not in self.configuration['auto_join']: # not custom game, load homepage to enter 1v1/ffa queues
dprint("GameManager.enter_game", f"Loading {Constants.GIO_HOME}")
self.driver.get(Constants.GIO_HOME)
else: # custom game, load custom game page
dprint("GameManager.enter_game", f"Loading {Constants.GIO_CUSTOM_GAME_PREFIX+self.configuration['auto_join'].split('/')[1]}")
self.driver.get(Constants.GIO_CUSTOM_GAME_PREFIX + self.configuration['auto_join'].split('/')[1])
if not self.configuration['auto_join']: # if we don't have a game mode, just stop here. run_game will wait
return
time.sleep(self.configuration['websocket_connection_delay']) # wait for websocket
if self.configuration['auto_join'].lower() == '1v1': # join 1v1 queue
self.driver.find_element(By.XPATH, Constants.GIO_HOME_PLAY_BUTTON_XPATH).click()
WebDriverWait(self.driver, 10).until(expected_conditions.element_to_be_clickable(
self.driver.find_element(By.XPATH, Constants.GIO_HOME_1v1_BUTTON_XPATH))).click()
if self.configuration['auto_join'].lower() == 'ffa': # join ffa queue
self.driver.find_element(By.XPATH, Constants.GIO_HOME_PLAY_BUTTON_XPATH).click()
WebDriverWait(self.driver, 10).until(expected_conditions.element_to_be_clickable(
self.driver.find_element(By.XPATH, Constants.GIO_HOME_FFA_BUTTON_XPATH))).click()
# if we made it here without triggering an if statement, then the auto_join parameter is invalid,
# and we will ask the user to start the game from the generals.io homepage
# TODO: add auto force start for ffa/1v1/custom
def wait_for_next_turn(self) -> float:
"""
Wait for the next turn by watching the turn counter in the top-left.
Assumes game in progress and will crash otherwise.
:return: the current turn
"""
turn_counter = self.driver.find_element(By.XPATH, Constants.GIO_GAME_TURN_COUNTER_XPATH)
while True:
check_it = turn_counter.text.split(" ")[1]
c_t = 0
if check_it.endswith('.'):
c_t = 0.5
c_t += int(check_it.replace(".", ""))
if c_t != self.current_turn:
self.current_turn = c_t
return self.current_turn
def is_game_over(self) -> int:
"""
A function to check if the game is over yet
:return: 0 if game is ongoing, 1 if we won, 2 if we lost by capture, 3 if we lost by afk
"""
elem = self.driver.find_elements(By.XPATH, Constants.GIO_GAME_OVER_XPATH)
if len(elem) == 0:
return 0
elem = elem[0]
reason = elem.find_element(By.XPATH, Constants.GIO_GAME_OVER_TEXT_XPATH).text
if 'You went AFK.' in reason:
return 3
if reason == '':
return 1
if 'You were defeated by' in reason:
return 2
def run_game(self) -> int:
"""
Run the game loop.
:return: 1 if we won, 0 if we lost.
"""
WebDriverWait(self.driver, 100000000).until(
expected_conditions.visibility_of_element_located((By.XPATH, Constants.GIO_GAME_TABLE_XPATH)))
body = self.driver.find_element(By.XPATH, Constants.GIO_SEND_KEY_XPATH)
leaderboard = Leaderboard(self.driver.find_element(By.XPATH, Constants.GIO_GAME_LEADERBOARD_XPATH))
chat = Chat(self.driver)
b = Board(leaderboard.leaderboard[self.configuration['bot_username']]['color'], leaderboard, chat)
bot = getattr(ai, ai_import[-1])() # nothing like dynamic class imports
for _ in range(5): # zoom out so we can see (and click) on everything
body.send_keys("9")
while True:
try:
leaderboard.update()
chat.update()
except selenium.common.exceptions.NoSuchElementException:
dprint("GameManager.run_game", "failed to update chat/leaderboard! game is probably over.")
outer_html = self.driver.find_element(By.XPATH, Constants.GIO_GAME_TABLE_XPATH).get_attribute('outerHTML')
b.update(moves_html=outer_html, current_turn=self.current_turn) # update squares and moves
bot_package = bot.get_move(b, Timer(time.time(), 250)) # get move (250 ms) TODO: dynamic move time?
try:
move, message = bot_package
except TypeError:
if type(bot_package) is None:
self.wait_for_next_turn()
continue
elif type(bot_package) == backend.Move.Move:
move = bot_package
message = ''
elif type(bot_package) == str:
move = None
message = bot_package
else:
dprint("GameManager.run_game", "Invalid return from bot function! assuming null move.")
self.wait_for_next_turn()
continue
if message:
chat.send(message)
if not move.is_null: # if we want to move, move
dprint("GameManager.run_game", "Playing move: ", move)
self.play_move(move)
i = self.is_game_over() # has the game ended?
if i == 1:
dprint("GameManager.run_game", "The bot has won!")
return 1
if i == 2:
dprint("GameManager.run_game", "The bot was captured :(")
return 0
if i == 3:
dprint("GameManager.run_game", "The bot went AFK xD")
return 0
self.wait_for_next_turn()
def click(self, column, row):
"""
Click on the square at (row, column)
:param column: the column the square is in
:param row: the row the square is in
:return: 1 if successfully clicked, 0 if not
"""
elem = self.driver. \
find_element("xpath", f"/html/body/div/div/div/div[2]/table/tbody/tr[{str(row + 1)}]/td[{str(column + 1)}]")
try:
elem.click()
return 1
except selenium.common.ElementClickInterceptedException:
dprint("Board.click", "Board click interception! Game over? this could also be an alert/popup")
return 0
def play_move(self, move):
"""
Play the Move object. TODO: ignore arrows (i think this works)
:param move:
:return:
"""
transform = [[0, -1], [1, 0], [0, 1], [-1, 0]]
if not self.click(move.column, move.row):
return
if move.is_50_percent:
self.click(move.column, move.row) # double click if we are playing a 50% move.
self.click(move.column + transform[move.direction][0], move.row + transform[move.direction][1])
g = GameManager(config)
while True:
g.enter_game()
g.run_game()