├── .gitignore ├── LICENSE ├── README.md ├── game.py ├── replays └── all.txt ├── requirements.txt ├── server.py └── static ├── Quicksand-Bold.otf ├── Quicksand-Light.otf ├── Quicksand-Regular.otf ├── city.png ├── crown.js ├── crown.png ├── game.html ├── gong.mp3 ├── index.html ├── main.css ├── main.js ├── mountain.png ├── obstacle.png ├── replays.html └── swamp.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | replays/*.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, mcfx0 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # generalsio_copy 2 | An imitation of custom-room mode of generals.io, written in python with flask_socketio. 3 | 4 | This imitation only guarantees that the rule is almost the same (I'm not sure if there's some differences) as the original ones, but not the APIs. 5 | 6 | Now also supports replay. 7 | 8 | ## Running 9 | ```shell 10 | pip3 install -r requirements.txt 11 | python3 server.py 12 | ``` 13 | 14 | Then just open `http://localhost:23333`. 15 | -------------------------------------------------------------------------------- /game.py: -------------------------------------------------------------------------------- 1 | import sys, time, json, math, base64, random, hashlib, eventlet, threading, requests 2 | 3 | # type: 0=empty 1=mountain 2=swamp -1=city -2=general 4 | 5 | default_width = 45 6 | max_city_ratio = 0.04 7 | max_swamp_ratio = 0.16 8 | max_mountain_ratio = 0.24 9 | left_game = 52 10 | 11 | 12 | def chkconn(grid_type, n, m): 13 | fa = [i for i in range(n * m)] 14 | sz = [1] * (n * m) 15 | 16 | def find(x): 17 | if x == fa[x]: 18 | return x 19 | fa[x] = find(fa[x]) 20 | return fa[x] 21 | 22 | def merge(x, y): 23 | x = find(x) 24 | y = find(y) 25 | if x != y: 26 | sz[y] += sz[x] 27 | fa[x] = y 28 | cnt = 0 29 | for i in range(n): 30 | for j in range(m): 31 | if grid_type[i][j] != 1: 32 | if i + 1 < n and grid_type[i + 1][j] != 1: 33 | merge(i * m + j, i * m + j + m) 34 | if j + 1 < m and grid_type[i][j + 1] != 1: 35 | merge(i * m + j, i * m + j + 1) 36 | else: 37 | cnt += 1 38 | for i in range(n * m): 39 | if sz[i] > (n * m - cnt) * 0.9: 40 | return (i // m, i % m) 41 | return (-1, -1) 42 | 43 | 44 | def get_st(grid_type, n, m, res): 45 | fa = [i for i in range(n * m)] 46 | sz = [1] * (n * m) 47 | 48 | def find(x): 49 | if x == fa[x]: 50 | return x 51 | fa[x] = find(fa[x]) 52 | return fa[x] 53 | 54 | def merge(x, y): 55 | x = find(x) 56 | y = find(y) 57 | if x != y: 58 | sz[y] += sz[x] 59 | fa[x] = y 60 | cnt = 0 61 | for i in range(n): 62 | for j in range(m): 63 | if grid_type[i][j] != 1: 64 | if i + 1 < n and grid_type[i + 1][j] != 1: 65 | merge(i * m + j, i * m + j + m) 66 | if j + 1 < m and grid_type[i][j + 1] != 1: 67 | merge(i * m + j, i * m + j + 1) 68 | else: 69 | cnt += 1 70 | max_sz = 0 71 | for i in range(n * m): 72 | max_sz = max(max_sz, sz[i]) 73 | for i in range(n * m): 74 | if sz[find(i)] == max_sz: 75 | res[i // m][i % m] = True 76 | 77 | 78 | def get_diff(a, b): 79 | res = [] 80 | for i in range(len(a)): 81 | if a[i] != b[i]: 82 | res.append(i) 83 | res.append(a[i]) 84 | return res 85 | 86 | 87 | class Game: 88 | def __init__(self, game_conf, update, emit_init_map, player_ids, rplayer_ids, chat_message, gid, md5, end_game): 89 | print('start game:', gid, player_ids, game_conf['player_names']) 90 | sys.stdout.flush() 91 | self.otime = time.time() 92 | self.md5 = md5 93 | self.end_game = end_game 94 | self.player_ids = player_ids 95 | self.player_ids_rev = {} 96 | for i in range(len(player_ids)): 97 | self.player_ids_rev[player_ids[i]] = i 98 | self.update = update 99 | self.pcnt = len(player_ids) 100 | self.speed = game_conf['speed'] 101 | self.names = game_conf['player_names'] 102 | self.team = game_conf['player_teams'] 103 | self.rpcnt = 0 104 | for i in self.team: 105 | if i: 106 | self.rpcnt += 1 107 | self.width_ratio = game_conf['width_ratio'] / 2 + 0.5 108 | self.height_ratio = game_conf['height_ratio'] / 2 + 0.5 109 | self.city_ratio = game_conf['city_ratio'] 110 | self.mountain_ratio = game_conf['mountain_ratio'] 111 | self.swamp_ratio = game_conf['swamp_ratio'] 112 | self.pstat = [0 for i in player_ids] 113 | self.pmove = [[] for i in player_ids] 114 | self.lst_move = [(-1, -1, -1, -1, False) for i in player_ids] 115 | self.watching = [True for i in player_ids] 116 | self.spec = [False for i in player_ids] 117 | self.grid_type_lst = [[] for i in player_ids] 118 | self.army_cnt_lst = [[] for i in player_ids] 119 | self.deadorder = [0] * len(player_ids) 120 | self.deadcount = 0 121 | self.chat_message = chat_message 122 | self.gid = gid 123 | self.lock = threading.RLock() 124 | self.turn = 0 125 | self.recentkills = {} 126 | self.history = [] 127 | if game_conf['custom_map'] != '': 128 | self.getcustommap(game_conf['custom_map']) 129 | else: 130 | self.genmap() 131 | self.sel_generals() 132 | for i in range(self.pcnt): 133 | emit_init_map(self.player_ids[i], {'n': self.n, 'm': self.m, 'player_ids': rplayer_ids, 'general': self.generals[i]}) 134 | 135 | def send_message(self, sid, data): 136 | id = self.player_ids_rev[sid] 137 | uid = self.names[id] 138 | if data['team']: 139 | for i in range(self.pcnt): 140 | if self.team[i] == self.team[id]: 141 | self.chat_message(self.player_ids[i], 'sid', uid, id + 1, data['text'], True) 142 | else: 143 | self.chat_message(self.gid, 'room', uid, id + 1, data['text']) 144 | 145 | def send_system_message(self, text): 146 | self.chat_message(self.gid, 'room', '', 0, text) 147 | 148 | def getcustommap(self, title): 149 | try: 150 | r = requests.get('http://generals.io/api/map', params={'name': title.encode('utf-8')}).json() 151 | n = r['height'] 152 | m = r['width'] 153 | t = r['map'].split(',') 154 | self.owner = [[0 for j in range(m)] for i in range(n)] 155 | self.army_cnt = [[0 for j in range(m)] for i in range(n)] 156 | self.grid_type = [[0 for j in range(m)] for i in range(n)] 157 | for i in range(n): 158 | for j in range(m): 159 | x = t[i * m + j].strip(' ') 160 | if x == 'm': 161 | self.grid_type[i][j] = 1 162 | elif x == 's': 163 | self.grid_type[i][j] = 2 164 | elif x == 'g': 165 | self.grid_type[i][j] = -2 166 | elif x == 's': 167 | self.grid_type[i][j] = 2 168 | elif len(x): 169 | if x[0] == 'n': 170 | self.army_cnt[i][j] = int(x[1:]) 171 | else: 172 | self.army_cnt[i][j] = int(x) 173 | self.grid_type[i][j] = -1 174 | self.n = n 175 | self.m = m 176 | self.st = [[False for j in range(m)] for i in range(n)] 177 | get_st(self.grid_type, n, m, self.st) 178 | self.is_custom = True 179 | except: 180 | self.genmap() 181 | 182 | def genmap(self): 183 | self.is_custom = False 184 | ni = random.randint(default_width - 5, default_width + 5) 185 | mi = default_width * default_width // ni 186 | self.n = n = int(ni * self.height_ratio) 187 | self.m = m = int(mi * self.width_ratio) 188 | city_ratio = max_city_ratio * self.city_ratio 189 | swamp_ratio = city_ratio + max_swamp_ratio * self.swamp_ratio 190 | mountain_ratio = swamp_ratio + max_mountain_ratio * self.mountain_ratio 191 | while True: 192 | grid_type = [[0 for j in range(m)] for i in range(n)] 193 | for i in range(n): 194 | for j in range(m): 195 | tmp = random.random() 196 | grid_type[i][j] = -1 if tmp < city_ratio else 2 if tmp < swamp_ratio else 1 if tmp < mountain_ratio else 0 197 | x, y = chkconn(grid_type, n, m) 198 | if x != -1: 199 | break 200 | self.grid_type = grid_type 201 | self.st = [[False for j in range(m)] for i in range(n)] 202 | get_st(grid_type, n, m, self.st) 203 | self.owner = [[0 for j in range(m)] for i in range(n)] 204 | self.army_cnt = [[0 for j in range(m)] for i in range(n)] 205 | for i in range(n): 206 | for j in range(m): 207 | if self.grid_type[i][j] == -1: 208 | self.army_cnt[i][j] = random.randint(40, 50) 209 | 210 | def sel_generals(self): 211 | ges = [] 212 | gevals = [] 213 | while len(ges) < 500: 214 | ge = [] 215 | sp = [] 216 | for i in range(self.n): 217 | for j in range(self.m): 218 | if self.st[i][j]: 219 | if self.grid_type[i][j] == -2: 220 | ge.append((i, j)) 221 | elif self.grid_type[i][j] == 0: 222 | sp.append((i, j)) 223 | random.shuffle(sp) 224 | if self.rpcnt > len(ge): 225 | for i in range(min(self.rpcnt - len(ge), len(sp))): 226 | ge.append(sp[i]) 227 | if self.rpcnt > len(ge): 228 | for i in range(self.pcnt - len(ge)): 229 | ge.append((-1, -1)) 230 | random.shuffle(ge) 231 | tv = 0 232 | for i in range(self.rpcnt): 233 | for j in range(i): 234 | tdis = abs(ge[i][0] - ge[j][0]) + abs(ge[i][1] - ge[j][1]) 235 | tv += 0.88**tdis + max(0, 9 - tdis) 236 | ges.append(ge) 237 | tv += 1e-8 238 | tv = 1 / tv 239 | if self.is_custom: 240 | tv = tv**1.2 241 | else: 242 | tv = tv**2.2 243 | gevals.append(tv) 244 | gmax = max(gevals) 245 | for i in range(len(ges)): 246 | gevals[i] = int(gevals[i] / gmax * 100000) 247 | gpos = random.randint(0, sum(gevals) - 1) 248 | for i in range(len(ges)): 249 | if gevals[i] > gpos: 250 | ge = ges[i] 251 | break 252 | gpos -= gevals[i] 253 | for i in range(self.n): 254 | for j in range(self.m): 255 | if self.st[i][j]: 256 | if self.grid_type[i][j] == -2: 257 | self.grid_type[i][j] = 0 258 | self.generals = [(-1, -1)for _ in range(self.pcnt)] 259 | cu = 0 260 | for i in range(self.pcnt): 261 | if self.team[i] == 0: 262 | self.pstat[i] = left_game 263 | else: 264 | if ge[cu] == (-1, -1): 265 | self.pstat[i] = left_game 266 | else: 267 | self.generals[i] = ge[cu] 268 | self.grid_type[ge[cu][0]][ge[cu][1]] = -2 269 | self.owner[ge[cu][0]][ge[cu][1]] = i + 1 270 | self.army_cnt[ge[cu][0]][ge[cu][1]] = 1 271 | cu += 1 272 | 273 | def chkxy(self, x, y): 274 | return x >= 0 and y >= 0 and x < self.n and y < self.m 275 | 276 | def sendmap(self, stat): 277 | history_hash = None 278 | dx = [0, -1, 1, 0, 0, -1, -1, 1, 1] 279 | dy = [0, 0, 0, -1, 1, -1, 1, -1, 1] 280 | pl_v = [[0, 0] for i in range(self.pcnt)] 281 | for i in range(self.n): 282 | for j in range(self.m): 283 | if self.owner[i][j]: 284 | pl_v[self.owner[i][j] - 1][0] += self.army_cnt[i][j] 285 | pl_v[self.owner[i][j] - 1][1] += 1 286 | kls = self.recentkills 287 | self.recentkills = {} 288 | leaderboard = [] 289 | for i in range(self.pcnt): 290 | cl = '' 291 | if self.pstat[i] == left_game: 292 | cl = 'dead' 293 | elif self.pstat[i]: 294 | cl = 'afk' 295 | if self.team[i]: 296 | leaderboard.append({'team': self.team[i], 'uid': self.names[i], 'army': pl_v[i][0], 'land': pl_v[i][1], 'class_': cl, 'dead': self.deadorder[i], 'id': i + 1}) 297 | for p in range(-1, self.pcnt): 298 | if p == -1 or self.watching[p]: 299 | rt = [[0 for j in range(self.m)] for i in range(self.n)] 300 | rc = [[0 for j in range(self.m)] for i in range(self.n)] 301 | for i in range(self.n): 302 | for j in range(self.m): 303 | if p == -1 or self.team[p] == 0 or self.spec[p]: 304 | rt[i][j] = 200 305 | else: 306 | rt[i][j] = 202 307 | for d in range(9): 308 | if self.chkxy(i + dx[d], j + dy[d]) and self.owner[i + dx[d]][j + dy[d]] != 0 and self.team[self.owner[i + dx[d]][j + dy[d]] - 1] == self.team[p]: 309 | rt[i][j] = 200 310 | rc[i][j] = self.army_cnt[i][j] if rt[i][j] == 200 else 0 311 | for i in range(self.n): 312 | for j in range(self.m): 313 | if self.grid_type[i][j] == 2: 314 | rt[i][j] = 205 if rt[i][j] == 202 else 204 if self.owner[i][j] == 0 else self.owner[i][j] + 150 315 | elif self.grid_type[i][j] == 1: 316 | rt[i][j] = 201 if rt[i][j] == 200 else 203 317 | elif self.grid_type[i][j] == -1: 318 | rt[i][j] = self.owner[i][j] + 50 if rt[i][j] == 200 else 203 319 | elif self.grid_type[i][j] == -2: 320 | rt[i][j] = self.owner[i][j] + 100 if rt[i][j] == 200 else 202 321 | elif self.grid_type[i][j] == 0: 322 | rt[i][j] = 202 if rt[i][j] == 202 else self.owner[i][j] if self.owner[i][j] or self.army_cnt[i][j] else 200 323 | rt2 = [] 324 | rc2 = [] 325 | for i in range(self.n): 326 | for j in range(self.m): 327 | rt2.append(rt[i][j]) 328 | rc2.append(rc[i][j]) 329 | tmp = self.lst_move[p] 330 | if p == -1 or self.turn % 50 == 0 or random.randint(0, 50) == 0: 331 | res_data = { 332 | 'grid_type': rt2, 333 | 'army_cnt': rc2, 334 | 'lst_move': {'x': tmp[0], 'y': tmp[1], 'dx': tmp[2], 'dy': tmp[3], 'half': tmp[4]}, 335 | 'leaderboard': leaderboard, 336 | 'turn': self.turn, 337 | 'kills': kls, 338 | 'game_end': stat, 339 | 'is_diff': False, 340 | } 341 | else: 342 | res_data = { 343 | 'grid_type': get_diff(rt2, self.grid_type_lst[p]), 344 | 'army_cnt': get_diff(rc2, self.army_cnt_lst[p]), 345 | 'lst_move': {'x': tmp[0], 'y': tmp[1], 'dx': tmp[2], 'dy': tmp[3], 'half': tmp[4]}, 346 | 'leaderboard': leaderboard, 347 | 'turn': self.turn, 348 | 'kills': kls, 349 | 'game_end': stat, 350 | 'is_diff': True, 351 | } 352 | if history_hash: 353 | res_data['replay'] = history_hash 354 | if p != -1: 355 | self.grid_type_lst[p] = rt2 356 | self.army_cnt_lst[p] = rc2 357 | self.lst_move[p] = (-1, -1, -1, -1, False) 358 | self.update(self.player_ids[p], res_data) 359 | else: 360 | self.history.append(res_data) 361 | if stat: 362 | history_hash = self.save_history() 363 | 364 | def add_move(self, player, x, y, dx, dy, half): 365 | player = self.player_ids_rev[player] 366 | self.lock.acquire() 367 | self.pmove[player].append((x, y, dx, dy, half)) 368 | self.lock.release() 369 | 370 | def clear_queue(self, player): 371 | player = self.player_ids_rev[player] 372 | self.lock.acquire() 373 | self.pmove[player] = [] 374 | self.lock.release() 375 | 376 | def pop_queue(self, player): 377 | player = self.player_ids_rev[player] 378 | self.lock.acquire() 379 | if len(self.pmove[player]): 380 | self.pmove[player].pop() 381 | self.lock.release() 382 | 383 | def kill(self, a, b): 384 | for i in range(self.n): 385 | for j in range(self.m): 386 | if self.owner[i][j] == b: 387 | self.owner[i][j] = a 388 | self.army_cnt[i][j] = (self.army_cnt[i][j] + 1) // 2 389 | if self.grid_type[i][j] == -2: 390 | self.grid_type[i][j] = -1 391 | self.pstat[b - 1] = left_game 392 | self.deadcount += 1 393 | self.deadorder[b - 1] = self.deadcount 394 | self.spec[b - 1] = True 395 | if a > 0 and b > 0: 396 | self.recentkills[self.md5(self.player_ids[b - 1])] = self.names[a - 1] 397 | self.send_system_message(self.names[a - 1] + ' captured ' + self.names[b - 1] + '.') 398 | 399 | def chkmove(self, x, y, dx, dy, p): 400 | return self.chkxy(x, y) and self.chkxy(dx, dy) and abs(x - dx) + abs(y - dy) == 1 and self.owner[x][y] == p + 1 and self.army_cnt[x][y] > 0 and self.grid_type[dx][dy] != 1 401 | 402 | def attack(self, x, y, dx, dy, half): 403 | cnt = self.army_cnt[x][y] - 1 404 | if half: 405 | cnt //= 2 406 | self.army_cnt[x][y] -= cnt 407 | if self.owner[dx][dy] == self.owner[x][y]: 408 | self.army_cnt[dx][dy] += cnt 409 | elif self.owner[dx][dy] > 0 and self.owner[x][y] > 0 and self.team[self.owner[dx][dy] - 1] == self.team[self.owner[x][y] - 1]: 410 | self.army_cnt[dx][dy] += cnt 411 | if self.grid_type[dx][dy] != -2: 412 | self.owner[dx][dy] = self.owner[x][y] 413 | else: 414 | if cnt <= self.army_cnt[dx][dy]: 415 | self.army_cnt[dx][dy] -= cnt 416 | else: 417 | tmp = cnt - self.army_cnt[dx][dy] 418 | if self.grid_type[dx][dy] == -2: 419 | self.kill(self.owner[x][y], self.owner[dx][dy]) 420 | self.grid_type[dx][dy] = -1 421 | self.army_cnt[dx][dy] = tmp 422 | self.owner[dx][dy] = self.owner[x][y] 423 | 424 | def game_tick(self): 425 | self.turn += 1 426 | if self.turn % 2 == 0: 427 | for i in range(self.n): 428 | for j in range(self.m): 429 | if self.grid_type[i][j] < 0 and self.owner[i][j] > 0: 430 | self.army_cnt[i][j] += 1 431 | elif self.grid_type[i][j] == 2 and self.owner[i][j] > 0: 432 | self.army_cnt[i][j] -= 1 433 | if self.army_cnt[i][j] == 0: 434 | self.owner[i][j] = 0 435 | if self.turn % 50 == 0: 436 | for i in range(self.n): 437 | for j in range(self.m): 438 | if self.owner[i][j] > 0: 439 | self.army_cnt[i][j] += 1 440 | for p in range(self.pcnt): 441 | if self.pstat[p]: 442 | self.pstat[p] = min(self.pstat[p] + 1, left_game) 443 | if self.pstat[p] == left_game - 1: 444 | self.kill(0, p + 1) 445 | tmp = range(self.pcnt) 446 | if self.turn % 2 == 1: 447 | tmp = list(reversed(tmp)) 448 | self.lock.acquire() 449 | for p in tmp: 450 | while len(self.pmove[p]): 451 | mv = self.pmove[p].pop(0) 452 | if not self.chkmove(mv[0], mv[1], mv[2], mv[3], p): 453 | continue 454 | self.attack(mv[0], mv[1], mv[2], mv[3], mv[4]) 455 | self.lst_move[p] = (mv[0], mv[1], mv[2], mv[3], mv[4]) 456 | break 457 | self.lock.release() 458 | alive_team = {} 459 | for p in tmp: 460 | if self.pstat[p] != left_game: 461 | alive_team[self.team[p]] = True 462 | stat = len(alive_team) <= 1 463 | self.sendmap(stat) 464 | return stat 465 | 466 | def leave_game(self, sid): 467 | id = self.player_ids_rev[sid] 468 | if self.pstat[id] == 0: 469 | self.pstat[id] = 1 470 | self.watching[id] = False 471 | self.send_system_message(self.names[id] + ' left.') 472 | 473 | def save_history(self): 474 | res = { 475 | 'n': self.n, 476 | 'm': self.m, 477 | 'history': self.history, 478 | } 479 | s = json.dumps(res, separators=(',', ':')) 480 | hs = base64.b64encode(hashlib.sha256(s.encode()).digest()[:9]).decode().replace('/', '-') 481 | open('replays/' + hs + '.json', 'w', encoding='utf-8').write(s) 482 | ranks = [x['uid']for x in sorted(self.history[-1]['leaderboard'], key=lambda x: x['dead'] + x['land'] * 100 + x['army'] * 10000000, reverse=True)] 483 | u = json.dumps({ 484 | 'time': int(time.time()), 485 | 'id': hs, 486 | 'rank': ranks, 487 | 'turn': self.history[-1]['turn'] // 2, 488 | }) + '\n' 489 | open('replays/all.txt', 'a', encoding='utf-8').write(u) 490 | return hs 491 | 492 | def game_loop(self): 493 | eventlet.sleep(max(0.01, self.otime + 2 - time.time())) 494 | lst = time.time() 495 | self.sendmap(False) 496 | while True: 497 | eventlet.sleep(max(0.01, 0.5 / self.speed - time.time() + lst)) 498 | lst = time.time() 499 | if self.game_tick(): 500 | break 501 | res = '' 502 | for p in range(self.pcnt): 503 | if self.pstat[p] != left_game: 504 | if res != '': 505 | res += ',' 506 | res += self.names[p] 507 | print('end game', self.gid) 508 | sys.stdout.flush() 509 | self.send_system_message(res + ' win.') 510 | self.end_game(self.gid) 511 | 512 | def start_game(self, socketio): 513 | socketio.start_background_task(target=self.game_loop) 514 | -------------------------------------------------------------------------------- /replays/all.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcfx/generals.io_copy/948acd220c59ffc476917e776f4b4b9adfc3ee35/replays/all.txt -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | flask_socketio 3 | eventlet -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, redirect, jsonify 2 | from flask.helpers import send_file 3 | from flask_socketio import SocketIO, join_room, leave_room, emit 4 | import time, json, random, string, hashlib 5 | from game import Game 6 | 7 | app = Flask(__name__, static_url_path='') 8 | socketio = SocketIO(app, async_mode='eventlet') 9 | 10 | game_uid = {} 11 | game_instance = {} 12 | 13 | 14 | def md5(x): 15 | if type(x) is bytes: 16 | return hashlib.md5(x).hexdigest() 17 | return hashlib.md5(x.encode('utf-8')).hexdigest() 18 | 19 | 20 | @app.route('/games/') 21 | def enter_room(game_id): 22 | if len(game_id) == 0 or len(game_id) > 15: 23 | return redirect('/games/' + (''.join([chr(random.randint(0, 25) + ord('a'))for i in range(4)]))) 24 | return app.send_static_file('game.html') 25 | 26 | 27 | @app.route('/') 28 | def index(): 29 | return app.send_static_file('index.html') 30 | 31 | 32 | @app.route('/replays') 33 | def replays(): 34 | return app.send_static_file('replays.html') 35 | 36 | 37 | @app.route('/games') 38 | def get_games(): 39 | res = '' 40 | cnt = 0 41 | for x in game_instance: 42 | cnt += 1 43 | res += 'Room%d: ' % cnt + ' '.join(game_instance[x].names) + '
' 44 | return res 45 | 46 | 47 | @app.errorhandler(404) 48 | def enter_random_room(_): 49 | return redirect('/games/' + (''.join([chr(random.randint(0, 25) + ord('a'))for i in range(4)]))) 50 | 51 | 52 | @app.route('/replays/') 53 | def get_replays(hs): 54 | for x in hs: 55 | if x not in string.digits + string.ascii_letters + '+-': 56 | return '' 57 | return app.send_static_file('game.html') 58 | 59 | 60 | @app.route('/api/getreplay/') 61 | def get_replay(hs): 62 | for x in hs: 63 | if x not in string.digits + string.ascii_letters + '+-': 64 | return '' 65 | return send_file('replays/' + hs + '.json') 66 | 67 | 68 | @app.route('/api/replays') 69 | def list_replay(): 70 | u = [] 71 | for x in open('replays/all.txt').readlines(): 72 | if x: 73 | u.append(json.loads(x)) 74 | u.sort(key=lambda x: -x['time']) 75 | return jsonify(u) 76 | 77 | 78 | @socketio.on('connect') 79 | def on_connect(): 80 | join_room('sid_' + request.sid) 81 | emit('set_id', md5(request.sid)) 82 | 83 | 84 | @socketio.on('attack') 85 | def on_attack(data): 86 | if request.sid in game_uid: 87 | game_instance[game_uid[request.sid]].add_move(request.sid, int(data['x']), int(data['y']), int(data['dx']), int(data['dy']), bool(data['half'])) 88 | 89 | 90 | @socketio.on('clear_queue') 91 | def on_clear_queue(): 92 | if request.sid in game_uid: 93 | game_instance[game_uid[request.sid]].clear_queue(request.sid) 94 | 95 | 96 | @socketio.on('pop_queue') 97 | def on_clear_queue(): 98 | if request.sid in game_uid: 99 | game_instance[game_uid[request.sid]].pop_queue(request.sid) 100 | 101 | 102 | def emit_init_map(sid, data): 103 | socketio.emit('init_map', data, room='sid_' + sid) 104 | 105 | 106 | def emit_update(sid, data): 107 | socketio.emit('update', data, room='sid_' + sid) 108 | 109 | 110 | max_teams = 16 111 | gr_val = {} 112 | gr_id = {} 113 | gr_conf = {} 114 | gr_players = {} 115 | 116 | 117 | def join_game_room(sid, uid, gid): 118 | gr_id[sid] = gid 119 | if gid not in gr_conf: 120 | gr_conf[gid] = {'width_ratio': 0.5, 'height_ratio': 0.5, 'city_ratio': 0.5, 'mountain_ratio': 0.5, 'swamp_ratio': 0.5, 'speed': 1, 'custom_map': ''} 121 | gr_players[gid] = [] 122 | tcnt = [0] * (max_teams + 1) 123 | for i in gr_players[gid]: 124 | tcnt[i[2]] += 1 125 | mi = 1e9 126 | mp = 0 127 | for i in range(1, max_teams + 1): 128 | if tcnt[i] < mi: 129 | mi = tcnt[i] 130 | mp = i 131 | gr_players[gid].append([sid, uid, mp, False]) 132 | 133 | 134 | def leave_game_room(sid, gid): 135 | for i in range(len(gr_players[gid])): 136 | if gr_players[gid][i][0] == sid: 137 | t = i 138 | res = gr_players[gid][i][1] 139 | gr_players[gid].pop(t) 140 | return res 141 | 142 | 143 | def get_req_ready(x): 144 | return x - int(x * 0.3) 145 | 146 | 147 | def get_req(x): 148 | cnt = 0 149 | for i in x: 150 | if i[2]: 151 | cnt += 1 152 | return get_req_ready(cnt) 153 | 154 | 155 | def gen_game_conf(gid): 156 | tmp = gr_conf[gid].copy() 157 | pl = [] 158 | cnt = 0 159 | for i in gr_players[gid]: 160 | pl.append({'sid': md5(i[0]), 'uid': i[1], 'team': i[2], 'ready': bool(i[3] and i[2])}) 161 | if i[2] and i[3]: 162 | cnt += 1 163 | need = get_req(gr_players[gid]) 164 | tmp['players'] = pl 165 | tmp['ready'] = cnt 166 | tmp['need'] = need 167 | return tmp 168 | 169 | 170 | def getval(gid): 171 | if gid not in gr_val: 172 | gr_val[gid] = md5(gid.encode('utf-8') + str(time.time()).encode()) 173 | return gr_val[gid] 174 | 175 | 176 | @socketio.on('join_game_room') 177 | def on_join_game_room(data): 178 | ioroom = getval(data['room']) 179 | if request.sid not in gr_id: 180 | join_game_room(request.sid, data['nickname'], data['room']) 181 | join_room('game_' + ioroom) 182 | emit('room_update', gen_game_conf(data['room']), room='game_' + ioroom) 183 | send_system_message(ioroom, data['nickname'] + ' joined the custom lobby.') 184 | 185 | 186 | @socketio.on('change_nickname') 187 | def on_change_nickname(data): 188 | if request.sid in gr_id and len(data['nickname']) < 15 and len(data['nickname']) > 0: 189 | gid = gr_id[request.sid] 190 | ioroom = getval(gid) 191 | for i in gr_players[gid]: 192 | if i[0] == request.sid: 193 | old_name = i[1] 194 | i[1] = data['nickname'] 195 | emit('room_update', gen_game_conf(gid), room='game_' + ioroom) 196 | if old_name != data['nickname']: 197 | send_system_message(ioroom, old_name + ' changed nickname to ' + data['nickname'] + '.') 198 | 199 | 200 | @socketio.on('change_team') 201 | def on_change_team(data): 202 | data['team'] = int(data['team']) 203 | if request.sid in gr_id and data['team'] >= 0 and data['team'] <= max_teams: 204 | gid = gr_id[request.sid] 205 | ioroom = getval(gid) 206 | for i in gr_players[gid]: 207 | if i[0] == request.sid: 208 | i[2] = data['team'] 209 | nickname = i[1] 210 | emit('room_update', gen_game_conf(gid), room='game_' + ioroom) 211 | teamname = 'the spectators' if data['team'] == 0 else 'team ' + str(data['team']) 212 | send_system_message(ioroom, nickname + ' joined ' + teamname + '.') 213 | 214 | 215 | @socketio.on('change_ready') 216 | def on_change_ready(data): 217 | data['ready'] = bool(data['ready']) 218 | if request.sid in gr_id: 219 | gid = gr_id[request.sid] 220 | ioroom = getval(gid) 221 | for i in gr_players[gid]: 222 | if i[0] == request.sid: 223 | i[3] = data['ready'] 224 | chk_ready(gid, ioroom) 225 | 226 | 227 | def chk_ready(gid, ioroom): 228 | rcnt = 0 229 | for i in gr_players[gid]: 230 | if i[3] and i[2]: 231 | rcnt += 1 232 | if rcnt >= get_req(gr_players[gid]) and get_req(gr_players[gid]): 233 | start_game(gid) 234 | else: 235 | emit('room_update', gen_game_conf(gid), room='game_' + ioroom) 236 | 237 | 238 | def chkfloat(x, l, r): 239 | x = float(x) 240 | if x < l or x > r: 241 | raise '' 242 | return x 243 | 244 | 245 | conf_str = {} 246 | conf_str['width_ratio'] = 'Width option' 247 | conf_str['height_ratio'] = 'Height option' 248 | conf_str['city_ratio'] = 'City Density option' 249 | conf_str['mountain_ratio'] = 'Mountain Density option' 250 | conf_str['swamp_ratio'] = 'Swamp Density option' 251 | conf_str['speed'] = 'Game Speed option' 252 | conf_str['custom_map'] = 'Custom Map' 253 | 254 | 255 | def getstr(x): 256 | if type(x) is str: 257 | return x 258 | return str(x) 259 | 260 | 261 | @socketio.on('change_game_conf') 262 | def on_change_game_conf(data): 263 | tmp = {} 264 | tmp['width_ratio'] = chkfloat(data['width_ratio'], 0, 1) 265 | tmp['height_ratio'] = chkfloat(data['height_ratio'], 0, 1) 266 | tmp['city_ratio'] = chkfloat(data['city_ratio'], 0, 1) 267 | tmp['mountain_ratio'] = chkfloat(data['mountain_ratio'], 0, 1) 268 | tmp['swamp_ratio'] = chkfloat(data['swamp_ratio'], 0, 1) 269 | tmp['speed'] = chkfloat(data['speed'], 0.25, 16) 270 | tmp['custom_map'] = data['custom_map'] 271 | if request.sid in gr_id and len(tmp['custom_map']) >= 0 and len(tmp['custom_map']) < 100: 272 | gid = gr_id[request.sid] 273 | ioroom = getval(gid) 274 | mess_q = [] 275 | if gr_players[gid][0][0] == request.sid: 276 | for i in tmp: 277 | if tmp[i] != gr_conf[gid][i]: 278 | mess_q.append(i) 279 | gr_conf[gid] = tmp 280 | emit('room_update', gen_game_conf(gid), room='game_' + ioroom) 281 | for i in mess_q: 282 | send_system_message(ioroom, gr_players[gid][0][1] + ' changed the ' + conf_str[i] + ' to ' + getstr(tmp[i]) + '.') 283 | 284 | 285 | def chk_leave(): 286 | if request.sid in gr_id: 287 | gid = gr_id.pop(request.sid) 288 | ioroom = getval(gid) 289 | leave_room('game_' + ioroom) 290 | uid = leave_game_room(request.sid, gid) 291 | emit('room_update', gen_game_conf(gid), room='game_' + ioroom) 292 | send_system_message(ioroom, uid + ' left the custom lobby.') 293 | chk_ready(gid, ioroom) 294 | elif request.sid in game_uid: 295 | gid = game_uid.pop(request.sid) 296 | leave_room('game_' + gid) 297 | game_instance[gid].leave_game(request.sid) 298 | 299 | 300 | @socketio.on('disconnect') 301 | def on_disconnect(): 302 | leave_room('sid_' + request.sid) 303 | chk_leave() 304 | 305 | 306 | @socketio.on('leave') 307 | def on_leave(): 308 | chk_leave() 309 | socketio.emit('left', {}, room='sid_' + request.sid) 310 | 311 | 312 | def start_game(gid): 313 | grc = gr_conf.pop(gid) 314 | grp = gr_players.pop(gid) 315 | tmp = gid 316 | gid = getval(tmp) 317 | gr_val.pop(tmp) 318 | for i in grp: 319 | gr_id.pop(i[0]) 320 | game_uid[i[0]] = gid 321 | player_sids = [] 322 | player_ids = [] 323 | player_teams = [] 324 | player_names = [] 325 | for i in grp: 326 | player_sids.append(i[0]) 327 | player_ids.append(md5(i[0])) 328 | player_names.append(i[1]) 329 | player_teams.append(i[2]) 330 | grc['player_names'] = player_names 331 | grc['player_teams'] = player_teams 332 | socketio.emit('starting', {}, room='game_' + gid) 333 | game = Game(grc, emit_update, emit_init_map, player_sids, player_ids, chat_message, gid, md5, end_game) 334 | game.start_game(socketio) 335 | game_instance[gid] = game 336 | 337 | 338 | def end_game(gid): 339 | def wait_remove(): 340 | socketio.sleep(1800) 341 | game_instance.pop(gid) 342 | socketio.start_background_task(wait_remove) 343 | 344 | 345 | def send_system_message(gid, text): 346 | chat_message(gid, 'room', '', 0, text) 347 | 348 | 349 | def chat_message(id, tp, sender, color, text, team=False): 350 | if tp == 'room': 351 | id = 'game_' + id 352 | elif tp == 'sid': 353 | id = 'sid_' + id 354 | socketio.emit('chat_message', {'sender': sender, 'color': color, 'text': text, 'team': team}, room=id) 355 | 356 | 357 | @socketio.on('send_message') 358 | def on_send_message(data): 359 | if request.sid in game_uid: 360 | game_instance[game_uid[request.sid]].send_message(request.sid, data) 361 | elif request.sid in gr_id: 362 | gid = gr_id[request.sid] 363 | ioroom = getval(gid) 364 | for i in range(len(gr_players[gid])): 365 | if gr_players[gid][i][0] == request.sid: 366 | color = i + 1 367 | uid = gr_players[gid][i][1] 368 | chat_message(ioroom, 'room', uid, color, data['text']) 369 | 370 | 371 | if __name__ == '__main__': 372 | socketio.run(app, port=23333, host='0.0.0.0') 373 | -------------------------------------------------------------------------------- /static/Quicksand-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcfx/generals.io_copy/948acd220c59ffc476917e776f4b4b9adfc3ee35/static/Quicksand-Bold.otf -------------------------------------------------------------------------------- /static/Quicksand-Light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcfx/generals.io_copy/948acd220c59ffc476917e776f4b4b9adfc3ee35/static/Quicksand-Light.otf -------------------------------------------------------------------------------- /static/Quicksand-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcfx/generals.io_copy/948acd220c59ffc476917e776f4b4b9adfc3ee35/static/Quicksand-Regular.otf -------------------------------------------------------------------------------- /static/city.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcfx/generals.io_copy/948acd220c59ffc476917e776f4b4b9adfc3ee35/static/city.png -------------------------------------------------------------------------------- /static/crown.js: -------------------------------------------------------------------------------- 1 | crown_html = ''; 2 | -------------------------------------------------------------------------------- /static/crown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcfx/generals.io_copy/948acd220c59ffc476917e776f4b4b9adfc3ee35/static/crown.png -------------------------------------------------------------------------------- /static/game.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | generals 14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 122 | 123 | 129 | 130 | 134 | 135 | 149 | 150 | 159 | 160 | 178 | 179 | 180 | 181 | -------------------------------------------------------------------------------- /static/gong.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcfx/generals.io_copy/948acd220c59ffc476917e776f4b4b9adfc3ee35/static/gong.mp3 -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | generals 10 | 11 | 12 | 13 | 18 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /static/main.css: -------------------------------------------------------------------------------- 1 | html,body{background-color:#222} 2 | body{width:100%;height:100%;top:0;left:0;margin:0;padding:0;position:absolute;overflow:hidden} 3 | input{padding:8px;outline:0;color:teal} 4 | button{background-color:white;box-shadow:2px 2px teal;color:teal;font-family:Quicksand;padding:10px 30px;margin:5px;font-size:22px;border:0;border-width:0 !important;outline:none !important;transition:all .2s ease-in-out;white-space:nowrap;border-radius:0} 5 | button:focus{outline:0 !important} 6 | button:hover{cursor:pointer;background-color:#bbb;box-shadow:3px 3px teal} 7 | button.inverted{background-color:teal;box-shadow:2px 2px black;color:white} 8 | button.inverted:hover{background-color:#006e6e;box-shadow:3px 3px black} 9 | @font-face{font-family:Quicksand;src:url('/Quicksand-Regular.otf')} 10 | @font-face{font-family:Quicksand-Light;src:url('/Quicksand-Light.otf')} 11 | @font-face{font-family:Quicksand-Bold;src:url('/Quicksand-Bold.otf')} 12 | *{font-family:Quicksand;color:white} 13 | 14 | .center{position:absolute;top:50%;left:50%;transform:translateY(-50%) translateX(-50%)} 15 | .center-tag,.center-tag *{text-align:center;margin-left:auto;margin-right:auto} 16 | #game-link{white-space:nowrap;margin:0;color:teal;background:white;padding:5px 10px} 17 | .tabs{margin:3px;white-space:nowrap;display:inline-block} 18 | .background{background-color:white;box-shadow:2px 2px teal} 19 | .unselectable{user-select:none} 20 | .inline-button{display:inline-block;text-align:center;font-size:16px;padding:6px;color:black;background-color:transparent;transition:background-color .2s ease-in-out;} 21 | .inline-button.inverted{background-color:teal;color:white} 22 | .inline-button:hover{cursor:pointer;background-color:#bbb} 23 | .inline-button.inverted:hover{background-color:#006e6e} 24 | .tabs div{min-width:35px;font-size:12px} 25 | .custom-host-message{padding:2px;font-size:10px;margin:4px 0} 26 | #username-input{font-size:24px;padding:5px 30px} 27 | 28 | .custom-queue-page-container{background-color:#333;margin-top:0;padding:6px;min-width:600px} 29 | .custom-queue-page-container h3{font-size:12px;margin:0 0 3px 0} 30 | .custom-queue-page-container p{font-size:10px;margin:0} 31 | 32 | .custom-team-container{display:inline-block;margin:5px !important;padding:6px;padding-top:4px;background-color:#222} 33 | .custom-team-container h4{margin:2px;font-family:Quicksand-Bold;border-bottom:1px solid white;padding:2px;font-size:10px} 34 | .custom-team-container p{display:inline;margin:2px;font-size:14px} 35 | .inline-color-block{display:inline-block;width:10px;height:10px;margin:0 2px} 36 | 37 | .slider-container{position:relative;background-color:#222;border:1px solid transparent;padding:4px 18px 18px 18px;width:50%;min-width:300px;border-radius:3px;transition:all .2s ease-in-out} 38 | .center-horizontal{position:absolute;left:50%;transform:translateX(-50%)} 39 | .slider-container .slider-value{bottom:5px;font-weight:600;font-size:14px} 40 | .slider-container .slider-min{position:absolute;left:5px;bottom:5px} 41 | .slider-container .slider-max{position:absolute;right:5px;bottom:5px} 42 | .slider-container input[type='range']{margin:0;background-color:transparent;width:100%} 43 | 44 | #you-are p{font-size:10px} 45 | 46 | #game-leaderboard{position:fixed;top:0;right:0;z-index:25;background:black} 47 | #game-leaderboard:hover{cursor:pointer} 48 | #game-leaderboard tr.dead{opacity:.4} 49 | #game-leaderboard tr.afk{opacity:.7;background-color:red} 50 | #game-leaderboard td{background:white;padding:6px;color:black;text-align:center;font-size:16px} 51 | #game-leaderboard tr.afk td{background:rgba(255,255,255,0.75)} 52 | #game-leaderboard td.leaderboard-name{color:white;max-width:350px} 53 | 54 | #turn-counter{position:fixed;top:0;left:0;padding:5px;font-size:16px;background:white;color:black;border-right:2px solid teal;border-bottom:2px solid teal;text-align:center;min-width:84px;z-index:25} 55 | 56 | #replay-top-left{position:fixed;top:35px;left:-10px;z-index:25} 57 | #replay-turn-jump{color:black;padding:5px 10px 5px 18px} 58 | 59 | .fixed-center-horizontal{position:fixed;left:50%;transform:translateX(-50%)} 60 | #replay-bottom{bottom:0;padding:0;margin:0;z-index:25} 61 | #replay-bottom-bar{display:inline-block;margin:5px;margin-bottom:10px;white-space:nowrap} 62 | #replay-bottom-bar div{color:black;display:inline-block;text-align:center;padding:10px;font-size:18px;transition:.2s ease-in-out;background-color:white} 63 | #replay-bottom-bar div span{color:black;font-family:Quicksand-Bold} 64 | #replay-bottom-bar div:hover{cursor:pointer;background-color:#eee} 65 | 66 | table.list{font-size:18px;text-align:center;border-collapse:collapse;background-color:white} 67 | table.list tr{background-color:white} 68 | table.list th{font-family:Quicksand-Bold;border-bottom:2px solid black;padding:5px 25px;color:black} 69 | table.list.selectable tbody tr{transition:all .2s ease-in-out} 70 | table.list.selectable tbody tr:hover{background-color:#eee;cursor:pointer} 71 | table.list td{border:1px solid #bbb;padding:5px 8px;overflow:hidden;max-width:250px;font-size:20px;color:black} 72 | table.list td *{font-size:14px;color:black} 73 | 74 | .mobile,.mobile td,.mobile input{padding:2px!important;font-size:10px!important} 75 | table.mobile{padding:0!important} 76 | .mobile input{width:60px} 77 | .mobile button{padding:4px 10px!important;font-size:12px!important} 78 | .mobile #replay-turn-jump{padding:0px 0px 0px 10px!important} 79 | .mobile#turn-counter{min-width:60px} 80 | .mobile#replay-top-left{top:20px} 81 | 82 | input[type=range]{-webkit-appearance:none;margin:10px 0;width:100%}input[type=range]:focus{outline:0}input[type=range]::-webkit-slider-runnable-track{width:100%;height:4px;cursor:pointer;animate:.2s;background:#ddd;border-radius:8px}input[type=range]::-webkit-slider-thumb{border:1px solid teal;height:18px;width:12px;border-radius:4px;background:#fff;cursor:pointer;-webkit-appearance:none;margin-top:-7.5px}input[type=range]:focus::-webkit-slider-runnable-track{background:#ddd}input[type=range]::-moz-range-track{width:100%;height:4px;cursor:pointer;animate:.2s;background:#ddd;border-radius:8px}input[type=range]::-moz-range-thumb{border:1px solid teal;height:18px;width:12px;border-radius:4px;background:#fff;cursor:pointer}input[type=range]::-ms-track{width:100%;height:4px;cursor:pointer;animate:.2s;background:transparent;border-color:transparent;color:transparent}input[type=range]::-ms-fill-lower{background:#ddd;border-radius:16px}input[type=range]::-ms-fill-upper{background:#ddd;border-radius:16px}input[type=range]::-ms-thumb{border:1px solid teal;height:18px;width:12px;border-radius:4px;background:#fff;cursor:pointer}input[type=range]:focus::-ms-fill-lower{background:#ddd}input[type=range]:focus::-ms-fill-upper{background:#ddd} 83 | 84 | .s1{width:20px;height:20px;max-width:20px;max-height:20px;min-width:20px;min-height:20px;background-size:16px 16px} 85 | .s1 .txt{font-size:10px;transform:scale(0.8)} 86 | .s2{width:25px;height:25px;max-width:25px;max-height:25px;min-width:25px;min-height:25px;font-size:10px;background-size:20px 20px} 87 | .s3{width:32px;height:32px;max-width:32px;max-height:32px;min-width:32px;min-height:32px;font-size:12px;background-size:25px 25px} 88 | .s4{width:40px;height:40px;max-width:40px;max-height:40px;min-width:40px;min-height:40px;font-size:14px;background-size:32px 32px} 89 | .s5{width:50px;height:50px;max-width:50px;max-height:50px;min-width:50px;min-height:50px;font-size:16px;background-size:40px 40px} 90 | .s6{width:60px;height:60px;max-width:60px;max-height:60px;min-width:60px;min-height:60px;font-size:18px;background-size:50px 50px} 91 | 92 | .c0{background-color:gray!important} 93 | .c1{background-color:red!important;fill:red!important} 94 | .c2{background-color:green!important} 95 | .c3{background-color:blue!important} 96 | .c4{background-color:purple!important} 97 | .c5{background-color:teal!important} 98 | .c6{background-color:#004600!important} 99 | .c7{background-color:orange!important} 100 | .c8{background-color:brown!important} 101 | .c9{background-color:maroon!important} 102 | .c10{background-color:#ec7063!important} 103 | .c11{background-color:#935116!important} 104 | .c12{background-color:#1a5276!important} 105 | .c13{background-color:#2ecc71!important} 106 | .c14{background-color:#641e16!important} 107 | .c15{background-color:#b7950b!important} 108 | .c16{background-color:#ff5733!important} 109 | .c17{background-color:#f321dd!important} 110 | 111 | #map{position:absolute;transform:translateX(-50%) translateY(-50%);z-index:1} 112 | #map table{border-spacing:0px} 113 | #map td{background-position:center;background-repeat:no-repeat;position:relative;border:1px solid black;text-align:center;text-shadow:0 0 2px black;padding:0;overflow:hidden;white-space:nowrap;color:white;user-select:none} 114 | #map_back{position:fixed;left:0;right:0;top:0;bottom:0} 115 | .empty{background-color:#dcdcdc;} 116 | .swamp{background-image:url('/swamp.png');background-color:gray} 117 | .obstacle{background-image:url('/obstacle.png')} 118 | .general{background-image:url('/crown.png')} 119 | .mountain{background-image:url('/mountain.png')} 120 | .city{background-color:gray;background-image:url('/city.png')} 121 | .fog{border:1px solid rgba(255,255,255,0)!important;background-color:rgba(255,255,255,0.1)} 122 | 123 | #chat{position:fixed;bottom:0;left:0;background:rgba(0,0,0,0.5);z-index:25} 124 | .chat-message{margin:5px;text-align:left;font-size:14px;word-wrap:break-word} 125 | .server-chat-message{font-family:Quicksand-Bold} 126 | #chatroom-input{width:100%;border:2px solid transparent} 127 | #chatroom-input:focus{border:2px solid teal} 128 | #chat-messages-container{width:400px;max-height:240px;overflow-y:scroll;cursor:pointer;transition:all .2s ease-in-out} 129 | .minimized{opacity:.5} 130 | #chat-messages-container.minimized{width:280px;max-height:48px} 131 | @media screen and (max-width:1000px){#chat-messages-container{width:320px}} 132 | @media screen and (max-height:600px),screen and (max-width:600px){#chat-messages-container{max-height:180px}.chat-message{font-size:13px}} 133 | @media screen and (max-height:400px),screen and (max-width:375px){#chat-messages-container{max-height:100px}} 134 | .username{font-family:Quicksand-Bold} 135 | 136 | .selectable,.attackable:hover{cursor:pointer} 137 | .selected{border:1px solid white!important} 138 | .selected50{text-shadow:1px 1px black} 139 | .attackable{opacity:.4} 140 | 141 | .arrow_u{position:absolute;left:50%;transform:translateX(-50%);top:0} 142 | .arrow_d{position:absolute;left:50%;transform:translateX(-50%);bottom:0} 143 | .arrow_l{position:absolute;top:50%;transform:translateY(-50%);left:0} 144 | .arrow_r{position:absolute;top:50%;transform:translateY(-50%);right:0} 145 | 146 | .alert{background:white;box-shadow:2px 2px teal;padding:20px;z-index:50;min-width:200px} 147 | .alert h1,.alert p,.alert p *{color:black} 148 | button.small{padding:8px 20px;font-size:18px} -------------------------------------------------------------------------------- /static/main.js: -------------------------------------------------------------------------------- 1 | //map format 2 | //n,m,turn 3 | //grid_type[n][m] byte 0~49=army 50~99=city 100~149=generals 150~199=swamp with army 200=empty 201=mountain 202=fog 203=obstacle 204=swamp 205=swamp+fog 4 | //army_cnt[n][m] int 5 | 6 | $(document).ready(function () { 7 | x = -1, y = -1; 8 | $('body').on('mousedown', function (e) { 9 | x = e.pageX, y = e.pageY; 10 | }); 11 | $('body').on('mousemove', function (e) { 12 | var w, X, Y; 13 | if (typeof (e.originalEvent.buttons) == "undefined") { 14 | w = e.which; 15 | } else { 16 | w = e.originalEvent.buttons; 17 | } 18 | X = e.clientX || e.originalEvent.clientX; 19 | Y = e.clientY || e.originalEvent.clientY; 20 | if (w == 1) { 21 | $('#map').css('left', parseInt($('#map').css('left')) - x + X); 22 | $('#map').css('top', parseInt($('#map').css('top')) - y + Y); 23 | x = e.pageX, y = e.pageY; 24 | } 25 | }); 26 | var touches = [], expected_scale; 27 | function startTouch(s) { 28 | expected_scale = scale_sizes[scale]; 29 | if (s.length <= 2) touches = s; 30 | else touches = []; 31 | } 32 | function handleMove(s) { 33 | var x = touches[0].pageX, y = touches[0].pageY, X = s[0].pageX, Y = s[0].pageY; 34 | $('#map').css('left', parseInt($('#map').css('left')) - x + X); 35 | $('#map').css('top', parseInt($('#map').css('top')) - y + Y); 36 | } 37 | function dis(a, b) { 38 | return Math.sqrt((a.pageX - b.pageX) * (a.pageX - b.pageX) + (a.pageY - b.pageY) * (a.pageY - b.pageY)); 39 | } 40 | function moveTouch(s) { 41 | console.log(s); 42 | if (touches.length == 0) return; 43 | if (touches.length == 1) { 44 | if (s.length == 1) { 45 | handleMove(s); 46 | touches = s; 47 | } else if (s.length == 2) { 48 | var dis1 = dis(touches[0], s[0]), dis2 = dis(touches[0], s[1]); 49 | if (dis1 > dis2) s = [s[1], s[0]]; 50 | handleMove(s); 51 | touches = s; 52 | } else { 53 | touches = []; 54 | } 55 | } else { 56 | if (s.length == 1) { 57 | var dis1 = dis(touches[0], s[0]), dis2 = dis(touches[1], s[0]); 58 | if (dis1 > dis2) touches = [touches[1], touches[0]]; 59 | handleMove(s); 60 | touches = s; 61 | } else if (s.length == 2) { 62 | var x = (touches[0].pageX + touches[1].pageX) / 2, y = (touches[0].pageY + touches[1].pageY) / 2; 63 | var X = (s[0].pageX + s[1].pageX) / 2, Y = (s[0].pageY + s[1].pageY) / 2; 64 | $('#map').css('left', parseInt($('#map').css('left')) - x + X); 65 | $('#map').css('top', parseInt($('#map').css('top')) - y + Y); 66 | var dis1 = dis(touches[0], touches[1]), dis2 = dis(s[0], s[1]); 67 | expected_scale *= dis2 / dis1; 68 | if (expected_scale.toString().toLowerCase().indexOf('n') != -1) { 69 | expected_scale = scale_sizes[scale]; 70 | } else { 71 | var pos, mi = 200; 72 | for (var i = 1; i < scale_sizes.length; i++) { 73 | var t = Math.abs(scale_sizes[i] - expected_scale); 74 | if (t < mi) mi = t, pos = i; 75 | } 76 | if (pos != scale) { 77 | scale = pos; 78 | if (typeof (localStorage) != "undefined") { 79 | localStorage.scale = scale.toString(); 80 | } 81 | render(); 82 | } 83 | } 84 | touches = s; 85 | } else { 86 | touches = []; 87 | } 88 | } 89 | } 90 | function endTouch() { 91 | touches = []; 92 | } 93 | function bindTouch(obj) { 94 | obj.addEventListener('touchstart', function (e) { 95 | if (!in_game) return; 96 | startTouch(e.targetTouches); 97 | }, false); 98 | obj.addEventListener('touchmove', function (e) { 99 | if (!in_game) return; 100 | moveTouch(e.targetTouches); 101 | }, false); 102 | obj.addEventListener('touchend', function (e) { 103 | if (!in_game) return; 104 | moveTouch(e.targetTouches); 105 | endTouch(); 106 | }, false); 107 | } 108 | bindTouch(document); 109 | 110 | if (window.innerWidth <= 1000) { 111 | // shoule be mobile 112 | $('#turn-counter').attr('class', 'mobile'); 113 | $('#game-leaderboard').attr('class', 'mobile'); 114 | $('#replay-top-left').attr('class', 'mobile'); 115 | } 116 | }); 117 | 118 | function htmlescape(x) { 119 | return $('
').text(x).html(); 120 | } 121 | 122 | const dire = [{ x: -1, y: 0 }, { x: 1, y: 0 }, { x: 0, y: -1 }, { x: 0, y: 1 }]; 123 | const dire_char = ['↑', '↓', '←', '→']; 124 | const dire_class = ['arrow_u', 'arrow_d', 'arrow_l', 'arrow_r']; 125 | 126 | const scale_sizes = [0, 20, 25, 32, 40, 50, 60]; 127 | 128 | var n, m, turn, player, scale, selx, sely, selt, in_game = false; 129 | var grid_type, army_cnt, have_route = Array(4); 130 | var route; 131 | 132 | var room_id = '', client_id, ready_state = 0, lost; 133 | var max_teams = 16; 134 | 135 | var chat_focus = false, is_team = false, starting_audio; 136 | 137 | var is_replay = false, replay_id = false, replay_data = [], rcnt = 0, cur_turn = 0, is_autoplaying = false, autoplay_speed = 1; 138 | 139 | if (location.pathname.substr(0, 8) == '/replays') { 140 | is_replay = true; 141 | replay_id = location.pathname.substr(9); 142 | $.get('/api/getreplay/' + replay_id, function (data) { 143 | replay_data = data; 144 | replayStart(); 145 | }); 146 | } 147 | 148 | function replayStart() { 149 | rcnt++; 150 | if (rcnt == 2) { 151 | init_map(replay_data.n, replay_data.m); 152 | in_game = true; 153 | update(replay_data.history[0]); 154 | } 155 | } 156 | 157 | function init_map(_n, _m, general) { 158 | chat_focus = false; 159 | $('#chatroom-input').blur(); 160 | n = _n, m = _m; 161 | grid_type = Array(n); 162 | for (var i = 0; i < n; i++) { 163 | grid_type[i] = Array(m); 164 | } 165 | army_cnt = Array(n); 166 | for (var i = 0; i < n; i++) { 167 | army_cnt[i] = Array(m); 168 | } 169 | for (var d = 0; d < 4; d++) { 170 | have_route[d] = Array(n); 171 | for (var i = 0; i < n; i++) { 172 | have_route[d][i] = Array(m); 173 | } 174 | } 175 | route = Array(); 176 | selx = -1, sely = -1; 177 | 178 | var ts = ""; 179 | for (var i = 0; i < n; i++) { 180 | ts += ''; 181 | for (var j = 0; j < m; j++) { 182 | ts += ''; 183 | } 184 | ts += ''; 185 | } 186 | $('#map').html('' + ts + '
'); 187 | 188 | if (!general || general[0] == -1) { 189 | general = [n / 2 - 0.5, m / 2 - 0.5]; 190 | } 191 | $('#map').css('left', $(document).width() / 2 + (m / 2 - general[1] - 0.5) * scale_sizes[scale] + 'px'); 192 | $('#map').css('top', $(document).height() / 2 + (n / 2 - general[0] - 0.5) * scale_sizes[scale] + 'px'); 193 | for (var i = 0; i < n; i++) { 194 | for (var j = 0; j < m; j++) { 195 | $('#t' + i + '_' + j).on('click', Function("click(" + i + "," + j + ")")); 196 | } 197 | } 198 | } 199 | 200 | function click(x, y, q) { 201 | if (typeof (q) == "undefined") q = true; 202 | if (x < 0 || y < 0 || x >= n || y >= m) return; 203 | if (x == selx && y == sely) { 204 | if (selt == 1) { 205 | selt = 2; 206 | } else { 207 | selx = sely = -1; 208 | } 209 | } else if (Math.abs(x - selx) + Math.abs(y - sely) == 1 && grid_type[x][y] != 201) { 210 | var d = 0; 211 | for (; selx + dire[d].x != x || sely + dire[d].y != y; d++); 212 | addroute(selx, sely, d, selt); 213 | selx = x, sely = y, selt = 1; 214 | } else if (grid_type[x][y] < 200 && grid_type[x][y] % 50 == player) { 215 | selx = x, sely = y, selt = 1; 216 | } else if (q) { 217 | selx = -1, sely = -1; 218 | } 219 | render(); 220 | } 221 | 222 | function keypress(key) { 223 | if (in_game && is_replay) { 224 | if (key == 'a' || key == 37) { 225 | backTurn(); 226 | } else if (key == 'd' || key == 39) { 227 | nextTurn(); 228 | } else if (key == ' ') { 229 | switchAutoplay(); 230 | } 231 | } 232 | else if (in_game) { 233 | if (key == 'z') { 234 | selt = 3 - selt; 235 | render(); 236 | } else if (key == 'w' || key == 38) { 237 | click(selx - 1, sely, false); 238 | } else if (key == 's' || key == 40) { 239 | click(selx + 1, sely, false); 240 | } else if (key == 'a' || key == 37) { 241 | click(selx, sely - 1, false); 242 | } else if (key == 'd' || key == 39) { 243 | click(selx, sely + 1, false); 244 | } else if (key == 'q') { 245 | clear_queue(); 246 | } else if (key == 'e') { 247 | pop_queue(); 248 | } else if (key == 't') { 249 | if (!chat_focus) { 250 | is_team = true; 251 | setTimeout(function () { 252 | $('#chatroom-input').focus(); 253 | checkChat(); 254 | }, 0); 255 | } 256 | } else if (key == 13) { 257 | if (!chat_focus) { 258 | is_team = false; 259 | setTimeout(function () { 260 | $('#chatroom-input').focus(); 261 | checkChat(); 262 | }, 0); 263 | } 264 | } else if (key == ' ') { 265 | selx = -1, sely = -1; 266 | render(); 267 | } 268 | } 269 | } 270 | 271 | $(document).ready(function () { 272 | $('body').on('keypress', function (e) { 273 | keypress(e.key.toLowerCase()); 274 | }); 275 | $('body').on('keydown', function (e) { 276 | keypress(e.keyCode); 277 | }); 278 | $('#map_back').on('click', function (e) { 279 | selx = -1, sely = -1; 280 | render(); 281 | }); 282 | $('body').bind('mousewheel', function (e) { 283 | if (in_game) { 284 | if (e.originalEvent.deltaY > 0) { 285 | scale = Math.max(scale - 1, 1); 286 | } else { 287 | scale = Math.min(scale + 1, 6); 288 | } 289 | if (typeof (localStorage) != "undefined") { 290 | localStorage.scale = scale.toString(); 291 | } 292 | render(); 293 | } 294 | }) 295 | if (typeof (localStorage) != "undefined") { 296 | if (typeof (localStorage.scale) == "undefined") { 297 | localStorage.scale = '3'; 298 | } 299 | scale = parseInt(localStorage.scale); 300 | } 301 | }); 302 | 303 | function render() { 304 | $('#menu').css('display', 'none'); 305 | $('#game-starting').css('display', 'none'); 306 | $('#game').css('display', ''); 307 | for (var d = 0; d < 4; d++) { 308 | for (var i = 0; i < n; i++) { 309 | for (var j = 0; j < m; j++) { 310 | have_route[d][i][j] = false; 311 | } 312 | } 313 | } 314 | for (var i = 0; i < route.length; i++) { 315 | have_route[route[i].d][route[i].x][route[i].y] = true; 316 | } 317 | for (var i = 0; i < n; i++) { 318 | for (var j = 0; j < m; j++) { 319 | var cls = 's' + scale, txt = ''; 320 | if (grid_type[i][j] < 200) { 321 | if (grid_type[i][j] < 50) { 322 | cls += ' c' + grid_type[i][j]; 323 | } else if (grid_type[i][j] < 100) { 324 | cls += ' c' + (grid_type[i][j] - 50) + ' city'; 325 | } else if (grid_type[i][j] < 150) { 326 | cls += ' c' + (grid_type[i][j] - 100) + ' general'; 327 | } else if (grid_type[i][j] < 200) { 328 | cls += ' c' + (grid_type[i][j] - 150) + ' swamp'; 329 | } 330 | if (grid_type[i][j] % 50 == player) { 331 | cls += ' selectable'; 332 | } 333 | if (army_cnt[i][j] || grid_type[i][j] == 50) txt = army_cnt[i][j]; 334 | } else if (grid_type[i][j] == 200) { 335 | cls += ' empty'; 336 | } else if (grid_type[i][j] == 201) { 337 | cls += ' mountain empty'; 338 | } else if (grid_type[i][j] == 202) { 339 | cls += ' fog'; 340 | } else if (grid_type[i][j] == 203) { 341 | cls += ' obstacle fog'; 342 | } else if (grid_type[i][j] == 204) { 343 | cls += ' swamp'; 344 | } else if (grid_type[i][j] == 205) { 345 | cls += ' swamp fog'; 346 | } 347 | if (i == selx && j == sely) { 348 | if (selt == 1) { 349 | cls += ' selected'; 350 | } else { 351 | cls += ' selected selected50'; 352 | txt = '50%'; 353 | } 354 | } else if (Math.abs(i - selx) + Math.abs(j - sely) == 1 && grid_type[i][j] != 201) { 355 | cls += ' attackable'; 356 | } 357 | if (txt != '' && scale == 1) txt = '
' + txt + '
'; 358 | for (var d = 0; d < 4; d++)if (have_route[d][i][j]) { 359 | if (scale > 1) txt += '
' + dire_char[d] + '
'; 360 | else txt += '
' + dire_char[d] + '
'; 361 | } 362 | if ($('#t' + i + '_' + j).attr('class') != cls) { 363 | $('#t' + i + '_' + j).attr('class', cls); 364 | } 365 | if ($('#t' + i + '_' + j).html() != txt) { 366 | $('#t' + i + '_' + j).html(txt); 367 | } 368 | } 369 | } 370 | } 371 | 372 | if (!is_replay) { 373 | var socket = io.connect(location.origin, { transports: ['websocket', 'polling'] }); 374 | } else { 375 | function socket() { } 376 | socket.on = function () { } 377 | } 378 | 379 | function update(data) { 380 | if (typeof (data.replay) != "undefined") replay_id = data.replay; 381 | if (data.is_diff) { 382 | for (var i = 0; i * 2 < data.grid_type.length; i++) { 383 | var t = data.grid_type[i * 2]; 384 | grid_type[parseInt(t / m)][t % m] = data.grid_type[i * 2 + 1]; 385 | } 386 | for (var i = 0; i * 2 < data.army_cnt.length; i++) { 387 | var t = data.army_cnt[i * 2]; 388 | army_cnt[parseInt(t / m)][t % m] = data.army_cnt[i * 2 + 1]; 389 | } 390 | } else { 391 | for (var i = 0, t = 0; i < n; i++) { 392 | for (var j = 0; j < m; j++) { 393 | grid_type[i][j] = data.grid_type[t++]; 394 | } 395 | } 396 | for (var i = 0, t = 0; i < n; i++) { 397 | for (var j = 0; j < m; j++) { 398 | army_cnt[i][j] = data.army_cnt[t++]; 399 | } 400 | } 401 | } 402 | if (route.length) { 403 | if (data.lst_move.x != -1) { 404 | while (route.length) { 405 | var t1 = data.lst_move, t2 = { x: route[0].x, y: route[0].y, dx: route[0].x + dire[route[0].d].x, dy: route[0].y + dire[route[0].d].y, half: route[0].type == 2 }; 406 | route = route.splice(1); 407 | if (t1.x == t2.x && t1.y == t2.y && t1.dx == t2.dx && t1.dy == t2.dy && t1.half == t2.half) break; 408 | } 409 | } else { 410 | while (route.length) { 411 | var x = route[0].x, y = route[0].y, dx = route[0].x + dire[route[0].d].x, dy = route[0].y + dire[route[0].d].y; 412 | if (grid_type[x][y] < 200 && grid_type[x][y] % 50 == player && army_cnt[x][y] > 1 && grid_type[dx][dy] != 201) break; 413 | route = route.splice(1); 414 | } 415 | } 416 | } 417 | render(); 418 | lb = data.leaderboard.sort(function (a, b) { 419 | if (a.army != b.army) return a.army > b.army ? -1 : 1; 420 | if (a.land != b.land) return a.land > b.land ? -1 : 1; 421 | if (a.class_ == 'dead') return a.dead > b.dead ? -1 : 1; 422 | return 0; 423 | }) 424 | var th = 'TeamPlayerArmyLand'; 425 | for (var i = 0; i < lb.length; i++) { 426 | th += '' + lb[i].team + '' + htmlescape(lb[i].uid) + '' + lb[i].army + '' + lb[i].land + ''; 427 | } 428 | $('#game-leaderboard').html(th); 429 | $('#game-leaderboard').css('display', ''); 430 | $('#turn-counter').html('Turn ' + Math.floor(data.turn / 2) + (data.turn % 2 == 1 ? '.' : '')); 431 | $('#turn-counter').css('display', ''); 432 | if (is_replay) return; 433 | if (typeof (data.kills[client_id]) != 'undefined') { 434 | $($('#status-alert').children()[0].children[0]).html('Game Over'); 435 | $($('#status-alert').children()[0].children[1]).html('You were defeated by ' + htmlescape(data.kills[client_id]) + '.'); 436 | $($('#status-alert').children()[0].children[1]).css('display', ''); 437 | $($('#status-alert').children()[0].children[2]).css('display', ''); 438 | $('#status-alert').css('display', ''); 439 | lost = true; 440 | } 441 | if (data.game_end) { 442 | if ($('#status-alert').css('display') == 'none') { 443 | if (lost) { 444 | $($('#status-alert').children()[0].children[0]).html('Game Ended'); 445 | } else { 446 | $($('#status-alert').children()[0].children[0]).html('You Win'); 447 | } 448 | $($('#status-alert').children()[0].children[1]).css('display', 'none'); 449 | } 450 | $('#status-alert').css('display', ''); 451 | $($('#status-alert').children()[0].children[2]).css('display', 'none'); 452 | if (replay_id) $($('#status-alert').children()[0].children[6]).css('display', ''); 453 | } 454 | } 455 | 456 | socket.on('update', update); 457 | 458 | socket.on('starting', function () { 459 | $('#menu').css('display', 'none'); 460 | $('#game-starting').css('display', ''); 461 | starting_audio.play(); 462 | }); 463 | 464 | function addroute(x, y, d, type) { 465 | route.push({ x: x, y: y, d: d, type: type }); 466 | socket.emit('attack', { x: x, y: y, dx: x + dire[d].x, dy: y + dire[d].y, half: type == 2 }); 467 | render(); 468 | } 469 | 470 | function clear_queue() { 471 | route = Array() 472 | socket.emit('clear_queue'); 473 | render(); 474 | } 475 | 476 | function pop_queue() { 477 | if (route.length) { 478 | var tmp = route.pop(); 479 | socket.emit('pop_queue'); 480 | if (tmp.x + dire[tmp.d].x == selx && tmp.y + dire[tmp.d].y == sely) { 481 | selx = tmp.x, sely = tmp.y; 482 | } 483 | render(); 484 | } 485 | } 486 | 487 | socket.on('set_id', function (data) { 488 | client_id = data; 489 | }); 490 | 491 | socket.on('init_map', function (data) { 492 | init_map(data.n, data.m, data.general); 493 | in_game = true; 494 | lost = false; 495 | console.log(data); 496 | for (var i = 0; i < data.player_ids.length; i++) { 497 | if (data.player_ids[i] == client_id) { 498 | player = i + 1; 499 | } 500 | } 501 | }); 502 | 503 | function backTurn() { 504 | if (is_autoplaying) switchAutoplay(); 505 | cur_turn = Math.max(0, cur_turn - 20); 506 | update(replay_data.history[cur_turn]); 507 | } 508 | 509 | function nextTurn(ignore = false) { 510 | if (is_autoplaying && !ignore) return; 511 | cur_turn = Math.min(replay_data.history.length - 1, cur_turn + 1); 512 | update(replay_data.history[cur_turn]); 513 | } 514 | 515 | function jumpToTurn() { 516 | if (is_autoplaying) switchAutoplay(); 517 | var uturn = $('#replay-turn-jump-input').val(), turn = 0; 518 | if (uturn[uturn.length - 1] == '.') turn = parseInt(uturn.substr(0, uturn.length - 1)) * 2 + 1; 519 | else turn = parseInt(uturn) * 2; 520 | for (var i = 0; i < replay_data.history.length; i++) { 521 | if (replay_data.history[i].turn == turn) { 522 | cur_turn = i; 523 | update(replay_data.history[cur_turn]); 524 | break; 525 | } 526 | } 527 | } 528 | 529 | function switchAutoplay() { 530 | is_autoplaying = !is_autoplaying; 531 | if (!is_autoplaying) { 532 | $($('#replay-top-left')[0].children[1]).attr('class', 'small'); 533 | $('#tabs-replay-autoplay').css('display', 'none'); 534 | return; 535 | } 536 | $($('#replay-top-left')[0].children[1]).attr('class', 'small inverted'); 537 | $('#tabs-replay-autoplay').css('display', 'inline-block'); 538 | setTimeout(autoplay, 500 / autoplay_speed); 539 | } 540 | 541 | function autoplay() { 542 | if (!is_autoplaying) return; 543 | nextTurn(true); 544 | setTimeout(autoplay, 500 / autoplay_speed); 545 | } 546 | 547 | function setAutoplayRate() { 548 | var tmp = $($('#tabs-replay-autoplay')[0].children[0]).val(); 549 | autoplay_speed = parseFloat(tmp.substr(0, tmp.length - 1)); 550 | } 551 | 552 | function _exit() { 553 | location.href = '/'; 554 | } 555 | 556 | $(document).ready(function () { 557 | if (is_replay) { 558 | $('#replay-top-left').css('display', ''); 559 | $('#replay-bottom').css('display', ''); 560 | $('#replay-turn-jump-input').on('keypress', function (e) { 561 | if (e.charCode == 10 || e.charCode == 13) jumpToTurn(); 562 | }); 563 | $('#replay-turn-jump-button').on('click', jumpToTurn); 564 | $($('#replay-bottom-bar')[0].children[0]).on('click', backTurn); 565 | $($('#replay-bottom-bar')[0].children[1]).on('click', switchAutoplay); 566 | $($('#replay-bottom-bar')[0].children[2]).on('click', nextTurn); 567 | $($('#replay-top-left')[0].children[1]).on('click', switchAutoplay); 568 | $($('#replay-top-left')[0].children[2]).on('click', _exit); 569 | $('#tabs-replay-autoplay').each(function () { 570 | for (var i = 1; i < this.children.length; i++) { 571 | initTab(this, this.children[i], setAutoplayRate); 572 | } 573 | }); 574 | replayStart(); 575 | return; 576 | } 577 | $('#chat').css('display', ''); 578 | $('#menu').css('display', ''); 579 | if (typeof (localStorage) != "undefined") { 580 | if (typeof (localStorage.username) == "undefined") { 581 | localStorage.username = 'Anonymous'; 582 | } 583 | nickname = localStorage.username; 584 | } else { 585 | nickname = 'Anonymous'; 586 | } 587 | var tmp = location.pathname; 588 | room_id = tmp.substr(tmp.indexOf('games/') + 6); 589 | starting_audio = new Audio('/gong.mp3'); 590 | socket.emit('join_game_room', { 'room': room_id, 'nickname': nickname }); 591 | }); 592 | 593 | socket.on('connect', function () { 594 | if (room_id != '') { 595 | socket.emit('join_game_room', { 'room': room_id, 'nickname': nickname }); 596 | } 597 | }); 598 | 599 | socket.on('room_update', function (data) { 600 | setRangeVal('map-height', data.height_ratio); 601 | setRangeVal('map-width', data.width_ratio); 602 | setRangeVal('city-density', data.city_ratio); 603 | setRangeVal('mountain-density', data.mountain_ratio); 604 | setRangeVal('swamp-density', data.swamp_ratio); 605 | setTabVal('game-speed', data.speed + 'x'); 606 | $('#custom-map').val(data.custom_map); 607 | var tmp = Array(max_teams + 1); 608 | for (var i = 0; i <= max_teams; i++) { 609 | tmp[i] = ''; 610 | } 611 | var isHost = data.players[0].sid == client_id; 612 | setRangeDisable('map-height', !isHost); 613 | setRangeDisable('map-width', !isHost); 614 | setRangeDisable('city-density', !isHost); 615 | setRangeDisable('mountain-density', !isHost); 616 | setRangeDisable('swamp-density', !isHost); 617 | if (isHost) $('#custom-map').removeAttr('disabled'); 618 | else $('#custom-map').attr('disabled', ''); 619 | $('#host-' + (isHost).toString()).css('display', ''); 620 | $('#host-' + (!isHost).toString()).css('display', 'none'); 621 | for (var i = 0; i < data.players.length; i++) { 622 | if (data.players[i].sid == client_id) { 623 | setTabVal('custom-team', data.players[i].team ? data.players[i].team.toString() : 'Spectator'); 624 | if (data.players[i].team) { 625 | $('#you-are').css('display', ''); 626 | $('#you-are-2').css('display', ''); 627 | $($('#you-are')[0].children[1]).attr('class', 'inline-color-block c' + (i + 1)); 628 | $($('#you-are-2')[0].children[1]).attr('class', 'inline-color-block c' + (i + 1)); 629 | } else { 630 | $('#you-are').css('display', 'none'); 631 | $('#you-are-2').css('display', 'none'); 632 | } 633 | if (data.players[i].uid == 'Anonymous') { 634 | $('#username-input').val(''); 635 | } else { 636 | $('#username-input').val(data.players[i].uid); 637 | } 638 | } 639 | tmp[data.players[i].team] += '
'; 640 | if (data.players[i].team) { 641 | if (i == 0) { 642 | tmp[data.players[i].team] += '' + crown_html + ''; 643 | } else { 644 | tmp[data.players[i].team] += ''; 645 | } 646 | } 647 | tmp[data.players[i].team] += '

'; 648 | if (data.players[i].ready) tmp[data.players[i].team] += ''; 649 | if (i == 0) tmp[data.players[i].team] += ''; 650 | tmp[data.players[i].team] += htmlescape(data.players[i].uid); 651 | if (i == 0) tmp[data.players[i].team] += ''; 652 | if (data.players[i].ready) tmp[data.players[i].team] += ''; 653 | tmp[data.players[i].team] += '

'; 654 | tmp[data.players[i].team] += '
'; 655 | } 656 | for (var i = 0; i <= max_teams; i++) { 657 | if (tmp[i] != '') { 658 | tmp[i] = '

' + (i ? 'Team ' + i : 'Spectators') + '

' + tmp[i] + '
'; 659 | } 660 | } 661 | var res_html = ''; 662 | for (var i = 1; i <= max_teams; i++) { 663 | res_html += tmp[i]; 664 | } 665 | res_html += tmp[0]; 666 | $('#teams').html(res_html); 667 | if (data.need > 1) { 668 | $('#force-start').css('display', 'block'); 669 | $('#force-start').html('Force Start ' + data.ready + ' / ' + data.need); 670 | } else { 671 | $('#force-start').css('display', 'none'); 672 | } 673 | if (ready_state) { 674 | $('#force-start').attr('class', 'inverted'); 675 | } else { 676 | $('#force-start').attr('class', ''); 677 | } 678 | }); 679 | 680 | function getConf() { 681 | var data = {}; 682 | data.height_ratio = getRangeVal('map-height'); 683 | data.width_ratio = getRangeVal('map-width'); 684 | data.city_ratio = getRangeVal('city-density'); 685 | data.mountain_ratio = getRangeVal('mountain-density'); 686 | data.swamp_ratio = getRangeVal('swamp-density'); 687 | data.speed = parseFloat(getTabVal('game-speed')); 688 | data.custom_map = $('#custom-map').val(); 689 | return data; 690 | } 691 | 692 | function updateConf() { 693 | socket.emit('change_game_conf', getConf()); 694 | } 695 | 696 | const delayUpdateConf = _.debounce(updateConf, 300); 697 | 698 | function updateTeam() { 699 | var team = getTabVal('custom-team'); 700 | if (team == 'Spectator') team = 0; 701 | socket.emit('change_team', { team: team }); 702 | } 703 | 704 | function getRangeVal(x) { 705 | return $($('#' + x)[0].children[0]).val(); 706 | } 707 | 708 | function setRangeVal(x, y) { 709 | $($('#' + x)[0].children[0]).val(y); 710 | $($('#' + x)[0].children[1]).html($($('#' + x)[0].children[0]).val()); 711 | } 712 | 713 | function setRangeDisable(x, y) { 714 | if (y) $($('#' + x)[0].children[0]).attr('disabled', ''); 715 | else $($('#' + x)[0].children[0]).removeAttr('disabled'); 716 | } 717 | 718 | function initRange(x) { 719 | $(x.children[0]).on('change', function () { 720 | $(x.children[1]).html($(x.children[0]).val()) 721 | delayUpdateConf(); 722 | }); 723 | $(x.children[0]).on('input', function () { 724 | $(x.children[1]).html($(x.children[0]).val()); 725 | delayUpdateConf(); 726 | }); 727 | } 728 | 729 | function getTabVal(x) { 730 | return $($('#tabs-' + x)[0].children[0]).val(); 731 | } 732 | 733 | function setTabVal(x, y) { 734 | var tmp = getTabVal(x), tabs = $('#tabs-' + x)[0].children; 735 | for (var i = 1; i < tabs.length; i++) { 736 | if ($(tabs[i]).html() == tmp) { 737 | $(tabs[i]).attr('class', 'inline-button'); 738 | } 739 | if ($(tabs[i]).html() == y) { 740 | $(tabs[i]).attr('class', 'inline-button inverted'); 741 | } 742 | } 743 | $($('#tabs-' + x)[0].children[0]).val(y); 744 | } 745 | 746 | function initTab(x, y, callback) { 747 | $(y).on('click', function () { 748 | setTabVal($(x).attr('id').substr(5), $(y).html()); 749 | callback(); 750 | }); 751 | } 752 | 753 | $(document).ready(function () { 754 | $('.slider-container').each(function () { initRange(this) }); 755 | $('#tabs-game-speed').each(function () { 756 | for (var i = 1; i < this.children.length; i++) { 757 | initTab(this, this.children[i], updateConf); 758 | } 759 | }); 760 | $('#tabs-custom-team').each(function () { 761 | for (var i = 1; i < this.children.length; i++) { 762 | initTab(this, this.children[i], updateTeam); 763 | } 764 | }); 765 | $('#force-start').on('click', function () { 766 | ready_state ^= 1; 767 | socket.emit('change_ready', { ready: ready_state }); 768 | }); 769 | function changeUsername() { 770 | var tmp = $('#username-input').val(); 771 | if (tmp == '') tmp = 'Anonymous'; 772 | socket.emit('change_nickname', { nickname: tmp }); 773 | if (typeof (localStorage) != "undefined") { 774 | localStorage.username = tmp; 775 | } 776 | } 777 | $('#username-input').on('change', _.debounce(changeUsername, 300)); 778 | $('#username-input').on('input', _.debounce(changeUsername, 300)); 779 | $('#custom-map').on('change', delayUpdateConf); 780 | $('#custom-map').on('input', delayUpdateConf); 781 | }); 782 | 783 | var chatStr = ''; 784 | 785 | function checkChat() { 786 | var tmp = $('#chatroom-input').val(), res; 787 | if (is_team) { 788 | if (tmp.substr(0, 7) == '[team] ') { 789 | res = tmp.substr(7); 790 | } else { 791 | res = chatStr; 792 | } 793 | } else { 794 | if (tmp.substr(0, 7) == '[team] ') { 795 | res = tmp.substr(7); 796 | } else { 797 | res = tmp; 798 | } 799 | } 800 | chatStr = res; 801 | $('#chatroom-input').val((is_team ? '[team] ' : '') + res); 802 | } 803 | 804 | socket.on('left', function () { 805 | var data = getConf(); 806 | if (typeof (localStorage) != "undefined") { 807 | if (typeof (localStorage.username) == "undefined") { 808 | localStorage.username = 'Anonymous'; 809 | } 810 | nickname = localStorage.username; 811 | } else { 812 | nickname = 'Anonymous'; 813 | } 814 | socket.emit('join_game_room', { 'room': room_id, 'nickname': nickname }); 815 | socket.emit('change_game_conf', data); 816 | $('#menu').css('display', ''); 817 | $('#game').css('display', 'none'); 818 | $('#game-leaderboard').css('display', 'none'); 819 | $('#turn-counter').css('display', 'none'); 820 | $('#chat-messages-container').html(''); 821 | $('#status-alert').css('display', 'none'); 822 | ready_state = 0; 823 | in_game = false; 824 | replay_id = false; 825 | }); 826 | 827 | $(document).ready(function () { 828 | var shown = true; 829 | $('#chat-messages-container').on('click', function () { 830 | $('#chat-messages-container').attr('class', shown ? 'minimized' : ''); 831 | $('#chatroom-input').attr('class', shown ? 'minimized' : ''); 832 | shown = !shown; 833 | }); 834 | socket.on('chat_message', function (data) { 835 | var th = ''; 836 | if (data.color) { 837 | th = '' + htmlescape(data.sender) + ': ' + htmlescape(data.text) + '

'; 838 | if (data.team) { 839 | th = '[team] ' + th; 840 | } 841 | th = '

' + th; 842 | } else { 843 | th = '

' + htmlescape(data.text) + '

' 844 | } 845 | $('#chat-messages-container')[0].innerHTML += th; 846 | $('#chat-messages-container').scrollTop(233333); 847 | }); 848 | $('#chatroom-input').on('keypress', function (data) { 849 | if (data.keyCode == 13) { 850 | console.log('b'); 851 | socket.emit('send_message', { text: chatStr, team: is_team }); 852 | chatStr = '', is_team = false; 853 | $('#chatroom-input').val(''); 854 | } 855 | }); 856 | $('#chatroom-input').focus(function () { 857 | chat_focus = true; 858 | }); 859 | $('#chatroom-input').blur(function () { 860 | chat_focus = false; 861 | is_team = false; 862 | checkChat(); 863 | }); 864 | $('#chatroom-input').on('change', checkChat); 865 | $('#chatroom-input').on('input', checkChat); 866 | $($('#status-alert').children()[0].children[2]).on('click', function (e) { 867 | $('#status-alert').css('display', 'none'); 868 | }); 869 | $($('#status-alert').children()[0].children[4]).on('click', function (e) { 870 | socket.emit('leave'); 871 | }); 872 | $($('#status-alert').children()[0].children[6]).on('click', function (e) { 873 | window.open('/replays/' + replay_id, '_blank'); 874 | }); 875 | $($('#status-alert').children()[0].children[8]).on('click', _exit); 876 | }); 877 | -------------------------------------------------------------------------------- /static/mountain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcfx/generals.io_copy/948acd220c59ffc476917e776f4b4b9adfc3ee35/static/mountain.png -------------------------------------------------------------------------------- /static/obstacle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcfx/generals.io_copy/948acd220c59ffc476917e776f4b4b9adfc3ee35/static/obstacle.png -------------------------------------------------------------------------------- /static/replays.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | generals 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 32 | 33 | 34 |
TimeTurnsResult
{{ getTime(replay.time) }}{{ replay.turn }} 28 |
29 | {{ name }} 30 |
31 |
35 |
36 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /static/swamp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcfx/generals.io_copy/948acd220c59ffc476917e776f4b4b9adfc3ee35/static/swamp.png --------------------------------------------------------------------------------