-
Notifications
You must be signed in to change notification settings - Fork 1
/
server.py
1364 lines (1042 loc) · 52.4 KB
/
server.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
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#! /usr/bin/env python
# =*= coding: utf=8 =*=
# Imports ===============================================================================================
# Load models to communicate with DB. Variable "db" can be accessed anywhere in this file
import pika
import peewee
# from threading import Thread, Lock
# import time
# import datetime
# Local imports
from models import *
from common import *
from ship_placement import *
from argparse import ArgumentParser # Parsing command line arguments
from redis import ConnectionPool, Redis # Redis middleware
def DELETE_ALL_QUEUES():
''' Delete all queues in RabbitMQ. It needs just to delete some unnecessary info'''
import requests
def rest_queue_list(user='guest', password='guest',
host=RABBITMQ_HOST, port=15672, virtual_host=RABBITMQ_VIRTUAL_HOST):
url = 'http://%s:%s/api/queues/%s' % (host, port, virtual_host or '')
response = requests.get(url, auth=(user, password))
queues = [q['name'] for q in response.json()]
return queues
credentials = pika.PlainCredentials("guest", "guest")
connection = pika.BlockingConnection(
pika.ConnectionParameters(host=RABBITMQ_HOST,
virtual_host=RABBITMQ_VIRTUAL_HOST,
credentials=credentials))
channel = connection.channel()
for queue_name in rest_queue_list():
channel.queue_delete(queue=queue_name)
connection.close()
print "All queues were deleted"
# DELETE_ALL_QUEUES()
def check_db_connection():
''' Check connect with MySQL dababase. If is not return False '''
db_connection = True
try:
db.get_conn().ping(True)
except peewee.OperationalError:
print "ERROR: Problem with DB connection"
db_connection = False
return db_connection
def refresh_db_connection(f):
''' It's a decorator to refresh DB connection '''
def tmp(*args, **kwargs):
# It's fix to avoid error "MySQL was gone away".
# Here we check whether our current db connection is accessible or not (if not, refresh it)
return f(*args, **kwargs) if check_db_connection() else None
return tmp
class Main_Server(object):
def __init__(self, server_name):
self.rabbitmq_connection = None
self.rabbitmq_channel = None
self.redis_conn = None
self.server_name = server_name
self.server_id = None
def server_online(self):
''' Put msg into queue "servers_online" to notify about server presence '''
server = Server.select().where(Server.name == self.server_name)
# Server already registered in DB, fetch its id
if server.count() > 0:
print "- Fetch existing server_id from DB (by server name)"
self.server_id = str(server.get().server_id)
# Register server in DB
else:
print "- Save new server name in DB"
Server.create(name=self.server_name)
self.server_id = self.server_id_by_name(self.server_name)
print "<< Server(%s) sent server_name to Redis about its presence" % self.server_name
# Add server name to the hash set "servers_online" in Redis
# (in format key:value)
if not self.redis_conn.hexists("servers_online", self.server_id):
self.redis_conn.hset("servers_online", self.server_id, self.server_name)
def server_offline(self):
''' Remove msg from the set "servers_online". It means server off-line '''
# Server goes off-line (remove server name from the set "servers_online")
if not self.redis_conn.hexists("servers_online", self.server_id):
self.redis_conn.hdel("servers_online", self.server_id)
def open_rabbitmq_connection(self, host, login, password, virtual_host):
#############
# Set up RabbitMQ connection
#############
try:
print "- Start establishing connection to RabbitMQ server."
credentials = pika.PlainCredentials(login, password)
self.rabbitmq_connection = pika.BlockingConnection(
pika.ConnectionParameters(host=host,
virtual_host=virtual_host,
credentials=credentials))
self.rabbitmq_channel = self.rabbitmq_connection.channel()
print "- Connection to RabbitMQ server is established."
# "durable" means that queue won't be lost even if RabbitMQ restarts
self.rabbitmq_channel.queue_declare(queue='register_nickname', durable=True)
# Attach triggers to queues
self.rabbitmq_channel.basic_consume(self.on_register_nickname, queue='register_nickname')
self.attach_handler_to_existing_players()
except:
print "ERROR: Can't access RabbitMQ server, please check connection parameters."
self.rabbitmq_connection = None
def open_redis_connection(self, host, port):
#############
# Set up Redis connection
#############
print "- Start establishing connection to Redis server."
pool = ConnectionPool(host=host, port=port, db=0)
self.redis_conn = Redis(connection_pool=pool)
# Check that Redis server is accessible
# If not, set redis connection to None
if not check_redis_connection(self.redis_conn):
self.redis_conn = None
else:
print "- Connection to Redis server is established."
def start_consuming(self):
print('[*] Waiting for messages. To exit press CTRL+C')
try:
self.rabbitmq_channel.start_consuming()
except SystemExit, KeyboardInterrupt:
self.rabbitmq_connection.close()
# Mark server off-line (in Redis)
self.server_offline()
def send_response(self, nickname, query):
'''
This function put query into the specified queue
:param nickname: (str) - needs to put query into correct queue for particular nickname (player)
:param query: (str) - compressed query
'''
print "<< Response sent: %s...." % query[:10]
reply_queue = "resp_" + nickname
self.rabbitmq_channel.basic_publish(exchange='',
routing_key=reply_queue,
body=query,
properties=pika.BasicProperties(
delivery_mode=2
))
@refresh_db_connection
def attach_handler_to_existing_players(self):
'''
Attach handler for existing users, to fetch new requests in the queue
'''
for player in Player.select():
queue_name = 'req_' + player.nickname
self.rabbitmq_channel.queue_declare(queue=queue_name, durable=True)
self.rabbitmq_channel.basic_consume(self.on_user_request, queue=queue_name)
# Create queue to put responses for particular user
self.rabbitmq_channel.queue_declare(queue='resp_' + player.nickname, durable=True)
###################
# Main functions =====================================================================================
###################
@refresh_db_connection
def server_id_by_name(self, server_name):
return Server.get(Server.name == server_name).server_id
@refresh_db_connection
def player_id_by_nickname(self, nickname):
return Player.get(Player.nickname == nickname).player_id
@refresh_db_connection
def map_exists(self, map_id):
return Map.select().where(Map.map_id == map_id,
Map.server == self.server_id).count() > 0
@refresh_db_connection
def register_nickname(self, nickname=""):
'''
:param nickname: (str)
:return: (enum) = response code
'''
nickname_exists = Player.select().where(Player.nickname == nickname).count()
resp_code = RESP.NICKNAME_ALREADY_EXISTS
# If nickname doesn't exist in DB, then create it
if not nickname_exists:
Player.create(nickname=nickname)
print "created nick" + nickname
resp_code = RESP.OK
# Register new queue with user name
req_nickname_queue = "req_" + nickname
resp_nickname_queue = "resp_" + nickname
self.rabbitmq_channel.queue_declare(queue=req_nickname_queue, durable=True)
self.rabbitmq_channel.queue_declare(queue=resp_nickname_queue, durable=True)
# print "req_nickname_queue: %s" % req_nickname_queue
self.rabbitmq_channel.basic_consume(self.on_user_request, queue=req_nickname_queue)
return resp_code
@refresh_db_connection
def get_damaged_player_id_by_shot(self, map_id, row, column):
# Check if someone stayed in this region, if yes hit = 1, otherwise hit = 0
# (whether hit was successful or not)
hit_query = Ship_to_map.select().where(Ship_to_map.map == map_id,
# Check by row
Ship_to_map.row_start <= row, Ship_to_map.row_end >= row,
# Check by column
Ship_to_map.column_start <= column, Ship_to_map.column_end >= column)
try:
record = hit_query.get()
return record.player.player_id
# Nobody was damaged
except Ship_to_map.DoesNotExist:
return None
@refresh_db_connection
def existing_shots(self, map_id):
''' Sends existing shots to player '''
# Compressed data in format (target_row, target_column, hit_successful, damaged_player_id)
shots_data = []
# Get all games which have not been started yet
shots_query = Player_hits.select().where(Player_hits.map == map_id)
for s in shots_query:
# Compress data
if s.hit == 1:
# print "sss, ", s.hit
damaged_player_id = self.get_damaged_player_id_by_shot(map_id, s.row, s.column)
else:
damaged_player_id = ""
# print damaged_player_id
shot = (s.map_id, s.row, s.column, s.hit, damaged_player_id)
for el in zip(shot):
shots_data.append(str(el[0]))
data = pack_data(shots_data)
return data
@refresh_db_connection
def another_player_made_shot(self, map_id, initiator_id, initiator_nickname, row, column, damaged_player_id, hit):
'''
Player made a shot, then notify other players about this shot (except player_id).
hit - 1 successful, 0 - missed
'''
if damaged_player_id == "":
damaged_player_id = 0
# Get all players on this map (except initiator and disconnected users)
players_on_map_query = Player_to_map.select().where(
Player_to_map.map == map_id,
~(Player_to_map.player << [initiator_id, damaged_player_id]))
# Send notification about this move
for record in players_on_map_query:
am_i_spectator = record.spectator_mode
data = pack_data([map_id, initiator_id, initiator_nickname, row, column, am_i_spectator, hit, damaged_player_id])
query = pack_resp(COMMAND.NOTIFICATION.SOMEONE_MADE_SHOT, RESP.OK, self.server_id, data)
# print record.player.nickname
# Put notification into the queue
self.send_response(nickname=record.player.nickname, query=query)
@refresh_db_connection
def is_game_end(self, map_id):
def remaining_players_on_map(map_id):
'''
:param map_id: (str)
:return: (list) of players_ids which are still playing on the map
(Means have ships on the map, which are not destroyed)
'''
query_players = Ship_to_map.select(Ship_to_map.player_id).distinct().where(
Ship_to_map.map == map_id,
Ship_to_map.totally_sank == "0"
)
# Return list of remaining_players
return [record.player_id for record in query_players]
remaining_players = remaining_players_on_map(map_id)
query = Player_to_map.select().distinct().where(
Player_to_map.map == map_id,
Player_to_map.player << remaining_players,
Player_to_map.kicked == 0,
Player_to_map.spectator_mode == 0
)
game_end = False
if query.count() < 2:
game_end = True
return game_end
@refresh_db_connection
def find_next_player(self, map_id, current_player_id):
'''
Find which player(by current_player_id) should move next.
(search among all players except kicked players and players in spectator mode
:param map_id: (str)
:param current_player_id: (str)
:return: next_player_id(int), next_player_nickname (str)
'''
players = Player_to_map.select().where(
# exclude players with sink ships
Player_to_map.player.in_(
Ship_to_map.select(Ship_to_map.player_id).where(
Ship_to_map.map == Player_to_map.map
)
),
Player_to_map.map == map_id,
Player_to_map.kicked == 0,
Player_to_map.spectator_mode == 0
).order_by(Player_to_map.id)
players_nicknames = [p.player.nickname for p in players]
players = [p.player_id for p in players]
try:
key = players.index(current_player_id)
except ValueError:
return None, None
# if it was the last player, then choose the 1st
if key + 1 >= len(players):
next_player_id = players[0]
next_player_nickname = players_nicknames[0]
# Otherwise, choose next player
else:
next_player_id = players[key + 1]
next_player_nickname = players_nicknames[key + 1]
return next_player_id, next_player_nickname
@refresh_db_connection
def kick_player(self, map_id, player_id_to_kick):
'''
:param map_id:
:param player_id_to_kick:
:return: resp_code, player_nickname
'''
resp_code, player_nickname = RESP.OK, ""
query = Player_to_map.select().where(
Player_to_map.player == player_id_to_kick,
Player_to_map.map == map_id
)
total_players_on_map = Player_to_map.select().where(
Player_to_map.map == map_id,
Player_to_map.kicked == 0,
Player_to_map.spectator_mode == 0
).count()
# If there're only 2 players or less on the map, then we can't kick this player
if total_players_on_map <= 2:
resp_code = RESP.MIN_NUMBER_OF_PLAYERS
elif query.count():
player = query.get()
# If it's player's turn, give turn to another player
if player.my_turn == 1:
next_player_id, next_player_nickname = self.find_next_player(map_id, current_player_id=player_id_to_kick)
# Set turn to "0" for kicked player, and to "1" for the next player
Player_to_map.update(my_turn=0) \
.where(Player_to_map.map == map_id, Player_to_map.player == player_id_to_kick).execute()
Player_to_map.update(my_turn=1) \
.where(Player_to_map.map == map_id, Player_to_map.player == next_player_id).execute()
# Delete record from DB
query.delete_instance()
# send notification to player that was kicked
query = pack_resp(COMMAND.NOTIFICATION.YOU_ARE_KICKED, RESP.OK, self.server_id, data=map_id)
self.send_response(nickname=player.player.nickname, query=query)
remaining_players = Player_to_map.select().where(
Player_to_map.map == map_id,
Player_to_map.kicked == 0,
Player_to_map.spectator_mode == 0
)
# Add notification to other players that this player was kicked
for p in remaining_players:
query = pack_resp(COMMAND.NOTIFICATION.ANOTHER_PLAYER_WAS_KICKED, RESP.OK, self.server_id,
data=pack_data([player_id_to_kick, player_nickname]))
self.send_response(p.player.nickname, query)
else:
resp_code = RESP.PLAYER_ALREADY_KICKED
return resp_code, player_id_to_kick
@refresh_db_connection
def make_hit(self, map_id, player_id, player_nickname, target_row, target_column):
'''
:param map_id: (str)
:param player_id: (int)
:param player_nickname: (str)
:param target_row: (str) x = coordinate where player made a shot
:param target_column: (str) y = coordinate where player made a shot
"packed data" contains target_row, target_column,
hit(result of shot. 0 = missed, 1 = hit), damaged_player_id
:return: (enum) = response code, packed data (str)
'''
# is_game_end can be "0" or "1"
resp_code = RESP.OK
hit, damaged_player_id = "0", ""
game_finished = False
hit_record = None
# Map doesn't exist
if not self.map_exists(map_id):
resp_code = RESP.MAP_DOES_NOT_EXIST
# Player already made shot in this region
elif Player_hits.select().where(Player_hits.map == map_id,
Player_hits.row == target_row,
Player_hits.column == target_column).count() > 0:
resp_code = RESP.SHOT_WAS_ALREADY_MADE_HERE
# Register new shot
else:
#####################
# Check if someone stayed in this region, if yes hit = 1, otherwise hit = 0
# (whether hit was successful or not)
hit_query = Ship_to_map.select().where(Ship_to_map.player != player_id,
Ship_to_map.map == map_id,
# Check by row
Ship_to_map.row_start <= target_row,
Ship_to_map.row_end >= target_row,
# And by column
Ship_to_map.column_start <= target_column,
Ship_to_map.column_end >= target_column)
try:
hit_record = hit_query.get()
hit = "1"
# Notify player whose ship was damaged
self.notify_about_ship_damage(map_id,
initiator_id=player_id,
target_player_name=hit_record.player.nickname,
target_row=target_row,
target_column=target_column)
# Player who was damaged
damaged_player_id = hit_record.player.player_id
# Nobody was damaged
except Ship_to_map.DoesNotExist:
pass
# Register hit in DB
shot_query = Player_hits(map=map_id, player=player_id,
row=int(target_row), column=int(target_column),
hit=hit)
shot_query.save()
#######################################
# Notify next player about his turn
if hit == "0":
next_player_id, next_player_nickname = self.find_next_player(map_id, current_player_id=player_id)
# Set turn to "0" for me, and to "1" for the next player
Player_to_map.update(my_turn=0) \
.where(Player_to_map.map == map_id, Player_to_map.player == player_id).execute()
Player_to_map.update(my_turn=1) \
.where(Player_to_map.map == map_id, Player_to_map.player == next_player_id).execute()
if next_player_id is not None:
query = pack_resp(COMMAND.NOTIFICATION.YOUR_TURN_TO_MOVE, RESP.OK, self.server_id, data=map_id)
self.send_response(nickname=next_player_nickname, query=query)
# Check whether the shot completely destroyed ship or not
else:
# Update location of the shot that caused damage
shot_query.ship_location_id = hit_record.location_id
shot_query.save()
# Get damaged ship record
damaged_ship_record = Ship_to_map.get(location_id=hit_record.location_id)
ship_size = damaged_ship_record.ship_type
# Get all shots in this ship
all_shots_in_ship = Player_hits.select().where(Player_hits.ship_location_id == hit_record.location_id)
# Update saved shot in case the ship was totally destroyed
if all_shots_in_ship.count() >= ship_size:
damaged_ship_record.totally_sank = 1
damaged_ship_record.save()
#######################################
# Notify other players about this shot in any case
self.another_player_made_shot(
map_id, player_id, player_nickname, target_row, target_column, damaged_player_id, hit)
#######################################
# Notify other players about next turn to move
self.notify_all_players_about_next_turn(map_id)
# is_game_end() return True/False
game_finished = self.is_game_end(map_id)
# print "game_finished", game_finished
# If game finished, notify all players about it
if game_finished:
map_info = Map.get(Map.map_id == map_id)
# Mark that the map as finished (code = 2)
map_info_inst = map_info
map_info_inst.game_started = 2
map_info_inst.save()
all_players = Player_to_map.select().where(Player_to_map.map == map_id)
data = pack_data([map_id, map_info.name, map_info.owner_id])
# Put notification about game end into the RabbitMQ
for record in all_players:
query = pack_resp(COMMAND.NOTIFICATION.GAME_FINISHED, RESP.OK, self.server_id, data)
self.send_response(nickname=record.player.nickname, query=query)
# If the game finished, player can't make shots any more
game_finished = "1" if game_finished else "0"
data = pack_data([target_row, target_column, hit, damaged_player_id, game_finished])
return resp_code, data
@refresh_db_connection
def kick_player(self, map_id, player_id_to_kick):
'''
:param map_id:
:param player_id_to_kick:
:return: resp_code, player_nickname
'''
resp_code, player_nickname = RESP.OK, ""
query = Player_to_map.select().where(
Player_to_map.player == player_id_to_kick,
Player_to_map.map == map_id
)
if query.count():
player = query.get()
player_nickname = player.player.nickname
# Delete record from DB
player.delete_instance()
# send notification to player that was kicked
query = pack_resp(COMMAND.NOTIFICATION.YOU_ARE_KICKED, RESP.OK, self.server_id, data=map_id)
self.send_response(nickname=player_nickname, query=query)
# TODO: Add notification to other players that this player was kicked
else:
resp_code = RESP.PLAYER_ALREADY_KICKED
return resp_code, player_id_to_kick
@refresh_db_connection
def start_game(self, initiator_id, map_id):
''' Map creator triggered to start game '''
resp_code = RESP.OK
current_map = Map.select().where(Map.map_id == map_id).get()
all_players = Player_to_map.select().where(
Player_to_map.map == map_id,
Player_to_map.kicked == 0,
Player_to_map.spectator_mode == 0
)
total_players_with_placed_ships = Ship_to_map.select(Ship_to_map.player).distinct().where(
Ship_to_map.map == map_id,
Ship_to_map.player.in_(all_players.select(Player_to_map.player).distinct())
).count()
my_own_ships = Ship_to_map.select().distinct(Ship_to_map.player).where(
Ship_to_map.map == map_id,
Ship_to_map.player == initiator_id
).count()
# Owner still haven't placed his ships
if my_own_ships == 0:
resp_code = RESP.YOU_DID_NOT_PLACE_SHIPS
# Not enough players to start the game
elif all_players.count() < 2:
resp_code = RESP.NOT_ENOUGH_PLAYERS
elif total_players_with_placed_ships < 2:
resp_code = RESP.NOT_ENOUGH_PLAYERS_WITH_PLACED_SHIPS
# If game has not been started yet
elif not current_map.game_started:
normal_players = Player_to_map.select().where(
Player_to_map.player.in_(
Ship_to_map.select(Ship_to_map.player_id).where(
Ship_to_map.map == Player_to_map.map
)
),
Player_to_map.map_id == map_id,
Player_to_map.player != initiator_id,
Player_to_map.kicked == 0,
Player_to_map.spectator_mode == 0
)
# These players will start to play now
for record in normal_players:
# Notify players on this map that game started
query = pack_resp(COMMAND.NOTIFICATION.GAME_STARTED, RESP.OK, self.server_id, data=map_id)
self.send_response(nickname=record.player.nickname, query=query)
# Notify players who didn't place ships that they kicked
players_to_kick = Player_to_map.select().where(
Player_to_map.player.not_in(
Ship_to_map.select(Ship_to_map.player_id).where(
Ship_to_map.map == Player_to_map.map
)
),
Player_to_map.map_id == map_id,
Player_to_map.player != initiator_id,
Player_to_map.spectator_mode != 1
)
# These players will be kicked now
for record in players_to_kick:
# Trigger notification method to notify kicked player
self.kick_player(map_id, player_id_to_kick=record.player)
# self.notify_about_kick(map_id, p.player.nickname)
# Update time for turn_start_time (to keep tack of elapsed time since player is able to move.
# It needs to kick (admin) player if turn_start_time exceeded 30 seconds
# Player_to_map.update(turn_start_time=datetime.datetime.now).where(
# Player_to_map.map == map_id,
# Player_to_map.player == initiator_id
# )
# Update record in DB that game started
current_map.game_started = 1
current_map.save()
else:
resp_code = RESP.GAME_ALREADY_STARTED
return resp_code
@refresh_db_connection
def create_new_map(self, owner_id, name, size):
'''
Create a new map with size of (rows x columns)
:param owner_id: (int)
:param name: (str) = map name
:param size: (str)
:return: (enum) = response code, (str) map_id
'''
resp_code, map_id = RESP.OK, ""
# Check that map with the same name doesn't exist in DB
if Map.select().where(Map.name == name, Map.server == self.server_id).count() == 0:
# 120 - is the number of cells needed to place all ships for 1 player
max_players = (int(size) ** 2) / 120
# Create new map
Map.create(owner=owner_id, server=self.server_id, name=name, size=size, max_players=max_players)
map_id = Map.select().order_by(Map.map_id.desc()).get().map_id
# Add player to map and mark that it's his turn to move
Player_to_map.create(map=map_id, player=owner_id, my_turn=1)
else:
resp_code = RESP.MAP_NAME_ALREADY_EXISTS
data = pack_data([map_id, name, size])
return resp_code, data
@refresh_db_connection
def join_game(self, map_id, player_id, player_nickname):
'''
Player wants to join existing game
:param map_id: (str) - map that user wants to join
:param player_id: (str)
:param player_nickname: (str)
:return: resp_code (enum)
'''
resp_code = RESP.OK
my_turn = "0"
ships_already_placed = "0"
game_already_started = "0"
owner_id = ""
# Query to check does player already place his ships
ships_placed = Ship_to_map.select().where(
Ship_to_map.map == map_id,
Ship_to_map.player == player_id
).count() > 0
already_joined = Player_to_map.select().where(
Player_to_map.map == map_id,
Player_to_map.player == player_id,
Player_to_map.kicked == 0 # and player's not kicked
)
# Map doesn't exist
if not self.map_exists(map_id):
resp_code = RESP.MAP_DOES_NOT_EXIST
else:
map_info = Map.get(Map.map_id == map_id, Map.server == self.server_id)
owner_id = map_info.owner.player_id
game_started = map_info.game_started
# All players on this map
all_players = Player_to_map.select().where(Player_to_map.map == map_id)
if ships_placed:
ships_already_placed = "1"
###########
# Game finished (negative response)
if game_started == 2:
resp_code = RESP.GAME_ALREADY_FINISHED
# Player is in spectator mode
elif already_joined.count() > 0 and already_joined.get().spectator_mode == 1:
resp_code = RESP.YOU_ARE_IN_SPECTATOR_MODE
data = self.spectator_mode(player_id, map_id)
return resp_code, data
# Game started (negative response)
elif game_started == 1 and not already_joined.count() > 0:
game_already_started = "2"
resp_code = RESP.GAME_ALREADY_STARTED
elif game_started == 1 and already_joined.count() > 0:
resp_code = RESP.ALREADY_JOINED_TO_MAP
game_already_started = "1"
self.mark_player_as_connected(map_id, player_id, player_nickname, notify_others=True)
if already_joined.get().my_turn:
my_turn = "1"
###########
# Game didn't started
elif game_started == 0 and already_joined.count() > 0:
self.mark_player_as_connected(map_id, player_id, player_nickname, notify_others=True)
elif game_started == 0 and not already_joined.count() > 0:
print "already_joined", already_joined, already_joined.count() > 0
# Exceeding limit of players
if all_players.count() + 1 > map_info.max_players:
data = map_info.name
resp_code = RESP.MAP_FULL
return resp_code, data
# Add player to the map
# (Player hasn't joined this map yet)
else:
Player_to_map.create(map_id=map_id, player=player_id)
# Notify all players (except this) about joining to the map
# Check that current player and creator of the map are different players
for record in all_players:
if record.player.player_id != player_id:
query = pack_resp(COMMAND.NOTIFICATION.PLAYER_JOINED_TO_GAME, RESP.OK, self.server_id,
data=pack_data([player_id, player_nickname]))
self.send_response(record.player.nickname, query)
data = pack_data([my_turn, ships_already_placed, game_already_started, owner_id])
return resp_code, data
@refresh_db_connection
def list_of_maps(self, player_id):
'''
:return: (data) - packed data in format (map_id, map_name, rows, columns)
'''
maps_data = []
# Get all games which have not been started yet
maps_query = Map.select().where(Map.server == self.server_id)
for m in maps_query:
game_started = m.game_started
if game_started == 1:
player_on_map = Player_to_map.select().where(
Player_to_map.map == m.map_id,
Player_to_map.player == player_id,
Player_to_map.kicked == 0
).count() > 0
# If player still can join this map, share access to this player
# But if he is kicked, he can't join this map
if player_on_map:
game_started = 0
map_data = (m.map_id, m.name, m.size, game_started)
# Compress data
for el in zip(map_data):
maps_data.append(str(el[0]))
data = pack_data(maps_data)
return RESP.OK, data
@refresh_db_connection
def spectator_mode(self, player_id, map_id):
''' Fetch all ships locations and send them to user in spectator mode '''
# Mark player that goes into spectator mode
Player_to_map.update(spectator_mode=1).where(
Player_to_map.map == map_id,
Player_to_map.player == player_id
).execute()
all_ships = []
all_ships_query = Ship_to_map.select().where(Ship_to_map.map == map_id)
for ship in all_ships_query:
x1, x2 = ship.row_start, ship.row_end
y1, y2 = ship.column_start, ship.column_end
ship_coord = (ship.player.player_id, x1, x2, y1, y2, ship.ship_type)
# Compress data
for el in zip(ship_coord):
all_ships.append(str(el[0]))
data = pack_data(all_ships)
return data
@refresh_db_connection
def players_on_map(self, player_id, map_id):
''' Return a compressed string (list of current players on requested map) '''
players_data = []
# Get all players on map except who requested this command
players_query = Player_to_map.select().where(Player_to_map.map == map_id,
Player_to_map.player != player_id)
for p in players_query:
# Compress data
disconnected = p.disconnected if p.disconnected is not None else "0"
kicked = p.kicked if p.kicked is not None else "0"
player_data = (p.map_id, p.player_id, p.player.nickname, disconnected, kicked)
for el in zip(player_data):
players_data.append(str(el[0]))
data = pack_data(players_data)
return data
@refresh_db_connection
def my_ships_on_map(self, player_id, map_id):
''' Return compressed str (list of ships locations for particular player '''
resp_code, ships_data = RESP.OK, []
# Get all games which have not been started yet
ships_query = Ship_to_map.select().where(Ship_to_map.map == map_id,
Ship_to_map.player_id == player_id)
for s in ships_query:
# Compress data
ship_data = (map_id, s.row_start, s.row_end, s.column_start, s.column_end, s.ship_type, s.totally_sank)
for el in zip(ship_data):
ships_data.append(str(el[0]))
data = pack_data(ships_data)
return resp_code, data
@refresh_db_connection
def disconnect_from_game(self, map_id, player_id, player_nickname):
''' Player can be disconnected, but he/she continues to play '''
resp_code = RESP.OK
Player_to_map.update(disconnected=1).where(
Player_to_map.map == map_id,
Player_to_map.player == player_id
).execute()
remaining_players = Player_to_map.select().where(
Player_to_map.map == map_id,
Player_to_map.player != player_id, # except current player,
# And also except disconnected, kicked, and players in spectator mode
Player_to_map.disconnected == 0,
Player_to_map.kicked == 0,
Player_to_map.spectator_mode == 0
)
# Send notification to remaining players about current player disconnection
for p in remaining_players:
query = pack_resp(COMMAND.NOTIFICATION.ANOTHER_PLAYER_DISCONNECTED,
RESP.OK, self.server_id, data=pack_data([map_id, player_id, player_nickname]))
self.send_response(p.player.nickname, query)
return resp_code
@refresh_db_connection
def mark_player_as_connected(self, map_id, player_id, player_nickname, notify_others=False):
''' Mark player as connected '''
# print "player connected", map_id, player_id
Player_to_map.update(disconnected=0)\
.where(Player_to_map.map == map_id, Player_to_map.player == player_id).execute()
if notify_others:
remaining_players = Player_to_map.select().where(
Player_to_map.map == map_id,
Player_to_map.player != player_id, # except current player
Player_to_map.kicked == 0,
Player_to_map.spectator_mode == 0
)
for p in remaining_players:
query = pack_resp(COMMAND.NOTIFICATION.PLAYER_JOINED_TO_GAME,
RESP.OK, self.server_id, data=pack_data([player_id, player_nickname]))
self.send_response(p.player.nickname, query)
@refresh_db_connection
def restart_game(self, map_id):
''' After game finished, admin can trigger restart the game '''
resp_code = RESP.OK
# Mark existing map as not started yet
map_info = Map.get(map_id=map_id, server=self.server_id)
map_info.game_started = 0
map_info.save()