-
Notifications
You must be signed in to change notification settings - Fork 84
/
Copy pathbanker.py
515 lines (440 loc) · 20.5 KB
/
banker.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
import socket
import threading
import os
import sys
import random
from style import MYCOLORS as COLORS, print_w_dots, choose_colorset
import screenspace as ss
import importlib
import gamemanager as gm
import networking as net
import validation as valid
import modules_directory.tictactoe as tictactoe
from modules_directory.deed_viewer import handle as handle_deed
from modules_directory.balance import handle as handle_balance
from modules_directory.casino import handle as handle_casino
import monopoly as mply
import select
from time import sleep
STARTING_CASH = 1500
clients = []
server_socket = None
port = 3131
num_players = 0
play_monopoly = True
handle_cmds = {}
TTT_Output = ss.OutputArea("TicTacToe", ss.TTT_OUTPUT_COORDINATES, 36, 9)
Casino_Output = ss.OutputArea("Casino", ss.CASINO_OUTPUT_COORDINATES, 36, 22)
Monopoly_Game_Output = ss.OutputArea("Monopoly", ss.MONOPOLY_OUTPUT_COORDINATES, 191, 6)
Main_Output = ss.OutputArea("Main", ss.MAIN_OUTPUT_COORDINATES, 79, 12)
class Client:
def __init__(self, socket: socket.socket, id: int, name: str, money: int, properties: list):
self.socket = socket
self.id = id
self.name = name
self.money = money
self.properties = properties
self.can_roll = True
self.num_rolls = 0
def add_to_output_area(output_type: str, text: str, color: str = COLORS.WHITE) -> None:
"""
Adds text to the specified output area.
This should replace all print statements in the code.
Args:
output_area (str): The output area to add text to.
text (str): The text to add.
Returns:
None
"""
if output_type == "Monopoly":
Monopoly_Game_Output.add_output(text, color)
elif output_type == "TicTacToe":
TTT_Output.add_output(text, color)
elif output_type == "Casino":
Casino_Output.add_output(text, color)
else:
Main_Output.add_output(text, color)
def start_server() -> socket.socket:
"""
Begins receiving server socket on local machine IP address and inputted port #.
Asks user for port # and begins server on the local machine at its IP address.
It then waits for a predetermined number of players to connect. Upon a full game,
it returns the transmitter socket.
Parameters: None
Returns: Transmitter socket aka the Banker's sender socket.
"""
global clients, port, server_socket
# Create a socket object
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
if "-local" in sys.argv:
ip_address = "localhost"
host = "localhost"
port = 33333
else:
# Get local machine name
host = socket.gethostname()
ip_address = socket.gethostbyname(host)
# Choose a port that is free
port = input("Choose a port, such as 3131: ")
while not valid.validate_port(port) or not valid.is_port_unused(int(port)):
port = input("Invalid port. Choose a port, such as 3131: ")
port = int(port) # Convert port to int for socket binding
# Bind to the port
server_socket.bind((host, port))
print_w_dots(f"Server started on {ip_address} port {port}")
server_socket.listen()
print_w_dots(f"Waiting for {num_players} clients...")
handshakes = [False] * num_players
game_full = False
while not game_full:
# Accepts connections while there are less than <num_players> players
if len(clients) < num_players:
client_socket, addr = server_socket.accept()
print(f"Got a connection from {addr}." if ss.VERBOSE else "Got a connection.")
client_handler = threading.Thread(target=handshake, args=(client_socket,handshakes))
client_handler.start()
else:
game_full = True
sleep(0.5)
print_w_dots("Game is full. Starting game...")
print_w_dots("")
print_w_dots("")
print_w_dots("")
# Send a message to each client that the game is starting, allowing them to see their terminals screen
for i in range(len(clients)):
clients[i].id = i
net.send_message(clients[i].socket, f"Game Start!{num_players} {i}")
sleep(0.5)
def start_receiver() -> None:
"""
This function handles all client-to-server requests (not the other way around).
Function binds an independent receiving socket at the same IP address, one port above.
For example, if the opened port was 3131, the receiver will open on 3132.
Parameters: None
Returns: None
"""
global player_data, port
add_to_output_area("Main", "[RECEIVER] Receiver started!", COLORS.GREEN)
# Credit to https://stackoverflow.com/a/43151772/19535084 for seamless server-client handling.
with socket.socket() as server:
host = socket.gethostname()
ip_address = socket.gethostbyname(host)
if "-local" in sys.argv:
ip_address = "localhost"
port = 33333
server.bind((ip_address,int(port+1)))
server.listen()
add_to_output_area("Main", f"[RECEIVER] Receiver accepting connections at {port+1}", COLORS.GREEN)
to_read = [server] # add server to list of readable sockets.
while True:
# check for a connection to the server or data ready from clients.
# readers will be empty on timeout.
readers,_,_ = select.select(to_read,[],[],0.1)
for reader in readers:
if reader is server:
player,address = reader.accept()
add_to_output_area("Main", f"Player connected from: {address[0]}", COLORS.GREEN)
to_read.append(player) # add client to list of readable sockets
else:
try:
data = net.receive_message(reader)
handle_data(data, reader)
except ConnectionResetError:
add_to_output_area("Main", f"Player at {address[0]} disconnected.", COLORS.RED)
to_read.remove(reader) # remove from monitoring
# TODO send a message to each player to query who is still connected, then properly remove
# the disconnected player from the game. Currently only removing the first player in clients list.
clients.pop(0)
# if not data: # No data indicates disconnect
# add_to_output_area("Main", f"Player at {address[0]} disconnected.", s.COLORS.RED)
# to_read.remove(reader) # remove from monitoring
if(len(to_read) == 1):
if "-stayopen" not in sys.argv:
add_to_output_area("Main", "[RECEIVER] All connections dropped. Receiver stopped.", COLORS.GREEN)
return
else:
add_to_output_area("Main", "[RECEIVER] All connections dropped. Receiver will stay open.", COLORS.GREEN)
# Reopen the server socket
server_socket.close()
start_server()
def set_unittest() -> None:
"""
Unit test function for the Banker module.
Add here as you think of more tests.
Parameters: None
Returns: None
"""
global num_players, STARTING_CASH, play_monopoly
ss.set_cursor_str(0, 0)
print(f"""
Enter to skip unit testing.
- Monopoly game will not start.
- num_players = 2
- STARTING_CASH = 1500
- No games added to the game manager.
Unit test -1: Create your own test.
- Set the number of players, starting cash to whatever you want.
- You may also indicate whether to start the Monopoly game or not.
Unit test 1:
- num_players = 1
- Starts the Monopoly game.
- STARTING_CASH = 2000
- Tests adding games to the game manager (1 Game).
Unit test 2:
- num_players = 2
- Starts the Monopoly game.
- STARTING_CASH = 1500
- Tests adding games to the game manager (4 Games).
Unit test 3:
- num_players = 4
- Does not start the Monopoly game.
- STARTING_CASH = 100
- No games added to the game manager.
Unit test 4 {COLORS.LIGHTBLUE}(Useful for locally testing modules without Monopoly){COLORS.RESET}:
- num_players = 1
- Does not start the Monopoly game.
- STARTING_CASH = 100
- No games added to the game manager.
Any other number will skip unit tests.
- Monopoly game will not start.
- num_players = 2
- STARTING_CASH = 1500
- No games added to the game manager.
""" if ss.VERBOSE else "")
if len(sys.argv) > 1:
if sys.argv[1].isdigit(): # If a test number is provided as a command line argument
test = int(sys.argv[1])
else:
test = ss.get_valid_int("Enter a test number: ", allowed=[' '])
else: # If no command line argument is provided, ask for a test number
test = ss.get_valid_int("Enter a test number: ", allowed=[' '])
if test == "":
play_monopoly = False
STARTING_CASH = 1500
num_players = 2
print("Skipping unit tests." if ss.VERBOSE else "")
return
if test == -1:
play_monopoly = ss.get_valid_int("Enter 1 to start Monopoly, 0 to skip: ", 0, 1) == 1
num_players = ss.get_valid_int("Enter the number of players: ")
STARTING_CASH = ss.get_valid_int("Enter the starting cash: ")
return
if (test == 1):
play_monopoly = True
num_players = 1
STARTING_CASH = 2000
gm.add_game(gm.Game('Fake Game', [Client(None, -1, "Null", 0, [])] * 4, 'board', 'other_data'))
elif (test == 2):
play_monopoly = True
num_players = 2
STARTING_CASH = 1500
gm.add_game(gm.Game('Battleship', [Client(None, -99, "Null", 0, [])] * 4, 'board', 'other_data'))
gm.add_game(gm.Game('Battleship', [Client(None, -98, None, 0, [])] * 2, 'board', 'other_data'))
gm.add_game(gm.Game('Battleship', [Client(None, -97, "Name", 0, [])] * 3, 'board', 'other_data'))
gm.add_game(gm.Game('TicTacToe', [Client(None, -96, "nada", 0, [])] * 2, 'board', None))
elif (test == 3):
play_monopoly = False
num_players = 4
STARTING_CASH = 100
elif (test == 4):
play_monopoly = False
num_players = 1
STARTING_CASH = 100
else:
play_monopoly = False
print("Invalid test number." if ss.VERBOSE else "")
print("Skipping unit tests." if ss.VERBOSE else "")
return
def change_balance(id: int, delta: int) -> int:
"""
Adjusts the balance of a specific player by a given amount.
This function updates the money attribute of the player identified by their ID.
A positive delta increases the player's balance, while a negative delta decreases it.
Args:
id (int): The unique identifier of the player whose balance needs to be adjusted.
delta (int): The amount to add or subtract from the player's balance.
Returns:
None
"""
clients[id].money += delta
return clients[id].money
def handle_data(data: str, client: socket.socket) -> None:
"""
Handles all data received from player sockets.
Parameters:
data (str): Data received from player sockets.
client (socket.socket): The client socket that sent the data.
Returns:
None
"""
current_client = None
try:
current_client = clients[int(data[0])] # Assume the data is prefixed by the client number AKA player_id.
data = data[1:]
except:
current_client = get_client_by_socket(client) # This is a backup in case the client data is not prefixed by client.
add_to_output_area("Main", f"Failed to get client from data. Data was not prefixed by client: {data}", COLORS.RED)
add_to_output_area("Main", f"Received data from {current_client.name}: \"{data}\"")
if data == 'request_board':
net.send_message(client, mply.get_gameboard())
elif data.startswith('mply'):
monopoly_game(current_client, data)
# elif data.startswith('ttt'):
# handle_ttt(data, current_client)
elif data.startswith('deed'):
handle_deed(data, client, mply)
elif data.startswith("bal"):
handle_balance(data, client, mply, current_client.money, current_client.properties)
elif data.startswith('casino'):
handle_casino(data, client, change_balance, add_to_output_area, current_client.id, current_client.name)
def handshake(client_socket: socket.socket, handshakes: list) -> None:
"""
As players connect, they attempt to handshake the server, this function handles that.
Player's name is also validated here. If an invalid (or empty) name is input, a default name is assigned.
Parameters:
client_socket (socket.socket) Server sender socket which players connect to at game initialization.
handshakes (list) Boolean list of successful handshakes. By default, all values are false.
Returns:
None
"""
global clients
# Attempt handshake
net.send_message(client_socket, "Welcome to the game!")
message = net.receive_message(client_socket)
if message.startswith("Connected!"):
handshakes[len(clients)-1] = True
name = message.split(',')[1]
clients.append(Client(client_socket, None, name, 2000, [])) # Append the client to the list of clients with a temporary id of None
else:
handshakes[len(clients)-1] = False
def get_client_by_socket(socket: socket.socket) -> Client:
"""
Returns the client object associated with the given socket.
Parameters:
socket (socket.socket): The socket of the client.
Returns:
obj (Client):
Client object associated with the given socket.
"""
for client in clients:
# Only checking the IP address for now. This will not work if two clients are on the same IP address.
# Think: locally testing. This has proven to be an issue while testing tic tac toe on the same machine.
# While this should work in a real-world scenario, it's not ideal for testing and is currently being
# ignored. TODO fix this. Not as simple as client.socket.getpeername()[1] == socket.getpeername()[1]
if client.socket.getpeername()[0] == socket.getpeername()[0]:
return client
def set_gamerules() -> None:
"""
Configure all gamerule variables according to Banker user input. Repeats until successful.
Parameters: None
Returns: None
"""
global STARTING_CASH, num_players
try:
STARTING_CASH = ss.get_valid_int("Enter the amount of money each player starts with: ")
num_players = ss.get_valid_int("Enter the number of players: ")
except:
print("Failed to set gamerules. Try again.")
input()
set_gamerules()
def monopoly_controller(unit_test) -> None:
"""
Controls the flow of the Monopoly game.
This function initializes the Monopoly game, waits for players to connect,
and then enters a loop to manage turns. It sends the game board to the
current player and prompts them to roll the dice.
This function does nothing if a Monopoly game is not set to play during Banker setup.
Returns:
None
"""
add_to_output_area("Monopoly", "About to start Monopoly game.")
if not play_monopoly:
add_to_output_area("Monopoly", "No players in the game. Not attempting to run Monopoly.")
return
sleep(5) # Temporary sleep to give all players time to connect to the receiver TODO remove this and implement a better way to check all are connected to rcvr
mply.unittest(unit_test)
net.send_monopoly(clients[mply.turn].socket, mply.get_gameboard() + ss.set_cursor_str(0, 38) + "Welcome to Monopoly! It's your turn. Type roll to roll the dice.")
add_to_output_area("Monopoly", "Sent gameboard to player 0.")
last_turn = 0
while True:
sleep(1)
if mply.turn != last_turn:
ss.set_cursor(0, 20)
last_turn = mply.turn
net.send_monopoly(clients[mply.turn].socket, mply.get_gameboard() + ss.set_cursor_str(0, 38) + "It's your turn. Type roll to roll the dice.")
clients[mply.turn].can_roll = True
ss.set_cursor(ss.MONOPOLY_OUTPUT_COORDINATES[0]+1, ss.MONOPOLY_OUTPUT_COORDINATES[1]+1)
add_to_output_area("Monopoly", f"Player turn: {mply.turn}. Sent gameboard to {clients[mply.turn].name}.")
def monopoly_game(client: Client = None, cmd: str = None) -> None:
"""
Description:
This is the main game loop for Monopoly.
It will be called from the main function in its own thread.
Notes:
Monopoly command looks like this: "mply,(action),(specific data),(even more specific data),(etc)"
player_roll all happens on the player side, so the player can handle all of that.
All data during player_roll will be sent to banker like the following:
recv_message() -> handle_data() -> monopoly_game()
Where monopoly_game parses the data and banker does not need to send anything back.
Now for player_choice, banker and player will do a bit more back and forth.
Most of the game logic can be handled on the player side, but banker will
have to preface the messages with cash, properties, etc.
"""
dice = (0, -1)
if mply.players[mply.turn].name == client.name: # Check if the client who sent data is the current player
#TODO restrict name values so identical names are disallowed
action = cmd.split(',')[1]
if action == None or action == '':
ret_val = mply.request_roll()
net.send_monopoly(client.socket, ret_val)
elif action == 'roll' and client.can_roll:
dice = mply.roll()
client.num_rolls += 1
ret_val = mply.process_roll(client.num_rolls, dice)
if ret_val.startswith("player_choice"):
ret_val.replace("player_choice", "")
client.can_roll = False
net.send_monopoly(client.socket, ret_val)
elif action == 'trybuy': #TODO Better handling of locations would be nice.
mply.buy_logic("banker", "b")
ret_val = mply.get_gameboard()
# Need to check if doubles were rolled, otherwise end the rolling phase
if dice[0] != dice[1]:
client.can_roll = False
net.send_monopoly(client.socket, ret_val)
elif action == 'propmgmt': #TODO This is almost complete. Still somewhat buggy.
try:
property_id = cmd.split(',')[2]
except:
property_id = ""
ret_val = mply.housing_logic(mply.players[0], "banker", property_id)
net.send_monopoly(client.socket, ret_val)
elif action == 'deed': #TODO This is not yet complete. Very buggy.
try:
property_id = cmd.split(',')[2]
except:
property_id = ""
mply.update_status(mply.players[0], "deed", [], "banker", property_id)
elif action == 'continue':
ret_val = mply.get_gameboard()
net.send_monopoly(client.socket, ret_val)
elif action == 'endturn':
mply.end_turn()
ret_val = "ENDOFTURN" + mply.get_gameboard()
net.send_monopoly(client.socket, ret_val)
if __name__ == "__main__":
os.system('cls' if os.name == 'nt' else 'clear')
print("Welcome to Terminal Monopoly, Banker!")
if "-skipcalib" not in sys.argv and "-local" not in sys.argv:
ss.calibrate_screen('banker')
if "-silent" in sys.argv:
ss.VERBOSE = False
set_unittest()
# set_gamerules()
start_server()
choose_colorset("DEFAULT_COLORS")
ss.print_banker_frames()
monopoly_unit_test = 6 # assume 1 player, 2 owned properties. See monopoly.py unittest for more options
game = mply.start_game(STARTING_CASH, num_players, [clients[i].name for i in range(num_players)])
threading.Thread(target=monopoly_controller, args=[monopoly_unit_test], daemon=True).start()
start_receiver()