Skip to content

Commit a3b851c

Browse files
crossplay refactor + package
1 parent 1c1a301 commit a3b851c

28 files changed

Lines changed: 622 additions & 1029 deletions

build.gradle

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,14 @@ headlessPy.mustRunAfter crossPlayPy
104104
// keep the client happy because it references this step
105105
task unpackClient() {}
106106

107-
task run(dependsOn: ['crossPlayPy', 'headlessPy', 'unpackClient']) {}
107+
task run(dependsOn: ['unpackClient']) {}
108+
109+
if (project.hasProperty('languageA') && project.property('languageA') == 'python' ||
110+
project.hasProperty('languageB') && project.property('languageB') == 'python') {
111+
run.dependsOn crossPlayPy, headlessPy
112+
} else {
113+
run.dependsOn headless
114+
}
108115

109116
task runClient {
110117
doLast {

engine/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ dependencies {
5454
[group: 'org.hibernate', name: 'hibernate-search', version: '3.1.0.GA'],
5555

5656
[group: 'net.sf.trove4j', name: 'trove4j', version: '2.1.0'],
57-
58-
[group: 'org.json', name: 'json', version: '20251224'],
57+
58+
[group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.15.2'],
5959
)
6060
// implementation files('lib/jsi-1.0.jar')
6161

engine/src/battlecode.py

Lines changed: 0 additions & 4 deletions
This file was deleted.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
**/_version.py
2+
*.egg-info/
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .enums import Team, Direction
2+
from .wrappers import rc, log
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import socket
2+
import struct
3+
import json
4+
import time
5+
from enum import Enum
6+
7+
# Connection constants
8+
IPC_HOST = "127.0.0.1"
9+
IPC_PORT = 27185
10+
BYTECODE_LIMIT = 20000 # Default, updated by game
11+
12+
13+
class CrossPlayException(Exception):
14+
def __init__(self, message):
15+
super().__init__(
16+
message
17+
+ " (If you are a competitor, please report this to the Battlecode staff.)"
18+
)
19+
20+
21+
class CrossPlayMethod(Enum):
22+
INVALID = 0
23+
START_TURN = 1
24+
END_TURN = 2
25+
RC_GET_ROUND_NUM = 3
26+
RC_GET_MAP_WIDTH = 4
27+
RC_GET_MAP_HEIGHT = 5
28+
LOG = 6
29+
30+
31+
class CrossPlayClient:
32+
def __init__(self):
33+
self.sock = None
34+
35+
def connect(self):
36+
while True:
37+
try:
38+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
39+
self.sock.connect((IPC_HOST, IPC_PORT))
40+
break
41+
except ConnectionRefusedError:
42+
time.sleep(0.1)
43+
except Exception as e:
44+
raise CrossPlayException(f"Failed to connect to Java server: {e}")
45+
46+
def send_json(self, data):
47+
if self.sock is None:
48+
raise CrossPlayException("Socket not connected")
49+
50+
json_bytes = json.dumps(data).encode("utf-8")
51+
length = len(json_bytes)
52+
try:
53+
self.sock.sendall(struct.pack(">I", length))
54+
self.sock.sendall(json_bytes)
55+
except Exception as e:
56+
raise CrossPlayException(f"Failed to send data: {e}")
57+
58+
def receive_json(self):
59+
if self.sock is None:
60+
raise CrossPlayException("Socket not connected")
61+
62+
try:
63+
length_bytes = self.recv_exactly(4)
64+
length = struct.unpack(">I", length_bytes)[0]
65+
data_bytes = self.recv_exactly(length)
66+
return json.loads(data_bytes.decode("utf-8"))
67+
except Exception as e:
68+
raise CrossPlayException(f"Failed to receive data: {e}")
69+
70+
def recv_exactly(self, n):
71+
if self.sock is None:
72+
raise CrossPlayException("Socket not connected")
73+
74+
data = b""
75+
while len(data) < n:
76+
packet = self.sock.recv(n - len(data))
77+
if not packet:
78+
raise CrossPlayException("Socket connection closed unexpectedly")
79+
data += packet
80+
return data
81+
82+
def close(self):
83+
if self.sock:
84+
self.sock.close()
85+
self.sock = None
86+
87+
88+
# Global client instance
89+
_client = CrossPlayClient()
90+
91+
92+
def connect():
93+
_client.connect()
94+
95+
96+
def close():
97+
_client.close()
98+
99+
100+
def send(method: CrossPlayMethod, params=None):
101+
if params is None:
102+
params = []
103+
104+
message = {"method": method.value, "params": params}
105+
106+
_client.send_json(message)
107+
108+
109+
def send_and_wait(method: CrossPlayMethod, params=None):
110+
send(method, params)
111+
return _client.receive_json()
112+
113+
114+
def receive():
115+
return _client.receive_json()
116+
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from enum import Enum as _Enum
2+
3+
class Team(_Enum):
4+
A = 0
5+
B = 1
6+
NEUTRAL = 2
7+
8+
def opposite(self):
9+
match self:
10+
case Team.A:
11+
return Team.B
12+
case Team.B:
13+
return Team.A
14+
case Team.NEUTRAL:
15+
return Team.NEUTRAL
16+
17+
dir_to_index = {
18+
(0, 1): 0,
19+
(1, 1): 1,
20+
(1, 0): 2,
21+
(1, -1): 3,
22+
(0, -1): 4,
23+
(-1, -1): 5,
24+
(-1, 0): 6,
25+
(-1, 1): 7,
26+
(0, 0): 8
27+
}
28+
29+
# TODO make sure this matches the Java Direction enum, this could cause bugs
30+
class Direction(_Enum):
31+
NORTH = (0, 1)
32+
NORTHEAST = (1, 1)
33+
EAST = (1, 0)
34+
SOUTHEAST = (1, -1)
35+
SOUTH = (0, -1)
36+
SOUTHWEST = (-1, -1)
37+
WEST = (-1, 0)
38+
NORTHWEST = (-1, 1)
39+
CENTER = (0, 0)
40+
41+
def opposite(self) -> 'Direction':
42+
if self == Direction.CENTER:
43+
return self
44+
return dir_order[(dir_to_index[self.value] + 4) % 8]
45+
46+
def rotate_left(self) -> 'Direction':
47+
if self == Direction.CENTER:
48+
return self
49+
return dir_order[(dir_to_index[self.value] - 1) % 8]
50+
51+
def rotate_right(self) -> 'Direction':
52+
if self == Direction.CENTER:
53+
return self
54+
return dir_order[(dir_to_index[self.value] + 1) % 8]
55+
56+
def all_directions():
57+
return Direction.__members__.values()
58+
59+
def cardinal_directions():
60+
return [Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST]
61+
62+
def get_dx(self):
63+
return self.value[0]
64+
65+
def get_dy(self):
66+
return self.value[1]
67+
68+
dir_order = [Direction.NORTH, Direction.NORTHEAST, Direction.EAST, Direction.SOUTHEAST, Direction.SOUTH, Direction.SOUTHWEST, Direction.WEST, Direction.NORTHWEST, Direction.CENTER]
File renamed without changes.

engine/src/crossplay_python/runner.py renamed to engine/src/crossplay_python/battlecode/runner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import dis
1111
import inspect
1212
from enum import Enum
13-
from crossplay_python.wrappers import RobotController
13+
from .wrappers import RobotController
1414

1515
class GameFinishedException(Exception):
1616
pass
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from .crossplay import (
2+
CrossPlayMethod as _m,
3+
send_and_wait as _wait,
4+
)
5+
from .enums import *
6+
7+
class RobotController:
8+
def get_round_num():
9+
return _wait(_m.RC_GET_ROUND_NUM)
10+
11+
def get_map_width():
12+
return _wait(_m.RC_GET_MAP_WIDTH)
13+
14+
def get_map_height():
15+
return _wait(_m.RC_GET_MAP_HEIGHT)
16+
17+
18+
rc = RobotController
19+
20+
21+
def log(message):
22+
return _wait(_m.LOG, [message])
23+
24+
25+
_GAME_METHODS = {
26+
"Direction": Direction,
27+
"Team": Team,
28+
"rc": rc,
29+
"log": (log, 1),
30+
}

0 commit comments

Comments
 (0)