Fix income tax handling, improve player detection, add monop_server and play_bot scripts
This commit is contained in:
parent
a02632e2a2
commit
98b4bd26ca
7 changed files with 1385 additions and 78 deletions
|
|
@ -4,6 +4,6 @@
|
||||||
"tls": true,
|
"tls": true,
|
||||||
"nick": "monopbot",
|
"nick": "monopbot",
|
||||||
"channel": "#monop-dev",
|
"channel": "#monop-dev",
|
||||||
"game_nick": "monop",
|
"game_nick": "monopoly",
|
||||||
"state_file": "../game-state.json"
|
"state_file": "../site/game-state.json"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,8 @@ class MonopBot:
|
||||||
nick = event.source.nick.lower() if event.source else ""
|
nick = event.source.nick.lower() if event.source else ""
|
||||||
msg = event.arguments[0] if event.arguments else ""
|
msg = event.arguments[0] if event.arguments else ""
|
||||||
|
|
||||||
|
print(f"[MSG] <{nick}> {msg[:80]}", flush=True)
|
||||||
|
|
||||||
# We care about messages from the game bot
|
# We care about messages from the game bot
|
||||||
if nick == self.game_nick or self._is_game_output(nick, msg):
|
if nick == self.game_nick or self._is_game_output(nick, msg):
|
||||||
self._process_game_line(msg)
|
self._process_game_line(msg)
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,24 @@ class MonopParser:
|
||||||
|
|
||||||
changed = False
|
changed = False
|
||||||
|
|
||||||
|
# --- Player detection from status lines ---
|
||||||
|
# "Alice (1) (cash $1500) on === GO ===" or "Alice (1) rolls 10"
|
||||||
|
m = re.match(r"(\w+) \((\d+)\) (?:rolls \d+|\(cash \$(\d+)\) on (.+))", line)
|
||||||
|
if m:
|
||||||
|
name = m.group(1)
|
||||||
|
num = int(m.group(2))
|
||||||
|
idx, player = self._find_player_by_name(name)
|
||||||
|
if idx is None:
|
||||||
|
# Auto-add player
|
||||||
|
self.add_player(name, num)
|
||||||
|
idx = len(self.players) - 1
|
||||||
|
player = self.players[idx]
|
||||||
|
if m.group(3):
|
||||||
|
player["money"] = int(m.group(3))
|
||||||
|
self.current_player_idx = idx
|
||||||
|
self._add_log(f"{name}'s turn", name)
|
||||||
|
return True
|
||||||
|
|
||||||
# --- Player setup ---
|
# --- Player setup ---
|
||||||
# "How many players? " -> game init
|
# "How many players? " -> game init
|
||||||
m = re.match(r"How many players\?", line)
|
m = re.match(r"How many players\?", line)
|
||||||
|
|
|
||||||
|
|
@ -1,103 +1,911 @@
|
||||||
{
|
{
|
||||||
"lastUpdated": "2026-02-20T21:30:00Z",
|
|
||||||
"currentPlayer": 1,
|
|
||||||
"players": [
|
"players": [
|
||||||
{
|
{
|
||||||
"name": "Alice",
|
"name": "Alice",
|
||||||
"number": 1,
|
"number": 1,
|
||||||
"money": 920,
|
"money": 1375,
|
||||||
"location": 24,
|
"location": 15,
|
||||||
"inJail": false,
|
"inJail": false,
|
||||||
"jailTurns": 0,
|
"jailTurns": 0,
|
||||||
"goJailFreeCards": 1,
|
"goJailFreeCards": 0,
|
||||||
"properties": [1, 3, 6, 8, 9, 24],
|
"properties": [],
|
||||||
"numRailroads": 0,
|
"numRailroads": 0,
|
||||||
"numUtilities": 0
|
"numUtilities": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Bob",
|
"name": "Bob",
|
||||||
"number": 2,
|
"number": 2,
|
||||||
"money": 1150,
|
"money": 890,
|
||||||
"location": 39,
|
"location": 35,
|
||||||
"inJail": false,
|
"inJail": false,
|
||||||
"jailTurns": 0,
|
"jailTurns": 0,
|
||||||
"goJailFreeCards": 0,
|
"goJailFreeCards": 0,
|
||||||
"properties": [5, 11, 15, 28, 37, 39],
|
"properties": [],
|
||||||
"numRailroads": 2,
|
"numRailroads": 0,
|
||||||
"numUtilities": 1
|
"numUtilities": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Charlie",
|
"name": "Charlie",
|
||||||
"number": 3,
|
"number": 3,
|
||||||
"money": 480,
|
"money": 1255,
|
||||||
"location": 10,
|
"location": 24,
|
||||||
"inJail": true,
|
"inJail": false,
|
||||||
"jailTurns": 2,
|
"jailTurns": 0,
|
||||||
"goJailFreeCards": 0,
|
"goJailFreeCards": 0,
|
||||||
"properties": [16, 18, 19, 25, 31, 32],
|
"properties": [],
|
||||||
"numRailroads": 1,
|
"numRailroads": 0,
|
||||||
"numUtilities": 0
|
"numUtilities": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"currentPlayer": 2,
|
||||||
"squares": [
|
"squares": [
|
||||||
{"id":0,"name":"Go","type":"safe","owner":-1,"cost":0,"mortgaged":false,"houses":0,"monopoly":false,"group":null,"rent":[0]},
|
{
|
||||||
{"id":1,"name":"Mediterranean Ave.","type":"property","owner":0,"cost":60,"mortgaged":false,"houses":3,"monopoly":true,"group":"purple","rent":[2,10,30,90,160,250]},
|
"id": 0,
|
||||||
{"id":2,"name":"Community Chest","type":"cc","owner":-1,"cost":0,"mortgaged":false,"houses":0,"monopoly":false,"group":null,"rent":[0]},
|
"name": "Go",
|
||||||
{"id":3,"name":"Baltic Ave.","type":"property","owner":0,"cost":60,"mortgaged":false,"houses":2,"monopoly":true,"group":"purple","rent":[4,20,60,180,320,450]},
|
"type": "safe",
|
||||||
{"id":4,"name":"Income Tax","type":"tax","owner":-1,"cost":0,"mortgaged":false,"houses":0,"monopoly":false,"group":null,"rent":[0]},
|
"owner": -1,
|
||||||
{"id":5,"name":"Reading Railroad","type":"railroad","owner":1,"cost":200,"mortgaged":false,"houses":0,"monopoly":false,"group":"railroad","rent":[0]},
|
"cost": 0,
|
||||||
{"id":6,"name":"Oriental Ave.","type":"property","owner":0,"cost":100,"mortgaged":false,"houses":0,"monopoly":true,"group":"lightblue","rent":[6,30,90,270,400,550]},
|
"mortgaged": false,
|
||||||
{"id":7,"name":"Chance","type":"chance","owner":-1,"cost":0,"mortgaged":false,"houses":0,"monopoly":false,"group":null,"rent":[0]},
|
"houses": 0,
|
||||||
{"id":8,"name":"Vermont Ave.","type":"property","owner":0,"cost":100,"mortgaged":false,"houses":0,"monopoly":true,"group":"lightblue","rent":[6,30,90,270,400,550]},
|
"monopoly": false,
|
||||||
{"id":9,"name":"Connecticut Ave.","type":"property","owner":0,"cost":120,"mortgaged":false,"houses":1,"monopoly":true,"group":"lightblue","rent":[8,40,100,300,450,600]},
|
"group": null,
|
||||||
{"id":10,"name":"Just Visiting","type":"jail","owner":-1,"cost":0,"mortgaged":false,"houses":0,"monopoly":false,"group":null,"rent":[0]},
|
"rent": [
|
||||||
{"id":11,"name":"St. Charles Place","type":"property","owner":1,"cost":140,"mortgaged":false,"houses":0,"monopoly":false,"group":"pink","rent":[10,50,150,450,625,750]},
|
0
|
||||||
{"id":12,"name":"Electric Company","type":"utility","owner":-1,"cost":150,"mortgaged":false,"houses":0,"monopoly":false,"group":"utility","rent":[0]},
|
]
|
||||||
{"id":13,"name":"States Ave.","type":"property","owner":-1,"cost":140,"mortgaged":false,"houses":0,"monopoly":false,"group":"pink","rent":[10,50,150,450,625,750]},
|
},
|
||||||
{"id":14,"name":"Virginia Ave.","type":"property","owner":-1,"cost":160,"mortgaged":false,"houses":0,"monopoly":false,"group":"pink","rent":[12,60,180,500,700,900]},
|
{
|
||||||
{"id":15,"name":"Pennsylvania Railroad","type":"railroad","owner":1,"cost":200,"mortgaged":false,"houses":0,"monopoly":false,"group":"railroad","rent":[0]},
|
"id": 1,
|
||||||
{"id":16,"name":"St. James Place","type":"property","owner":2,"cost":180,"mortgaged":false,"houses":4,"monopoly":true,"group":"orange","rent":[14,70,200,550,750,950]},
|
"name": "Mediterranean Ave.",
|
||||||
{"id":17,"name":"Community Chest","type":"cc","owner":-1,"cost":0,"mortgaged":false,"houses":0,"monopoly":false,"group":null,"rent":[0]},
|
"type": "property",
|
||||||
{"id":18,"name":"Tennessee Ave.","type":"property","owner":2,"cost":180,"mortgaged":false,"houses":3,"monopoly":true,"group":"orange","rent":[14,70,200,550,750,950]},
|
"owner": -1,
|
||||||
{"id":19,"name":"New York Ave.","type":"property","owner":2,"cost":200,"mortgaged":false,"houses":2,"monopoly":true,"group":"orange","rent":[16,80,220,600,800,1000]},
|
"cost": 60,
|
||||||
{"id":20,"name":"Free Parking","type":"safe","owner":-1,"cost":0,"mortgaged":false,"houses":0,"monopoly":false,"group":null,"rent":[0]},
|
"mortgaged": false,
|
||||||
{"id":21,"name":"Kentucky Ave.","type":"property","owner":-1,"cost":220,"mortgaged":false,"houses":0,"monopoly":false,"group":"red","rent":[18,90,250,700,875,1050]},
|
"houses": 0,
|
||||||
{"id":22,"name":"Chance","type":"chance","owner":-1,"cost":0,"mortgaged":false,"houses":0,"monopoly":false,"group":null,"rent":[0]},
|
"monopoly": false,
|
||||||
{"id":23,"name":"Indiana Ave.","type":"property","owner":-1,"cost":220,"mortgaged":false,"houses":0,"monopoly":false,"group":"red","rent":[18,90,250,700,875,1050]},
|
"group": "purple",
|
||||||
{"id":24,"name":"Illinois Ave.","type":"property","owner":0,"cost":240,"mortgaged":true,"houses":0,"monopoly":false,"group":"red","rent":[20,100,300,750,925,1100]},
|
"rent": [
|
||||||
{"id":25,"name":"B&O Railroad","type":"railroad","owner":2,"cost":200,"mortgaged":false,"houses":0,"monopoly":false,"group":"railroad","rent":[0]},
|
2,
|
||||||
{"id":26,"name":"Atlantic Ave.","type":"property","owner":-1,"cost":260,"mortgaged":false,"houses":0,"monopoly":false,"group":"yellow","rent":[22,110,330,800,975,1150]},
|
10,
|
||||||
{"id":27,"name":"Ventnor Ave.","type":"property","owner":-1,"cost":260,"mortgaged":false,"houses":0,"monopoly":false,"group":"yellow","rent":[22,110,330,800,975,1150]},
|
30,
|
||||||
{"id":28,"name":"Water Works","type":"utility","owner":1,"cost":150,"mortgaged":false,"houses":0,"monopoly":false,"group":"utility","rent":[0]},
|
90,
|
||||||
{"id":29,"name":"Marvin Gardens","type":"property","owner":-1,"cost":280,"mortgaged":false,"houses":0,"monopoly":false,"group":"yellow","rent":[24,120,360,850,1025,1200]},
|
160,
|
||||||
{"id":30,"name":"Go to Jail","type":"gotojail","owner":-1,"cost":0,"mortgaged":false,"houses":0,"monopoly":false,"group":null,"rent":[0]},
|
250
|
||||||
{"id":31,"name":"Pacific Ave.","type":"property","owner":2,"cost":300,"mortgaged":false,"houses":0,"monopoly":false,"group":"green","rent":[26,130,390,900,1100,1275]},
|
]
|
||||||
{"id":32,"name":"North Carolina Ave.","type":"property","owner":2,"cost":300,"mortgaged":false,"houses":0,"monopoly":false,"group":"green","rent":[26,130,390,900,1100,1275]},
|
},
|
||||||
{"id":33,"name":"Community Chest","type":"cc","owner":-1,"cost":0,"mortgaged":false,"houses":0,"monopoly":false,"group":null,"rent":[0]},
|
{
|
||||||
{"id":34,"name":"Pennsylvania Ave.","type":"property","owner":-1,"cost":320,"mortgaged":false,"houses":0,"monopoly":false,"group":"green","rent":[28,150,450,1000,1200,1400]},
|
"id": 2,
|
||||||
{"id":35,"name":"Short Line Railroad","type":"railroad","owner":-1,"cost":200,"mortgaged":false,"houses":0,"monopoly":false,"group":"railroad","rent":[0]},
|
"name": "Community Chest",
|
||||||
{"id":36,"name":"Chance","type":"chance","owner":-1,"cost":0,"mortgaged":false,"houses":0,"monopoly":false,"group":null,"rent":[0]},
|
"type": "cc",
|
||||||
{"id":37,"name":"Park Place","type":"property","owner":1,"cost":350,"mortgaged":false,"houses":0,"monopoly":false,"group":"darkblue","rent":[35,175,500,1100,1300,1500]},
|
"owner": -1,
|
||||||
{"id":38,"name":"Luxury Tax","type":"tax","owner":-1,"cost":0,"mortgaged":false,"houses":0,"monopoly":false,"group":null,"rent":[0]},
|
"cost": 0,
|
||||||
{"id":39,"name":"Boardwalk","type":"property","owner":1,"cost":400,"mortgaged":false,"houses":5,"monopoly":true,"group":"darkblue","rent":[50,200,600,1400,1700,2000]}
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": null,
|
||||||
|
"rent": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"name": "Baltic Ave.",
|
||||||
|
"type": "property",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 60,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "purple",
|
||||||
|
"rent": [
|
||||||
|
4,
|
||||||
|
20,
|
||||||
|
60,
|
||||||
|
180,
|
||||||
|
320,
|
||||||
|
450
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"name": "Income Tax",
|
||||||
|
"type": "tax",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 0,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": null,
|
||||||
|
"rent": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"name": "Reading Railroad",
|
||||||
|
"type": "railroad",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 200,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "railroad",
|
||||||
|
"rent": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"name": "Oriental Ave.",
|
||||||
|
"type": "property",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 100,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "lightblue",
|
||||||
|
"rent": [
|
||||||
|
6,
|
||||||
|
30,
|
||||||
|
90,
|
||||||
|
270,
|
||||||
|
400,
|
||||||
|
550
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"name": "Chance",
|
||||||
|
"type": "chance",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 0,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": null,
|
||||||
|
"rent": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"name": "Vermont Ave.",
|
||||||
|
"type": "property",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 100,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "lightblue",
|
||||||
|
"rent": [
|
||||||
|
6,
|
||||||
|
30,
|
||||||
|
90,
|
||||||
|
270,
|
||||||
|
400,
|
||||||
|
550
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"name": "Connecticut Ave.",
|
||||||
|
"type": "property",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 120,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "lightblue",
|
||||||
|
"rent": [
|
||||||
|
8,
|
||||||
|
40,
|
||||||
|
100,
|
||||||
|
300,
|
||||||
|
450,
|
||||||
|
600
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"name": "Just Visiting",
|
||||||
|
"type": "jail",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 0,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": null,
|
||||||
|
"rent": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"name": "St. Charles Place",
|
||||||
|
"type": "property",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 140,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "pink",
|
||||||
|
"rent": [
|
||||||
|
10,
|
||||||
|
50,
|
||||||
|
150,
|
||||||
|
450,
|
||||||
|
625,
|
||||||
|
750
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"name": "Electric Company",
|
||||||
|
"type": "utility",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 150,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "utility",
|
||||||
|
"rent": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13,
|
||||||
|
"name": "States Ave.",
|
||||||
|
"type": "property",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 140,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "pink",
|
||||||
|
"rent": [
|
||||||
|
10,
|
||||||
|
50,
|
||||||
|
150,
|
||||||
|
450,
|
||||||
|
625,
|
||||||
|
750
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 14,
|
||||||
|
"name": "Virginia Ave.",
|
||||||
|
"type": "property",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 160,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "pink",
|
||||||
|
"rent": [
|
||||||
|
12,
|
||||||
|
60,
|
||||||
|
180,
|
||||||
|
500,
|
||||||
|
700,
|
||||||
|
900
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 15,
|
||||||
|
"name": "Pennsylvania Railroad",
|
||||||
|
"type": "railroad",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 200,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "railroad",
|
||||||
|
"rent": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 16,
|
||||||
|
"name": "St. James Place",
|
||||||
|
"type": "property",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 180,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "orange",
|
||||||
|
"rent": [
|
||||||
|
14,
|
||||||
|
70,
|
||||||
|
200,
|
||||||
|
550,
|
||||||
|
750,
|
||||||
|
950
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 17,
|
||||||
|
"name": "Community Chest",
|
||||||
|
"type": "cc",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 0,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": null,
|
||||||
|
"rent": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 18,
|
||||||
|
"name": "Tennessee Ave.",
|
||||||
|
"type": "property",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 180,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "orange",
|
||||||
|
"rent": [
|
||||||
|
14,
|
||||||
|
70,
|
||||||
|
200,
|
||||||
|
550,
|
||||||
|
750,
|
||||||
|
950
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 19,
|
||||||
|
"name": "New York Ave.",
|
||||||
|
"type": "property",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 200,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "orange",
|
||||||
|
"rent": [
|
||||||
|
16,
|
||||||
|
80,
|
||||||
|
220,
|
||||||
|
600,
|
||||||
|
800,
|
||||||
|
1000
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 20,
|
||||||
|
"name": "Free Parking",
|
||||||
|
"type": "safe",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 0,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": null,
|
||||||
|
"rent": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 21,
|
||||||
|
"name": "Kentucky Ave.",
|
||||||
|
"type": "property",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 220,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "red",
|
||||||
|
"rent": [
|
||||||
|
18,
|
||||||
|
90,
|
||||||
|
250,
|
||||||
|
700,
|
||||||
|
875,
|
||||||
|
1050
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 22,
|
||||||
|
"name": "Chance",
|
||||||
|
"type": "chance",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 0,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": null,
|
||||||
|
"rent": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23,
|
||||||
|
"name": "Indiana Ave.",
|
||||||
|
"type": "property",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 220,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "red",
|
||||||
|
"rent": [
|
||||||
|
18,
|
||||||
|
90,
|
||||||
|
250,
|
||||||
|
700,
|
||||||
|
875,
|
||||||
|
1050
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 24,
|
||||||
|
"name": "Illinois Ave.",
|
||||||
|
"type": "property",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 240,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "red",
|
||||||
|
"rent": [
|
||||||
|
20,
|
||||||
|
100,
|
||||||
|
300,
|
||||||
|
750,
|
||||||
|
925,
|
||||||
|
1100
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 25,
|
||||||
|
"name": "B&O Railroad",
|
||||||
|
"type": "railroad",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 200,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "railroad",
|
||||||
|
"rent": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 26,
|
||||||
|
"name": "Atlantic Ave.",
|
||||||
|
"type": "property",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 260,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "yellow",
|
||||||
|
"rent": [
|
||||||
|
22,
|
||||||
|
110,
|
||||||
|
330,
|
||||||
|
800,
|
||||||
|
975,
|
||||||
|
1150
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 27,
|
||||||
|
"name": "Ventnor Ave.",
|
||||||
|
"type": "property",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 260,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "yellow",
|
||||||
|
"rent": [
|
||||||
|
22,
|
||||||
|
110,
|
||||||
|
330,
|
||||||
|
800,
|
||||||
|
975,
|
||||||
|
1150
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 28,
|
||||||
|
"name": "Water Works",
|
||||||
|
"type": "utility",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 150,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "utility",
|
||||||
|
"rent": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 29,
|
||||||
|
"name": "Marvin Gardens",
|
||||||
|
"type": "property",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 280,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "yellow",
|
||||||
|
"rent": [
|
||||||
|
24,
|
||||||
|
120,
|
||||||
|
360,
|
||||||
|
850,
|
||||||
|
1025,
|
||||||
|
1200
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 30,
|
||||||
|
"name": "Go to Jail",
|
||||||
|
"type": "gotojail",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 0,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": null,
|
||||||
|
"rent": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 31,
|
||||||
|
"name": "Pacific Ave.",
|
||||||
|
"type": "property",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 300,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "green",
|
||||||
|
"rent": [
|
||||||
|
26,
|
||||||
|
130,
|
||||||
|
390,
|
||||||
|
900,
|
||||||
|
1100,
|
||||||
|
1275
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 32,
|
||||||
|
"name": "North Carolina Ave.",
|
||||||
|
"type": "property",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 300,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "green",
|
||||||
|
"rent": [
|
||||||
|
26,
|
||||||
|
130,
|
||||||
|
390,
|
||||||
|
900,
|
||||||
|
1100,
|
||||||
|
1275
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 33,
|
||||||
|
"name": "Community Chest",
|
||||||
|
"type": "cc",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 0,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": null,
|
||||||
|
"rent": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 34,
|
||||||
|
"name": "Pennsylvania Ave.",
|
||||||
|
"type": "property",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 320,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "green",
|
||||||
|
"rent": [
|
||||||
|
28,
|
||||||
|
150,
|
||||||
|
450,
|
||||||
|
1000,
|
||||||
|
1200,
|
||||||
|
1400
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 35,
|
||||||
|
"name": "Short Line Railroad",
|
||||||
|
"type": "railroad",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 200,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "railroad",
|
||||||
|
"rent": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 36,
|
||||||
|
"name": "Chance",
|
||||||
|
"type": "chance",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 0,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": null,
|
||||||
|
"rent": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 37,
|
||||||
|
"name": "Park Place",
|
||||||
|
"type": "property",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 350,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "darkblue",
|
||||||
|
"rent": [
|
||||||
|
35,
|
||||||
|
175,
|
||||||
|
500,
|
||||||
|
1100,
|
||||||
|
1300,
|
||||||
|
1500
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 38,
|
||||||
|
"name": "Luxury Tax",
|
||||||
|
"type": "tax",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 0,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": null,
|
||||||
|
"rent": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 39,
|
||||||
|
"name": "Boardwalk",
|
||||||
|
"type": "property",
|
||||||
|
"owner": -1,
|
||||||
|
"cost": 400,
|
||||||
|
"mortgaged": false,
|
||||||
|
"houses": 0,
|
||||||
|
"monopoly": false,
|
||||||
|
"group": "darkblue",
|
||||||
|
"rent": [
|
||||||
|
50,
|
||||||
|
200,
|
||||||
|
600,
|
||||||
|
1400,
|
||||||
|
1700,
|
||||||
|
2000
|
||||||
|
]
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"log": [
|
"log": [
|
||||||
{"timestamp":"2026-02-20T21:20:00Z","text":"roll is 5, 6","player":"Alice"},
|
{
|
||||||
{"timestamp":"2026-02-20T21:20:05Z","text":"Passed Go, collected $200","player":"Alice"},
|
"timestamp": "2026-02-20T21:38:18.018187+00:00",
|
||||||
{"timestamp":"2026-02-20T21:20:06Z","text":"Landed on Illinois Ave.","player":"Alice"},
|
"text": "Alice joined as player 1",
|
||||||
{"timestamp":"2026-02-20T21:20:10Z","text":"Bought Illinois Ave. for $240","player":"Alice"},
|
"player": "Alice"
|
||||||
{"timestamp":"2026-02-20T21:21:00Z","text":"roll is 6, 6","player":"Bob"},
|
},
|
||||||
{"timestamp":"2026-02-20T21:21:02Z","text":"Bob rolled doubles","player":"Bob"},
|
{
|
||||||
{"timestamp":"2026-02-20T21:21:05Z","text":"Landed on Boardwalk","player":"Bob"},
|
"timestamp": "2026-02-20T21:38:18.018200+00:00",
|
||||||
{"timestamp":"2026-02-20T21:21:10Z","text":"roll is 3, 5","player":"Bob"},
|
"text": "Alice's turn",
|
||||||
{"timestamp":"2026-02-20T21:21:15Z","text":"Paid $750 rent (4 houses)","player":"Bob"},
|
"player": "Alice"
|
||||||
{"timestamp":"2026-02-20T21:22:00Z","text":"Charlie's turn","player":"Charlie"},
|
},
|
||||||
{"timestamp":"2026-02-20T21:22:05Z","text":"Still in jail (turn 2)","player":"Charlie"},
|
{
|
||||||
{"timestamp":"2026-02-20T21:23:00Z","text":"roll is 4, 2","player":"Alice"},
|
"timestamp": "2026-02-20T21:38:18.168441+00:00",
|
||||||
{"timestamp":"2026-02-20T21:23:05Z","text":"Landed on Atlantic Ave.","player":"Alice"},
|
"text": "Bob joined as player 2",
|
||||||
{"timestamp":"2026-02-20T21:24:00Z","text":"roll is 1, 5","player":"Bob"},
|
"player": "Bob"
|
||||||
{"timestamp":"2026-02-20T21:24:05Z","text":"Passed Go, collected $200","player":"Bob"},
|
},
|
||||||
{"timestamp":"2026-02-20T21:24:06Z","text":"Landed on Oriental Ave.","player":"Bob"},
|
{
|
||||||
{"timestamp":"2026-02-20T21:24:10Z","text":"Paid $12 rent","player":"Bob"}
|
"timestamp": "2026-02-20T21:38:18.168455+00:00",
|
||||||
]
|
"text": "Bob's turn",
|
||||||
}
|
"player": "Bob"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:38:18.318956+00:00",
|
||||||
|
"text": "Charlie joined as player 3",
|
||||||
|
"player": "Charlie"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:38:18.318975+00:00",
|
||||||
|
"text": "Charlie's turn",
|
||||||
|
"player": "Charlie"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:38:18.619242+00:00",
|
||||||
|
"text": "Charlie's turn",
|
||||||
|
"player": "Charlie"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:38:23.326156+00:00",
|
||||||
|
"text": "roll is 3, 6",
|
||||||
|
"player": "Charlie"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:38:23.476516+00:00",
|
||||||
|
"text": "Landed on Connecticut ave. (L)",
|
||||||
|
"player": "Charlie"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:38:25.129353+00:00",
|
||||||
|
"text": "Alice's turn",
|
||||||
|
"player": "Alice"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:38:29.936248+00:00",
|
||||||
|
"text": "roll is 2, 5",
|
||||||
|
"player": "Alice"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:38:30.086109+00:00",
|
||||||
|
"text": "Landed on Chance i",
|
||||||
|
"player": "Alice"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:38:34.091033+00:00",
|
||||||
|
"text": "Bob's turn",
|
||||||
|
"player": "Bob"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:38:36.095336+00:00",
|
||||||
|
"text": "roll is 5, 5",
|
||||||
|
"player": "Bob"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:38:37.097140+00:00",
|
||||||
|
"text": "Landed on Just Visiting",
|
||||||
|
"player": "Bob"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:38:39.438836+00:00",
|
||||||
|
"text": "Bob rolled doubles",
|
||||||
|
"player": "Bob"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:38:40.015566+00:00",
|
||||||
|
"text": "Bob's turn",
|
||||||
|
"player": "Bob"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:38:42.169426+00:00",
|
||||||
|
"text": "roll is 4, 4",
|
||||||
|
"player": "Bob"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:38:43.353040+00:00",
|
||||||
|
"text": "Landed on Tennessee ave. (O)",
|
||||||
|
"player": "Bob"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:38:47.168359+00:00",
|
||||||
|
"text": "Bob rolled doubles",
|
||||||
|
"player": "Bob"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:38:48.253823+00:00",
|
||||||
|
"text": "Bob's turn",
|
||||||
|
"player": "Bob"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:38:51.108736+00:00",
|
||||||
|
"text": "roll is 1, 2",
|
||||||
|
"player": "Bob"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:38:52.051144+00:00",
|
||||||
|
"text": "Landed on Kentucky ave. (R)",
|
||||||
|
"player": "Bob"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:38:56.080168+00:00",
|
||||||
|
"text": "Charlie's turn",
|
||||||
|
"player": "Charlie"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:38:59.017009+00:00",
|
||||||
|
"text": "roll is 5, 1",
|
||||||
|
"player": "Charlie"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:39:00.039361+00:00",
|
||||||
|
"text": "Landed on Pennsylvania RR",
|
||||||
|
"player": "Charlie"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:39:04.016897+00:00",
|
||||||
|
"text": "Alice's turn",
|
||||||
|
"player": "Alice"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:39:07.641072+00:00",
|
||||||
|
"text": "roll is 6, 2",
|
||||||
|
"player": "Alice"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:39:08.092792+00:00",
|
||||||
|
"text": "Landed on Pennsylvania RR",
|
||||||
|
"player": "Alice"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:39:11.364880+00:00",
|
||||||
|
"text": "Bob's turn",
|
||||||
|
"player": "Bob"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:39:13.048717+00:00",
|
||||||
|
"text": "roll is 3, 3",
|
||||||
|
"player": "Bob"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:39:14.130462+00:00",
|
||||||
|
"text": "Landed on Ventnor ave. (Y)",
|
||||||
|
"player": "Bob"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:39:18.037025+00:00",
|
||||||
|
"text": "Bob rolled doubles",
|
||||||
|
"player": "Bob"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:39:19.034618+00:00",
|
||||||
|
"text": "Bob's turn",
|
||||||
|
"player": "Bob"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:39:22.779902+00:00",
|
||||||
|
"text": "roll is 6, 2",
|
||||||
|
"player": "Bob"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:39:23.123928+00:00",
|
||||||
|
"text": "Landed on Short Line RR",
|
||||||
|
"player": "Bob"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:39:27.279901+00:00",
|
||||||
|
"text": "Charlie's turn",
|
||||||
|
"player": "Charlie"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:39:30.019353+00:00",
|
||||||
|
"text": "roll is 3, 6",
|
||||||
|
"player": "Charlie"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-20T21:39:31.074039+00:00",
|
||||||
|
"text": "Landed on Illinois ave. (R)",
|
||||||
|
"player": "Charlie"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastUpdated": "2026-02-20T21:39:31.074058+00:00"
|
||||||
|
}
|
||||||
230
test/monop_server.py
Normal file
230
test/monop_server.py
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Monop IRC game server: runs monop in a subprocess, bridges stdin/stdout to IRC.
|
||||||
|
Players prefix commands with a dot (.) in the channel.
|
||||||
|
Also includes scripted AI players for testing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import ssl
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import pexpect
|
||||||
|
import irc.client
|
||||||
|
import irc.connection
|
||||||
|
|
||||||
|
SERVER = "irc.darkscience.net"
|
||||||
|
PORT = 6697
|
||||||
|
CHANNEL = "#monop-dev"
|
||||||
|
NICK = "monopoly"
|
||||||
|
MONOP = "/usr/games/monop"
|
||||||
|
NUM_PLAYERS = 3
|
||||||
|
PLAYER_NAMES = ["Alice", "Bob", "Charlie"]
|
||||||
|
|
||||||
|
|
||||||
|
class MonopServer:
|
||||||
|
def __init__(self):
|
||||||
|
self.connection = None
|
||||||
|
self.child = None
|
||||||
|
self.joined = False
|
||||||
|
self.game_started = False
|
||||||
|
self.setup_phase = True
|
||||||
|
self.setup_step = 0 # 0=waiting, 1=num_players sent, 2+=names
|
||||||
|
self.output_queue = []
|
||||||
|
self.last_output_time = 0
|
||||||
|
self.auto_play = True
|
||||||
|
self.turn_count = 0
|
||||||
|
|
||||||
|
def start_monop(self):
|
||||||
|
"""Start the monop process."""
|
||||||
|
self.child = pexpect.spawn(MONOP, encoding='utf-8', timeout=2)
|
||||||
|
print("monop process started", flush=True)
|
||||||
|
|
||||||
|
def read_monop(self):
|
||||||
|
"""Read any available output from monop."""
|
||||||
|
lines = []
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
# Read one line at a time
|
||||||
|
self.child.expect('\r?\n', timeout=0.3)
|
||||||
|
text = self.child.before.strip()
|
||||||
|
if text:
|
||||||
|
lines.append(text)
|
||||||
|
except pexpect.TIMEOUT:
|
||||||
|
# Also grab any partial prompt
|
||||||
|
if self.child.before:
|
||||||
|
partial = self.child.before.strip()
|
||||||
|
if partial and partial not in lines:
|
||||||
|
lines.append(partial)
|
||||||
|
except pexpect.EOF:
|
||||||
|
pass
|
||||||
|
return lines
|
||||||
|
|
||||||
|
def send_to_monop(self, text):
|
||||||
|
"""Send a line to monop stdin."""
|
||||||
|
if self.child and self.child.isalive():
|
||||||
|
print(f" >> monop: {text!r}", flush=True)
|
||||||
|
self.child.sendline(text)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
def send_to_irc(self, text):
|
||||||
|
"""Send a line to IRC channel."""
|
||||||
|
if self.connection and text.strip():
|
||||||
|
# Truncate long lines
|
||||||
|
for line in text.split('\n'):
|
||||||
|
line = line.strip()[:450]
|
||||||
|
if line:
|
||||||
|
try:
|
||||||
|
self.connection.privmsg(CHANNEL, line)
|
||||||
|
time.sleep(0.15)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"IRC send error: {e}", flush=True)
|
||||||
|
|
||||||
|
def on_connect(self, conn, event):
|
||||||
|
print(f"Connected to IRC", flush=True)
|
||||||
|
self.connection = conn
|
||||||
|
time.sleep(2) # wait before joining
|
||||||
|
conn.join(CHANNEL)
|
||||||
|
|
||||||
|
def on_join(self, conn, event):
|
||||||
|
if event.source.nick == NICK:
|
||||||
|
print(f"Joined {CHANNEL}", flush=True)
|
||||||
|
self.joined = True
|
||||||
|
self.connection.privmsg(CHANNEL, "🎲 Monopoly game starting! Setting up with AI players...")
|
||||||
|
time.sleep(1)
|
||||||
|
self.start_monop()
|
||||||
|
self.run_setup()
|
||||||
|
|
||||||
|
def on_pubmsg(self, conn, event):
|
||||||
|
"""Handle player commands (dot-prefixed)."""
|
||||||
|
nick = event.source.nick if event.source else ""
|
||||||
|
msg = event.arguments[0] if event.arguments else ""
|
||||||
|
|
||||||
|
if nick == NICK:
|
||||||
|
return # ignore own messages
|
||||||
|
|
||||||
|
if msg.startswith("."):
|
||||||
|
cmd = msg[1:].strip()
|
||||||
|
print(f" Player command from {nick}: {cmd!r}", flush=True)
|
||||||
|
self.send_to_monop(cmd)
|
||||||
|
time.sleep(0.5)
|
||||||
|
self.flush_and_send()
|
||||||
|
|
||||||
|
def run_setup(self):
|
||||||
|
"""Automated game setup: set player count and names."""
|
||||||
|
time.sleep(1)
|
||||||
|
lines = self.read_monop()
|
||||||
|
for l in lines:
|
||||||
|
print(f" monop: {l}", flush=True)
|
||||||
|
self.send_to_irc(l)
|
||||||
|
|
||||||
|
# Send number of players
|
||||||
|
self.send_to_monop(str(NUM_PLAYERS))
|
||||||
|
time.sleep(0.5)
|
||||||
|
self.flush_and_send()
|
||||||
|
|
||||||
|
# Send player names
|
||||||
|
for name in PLAYER_NAMES:
|
||||||
|
time.sleep(0.5)
|
||||||
|
self.send_to_monop(name)
|
||||||
|
time.sleep(0.5)
|
||||||
|
self.flush_and_send()
|
||||||
|
|
||||||
|
# Read initial rolls
|
||||||
|
time.sleep(1)
|
||||||
|
self.flush_and_send()
|
||||||
|
time.sleep(1)
|
||||||
|
self.flush_and_send()
|
||||||
|
|
||||||
|
self.setup_phase = False
|
||||||
|
self.game_started = True
|
||||||
|
self.connection.privmsg(CHANNEL, "Game is set up! Auto-playing turns now...")
|
||||||
|
|
||||||
|
def flush_and_send(self):
|
||||||
|
"""Read monop output and relay to IRC."""
|
||||||
|
lines = self.read_monop()
|
||||||
|
for l in lines:
|
||||||
|
print(f" monop: {l}", flush=True)
|
||||||
|
self.send_to_irc(l)
|
||||||
|
return lines
|
||||||
|
|
||||||
|
def auto_turn(self):
|
||||||
|
"""Play a turn automatically."""
|
||||||
|
if not self.game_started or not self.child or not self.child.isalive():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Send empty line (= roll / default action)
|
||||||
|
self.send_to_monop("")
|
||||||
|
time.sleep(0.8)
|
||||||
|
lines = self.flush_and_send()
|
||||||
|
|
||||||
|
# Handle prompts
|
||||||
|
full = "\n".join(lines)
|
||||||
|
if "Do you want to buy?" in full:
|
||||||
|
self.send_to_monop("yes")
|
||||||
|
time.sleep(0.5)
|
||||||
|
self.flush_and_send()
|
||||||
|
elif "lose 10%" in full or "10%% of your total" in full:
|
||||||
|
# Income tax - choose $200 flat
|
||||||
|
self.send_to_monop("$200")
|
||||||
|
time.sleep(0.5)
|
||||||
|
self.flush_and_send()
|
||||||
|
elif "mortgage?" in full.lower() or "do you wish to" in full.lower():
|
||||||
|
self.send_to_monop("yes")
|
||||||
|
time.sleep(0.5)
|
||||||
|
self.flush_and_send()
|
||||||
|
elif "Bid for" in full:
|
||||||
|
self.send_to_monop("0")
|
||||||
|
time.sleep(0.5)
|
||||||
|
self.flush_and_send()
|
||||||
|
elif "How much" in full:
|
||||||
|
self.send_to_monop("0")
|
||||||
|
time.sleep(0.5)
|
||||||
|
self.flush_and_send()
|
||||||
|
elif "Illegal response" in full:
|
||||||
|
# Try different responses
|
||||||
|
self.send_to_monop("$200")
|
||||||
|
time.sleep(0.5)
|
||||||
|
self.flush_and_send()
|
||||||
|
|
||||||
|
self.turn_count += 1
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
reactor = irc.client.Reactor()
|
||||||
|
ssl_ctx = ssl.create_default_context()
|
||||||
|
factory = irc.connection.Factory(
|
||||||
|
wrapper=lambda s: ssl_ctx.wrap_socket(s, server_hostname=SERVER))
|
||||||
|
|
||||||
|
server = reactor.server()
|
||||||
|
server.connect(SERVER, PORT, NICK, connect_factory=factory)
|
||||||
|
server.add_global_handler("welcome", self.on_connect)
|
||||||
|
server.add_global_handler("join", self.on_join)
|
||||||
|
server.add_global_handler("pubmsg", self.on_pubmsg)
|
||||||
|
|
||||||
|
print(f"Connecting to {SERVER}:{PORT} as {NICK}...", flush=True)
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
last_auto = time.time()
|
||||||
|
max_turns = 50
|
||||||
|
|
||||||
|
while time.time() - start < 300: # 5 min timeout
|
||||||
|
reactor.process_once(timeout=0.5)
|
||||||
|
|
||||||
|
# Auto-play turns every 3 seconds
|
||||||
|
if self.auto_play and self.game_started and time.time() - last_auto > 3:
|
||||||
|
if self.turn_count < max_turns:
|
||||||
|
self.auto_turn()
|
||||||
|
last_auto = time.time()
|
||||||
|
elif self.turn_count == max_turns:
|
||||||
|
self.connection.privmsg(CHANNEL, f"--- Auto-play complete ({max_turns} turns). Game state saved. ---")
|
||||||
|
self.turn_count += 1 # prevent repeating
|
||||||
|
|
||||||
|
print("Server shutting down", flush=True)
|
||||||
|
if self.connection:
|
||||||
|
self.connection.quit("Game over!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
ms = MonopServer()
|
||||||
|
ms.run()
|
||||||
150
test/play_bot.py
Normal file
150
test/play_bot.py
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Simple IRC bot that plays monop by sending dot-prefixed commands."""
|
||||||
|
|
||||||
|
import ssl
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
import irc.client
|
||||||
|
import irc.connection
|
||||||
|
|
||||||
|
SERVER = "irc.darkscience.net"
|
||||||
|
PORT = 6697
|
||||||
|
CHANNEL = "#monop-dev"
|
||||||
|
|
||||||
|
class PlayerBot:
|
||||||
|
def __init__(self, nick, commands):
|
||||||
|
self.nick = nick
|
||||||
|
self.commands = list(commands) # list of (delay, command) tuples
|
||||||
|
self.cmd_idx = 0
|
||||||
|
self.connection = None
|
||||||
|
self.joined = False
|
||||||
|
self.last_cmd_time = 0
|
||||||
|
|
||||||
|
def on_connect(self, conn, event):
|
||||||
|
print(f"[{self.nick}] Connected, joining {CHANNEL}", flush=True)
|
||||||
|
self.connection = conn
|
||||||
|
conn.join(CHANNEL)
|
||||||
|
|
||||||
|
def on_join(self, conn, event):
|
||||||
|
if event.source.nick == self.nick:
|
||||||
|
print(f"[{self.nick}] Joined {CHANNEL}", flush=True)
|
||||||
|
self.joined = True
|
||||||
|
self.last_cmd_time = time.time()
|
||||||
|
|
||||||
|
def on_pubmsg(self, conn, event):
|
||||||
|
msg = event.arguments[0] if event.arguments else ""
|
||||||
|
nick = event.source.nick if event.source else ""
|
||||||
|
# React to game prompts
|
||||||
|
if nick == "monopoly" and self.joined:
|
||||||
|
if "Do you want to buy?" in msg:
|
||||||
|
time.sleep(0.5)
|
||||||
|
conn.privmsg(CHANNEL, ".yes")
|
||||||
|
elif "How many players?" in msg:
|
||||||
|
time.sleep(0.5)
|
||||||
|
conn.privmsg(CHANNEL, ".3")
|
||||||
|
|
||||||
|
def send_next_command(self):
|
||||||
|
if self.cmd_idx < len(self.commands) and self.joined:
|
||||||
|
delay, cmd = self.commands[self.cmd_idx]
|
||||||
|
if time.time() - self.last_cmd_time >= delay:
|
||||||
|
print(f"[{self.nick}] Sending: {cmd}", flush=True)
|
||||||
|
self.connection.privmsg(CHANNEL, cmd)
|
||||||
|
self.last_cmd_time = time.time()
|
||||||
|
self.cmd_idx += 1
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Script the game setup and several turns
|
||||||
|
# Player 1 starts the game with ".3" (3 players)
|
||||||
|
# Then each player provides their name
|
||||||
|
# Then they take turns rolling (just press enter = ".roll" or ".")
|
||||||
|
|
||||||
|
alice_cmds = [
|
||||||
|
(3, ".3"), # start 3 player game
|
||||||
|
(2, ".Alice"), # enter name
|
||||||
|
(8, "."), # roll (first turn after setup)
|
||||||
|
(4, "."), # action/roll
|
||||||
|
(6, "."),
|
||||||
|
(6, "."),
|
||||||
|
(6, "."),
|
||||||
|
(6, "."),
|
||||||
|
(6, "."),
|
||||||
|
(6, "."),
|
||||||
|
(6, "."),
|
||||||
|
(6, "."),
|
||||||
|
]
|
||||||
|
|
||||||
|
bob_cmds = [
|
||||||
|
(6, ".Bob"), # enter name
|
||||||
|
(10, "."), # roll
|
||||||
|
(6, "."),
|
||||||
|
(6, "."),
|
||||||
|
(6, "."),
|
||||||
|
(6, "."),
|
||||||
|
(6, "."),
|
||||||
|
(6, "."),
|
||||||
|
(6, "."),
|
||||||
|
(6, "."),
|
||||||
|
]
|
||||||
|
|
||||||
|
charlie_cmds = [
|
||||||
|
(8, ".Charlie"), # enter name
|
||||||
|
(12, "."), # roll
|
||||||
|
(6, "."),
|
||||||
|
(6, "."),
|
||||||
|
(6, "."),
|
||||||
|
(6, "."),
|
||||||
|
(6, "."),
|
||||||
|
(6, "."),
|
||||||
|
(6, "."),
|
||||||
|
(6, "."),
|
||||||
|
]
|
||||||
|
|
||||||
|
bots = [
|
||||||
|
PlayerBot("alice_m", alice_cmds),
|
||||||
|
PlayerBot("bob_m", bob_cmds),
|
||||||
|
PlayerBot("charlie_m", charlie_cmds),
|
||||||
|
]
|
||||||
|
|
||||||
|
reactor = irc.client.Reactor()
|
||||||
|
ssl_ctx = ssl.create_default_context()
|
||||||
|
|
||||||
|
for bot in bots:
|
||||||
|
factory = irc.connection.Factory(
|
||||||
|
wrapper=lambda s: ssl_ctx.wrap_socket(s, server_hostname=SERVER))
|
||||||
|
server = reactor.server()
|
||||||
|
server.connect(SERVER, PORT, bot.nick, connect_factory=factory)
|
||||||
|
server.add_global_handler("welcome", bot.on_connect)
|
||||||
|
server.add_global_handler("join", bot.on_join)
|
||||||
|
server.add_global_handler("pubmsg", bot.on_pubmsg)
|
||||||
|
time.sleep(1) # stagger connections
|
||||||
|
|
||||||
|
print("All bots connecting...", flush=True)
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
timeout = 180 # 3 minutes
|
||||||
|
|
||||||
|
while time.time() - start < timeout:
|
||||||
|
reactor.process_once(timeout=0.5)
|
||||||
|
for bot in bots:
|
||||||
|
bot.send_next_command()
|
||||||
|
|
||||||
|
# Check if all bots are done
|
||||||
|
if all(b.cmd_idx >= len(b.commands) for b in bots):
|
||||||
|
print("All commands sent. Waiting 10s for final output...", flush=True)
|
||||||
|
end = time.time() + 10
|
||||||
|
while time.time() < end:
|
||||||
|
reactor.process_once(timeout=0.5)
|
||||||
|
break
|
||||||
|
|
||||||
|
print("Done!", flush=True)
|
||||||
|
for bot in bots:
|
||||||
|
if bot.connection:
|
||||||
|
bot.connection.quit("Game over!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
99
test/run_monop_irc.sh
Executable file
99
test/run_monop_irc.sh
Executable file
|
|
@ -0,0 +1,99 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Run monop-irc game server on IRC via ii + socat TLS proxy
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SERVER=irc.darkscience.net
|
||||||
|
PORT=6697
|
||||||
|
CHANNEL="#monop-dev"
|
||||||
|
NICK="monopoly"
|
||||||
|
PREFIX=/tmp/monop-irc-session
|
||||||
|
MONOP=/usr/games/monop
|
||||||
|
SOCAT_PORT=16667
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
echo "Cleaning up..."
|
||||||
|
kill %1 %2 %3 2>/dev/null
|
||||||
|
killall -q ii 2>/dev/null
|
||||||
|
rm -rf "$PREFIX"
|
||||||
|
kill $(cat /tmp/socat-irc.pid 2>/dev/null) 2>/dev/null
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
trap cleanup INT TERM EXIT
|
||||||
|
|
||||||
|
# Clean previous
|
||||||
|
killall -q ii 2>/dev/null || true
|
||||||
|
kill $(cat /tmp/socat-irc.pid 2>/dev/null) 2>/dev/null || true
|
||||||
|
rm -rf "$PREFIX"
|
||||||
|
mkdir -p "$PREFIX"
|
||||||
|
|
||||||
|
# Start TLS proxy with socat
|
||||||
|
echo "Starting TLS proxy on localhost:$SOCAT_PORT -> $SERVER:$PORT"
|
||||||
|
socat TCP-LISTEN:$SOCAT_PORT,fork,reuseaddr OPENSSL:$SERVER:$PORT,verify=0 &
|
||||||
|
echo $! > /tmp/socat-irc.pid
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Start ii connecting to local socat proxy
|
||||||
|
echo "Starting ii..."
|
||||||
|
ii -s 127.0.0.1 -p $SOCAT_PORT -n "$NICK" -i "$PREFIX" &
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Wait for connection
|
||||||
|
IN="$PREFIX/127.0.0.1/in"
|
||||||
|
echo "Waiting for ii connection..."
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
[ -p "$IN" ] && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
if [ ! -p "$IN" ]; then
|
||||||
|
echo "ERROR: ii failed to connect"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Connected!"
|
||||||
|
|
||||||
|
# Join channel
|
||||||
|
echo "/j $CHANNEL" > "$IN"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Wait for channel
|
||||||
|
CHAN_DIR="$PREFIX/127.0.0.1/${CHANNEL}"
|
||||||
|
INCHAN="$CHAN_DIR/in"
|
||||||
|
OUTCHAN="$CHAN_DIR/out"
|
||||||
|
|
||||||
|
echo "Waiting for channel..."
|
||||||
|
for i in $(seq 1 20); do
|
||||||
|
[ -p "$INCHAN" ] && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
if [ ! -p "$INCHAN" ]; then
|
||||||
|
echo "ERROR: failed to join channel"
|
||||||
|
ls -la "$PREFIX/127.0.0.1/" 2>/dev/null
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Joined $CHANNEL"
|
||||||
|
|
||||||
|
# Send welcome message
|
||||||
|
echo "🎲 Monopoly IRC Game Server ready! Prefix commands with a dot (.) — e.g. type '.roll' to roll dice." > "$INCHAN"
|
||||||
|
echo "Type '.3' to start a 3-player game, or '.N' for N players." > "$INCHAN"
|
||||||
|
|
||||||
|
# Wait for out file
|
||||||
|
for i in $(seq 1 10); do
|
||||||
|
[ -f "$OUTCHAN" ] && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Game server running. Watching for commands..."
|
||||||
|
|
||||||
|
# The pipeline:
|
||||||
|
# 1. tail -f channel output (IRC messages)
|
||||||
|
# 2. sed extracts lines where users prefix with "." (e.g. ".roll" -> "username roll")
|
||||||
|
# 3. Filter out our own messages
|
||||||
|
# 4. Pipe to monop stdin; monop stdout goes back to channel
|
||||||
|
tail -f "$OUTCHAN" | \
|
||||||
|
sed -u -En 's/^[0-9-]+ [0-9:]+ <([a-zA-Z0-9_]+)> \.(.*)$/\2/p' | \
|
||||||
|
grep --line-buffered -v "^$" | \
|
||||||
|
$MONOP 2>&1 | \
|
||||||
|
while IFS= read -r line; do
|
||||||
|
echo "$line" > "$INCHAN"
|
||||||
|
# Small delay to avoid flood
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
Loading…
Reference in a new issue