This repository has been archived by the owner on Nov 6, 2024. It is now read-only.
forked from TecProg-grupo4-2018-2/panel-attack
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathserver.lua
1494 lines (1406 loc) · 57.9 KB
/
server.lua
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
local socket = require("socket")
require("class")
json = require("dkjson")
require("stridx")
require("gen_panels")
require("csprng")
require("server_file_io")
require("util")
require("timezones")
local lfs = require("lfs")
local byte = string.byte
local char = string.char
local pairs = pairs
local ipairs = ipairs
local random = math.random
local lobby_changed = false
local time = os.time
local floor = math.floor
local TIMEOUT = 10
local CHARACTERSELECT = "character select" -- room states
local PLAYING = "playing" -- room states
local sep = package.config:sub(1, 1) --determines os directory separator (i.e. "/" or "\")
local VERSION = "045"
local type_to_length = {H=4, E=4, F=4, P=8, I=2, L=2, Q=8, U=2}
local INDEX = 1
local connections = {}
local ROOMNUMBER = 1
local rooms = {}
local name_to_idx = {}
local socket_to_idx = {}
local proposals = {}
local playerbases = {}
local loaded_placement_matches = {incomplete={},
complete={}}
function lobby_state()
local names = {}
for _,v in pairs(connections) do
if v.state == "lobby" then
names[#names+1] = v.name
end
end
local spectatableRooms = {}
for _,v in pairs(rooms) do
spectatableRooms[#spectatableRooms+1] = {roomNumber=v.roomNumber, name=v.name , a=v.a.name, b=v.b.name, state=v:state()}
end
return {unpaired = names, spectatable=spectatableRooms}
end
function propose_game(sender, receiver, message)
local s_c, r_c = name_to_idx[sender], name_to_idx[receiver]
if s_c then s_c = connections[s_c] end
if r_c then r_c = connections[r_c] end
if s_c and s_c.state == "lobby" and r_c and r_c.state == "lobby" then
proposals[sender] = proposals[sender] or {}
proposals[receiver] = proposals[receiver] or {}
if proposals[sender][receiver] then
if proposals[sender][receiver][receiver] then
create_room(s_c, r_c)
end
else
r_c:send(message)
local prop = {[sender]=true}
proposals[sender][receiver] = prop
proposals[receiver][sender] = prop
end
end
end
function clear_proposals(name)
if proposals[name] then
for othername,_ in pairs(proposals[name]) do
proposals[name][othername] = nil
if proposals[othername] then
proposals[othername][name] = nil
end
end
proposals[name] = nil
end
end
function create_room(a, b)
lobby_changed = true
clear_proposals(a.name)
clear_proposals(b.name)
local new_room = Room(a,b)
local a_msg, b_msg = {create_room = true}, {create_room = true}
a_msg.your_player_number = 1
a_msg.op_player_number = 2
a_msg.opponent = new_room.b.name
b_msg.opponent = new_room.a.name
new_room.b.cursor = "__Ready"
new_room.a.cursor = "__Ready"
b_msg.your_player_number = 2
b_msg.op_player_number = 1
a_msg.a_menu_state = new_room.a:menu_state()
a_msg.b_menu_state = new_room.b:menu_state()
b_msg.b_menu_state = new_room.b:menu_state()
b_msg.a_menu_state = new_room.a:menu_state()
new_room.a.opponent = new_room.b
new_room.b.opponent = new_room.a
new_room:prepare_character_select()
a_msg.ratings = new_room.ratings
b_msg.ratings = new_room.ratings
a_msg.rating_updates = true
b_msg.rating_updates = true
new_room.a:send(a_msg)
new_room.b:send(b_msg)
end
function start_match(a, b)
if (a.player_number ~= 1) then
print("Match starting, players a and b need to be swapped.")
a, b = b, a
if(a.player_number == 1) then
print("Success, player a now has player_number 1.")
else
print("ERROR: player a still doesn't have player_number 1.")
end
end
a.room.stage = math.random(1,2)==1 and a.stage or b.stage
local msg = {match_start = true, ranked = false, stage=a.room.stage,
player_settings = {character = a.character, character_display_name=a.character_display_name, level = a.level, panels_dir = a.panels_dir, player_number = a.player_number},
opponent_settings = {character = b.character, character_display_name=b.character_display_name, level = b.level, panels_dir = b.panels_dir, player_number = b.player_number}}
local room_is_ranked, reasons = a.room:rating_adjustment_approved()
if room_is_ranked then
a.room.replay.vs.ranked=true
msg.ranked = true
if leaderboard.players[a.user_id] then
msg.player_settings.rating = round(leaderboard.players[a.user_id].rating)
else
msg.player_settings.rating = DEFAULT_RATING
end
if leaderboard.players[b.user_id] then
msg.opponent_settings.rating = round(leaderboard.players[b.user_id].rating)
else
msg.opponent_settings.rating = DEFAULT_RATING
end
end
a.room.replay.vs.P1_name=a.name
a.room.replay.vs.P2_name=b.name
a.room.replay.vs.P1_char=a.character
a.room.replay.vs.P2_char=b.character
a:send(msg)
a.room:send_to_spectators(msg)
msg.player_settings, msg.opponent_settings = msg.opponent_settings, msg.player_settings
b:send(msg)
lobby_changed = true
a:setup_game()
b:setup_game()
if not a.room then
print("ERROR: In start_match, Player A "..(a.name or "nil").." doesn't have a room\nCannot run setup_game() for spectators!")
end
for k,v in pairs(a.room.spectators) do
v:setup_game()
end
end
Room = class(function(self, a, b)
--TODO: it would be nice to call players a and b something more like self.players[1] and self.players[2]
self.a = a --player a
self.b = b --player b
self.stage = nil
self.name = a.name.." vs "..b.name
if not self.a.room or not self.b.room then
self.roomNumber = ROOMNUMBER
ROOMNUMBER = ROOMNUMBER + 1
self.a.room = self
self.b.room = self
self.spectators = {}
self.win_counts = {}
self.win_counts[1] = 0
self.win_counts[2] = 0
local a_rating, b_rating
local a_placement_match_progress, b_placement_match_progress
if a.user_id then
if leaderboard.players[a.user_id] and leaderboard.players[a.user_id].rating then
a_rating = round(leaderboard.players[a.user_id].rating)
end
local a_qualifies, a_progress = qualifies_for_placement(a.user_id)
if not (leaderboard.players[a.user_id] and leaderboard.players[a.user_id].placement_done) and not a_qualifies then
a_placement_match_progress = a_progress
end
end
if b.user_id then
if leaderboard.players[b.user_id] and leaderboard.players[b.user_id].rating then
b_rating = round(leaderboard.players[b.user_id].rating or 0)
end
local b_qualifies, b_progress = qualifies_for_placement(b.user_id)
if not (leaderboard.players[b.user_id] and leaderboard.players[b.user_id].placement_done) and not b_qualifies then
b_placement_match_progress = b_progress
end
end
self.ratings = {{old=a_rating or 0, new=a_rating or 0, difference=0, league=get_league(a_rating or 0), placement_match_progress=a_placement_match_progress},
{old=b_rating or 0, new=b_rating or 0, difference=0, league=get_league(b_rating or 0), placement_match_progress=b_placement_match_progress}}
else
self.win_counts = self.a.room.win_counts
self.spectators = self.a.room.spectators
self.roomNumber = self.a.room.roomNumber
end
self.game_outcome_reports = {}
rooms[self.roomNumber] = self
end)
function Room.character_select(self)
self:prepare_character_select()
self:send({character_select=true, create_room=true, rating_updates=true, ratings=self.ratings, a_menu_state=self.a:menu_state(), b_menu_state=self.b:menu_state()})
end
function Room.prepare_character_select(self)
print("Called Server.lua Room.character_select")
self.a.state = "character select"
self.b.state = "character select"
if self.a.player_number and self.a.player_number ~= 0 and self.a.player_number ~= 1 then
print("initializing room. player a does not have player_number 1. Swapping players a and b")
self.a, self.b = self.b, self.a
if self.a.player_number == 1 then
print("Success. player a has player_number 1 now.")
else
print("ERROR. Player a still doesn't have player_number 1")
end
else
self.a.player_number = 1
self.b.player_number = 2
end
self.a.cursor = "__Ready"
self.b.cursor = "__Ready"
self.a.ready = false
self.b.ready = false
-- local msg = {spectate_request_granted = true, spectate_request_rejected = false, rating_updates=true, ratings=self.ratings, a_menu_state=self.a:menu_state(), b_menu_state=self.b:menu_state()}
-- for k,v in ipairs(self.spectators) do
-- self.spectators[k]:send(msg)
-- end
end
function Room.state(self)
if self.a.state == "character select" then
return CHARACTERSELECT
elseif self.a.state == "playing" then
return PLAYING
else
return self.a.state
end
end
function Room.is_spectatable(self)
return self.a.state == "character select"
end
function Room.add_spectator(self, new_spectator_connection)
new_spectator_connection.state = "spectating"
new_spectator_connection.room = self
self.spectators[#self.spectators+1] = new_spectator_connection
print(new_spectator_connection.name .. " joined " .. self.name .. " as a spectator")
msg = {spectate_request_granted = true, spectate_request_rejected = false, rating_updates=true, ratings=self.ratings, a_menu_state=self.a:menu_state(), b_menu_state=self.b:menu_state(), win_counts=self.win_counts, match_start=replay_of_match_so_far~=nil, stage=self.stage, replay_of_match_so_far = self.replay, ranked = self:rating_adjustment_approved(),
player_settings = {character = self.a.character, character_display_name=self.a.character_display_name, level = self.a.level, player_number = self.a.player_number},
opponent_settings = {character = self.b.character, character_display_name=self.b.character_display_name, level = self.b.level, player_number = self.b.player_number}}
new_spectator_connection:send(msg)
msg = {spectators=self:spectator_names()}
print("sending spectator list: "..json.encode(msg))
self:send(msg)
lobby_changed = true
end
function Room.spectator_names(self)
local list = {}
for k,v in pairs(self.spectators) do
list[#list+1] = v.name
end
return list
end
function Room.remove_spectator(self, connection)
for k,v in pairs(self.spectators) do
if v.name == connection.name then
self.spectators[k].state = "lobby"
print(connection.name .. " left " .. self.name .. " as a spectator")
self.spectators[k] = nil
lobby_changed = true
end
end
msg = {spectators=self:spectator_names()}
print("sending spectator list: "..json.encode(msg))
self:send(msg)
end
function Room.close(self)
if self.a then
self.a.player_number = 0
self.a.state = "lobby"
self.a.room = nil
end
if self.b then
self.b.player_number = 0
self.b.state = "lobby"
self.b.room = nil
end
for k,v in pairs(self.spectators) do
if v.room then
v.room = nil
v.state = "lobby"
end
end
if rooms[self.roomNumber] then
rooms[self.roomNumber] = nil
end
self:send_to_spectators({leave_room = true})
end
function roomNumberToRoom(roomNr)
for k,v in pairs(rooms) do
if rooms[k].roomNumber and rooms[k].roomNumber == roomNr then
return v
end
end
end
--TODO: maybe support multiple playerbases
Playerbase = class(function (s, name)
s.name = name
s.players = {}--{["e2016ef09a0c7c2fa70a0fb5b99e9674"]="Bob",
--["d28ac48ba5e1a82e09b9579b0a5a7def"]="Alice"}
s.deleted_players = {}
playerbases[#playerbases+1] = s
end)
function Playerbase.update(self, user_id, user_name)
self.players[user_id] = user_name
write_players_file()
end
function Playerbase.delete_player(self, user_id)
-- returns whether a player was deleted
if self.players[user_id] then
self.deleted_players[user_id] = self.players[user_id]
self.players[user_id] = nil
write_players_file()
write_deleted_players_file()
return true
else
return false
end
end
function generate_new_user_id()
new_user_id = cs_random()
print("new_user_id: "..new_user_id)
return tostring(new_user_id)
end
--TODO: support multiple leaderboards
Leaderboard = class(function (s, name)
s.name = name
s.players = {}
end)
function Leaderboard.update(self, user_id, new_rating, match_details)
print("in Leaderboard.update")
if self.players[user_id] then
self.players[user_id].rating = new_rating
else
self.players[user_id] = {rating=new_rating}
end
if match_details and match_details ~= "" then
for k,v in pairs(match_details) do
self.players[user_id].ranked_games_won = (self.players[user_id].games_won or 0) + v.outcome
self.players[user_id].ranked_games_played = (self.players[user_id].ranked_games_played or 0) + 1
end
end
print("new_rating = "..new_rating)
print("about to write_leaderboard_file")
write_leaderboard_file()
print("done with Leaderboard.update")
end
function Leaderboard.get_report(self, user_id_of_requester)
--returns the leaderboard as an array sorted from highest rating to lowest,
--with usernames from playerbase.players instead of user_ids
--ie report[1] will give the highest rating player's user_name and how many points they have. Like this:
--report[1] might return {user_name="Alice",rating=2250}
--report[2] might return {user_name="Bob",rating=2100,is_you=true} if Bob requested the leaderboard
local report = {}
local leaderboard_player_count = 0
--count how many entries there are in self.players since #self.players will not give us an accurate answer for sparse tables
for k,v in pairs(self.players) do
leaderboard_player_count = leaderboard_player_count + 1
end
for k,v in pairs(self.players) do
for insert_index=1, leaderboard_player_count do
local player_is_leaderboard_requester = nil
if playerbase.players[k] then --only include in the report players who are still listed in the playerbase
if v.placement_done then --don't include players who haven't finished placement
if v.rating then -- don't include entries who's rating is nil (which shouldn't happen anyway)
if k == user_id_of_requester then
player_is_leaderboard_requester = true
end
if report[insert_index] and report[insert_index].rating and v.rating >= report[insert_index].rating then
table.insert(report, insert_index, {user_name=playerbase.players[k],rating=v.rating,is_you=player_is_leaderboard_requester})
break
elseif insert_index == leaderboard_player_count or #report == 0 then
table.insert(report, {user_name=playerbase.players[k],rating=v.rating,is_you=player_is_leaderboard_requester}) -- at the end of the table.
break
end
end
end
end
end
end
for k,v in pairs(report) do
v.rating = round(v.rating)
end
return report
end
Connection = class(function(s, socket)
s.index = INDEX
INDEX = INDEX + 1
connections[s.index] = s
socket_to_idx[socket] = s.index
s.socket = socket
socket:settimeout(0)
s.leftovers = ""
s.state = "needs_name"
s.room = nil
s.last_read = time()
s.player_number = 0 -- 0 if not a player in a room, 1 if player "a" in a room, 2 if player "b" in a room
s.logged_in = false --whether connection has successfully logged into the rating system.
s.user_id = nil
s.wants_ranked_match = false --TODO: let the user change wants_ranked_match
end)
function Connection.menu_state(self)
state = {cursor=self.cursor, stage=self.stage, stage_is_random=self.stage_is_random, ready=self.ready, character=self.character, character_is_random=self.character_is_random, character_display_name=self.character_display_name, panels_dir=self.panels_dir, level=self.level, ranked=self.wants_ranked_match}
return state
--note: player_number here is the player_number of the connection as according to the server, not the "which" of any Stack
end
function Connection.send(self, stuff)
if type(stuff) == "table" then
local json = json.encode(stuff)
local len = json:len()
local prefix = "J"..char(floor(len/65536))..char(floor((len/256)%256))..char(len%256)
--print(byte(prefix[1]), byte(prefix[2]), byte(prefix[3]), byte(prefix[4]))
print("sending json "..json)
stuff = prefix..json
else
if stuff[1] ~= "I" and stuff[1] ~= "U" and stuff[1] ~= "E" then
print("sending non-json "..stuff)
end
end
local retry_count = 0
local times_to_retry = 5
local foo = {}
while not foo[1] and retry_count <= 5 do
foo = {self.socket:send(stuff)}
if stuff[1] ~= "I" and stuff[1] ~= "U" and stuff[1] ~= "E" then
print(unpack(foo))
end
if not foo[1] then
print("WARNING: Connection.send failed. will retry...")
retry_count = retry_count + 1
end
end
if not foo[1] then
print("Closing connection for "..(self.name or "nil")..". During Connection.send, foo[1] was nil after "..times_to_retry.." retries were attempted")
self:close()
end
end
function Connection.login(self, user_id)
--returns whether the login was successful
--print("Connection.login was called!")
self.user_id = user_id
self.logged_in = false
local IP_logging_in, port = self.socket:getpeername()
print("New login attempt: "..IP_logging_in..":"..port)
if is_banned(IP_logging_in) then
deny_login(self, "Awaiting ban timeout")
elseif not self.name then
deny_login(self, "Player has no name")
print("Login failure: Player has no name")
elseif not self.user_id then
deny_login(self, "Client did not send a user_id in the login request")
success = false
elseif self.user_id == "need a new user id" and self.name then
print(self.name.." needs a new user id!")
local their_new_user_id
while not their_new_user_id or playerbase.players[their_new_user_id] do
their_new_user_id = generate_new_user_id()
end
playerbase:update(their_new_user_id, self.name)
self:send({login_successful=true, new_user_id=their_new_user_id})
self.user_id = their_new_user_id
self.logged_in = true
print("Connection with name "..self.name.." was assigned a new user_id")
elseif not playerbase.players[self.user_id] then
deny_login(self, "The user_id provided was not found on this server")
print("Login failure: "..self.name.." specified an invalid user_id")
elseif playerbase.players[self.user_id] ~= self.name then
local the_old_name = playerbase.players[self.user_id]
playerbase:update(self.user_id, self.name)
self.logged_in = true
self:send({login_successful=true, name_changed=true , old_name=the_old_name, new_name=self.name})
print("Login successful and changed name "..the_old_name.." to "..self.name)
elseif playerbase.players[self.user_id] then
self.logged_in = true
self:send({login_successful=true})
else
deny_login(self, "Unknown")
end
if self.logged_in then
self:send(lobby_state())
end
return self.logged_in
end
--TODO: revisit this to determine whether it is good.
function deny_login(connection, reason)
local new_violation_count = 0
local IP, port = connection.socket:getsockname()
if is_banned(IP) then
--don't adjust ban_list
elseif ban_list[IP] and reason == "The user_id provided was not found on this server" then
ban_list[IP].violation_count = ban_list[IP].violation_count + 1
ban_list[IP].unban_time = os.time()+60*ban_list[IP].violation_count
elseif reason == "The user_id provided was not found on this server" then
ban_list[IP] = {violation_count=1, unban_time = os.time()+60}
else
ban_list[IP] = {violation_count=0, unban_time = os.time()}
end
ban_list[IP].user_name = connection.name or ""
ban_list[IP].reason = reason
connection:send({login_denied=true, reason=reason,
ban_duration=math.floor((ban_list[IP].unban_time-os.time())/60).."min"..((ban_list[IP].unban_time-os.time())%60).."sec",
violation_count = ban_list[IP].violation_count})
print("login denied. Reason: "..reason)
end
function unban(connection)
local IP, port = connection.socket:getsockname()
if ban_list[IP] then
ban_list[IP] = nil
end
end
function is_banned(IP)
local is_banned = false
if ban_list[IP] and ban_list[IP].unban_time - os.time() > 0 then
is_banned = true
end
return is_banned
end
function Connection.opponent_disconnected(self)
self.opponent = nil
self.state = "lobby"
lobby_changed = true
if self.room then
print("Closing room for "..(self.name or "nil").." because opponent disconnected.")
self.room:close()
end
self:send({leave_room = true})
end
function Connection.setup_game(self)
if self.state ~= "spectating" then
self.state = "playing"
end
lobby_changed = true --TODO: remove this line when we implement joining games in progress
self.vs_mode = true
self.metal = false
self.rows_left = 14+random(1,8)
self.prev_metal_col = nil
self.metal_col = nil
self.first_seven = nil
end
function Connection.close(self)
if self.state == "lobby" then
lobby_changed = true
end
if self.room and (self.room.a.name == self.name or self.room.b.name == self.name) then
print("about to close room for "..(self.name or "nil")..". Connection.close was called")
self.room:close()
elseif self.room then
self.room:remove_spectator(self)
end
clear_proposals(self.name)
if self.opponent then
self.opponent:opponent_disconnected()
end
if self.name then
name_to_idx[self.name] = nil
end
socket_to_idx[self.socket] = nil
connections[self.index] = nil
self.socket:close()
end
function Connection.H(self, version)
if version ~= VERSION then
self:send("N")
else
self:send("H")
end
end
function Connection.I(self, message)
if self.opponent then
self.opponent:send("I"..message)
if not self.room then
print("WARNING: missing room")
print(self.name)
print("doesn't have a room")
print("we are wondering if this disconnects spectators")
end
if self.player_number == 1 and self.room then
self.room:send_to_spectators("U"..message)
self.room.replay.vs.in_buf = self.room.replay.vs.in_buf..message
elseif self.player_number == 2 and self.room then
self.room:send_to_spectators("I"..message)
self.room.replay.vs.I = self.room.replay.vs.I..message
end
end
end
function Room.send_to_spectators(self, message)
--TODO: maybe try to do this in a different thread?
for k,v in pairs(self.spectators) do
if v then
v:send(message)
end
end
end
function Room.send(self, message)
if self.a then
self.a:send(message)
end
if self.b then
self.b:send(message)
end
self:send_to_spectators(message)
end
function Room.resolve_game_outcome(self)
--Note: return value is whether the outcome could be resolved
if not self.game_outcome_reports[1] or not self.game_outcome_reports[2] then
return false
else
local outcome = nil
if self.game_outcome_reports[1] ~= self.game_outcome_reports[2] then
--if clients disagree, the server needs to decide the outcome, perhaps by watching a replay it had created during the game.
--for now though...
print("clients "..self.a.name.." and "..self.b.name.." disagree on their game outcome. So the server will decide.")
outcome = 0
else
outcome = self.game_outcome_reports[1]
end
print("resolve_game_outcome says: "..outcome)
--outcome is the player number of the winner, or 0 for a tie
if self.a.save_replays_publicly ~= "not at all" and self.b.save_replays_publicly ~= "not at all" then
--use UTC time for dates on replays
local now = os.date("*t",to_UTC(os.time()))
local path = "ftp"..sep.."replays"..sep.."v"..VERSION..sep..string.format("%04d"..sep.."%02d"..sep.."%02d", now.year, now.month, now.day)
local rep_a_name, rep_b_name = self.a.name, self.b.name
if self.a.save_replays_publicly == "anonymously" then
rep_a_name = "anonymous"
self.replay.P1_name = "anonymous"
end
if self.b.save_replays_publicly == "anonymously" then
rep_b_name = "anonymous"
self.replay.P2_name = "anonymous"
end
--sort player names alphabetically for folder name so we don't have a folder "a-vs-b" and also "b-vs-a"
--don't switch to put "anonymous" first though
if rep_b_name < rep_a_name and rep_b_name ~= "anonymous" then
path = path..sep..rep_b_name.."-vs-"..rep_a_name
else
path = path..sep..rep_a_name.."-vs-"..rep_b_name
end
local filename = "v"..VERSION.."-"..string.format("%04d-%02d-%02d-%02d-%02d-%02d", now.year, now.month, now.day, now.hour, now.min, now.sec).."-"..rep_a_name.."-L"..self.replay.vs.P1_level.."-vs-"..rep_b_name.."-L"..self.replay.vs.P2_level
if self.replay.vs.ranked then
filename = filename.."-Ranked"
else
filename = filename.."-Casual"
end
if outcome == 1 or outcome == 2 then
filename = filename.."-P"..outcome.."wins"
elseif outcome == 0 then
filename = filename.."-draw"
end
filename = filename..".txt"
print("saving replay as "..path..sep..filename)
write_replay_file(self.replay, path, filename)
--write_replay_file(self.replay, "replay.txt")
else
print("replay not saved because a player didn't want it saved")
end
self.replay = nil
if outcome == 0 then
print("tie. Nobody scored")
--do nothing. no points or rating adjustments for ties.
return true
else
local someone_scored = false
for i=1,2,1--[[or Number of players if we implement more than 2 players]] do
print("checking if player "..i.." scored...")
if outcome == i then
print("Player "..i.." scored")
self.win_counts[i] = self.win_counts[i] + 1
adjust_ratings(self, i)
someone_scored = true
end
end
if someone_scored then
local msg = {win_counts=self.win_counts}
self.a:send(msg)
self.b:send(msg)
self:send_to_spectators(msg)
end
return true
end
end
end
function Room.rating_adjustment_approved(self)
--returns whether both players in the room have game states such that rating adjustment should be approved
local players = {self.a, self.b}
local reasons = {}
local caveats = {}
local prev_player_level = players[1].level
local both_players_are_placed = nil
if PLACEMENT_MATCHES_ENABLED then
if leaderboard.players[players[1].user_id] and leaderboard.players[players[1].user_id].placement_done
and leaderboard.players[players[2].user_id] and leaderboard.players[players[2].user_id].placement_done then
both_players_are_placed = true
--both players are placed on the leaderboard.
elseif not (leaderboard.players[players[1].user_id] and leaderboard.players[players[1].user_id].placement_done)
and not (leaderboard.players[players[2].user_id] and leaderboard.players[players[2].user_id].placement_done) then
reasons[#reasons+1] = "Neither player has finished enough placement matches against already ranked players"
end
else
both_players_are_placed = true
end
--don't let players too far apart in rating play ranked
local ratings = {}
for k,v in ipairs(players) do
if leaderboard.players[v.user_id] then
if not leaderboard.players[v.user_id].placement_done and leaderboard.players[v.user_id].placement_rating then
ratings[k] = leaderboard.players[v.user_id].placement_rating
elseif leaderboard.players[v.user_id].rating and leaderboard.players[v.user_id].rating ~= 0 then
ratings[k] = leaderboard.players[v.user_id].rating
else
ratings[k] = DEFAULT_RATING
end
else
ratings[k] = DEFAULT_RATING
end
end
if math.abs(ratings[1] - ratings[2]) > RATING_SPREAD_MODIFIER * ALLOWABLE_RATING_SPREAD_MULITPLIER then
reasons[#reasons+1] = "Players' ratings are too far apart"
end
local player_level_out_of_bounds_for_ranked = false
for i=1,2 do --we'll change 2 here when more players are allowed.
if (players[i].level < MIN_LEVEL_FOR_RANKED or players[i].level > MAX_LEVEL_FOR_RANKED) then
player_level_out_of_bounds_for_ranked = true
end
end
if player_level_out_of_bounds_for_ranked then
reasons[#reasons+1] = "Only levels between "..MIN_LEVEL_FOR_RANKED.." and "..MAX_LEVEL_FOR_RANKED.." are allowed for ranked play."
end
if players[1].level ~= players[2].level then
reasons[#reasons+1] = "Levels don't match"
end
for player_number = 1,2 do
if not playerbase.players[players[player_number].user_id] or not players[player_number].logged_in or playerbase.deleted_players[players[player_number].user_id]then
reasons[#reasons+1] = players[player_number].name.." didn't log in"
end
if not players[player_number].wants_ranked_match then
reasons[#reasons+1] = players[player_number].name.." doesn't want ranked"
end
end
if reasons[1] then
return false, reasons
else
if PLACEMENT_MATCHES_ENABLED and not both_players_are_placed
and
((leaderboard.players[players[1].user_id] and leaderboard.players[players[1].user_id].placement_done)
or (leaderboard.players[players[2].user_id] and leaderboard.players[players[2].user_id].placement_done)) then
caveats[#caveats+1] = "Note: Rating adjustments for these matches will be processed when the newcomer finishes placement."
end
return true, caveats
end
end
function calculate_rating_adjustment(Rc, Ro, Oa, k)
--[[ --Algorithm we are implementing, per community member Bbforky:
Formula for Calculating expected outcome:
RATING_SPREAD_MODIFIER = 400
Oe=1/(1+10^((Ro-Rc)/RATING_SPREAD_MODIFIER)))
Oe= Expected Outcome
Ro= Current rating of opponent
Rc= Current rating
Formula for Calculating new rating:
Rn=Rc+k(Oa-Oe)
Rn=New Rating
Oa=Actual Outcome (0 for loss, 1 for win)
k= Constant (Probably will use 10)
]]--
-- print("calculating expected outcome for")
-- print(players[player_number].name.." Ranking: "..leaderboard.players[players[player_number].user_id].rating)
-- print("vs")
-- print(players[player_number].opponent.name.." Ranking: "..leaderboard.players[players[player_number].opponent.user_id].rating)
Oe = 1/(1+10^((Ro-Rc)/RATING_SPREAD_MODIFIER))
-- print("expected outcome: "..Oe)
Rn = Rc + k*(Oa-Oe)
return Rn
end
function adjust_ratings(room, winning_player_number)
print("We'd be adjusting the rating of "..room.a.name.." and "..room.b.name..". Player "..winning_player_number.." wins!")
local players = {room.a, room.b}
local continue = true
local placement_match_progress
--check that it's ok to adjust ratings
continue, reasons = room:rating_adjustment_approved()
if continue then
room.ratings = {}
for player_number = 1,2 do
--if they aren't on the leaderboard yet, give them the default rating
if not leaderboard.players[players[player_number].user_id] or not leaderboard.players[players[player_number].user_id].rating then
leaderboard.players[players[player_number].user_id] = {user_name=playerbase.players[players[player_number].user_id], rating=DEFAULT_RATING}
print("Gave "..playerbase.players[players[player_number].user_id].." a new rating of "..DEFAULT_RATING)
if not PLACEMENT_MATCHES_ENABLED then
leaderboard.players[players[player_number].user_id].placement_done = true
end
write_leaderboard_file()
end
end
local placement_done = {}
for player_number = 1,2 do
placement_done[players[player_number].user_id] = leaderboard.players[players[player_number].user_id].placement_done
end
for player_number = 1,2 do
local k, Oa --max point change per match, actual outcome
room.ratings[player_number] = {}
if placement_done[players[player_number].user_id] == true then
k = 10
else
k = 50
end
if players[player_number].player_number == winning_player_number then
Oa = 1
else
Oa = 0
end
if placement_done[players[player_number].user_id] then
if placement_done[players[player_number].opponent.user_id] then
print("Player "..player_number.." played a non-placement ranked match. Updating his rating now.")
room.ratings[player_number].new = calculate_rating_adjustment(leaderboard.players[players[player_number].user_id].rating, leaderboard.players[players[player_number].opponent.user_id].rating, Oa, k)
else
print("Player "..player_number.." played ranked against an unranked opponent. We'll process this match when his opponent has finished placement")
room.ratings[player_number].placement_matches_played = leaderboard.players[players[player_number].user_id].ranked_games_played
room.ratings[player_number].new = round(leaderboard.players[players[player_number].user_id].rating)
room.ratings[player_number].old = round(leaderboard.players[players[player_number].user_id].rating)
room.ratings[player_number].difference = 0
end
else -- this player has not finished placement
if placement_done[players[player_number].opponent.user_id] then
print("Player "..player_number.." (unranked) just played a placement match against a ranked player.")
print("Adding this match to the list of matches to be processed when player finishes placement")
load_placement_matches(players[player_number].user_id)
local pm_count = #loaded_placement_matches.incomplete[players[player_number].user_id]
loaded_placement_matches.incomplete[players[player_number].user_id][pm_count+1] =
{ op_user_id=players[player_number].opponent.user_id,
op_name=playerbase.players[players[player_number].opponent.user_id],
op_rating=leaderboard.players[players[player_number].opponent.user_id].rating,
outcome = Oa}
print("PRINTING PLACEMENT MATCHES FOR USER")
print(json.encode(loaded_placement_matches.incomplete[players[player_number].user_id]))
write_user_placement_match_file(players[player_number].user_id,loaded_placement_matches.incomplete[players[player_number].user_id])
--adjust newcomer's placement_rating
if not leaderboard.players[players[player_number].user_id] then
leaderboard.players[players[player_number].user_id] = {}
end
leaderboard.players[players[player_number].user_id].placement_rating = calculate_rating_adjustment(leaderboard.players[players[player_number].user_id].placement_rating or DEFAULT_RATING, leaderboard.players[players[player_number].opponent.user_id].rating, Oa, PLACEMENT_MATCH_K)
print("New newcomer rating: "..leaderboard.players[players[player_number].user_id].placement_rating)
leaderboard.players[players[player_number].user_id].ranked_games_played = (leaderboard.players[players[player_number].user_id].ranked_games_played or 0) + 1
if Oa == 1 then
leaderboard.players[players[player_number].user_id].ranked_games_won = (leaderboard.players[players[player_number].user_id].ranked_games_won or 0) + 1
end
local process_them, reason = qualifies_for_placement(players[player_number].user_id)
if process_them then
local op_player_number = players[player_number].opponent.player_number
print("op_player_number: "..op_player_number)
room.ratings[player_number].old = 0
if not room.ratings[op_player_number] then
room.ratings[op_player_number] = {}
end
room.ratings[op_player_number].old = round(leaderboard.players[players[op_player_number].user_id].rating)
process_placement_matches(players[player_number].user_id)
room.ratings[player_number].new = round(leaderboard.players[players[player_number].user_id].rating)
room.ratings[player_number].difference = round(room.ratings[player_number].new - room.ratings[player_number].old)
room.ratings[player_number].league = get_league(room.ratings[player_number].new)
room.ratings[op_player_number].new
= round(leaderboard.
players
[players
[op_player_number]
.user_id].
rating)
room.ratings[op_player_number].difference = round(room.ratings[op_player_number].new - room.ratings[op_player_number].old)
room.ratings[op_player_number].league = get_league(room.ratings[player_number].new)
return
else
placement_match_progress = reason
end
else
print("Neither player is done with placement. We should not have gotten to this line of code")
end
if not process_them then
room.ratings[player_number].new = 0
room.ratings[player_number].old = 0
room.ratings[player_number].difference = 0
end
end
print("room.ratings["..player_number.."].new = "..(room.ratings[player_number].new or ""))
end
--check that both player's new room.ratings are numeric (and not nil)
if not process_them then
for player_number = 1,2 do
if tonumber(room.ratings[player_number].new) then
print()
continue = true
else
print(players[player_number].name.."'s new rating wasn't calculated properly. Not adjusting the rating for this match")
continue = false
end
end
end
if continue and not process_them then
--now that both new room.ratings have been calculated properly, actually update the leaderboard
for player_number = 1,2 do
print(playerbase.players[players[player_number].user_id])
print("Old rating:"..leaderboard.players[players[player_number].user_id].rating)
room.ratings[player_number].old = leaderboard.players[players[player_number].user_id].rating
leaderboard.players[players[player_number].user_id].ranked_games_played = (leaderboard.players[players[player_number].user_id].ranked_games_played or 0) + 1
leaderboard:update(players[player_number].user_id, room.ratings[player_number].new)
print("New rating:"..leaderboard.players[players[player_number].user_id].rating)
end
for player_number = 1,2 do
--round and calculate rating gain or loss (difference) to send to the clients
if placement_done[players[player_number].user_id] then
room.ratings[player_number].old
= round(room.ratings[player_number].old
or leaderboard.players[players[player_number].user_id].rating)
room.ratings[player_number].new
= round(room.ratings[player_number].new
or leaderboard.players[players[player_number].user_id].rating)
room.ratings[player_number].difference = room.ratings[player_number].new - room.ratings[player_number].old
else
room.ratings[player_number].old = 0
room.ratings[player_number].new = 0
room.ratings[player_number].difference = 0
room.ratings[player_number].placement_match_progress = placement_match_progress
end
room.ratings[player_number].league = get_league(room.ratings[player_number].new)
end
-- msg = {rating_updates=true, ratings=room.ratings, placement_match_progress=placement_match_progress}
-- room:send(msg)