Add standalone autopilot players script
This commit is contained in:
parent
44b9ed1ab1
commit
5aa96d2163
1 changed files with 570 additions and 0 deletions
570
monop_players.py
Normal file
570
monop_players.py
Normal file
|
|
@ -0,0 +1,570 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Autopilot players for monop-irc.
|
||||||
|
|
||||||
|
Spawns one IRC connection per player. Each player watches monop bot output
|
||||||
|
and responds appropriately to keep the game moving. Players are not
|
||||||
|
sophisticated — they just roll, buy when affordable, and handle prompts.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 monop_players.py [--host HOST] [--port PORT] [--channel CHAN] [--players alice,bob]
|
||||||
|
"""
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
import random
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
HOST = "127.0.0.1"
|
||||||
|
PORT = 6667
|
||||||
|
CHANNEL = "#monop"
|
||||||
|
PREFIX = "."
|
||||||
|
BOT_NICK = "monop"
|
||||||
|
|
||||||
|
# How long to wait before responding (avoids flooding)
|
||||||
|
RESPONSE_DELAY = 0.8
|
||||||
|
|
||||||
|
|
||||||
|
class PlayerBot:
|
||||||
|
"""A single IRC-connected monop player."""
|
||||||
|
|
||||||
|
def __init__(self, nick, channel, host, port, player_names, player_index):
|
||||||
|
self.nick = nick
|
||||||
|
self.channel = channel
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.player_names = player_names
|
||||||
|
self.player_index = player_index # which player number we are (0-based)
|
||||||
|
self.num_players = len(player_names)
|
||||||
|
|
||||||
|
self.sock = None
|
||||||
|
self.buffer = ""
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
|
# Game state tracking (minimal — just enough to respond correctly)
|
||||||
|
self.setup_phase = True
|
||||||
|
self.setup_registrations_seen = 0
|
||||||
|
self.current_player = None # whose turn it is
|
||||||
|
self.my_money = 1500
|
||||||
|
self.in_jail = False
|
||||||
|
self.jail_turns = 0
|
||||||
|
self.in_debt = False
|
||||||
|
self.in_auction = False
|
||||||
|
self.auction_bid = 0
|
||||||
|
self.awaiting_prompt = None # what kind of prompt we're waiting to answer
|
||||||
|
self.game_started = False
|
||||||
|
self.game_over = False
|
||||||
|
self.rolled_this_turn = False
|
||||||
|
# Properties we own (just names, for mortgage decisions)
|
||||||
|
self.my_properties = []
|
||||||
|
self.mortgaged = set()
|
||||||
|
# Track if we already responded to current prompt
|
||||||
|
self._prompt_answered = False
|
||||||
|
# Seen "goes first" yet
|
||||||
|
self._first_player_announced = False
|
||||||
|
|
||||||
|
def log(self, msg):
|
||||||
|
print(f"[{self.nick}] {msg}", flush=True)
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
"""Connect to IRC and join channel."""
|
||||||
|
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
self.sock.connect((self.host, self.port))
|
||||||
|
self._send(f"NICK {self.nick}")
|
||||||
|
self._send(f"USER {self.nick} 0 * :{self.nick}")
|
||||||
|
|
||||||
|
# Wait for registration
|
||||||
|
deadline = time.time() + 15
|
||||||
|
while time.time() < deadline:
|
||||||
|
data = self.sock.recv(4096)
|
||||||
|
if not data:
|
||||||
|
raise ConnectionError("Connection closed during registration")
|
||||||
|
self.buffer += data.decode("utf-8", errors="replace")
|
||||||
|
# Handle pings during registration
|
||||||
|
while "\r\n" in self.buffer:
|
||||||
|
line, self.buffer = self.buffer.split("\r\n", 1)
|
||||||
|
if line.startswith("PING"):
|
||||||
|
self._send("PONG" + line[4:])
|
||||||
|
if " 001 " in self.buffer:
|
||||||
|
break
|
||||||
|
|
||||||
|
self._send(f"JOIN {self.channel}")
|
||||||
|
time.sleep(0.3)
|
||||||
|
self.buffer = ""
|
||||||
|
self.log(f"Connected and joined {self.channel}")
|
||||||
|
|
||||||
|
def _send(self, line):
|
||||||
|
with self.lock:
|
||||||
|
self.sock.sendall((line + "\r\n").encode())
|
||||||
|
|
||||||
|
def say(self, msg):
|
||||||
|
"""Send a prefixed message to the game channel."""
|
||||||
|
self._send(f"PRIVMSG {self.channel} :{PREFIX}{msg}")
|
||||||
|
self.log(f" -> .{msg}")
|
||||||
|
|
||||||
|
def say_delayed(self, msg, delay=None):
|
||||||
|
"""Send after a short delay (avoids flooding, more natural)."""
|
||||||
|
d = delay if delay is not None else RESPONSE_DELAY
|
||||||
|
def _do():
|
||||||
|
time.sleep(d)
|
||||||
|
self.say(msg)
|
||||||
|
threading.Thread(target=_do, daemon=True).start()
|
||||||
|
|
||||||
|
def is_my_turn(self):
|
||||||
|
return (self.current_player and
|
||||||
|
self.current_player.lower() == self.nick.lower())
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Main loop: read IRC messages and respond."""
|
||||||
|
self.connect()
|
||||||
|
while not self.game_over:
|
||||||
|
try:
|
||||||
|
data = self.sock.recv(4096)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
self.buffer += data.decode("utf-8", errors="replace")
|
||||||
|
while "\r\n" in self.buffer:
|
||||||
|
line, self.buffer = self.buffer.split("\r\n", 1)
|
||||||
|
self._handle_irc_line(line)
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"Error: {e}")
|
||||||
|
break
|
||||||
|
self.log("Exiting")
|
||||||
|
|
||||||
|
def _handle_irc_line(self, line):
|
||||||
|
"""Handle a raw IRC line."""
|
||||||
|
if line.startswith("PING"):
|
||||||
|
self._send("PONG" + line[4:])
|
||||||
|
return
|
||||||
|
|
||||||
|
# Parse PRIVMSG from monop bot
|
||||||
|
m = re.match(r':(\S+?)!\S+\s+PRIVMSG\s+(\S+)\s+:(.+)', line)
|
||||||
|
if not m:
|
||||||
|
return
|
||||||
|
|
||||||
|
sender = m.group(1)
|
||||||
|
target = m.group(2)
|
||||||
|
message = m.group(3).strip()
|
||||||
|
|
||||||
|
if target.lower() != self.channel.lower():
|
||||||
|
return
|
||||||
|
if sender.lower() != BOT_NICK.lower():
|
||||||
|
return
|
||||||
|
|
||||||
|
self._handle_bot_msg(message)
|
||||||
|
|
||||||
|
def _handle_bot_msg(self, msg):
|
||||||
|
"""Process a message from the monop bot."""
|
||||||
|
if not msg:
|
||||||
|
return
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# SETUP PHASE
|
||||||
|
# ============================================================
|
||||||
|
if self.setup_phase:
|
||||||
|
if self._handle_setup(msg):
|
||||||
|
return
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# GAME OVER
|
||||||
|
# ============================================================
|
||||||
|
if "WINS!!!!!" in msg or msg == "The party is over.":
|
||||||
|
self.game_over = True
|
||||||
|
self.log(f"Game over: {msg}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# CHECKPOINT LINE: "{name} ({N}) (cash ${M}) on {square}"
|
||||||
|
# This marks the START of someone's turn.
|
||||||
|
# ============================================================
|
||||||
|
m = re.match(
|
||||||
|
r'^(.+?)\s+\((\d+)\)\s+\(cash\s+\$(-?\d+)\)\s+on\s+(.+)$', msg
|
||||||
|
)
|
||||||
|
if m:
|
||||||
|
name = m.group(1)
|
||||||
|
money = int(m.group(3))
|
||||||
|
self.current_player = name
|
||||||
|
self.in_debt = False
|
||||||
|
self.awaiting_prompt = None
|
||||||
|
self._prompt_answered = False
|
||||||
|
self.rolled_this_turn = False
|
||||||
|
|
||||||
|
if name.lower() == self.nick.lower():
|
||||||
|
self.my_money = money
|
||||||
|
self.in_jail = False
|
||||||
|
self.jail_turns = 0
|
||||||
|
|
||||||
|
if self.is_my_turn():
|
||||||
|
self.say_delayed("roll")
|
||||||
|
self.rolled_this_turn = True
|
||||||
|
return
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# JAIL DETECTION
|
||||||
|
# ============================================================
|
||||||
|
m = re.match(r'^\(This is your (\w+) turn in JAIL\)$', msg)
|
||||||
|
if m:
|
||||||
|
if self.is_my_turn():
|
||||||
|
self.in_jail = True
|
||||||
|
ordinal = m.group(1)
|
||||||
|
if ordinal == "3rd":
|
||||||
|
self.jail_turns = 3
|
||||||
|
elif ordinal == "2nd":
|
||||||
|
self.jail_turns = 2
|
||||||
|
else:
|
||||||
|
self.jail_turns = 1
|
||||||
|
# Try rolling doubles
|
||||||
|
self.say_delayed("roll")
|
||||||
|
return
|
||||||
|
|
||||||
|
if msg == "Double roll gets you out.":
|
||||||
|
if self.is_my_turn():
|
||||||
|
self.in_jail = False
|
||||||
|
return
|
||||||
|
|
||||||
|
if msg.startswith("It's your third turn"):
|
||||||
|
# Forced to pay $50
|
||||||
|
if self.is_my_turn():
|
||||||
|
self.in_jail = False
|
||||||
|
return
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# DOUBLES — ROLL AGAIN
|
||||||
|
# ============================================================
|
||||||
|
m = re.match(r'^(.+?) rolled doubles\.\s+Goes again$', msg)
|
||||||
|
if m:
|
||||||
|
name = m.group(1)
|
||||||
|
if name.lower() == self.nick.lower():
|
||||||
|
self.rolled_this_turn = False
|
||||||
|
self.say_delayed("roll")
|
||||||
|
self.rolled_this_turn = True
|
||||||
|
return
|
||||||
|
|
||||||
|
# Triple doubles -> jail
|
||||||
|
if msg == "That's 3 doubles. You go to jail":
|
||||||
|
if self.is_my_turn():
|
||||||
|
self.in_jail = True
|
||||||
|
return
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# BUY PROPERTY
|
||||||
|
# ============================================================
|
||||||
|
if msg == "Do you want to buy?":
|
||||||
|
if self.is_my_turn():
|
||||||
|
# Buy if we can afford it (we'll always say yes for simplicity)
|
||||||
|
self.say_delayed("yes")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Track what we bought
|
||||||
|
m = re.match(r'^That would cost \$(\d+)$', msg)
|
||||||
|
if m:
|
||||||
|
# Just informational
|
||||||
|
return
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# AUCTION
|
||||||
|
# ============================================================
|
||||||
|
if msg.startswith("So it goes up for auction"):
|
||||||
|
self.in_auction = True
|
||||||
|
self.auction_bid = 0
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.in_auction:
|
||||||
|
# Bid prompt: "name:" (just the name followed by colon)
|
||||||
|
m = re.match(r'^(.+?)\s*:\s*$', msg)
|
||||||
|
if m:
|
||||||
|
name = m.group(1).strip()
|
||||||
|
if name.lower() == self.nick.lower():
|
||||||
|
if self.auction_bid == 0:
|
||||||
|
# Open with a small bid
|
||||||
|
self.say_delayed("1")
|
||||||
|
elif self.auction_bid < min(200, self.my_money // 2):
|
||||||
|
bid = self.auction_bid + random.randint(5, 20)
|
||||||
|
self.say_delayed(str(bid))
|
||||||
|
else:
|
||||||
|
# Drop out
|
||||||
|
self.say_delayed("0")
|
||||||
|
return
|
||||||
|
|
||||||
|
m = re.match(r'^You must bid higher than (\d+)', msg)
|
||||||
|
if m:
|
||||||
|
self.auction_bid = int(m.group(1))
|
||||||
|
return
|
||||||
|
|
||||||
|
if msg.startswith("It goes to") or msg.startswith("Nobody seems"):
|
||||||
|
self.in_auction = False
|
||||||
|
return
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# YES/NO PROMPTS
|
||||||
|
# ============================================================
|
||||||
|
if msg in (
|
||||||
|
"Do you want to mortgage it?",
|
||||||
|
"Do you want to unmortgage it?",
|
||||||
|
"Is that ok?",
|
||||||
|
):
|
||||||
|
if self.is_my_turn():
|
||||||
|
self.say_delayed("yes")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Trade confirmation: "{name}, is the trade ok?"
|
||||||
|
m = re.match(r'^(.+?), is the trade ok\?$', msg)
|
||||||
|
if m:
|
||||||
|
name = m.group(1)
|
||||||
|
if name.lower() == self.nick.lower():
|
||||||
|
self.say_delayed("yes")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Resign confirmation
|
||||||
|
if msg == "Do you really want to resign?":
|
||||||
|
if self.is_my_turn():
|
||||||
|
self.say_delayed("yes")
|
||||||
|
return
|
||||||
|
|
||||||
|
if msg.startswith("Who do you wish to resign to?"):
|
||||||
|
if self.is_my_turn():
|
||||||
|
self.say_delayed("bank")
|
||||||
|
return
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# DEBT / FORCED MORTGAGE
|
||||||
|
# ============================================================
|
||||||
|
if msg == "How are you going to fix it up?":
|
||||||
|
self.in_debt = True
|
||||||
|
if self.is_my_turn():
|
||||||
|
self.say_delayed("mortgage")
|
||||||
|
return
|
||||||
|
|
||||||
|
if msg == "-- You are now Solvent ---":
|
||||||
|
self.in_debt = False
|
||||||
|
return
|
||||||
|
|
||||||
|
if msg == "that leaves you broke":
|
||||||
|
return
|
||||||
|
|
||||||
|
if msg == "You don't have any un-mortgaged property.":
|
||||||
|
if self.is_my_turn() and self.in_debt:
|
||||||
|
self.say_delayed("sell houses")
|
||||||
|
return
|
||||||
|
|
||||||
|
if msg == "You don't have any houses to sell!!":
|
||||||
|
if self.is_my_turn() and self.in_debt:
|
||||||
|
self.say_delayed("resign")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Mortgage property selection
|
||||||
|
if msg == "Which property do you want to mortgage?":
|
||||||
|
# monop will present options via getinp; it takes the property
|
||||||
|
# name. We'll wait for the valid inputs prompt.
|
||||||
|
self.awaiting_prompt = "mortgage_choice"
|
||||||
|
return
|
||||||
|
|
||||||
|
if msg == "Which property do you want to unmortgage?":
|
||||||
|
self.awaiting_prompt = "unmortgage_choice"
|
||||||
|
return
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# TAX CHOICE
|
||||||
|
# ============================================================
|
||||||
|
# "Do you wish to lose 10% of your total worth or $200? "
|
||||||
|
if msg.startswith("Do you wish to lose 10"):
|
||||||
|
if self.is_my_turn():
|
||||||
|
# 10% is usually cheaper early game
|
||||||
|
self.say_delayed("percentage")
|
||||||
|
return
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# HOUSE BUYING/SELLING
|
||||||
|
# ============================================================
|
||||||
|
# "How many houses do you wish to buy for" / "sell from"
|
||||||
|
if msg.startswith("How many houses do you wish to"):
|
||||||
|
# We don't buy/sell houses proactively, but if prompted
|
||||||
|
# during debt resolution, respond with 0 or done
|
||||||
|
return
|
||||||
|
|
||||||
|
# Property prompt during house buy/sell: "{name} ({n}):" or "{name} (H):"
|
||||||
|
m = re.match(r'^(.+?)\s+\((?:\d+|H)\)\s*:\s*$', msg)
|
||||||
|
if m:
|
||||||
|
if self.is_my_turn():
|
||||||
|
# Don't buy/sell any houses
|
||||||
|
self.say_delayed("0")
|
||||||
|
return
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# COMMAND PROMPT
|
||||||
|
# ============================================================
|
||||||
|
if msg == "-- Command:":
|
||||||
|
if self.is_my_turn():
|
||||||
|
if self.in_debt:
|
||||||
|
self.say_delayed("mortgage")
|
||||||
|
elif not self.rolled_this_turn:
|
||||||
|
self.say_delayed("roll")
|
||||||
|
self.rolled_this_turn = True
|
||||||
|
else:
|
||||||
|
# Already rolled, just end turn (empty/done)
|
||||||
|
self.say_delayed("roll")
|
||||||
|
return
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# VALID INPUTS (help/error recovery)
|
||||||
|
# ============================================================
|
||||||
|
if msg.startswith("Valid inputs are:"):
|
||||||
|
if self.is_my_turn():
|
||||||
|
options_str = msg[len("Valid inputs are:"):].strip()
|
||||||
|
options = [o.strip().strip("'\"") for o in options_str.split(",")]
|
||||||
|
self.log(f"Valid options: {options}")
|
||||||
|
# Pick first sensible option
|
||||||
|
for opt in options:
|
||||||
|
opt_l = opt.lower().strip()
|
||||||
|
if opt_l in ("quit", "save", "restore", ""):
|
||||||
|
continue
|
||||||
|
if opt_l == "done":
|
||||||
|
self.say_delayed("done")
|
||||||
|
return
|
||||||
|
# For yes/no, pick yes
|
||||||
|
if opt_l == "yes":
|
||||||
|
self.say_delayed("yes")
|
||||||
|
return
|
||||||
|
if opt_l == "no":
|
||||||
|
self.say_delayed("no")
|
||||||
|
return
|
||||||
|
# For property/player names, pick first
|
||||||
|
self.say_delayed(opt)
|
||||||
|
return
|
||||||
|
return
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# ILLEGAL ACTION — wrong player tried to act
|
||||||
|
# ============================================================
|
||||||
|
m = re.match(
|
||||||
|
r"^Illegal action: bad player \((.+?)'s turn, not (.+?)\)$", msg
|
||||||
|
)
|
||||||
|
if m:
|
||||||
|
correct = m.group(1)
|
||||||
|
wrong = m.group(2)
|
||||||
|
self.current_player = correct
|
||||||
|
self.log(f"Turn correction: {correct}'s turn (not {wrong})")
|
||||||
|
# If it's now our turn, roll
|
||||||
|
if self.is_my_turn() and not self.rolled_this_turn:
|
||||||
|
self.say_delayed("roll", delay=1.5)
|
||||||
|
self.rolled_this_turn = True
|
||||||
|
return
|
||||||
|
|
||||||
|
# Illegal response
|
||||||
|
if msg.startswith('Illegal response:'):
|
||||||
|
self.log(f"Illegal response error: {msg}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# OWNERSHIP TRACKING (for mortgage decisions)
|
||||||
|
# ============================================================
|
||||||
|
# "That got you $X" (mortgage succeeded)
|
||||||
|
m = re.match(r'^That got you \$(\d+)$', msg)
|
||||||
|
if m:
|
||||||
|
return
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# EVERYTHING ELSE — ignore silently
|
||||||
|
# ============================================================
|
||||||
|
# Card text, rent notifications, lucky messages, etc.
|
||||||
|
# We don't need to respond to these.
|
||||||
|
|
||||||
|
def _handle_setup(self, msg):
|
||||||
|
"""Handle setup phase. Returns True if message was handled."""
|
||||||
|
|
||||||
|
if msg == "How many players?":
|
||||||
|
# Only the first player sends the number
|
||||||
|
if self.player_index == 0:
|
||||||
|
self.say_delayed(str(self.num_players))
|
||||||
|
return True
|
||||||
|
|
||||||
|
if msg.startswith("Sorry. Number must range from"):
|
||||||
|
if self.player_index == 0:
|
||||||
|
self.say_delayed(str(self.num_players))
|
||||||
|
return True
|
||||||
|
|
||||||
|
# "Player N, say ''me'' please."
|
||||||
|
m = re.match(r"^Player (\d+), say ''me'' please\.$", msg)
|
||||||
|
if m:
|
||||||
|
player_num = int(m.group(1))
|
||||||
|
# Player N is 1-indexed, our index is 0-indexed
|
||||||
|
if player_num - 1 == self.player_index:
|
||||||
|
self.say_delayed(self.nick, delay=1.0)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Duplicate name error
|
||||||
|
if "the same person" in msg:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Roll results during setup
|
||||||
|
m = re.match(r'^(.+?)\s+\((\d+)\)\s+rolls\s+(\d+)$', msg)
|
||||||
|
if m:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Re-roll for ties
|
||||||
|
if "rolled the same thing" in msg:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Who goes first — end of setup
|
||||||
|
m = re.match(r'^(.+?)\s+\((\d+)\)\s+goes first$', msg)
|
||||||
|
if m:
|
||||||
|
self.setup_phase = False
|
||||||
|
self.game_started = True
|
||||||
|
self.current_player = m.group(1)
|
||||||
|
self.log(f"Game started! {self.current_player} goes first.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Monop autopilot players")
|
||||||
|
parser.add_argument("--host", default=HOST)
|
||||||
|
parser.add_argument("--port", type=int, default=PORT)
|
||||||
|
parser.add_argument("--channel", default=CHANNEL)
|
||||||
|
parser.add_argument(
|
||||||
|
"--players", default="alice,bob",
|
||||||
|
help="Comma-separated player names"
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
player_names = [n.strip() for n in args.players.split(",")]
|
||||||
|
print(f"Starting {len(player_names)} autopilot players: {player_names}")
|
||||||
|
print(f"Connecting to {args.host}:{args.port} {args.channel}")
|
||||||
|
|
||||||
|
threads = []
|
||||||
|
bots = []
|
||||||
|
|
||||||
|
for i, name in enumerate(player_names):
|
||||||
|
bot = PlayerBot(
|
||||||
|
nick=name,
|
||||||
|
channel=args.channel,
|
||||||
|
host=args.host,
|
||||||
|
port=args.port,
|
||||||
|
player_names=player_names,
|
||||||
|
player_index=i,
|
||||||
|
)
|
||||||
|
bots.append(bot)
|
||||||
|
t = threading.Thread(target=bot.run, daemon=True, name=f"player-{name}")
|
||||||
|
threads.append(t)
|
||||||
|
|
||||||
|
# Stagger connections slightly to avoid race conditions
|
||||||
|
for i, t in enumerate(threads):
|
||||||
|
t.start()
|
||||||
|
time.sleep(1.0)
|
||||||
|
|
||||||
|
# Wait for all to finish (or ctrl-c)
|
||||||
|
try:
|
||||||
|
while any(t.is_alive() for t in threads):
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nShutting down...")
|
||||||
|
for bot in bots:
|
||||||
|
bot.game_over = True
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in a new issue