Skip to content

Commit 3895aa6

Browse files
committed
Update
1 parent b3a5d6a commit 3895aa6

20 files changed

+298
-70
lines changed

AIService/Server.py

+47-27
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
from threading import Lock
2-
from typing import List
1+
from typing import Tuple
32
import grpc
43
from concurrent import futures
54

@@ -10,15 +9,17 @@
109
from ScriptsOfTribute.enums import PatronId
1110

1211
class AIService(main_pb2_grpc.AIServiceServicer):
13-
def __init__(self, ai: BaseAI, server_instance):
12+
def __init__(self, ai: BaseAI, server_instance, engine_service_port:int):
1413
self.ai = ai
1514
self.server_instance = server_instance
15+
self.engine_service_port = engine_service_port
1616
self.engine_service_stub = None
1717

18-
def set_engine_service_stub(self, stub):
19-
self.engine_service_stub = stub
20-
2118
def RegisterBot(self, request, context):
19+
engine_service_channel1 = grpc.insecure_channel(f"localhost:{self.engine_service_port}")
20+
self.engine_service_stub = main_pb2_grpc.EngineServiceStub(engine_service_channel1)
21+
print(f"Registering {self.ai.bot_name}")
22+
print(f"localhost:{self.engine_service_port}")
2223
return main_pb2.RegistrationStatus(name=self.ai.bot_name, message="")
2324

2425
def PregamePrepare(self, request, context):
@@ -32,10 +33,11 @@ def SelectPatron(self, request, context):
3233

3334
def Play(self, request, context):
3435
game_state = build_game_state(request.gameState, self.engine_service_stub)
35-
# game_state.debug_print()
36+
#game_state.debug_print()
3637
moves = [from_proto_move(proto_move) for proto_move in request.possibleMoves]
37-
move = self.ai.play(game_state, moves)
38-
return move.to_proto()
38+
move = self.ai.play(game_state, moves, request.remainingTimeMs).to_proto()
39+
# print(move)
40+
return move
3941

4042
def GameEnd(self, request, context):
4143
self.ai.game_end(request)
@@ -53,36 +55,54 @@ class Server:
5355

5456
def __init__(self):
5557
self.active_bots = 0
58+
self.server = None
5659
def add_bot(self):
5760
self.active_bots += 1
58-
print(f"Bot connected. Active bots: {self.active_bots}")
61+
#print(f"Bot connected. Active bots: {self.active_bots}")
5962

6063
def bot_disconnected(self):
6164
self.active_bots -= 1
62-
print(f"Bot disconnected. Active bots: {self.active_bots}")
65+
#print(f"Bot disconnected. Active bots: {self.active_bots}")
6366
if self.active_bots == 0:
6467
self.shutdown_server()
6568

6669
def shutdown_server(self):
6770
if self.server:
68-
print("No active bots. Shutting down the server...")
71+
#print("No active bots. Shutting down the server...")
6972
self.server.stop(0)
7073
else:
7174
print("Server is already stopped.")
7275

73-
def run_grpc_server(self, ai_instances: List[BaseAI], port=50000, debug_prints=True):
74-
self.server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
75-
for i, ai in enumerate(ai_instances):
76-
self.add_bot()
77-
ai_service = AIService(ai, self)
78-
assigned_port = port + i
79-
self.server.add_insecure_port(f"localhost:{assigned_port}")
80-
engine_service_channel = grpc.insecure_channel(f"localhost:{assigned_port}")
81-
engine_service_stub = main_pb2_grpc.EngineServiceStub(engine_service_channel)
82-
if debug_prints:
83-
print(f"Bot {ai.bot_name} listening on localhost:{assigned_port}")
84-
ai_service.set_engine_service_stub(engine_service_stub)
76+
def run_grpc_server(
77+
bot1: BaseAI | None,
78+
bot2: BaseAI | None,
79+
base_client_ports: Tuple[int, int]=(50000, 50001),
80+
base_server_ports: Tuple[int, int]=(49000, 49001),
81+
debug_prints=True
82+
):
83+
if bot1 is not None:
84+
server1 = Server()
85+
server1.server = grpc.server(futures.ThreadPoolExecutor(max_workers=5))
86+
server1.add_bot()
87+
ai_service1 = AIService(bot1, server1, base_server_ports[0])
88+
server1.server.add_insecure_port(f"localhost:{base_client_ports[0]}")
89+
main_pb2_grpc.add_AIServiceServicer_to_server(ai_service1, server1.server)
90+
if debug_prints:
91+
print(f"Bot {bot1.bot_name} listening on localhost:{base_client_ports[0]}, channel for engine service open on: {base_server_ports[0]}")
92+
server1.server.start()
93+
94+
if bot2 is not None:
95+
server2 = Server()
96+
server2.server = grpc.server(futures.ThreadPoolExecutor(max_workers=5))
97+
server2.add_bot()
98+
ai_service2 = AIService(bot2, server2, base_server_ports[1])
99+
server2.server.add_insecure_port(f"localhost:{base_client_ports[1]}")
100+
main_pb2_grpc.add_AIServiceServicer_to_server(ai_service2, server2.server)
101+
if debug_prints:
102+
print(f"Bot {bot2.bot_name} listening on localhost:{base_client_ports[1]}, channel for engine service open on: {base_server_ports[1]}")
103+
server2.server.start()
85104

86-
main_pb2_grpc.add_AIServiceServicer_to_server(ai_service, self.server)
87-
self.server.start()
88-
return self.server
105+
if bot1 is not None:
106+
server1.server.wait_for_termination()
107+
if bot2 is not None:
108+
server2.server.wait_for_termination()

AIService/base_ai.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def pregame_prepare(self):
1414
def select_patron(self, available_patrons: List[PatronId]):
1515
raise NotImplementedError
1616

17-
def play(self, game_state: GameState, possible_moves: List[BasicMove]) -> BasicMove:
17+
def play(self, game_state: GameState, possible_moves: List[BasicMove], remaining_time: int) -> BasicMove:
1818
raise NotImplementedError
1919

2020
def game_end(self, final_state):

Bots/MaxPrestigeBot.py

+36-6
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,54 @@
11
import random
22

33
from AIService.base_ai import BaseAI
4+
from ScriptsOfTribute.enums import PlayerEnum, MoveEnum
5+
from ScriptsOfTribute.move import BasicMove
46

57
class MaxPrestigeBot(BaseAI):
8+
9+
def __init__(self, bot_name):
10+
super().__init__(bot_name)
11+
self.player_id: PlayerEnum = PlayerEnum.NO_PLAYER_SELECTED
12+
self.start_of_game: bool = True
613

714
def select_patron(self, available_patrons):
8-
#print(f"MaxPrestigeBot select patron: {available_patrons}")
9-
return random.choice(available_patrons)
15+
pick = random.choice(available_patrons)
16+
return pick
1017

11-
def play(self, game_state, possible_moves):
18+
def play(self, game_state, possible_moves, remaining_time):
1219
best_move = None
13-
best_prestige = -1
20+
best_move_val = -1
21+
if self.start_of_game:
22+
self.player_id = game_state.current_player.player_id
23+
self.start_of_game = False
1424

1525
for first_move in possible_moves:
1626
new_game_state, new_moves = game_state.apply_move(first_move)
1727

28+
if new_game_state.end_game_state is not None: # check if game is over, if we win we are fine with this move
29+
if new_game_state.end_game_state.winner == self.player_id:
30+
return first_move
31+
32+
if len(new_moves) == 1 and new_moves[0].command == MoveEnum.END_TURN: # if there are no moves possible then lets just check value of this game state
33+
curr_val = new_game_state.current_player.prestige + new_game_state.current_player.power
34+
if curr_val > best_move_val:
35+
best_move = first_move
36+
best_move_val = curr_val
37+
1838
for second_move in new_moves:
39+
if second_move.command == MoveEnum.END_TURN:
40+
continue
1941
final_game_state, _ = new_game_state.apply_move(second_move)
20-
21-
return random.choice(possible_moves)
42+
if final_game_state.end_game_state is not None:
43+
if final_game_state.end_game_state.winner == self.player_id:
44+
return second_move
45+
curr_val = final_game_state.current_player.prestige + final_game_state.current_player.power
46+
if curr_val > best_move_val:
47+
best_move = first_move
48+
best_move_val = curr_val
49+
if best_move is None:
50+
return BasicMove(command=MoveEnum.END_TURN)
51+
return best_move
2252

2353
def game_end(self, final_state):
2454
pass

Bots/RandomBot.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
class RandomBot(BaseAI):
66

77
def select_patron(self, available_patrons):
8-
print("RandomBot select patron")
8+
#print("RandomBot select patron")
99
return random.choice(available_patrons)
1010

11-
def play(self, game_state, possible_moves):
11+
def play(self, game_state, possible_moves, remaining_time):
1212
# game_state.debug_print()
13-
return random.choice(possible_moves)
13+
pick = random.choice(possible_moves)
14+
return pick
1415

1516
def game_end(self, final_state):
1617
pass

Game/game.py

+85-19
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,105 @@
1+
import atexit
2+
import multiprocessing
3+
4+
import os
5+
import signal
6+
import time
17
from typing import List
28
from AIService.base_ai import BaseAI
39
from Game.runner import run_game_runner
4-
from AIService.Server import Server
10+
from AIService.Server import run_grpc_server
511

612
class Game:
713
def __init__(self):
814
self.bots: List[BaseAI] = []
15+
self.processes: List[multiprocessing.Process] = []
16+
17+
atexit.register(self._cleanup_processes)
918

1019
def register_bot(self, bot_instance: BaseAI):
20+
# We register bots here, because some run invokes might involve C# native bots, so we always pass str there
1121
self.bots.append(bot_instance)
1222

13-
def run(self, bot1Name: str, bot2Name: str, start_game_runner=True, runs=1, threads=1, enable_logs="NONE", log_destination="", seed=None, timeout=30):
14-
bots = filter(lambda bot: bot.bot_name == bot1Name or bot.bot_name == bot2Name, self.bots)
23+
def run(
24+
self,
25+
bot1Name: str,
26+
bot2Name: str,
27+
start_game_runner=True,
28+
runs=1,
29+
threads=1,
30+
enable_logs="NONE",
31+
log_destination="",
32+
seed=None,
33+
timeout=30,
34+
base_client_port=50000,
35+
base_server_port=49000
36+
):
37+
bot1 = next((bot for bot in self.bots if bot.bot_name == bot1Name), None)
38+
bot2 = next((bot for bot in self.bots if bot.bot_name == bot2Name), None)
39+
self.processes = []
40+
if bot1 is not None or bot2 is not None:
41+
self.processes.extend(self._run_bot_instances(bot1, bot2, threads, base_client_port, base_server_port))
1542
if start_game_runner:
43+
time.sleep(2) # give servers some time to start
1644
if any([bot1Name == bot.bot_name for bot in self.bots]):
1745
bot1Name = "grpc:" + bot1Name
1846
if any([bot2Name == bot.bot_name for bot in self.bots]):
1947
bot2Name = "grpc:" + bot2Name
20-
run_game_runner(
21-
bot1Name,
22-
bot2Name,
23-
runs=runs,
24-
threads=threads,
25-
enable_logs=enable_logs,
26-
log_destination=log_destination,
27-
seed=seed,
28-
timeout=timeout
48+
game_runner_process = multiprocessing.Process(
49+
target=run_game_runner,
50+
name='GameRunner',
51+
args=(bot1Name, bot2Name, runs, threads, enable_logs, log_destination, seed, timeout),
52+
daemon=True
2953
)
30-
31-
def _run_bot_instances(self, bots: List[BaseAI]):
32-
server = Server()
33-
grpc_server = server.run_grpc_server(bots)
34-
54+
game_runner_process.start()
55+
self.processes.append(game_runner_process)
3556
try:
36-
grpc_server.wait_for_termination()
57+
for p in self.processes:
58+
p.join() # Wait for all processes to finish
59+
print(f'Finished {p.name}')
3760
except KeyboardInterrupt:
38-
grpc_server.close()
3961
print("Server interrupted by user.")
62+
for p in self.processes:
63+
p.terminate() # Terminate all processes on interruption
64+
65+
def _run_bot_instances(
66+
self,
67+
bot1: BaseAI | None,
68+
bot2: BaseAI | None,
69+
num_threads: int,
70+
base_client_port: int,
71+
base_server_port: int,
72+
):
73+
processes = []
74+
for i in range(num_threads):
75+
client_port1 = base_client_port + i
76+
server_port1 = base_server_port + i
77+
client_port2 = base_client_port + num_threads + i
78+
server_port2 = base_server_port + num_threads + i
79+
80+
p = multiprocessing.Process(
81+
target=run_grpc_server,
82+
name=f"{bot1.bot_name if bot1 else 'C# bot'} - {bot2.bot_name if bot2 else 'C# bot'} on {(client_port1, client_port2)}, {(server_port1, server_port2)}",
83+
args=(bot1, bot2, (client_port1, client_port2), (server_port1, server_port2)),
84+
#daemon=True
85+
)
86+
p.start()
87+
processes.append(p)
88+
89+
return processes
90+
91+
92+
def _cleanup_processes(self):
93+
print("Cleaning up all processes...")
94+
95+
for p in self.processes:
96+
if p.is_alive():
97+
print(f"Terminating {p.name} (PID {p.pid})")
98+
p.terminate()
99+
p.join(timeout=5)
100+
101+
if p.is_alive():
102+
print(f"Forcing kill on {p.name} (PID {p.pid})")
103+
os.kill(p.pid, signal.SIGKILL)
104+
105+
self.processes.clear()

Game/runner.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def run_game_runner(bot1: str, bot2: str, runs=1, threads=1, enable_logs="NONE",
2222
if seed:
2323
args += ["-s", str(seed)]
2424
args += ["-to", str(timeout)]
25-
25+
print(f'Running: {args}')
2626
try:
2727
result = subprocess.run(
2828
args,

GameRunner/GameRunner.dll

0 Bytes
Binary file not shown.

GameRunner/GameRunner.exe

0 Bytes
Binary file not shown.

GameRunner/GameRunner.pdb

-16 Bytes
Binary file not shown.

GameRunner/appsettings.json

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Warning",
5+
"Microsoft": "Warning",
6+
"Microsoft.Hosting.Lifetime": "None"
7+
}
8+
}
9+
}

GameRunner/gRPC.dll

1 KB
Binary file not shown.

GameRunner/gRPC.pdb

-120 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)