Skip to content

Commit dedb2bc

Browse files
Merge remote-tracking branch 'origin/crossplay'
2 parents f1b4652 + 18b6a95 commit dedb2bc

30 files changed

+2046
-30
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ frontend/build
2323
### Matches should be ignored
2424
/matches/
2525

26+
### Cross-play temp files
27+
/crossplay_temp/
28+
2629
### Java
2730
*.classpath
2831
*.project

build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ task headless(type: JavaExec, dependsOn: [':engine:build', ':example-bots:build'
7878
'-Dbc.engine.show-indicators=' + (project.findProperty('showIndicators') ?: 'true'),
7979
'-Dbc.game.team-a=' + project.property('teamA'),
8080
'-Dbc.game.team-b=' + project.property('teamB'),
81+
'-Dbc.game.team-a.language=' + (project.findProperty('languageA') ?: 'java'),
82+
'-Dbc.game.team-b.language=' + (project.findProperty('languageB') ?: 'java'),
8183
'-Dbc.game.team-a.url=' + (project.findProperty('classLocationA') ?: defaultClassLocation),
8284
'-Dbc.game.team-b.url=' + (project.findProperty('classLocationB') ?: defaultClassLocation),
8385
'-Dbc.game.team-a.package=' + (project.findProperty('packageNameA') ?: project.property('teamA')),
@@ -89,6 +91,10 @@ task headless(type: JavaExec, dependsOn: [':engine:build', ':example-bots:build'
8991
]
9092
}
9193

94+
task crossPlayPy(type: Exec, dependsOn: [':engine:build']) {
95+
commandLine 'python', 'engine/src/crossplay_python/main.py', '--teamA', project.property('teamA'), '--teamB', project.property('teamB')
96+
}
97+
9298
// keep the client happy because it references this step
9399
task unpackClient() {}
94100

engine/build.gradle

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,9 @@ dependencies {
5353
// We only use WeakIdentityHashMap which doesn't depend on anything
5454
[group: 'org.hibernate', name: 'hibernate-search', version: '3.1.0.GA'],
5555

56-
// Java Spatial Index, RTree indexing
57-
// The official Maven repositories do not host net.sourceforge.jsi:jsi.
58-
// There are no valid released versions on Maven Central or Sonatype.
59-
// If you need JSI, download jsi-1.0.jar manually and use:
6056
[group: 'net.sf.trove4j', name: 'trove4j', version: '2.1.0'],
57+
58+
[group: 'org.json', name: 'json', version: '20250517'],
6159
)
6260
// implementation files('lib/jsi-1.0.jar')
6361

engine/src/battlecode.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import sys as _sys
2+
_sys.path.append("engine/src")
3+
4+
from crossplay_python.wrappers import *
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
import os
2+
import json
3+
import time
4+
from typing import override
5+
6+
from crossplay_python.enums import Team
7+
8+
from enum import Enum
9+
10+
BYTECODE_LIMIT = 5800
11+
MESSAGE_DIR = "crossplay_temp"
12+
MESSAGE_FILE_JAVA = "messages_java.json"
13+
MESSAGE_FILE_OTHER = "messages_other.json"
14+
LOCK_FILE_JAVA = "lock_java.txt"
15+
LOCK_FILE_OTHER = "lock_other.txt"
16+
STARTED_FILE_JAVA = "started_java.txt"
17+
STARTED_FILE_OTHER = "started_other.txt"
18+
19+
class CrossPlayException(Exception):
20+
def __init__(self, message):
21+
super().__init__(message + " (If you are a competitor, please report this to the Battlecode staff."
22+
" This is not an error in your code.)")
23+
24+
class CrossPlayObjectType(Enum):
25+
INVALID = 0
26+
CALL = 1
27+
NULL = 2
28+
INTEGER = 3
29+
STRING = 4
30+
BOOLEAN = 5
31+
DOUBLE = 6
32+
ARRAY = 7
33+
DIRECTION = 8
34+
MAP_LOCATION = 9
35+
MESSAGE = 10
36+
ROBOT_CONTROLLER = 11
37+
ROBOT_INFO = 12
38+
TEAM = 13
39+
# TODO add more types
40+
41+
class CrossPlayMethod(Enum):
42+
INVALID = 0
43+
START_TURN = 1 # returns [rc, round, team, id, end]
44+
END_TURN = 2 # params: [bytecode_used]
45+
RC_GET_ROUND_NUM = 3
46+
RC_GET_MAP_WIDTH = 4
47+
RC_GET_MAP_HEIGHT = 5
48+
LOG = 6
49+
# TODO add more methods
50+
51+
class CrossPlayObject:
52+
def __init__(self, object_type):
53+
self.object_type = object_type
54+
55+
def __str__(self):
56+
return f"CrossPlayObject(type={self.object_type})"
57+
58+
def to_json(self):
59+
return {"type": self.object_type.value}
60+
61+
@classmethod
62+
def from_json(cls, json_data):
63+
if "value" in json_data:
64+
return CrossPlayLiteral.from_json(json_data)
65+
elif "oid" in json_data:
66+
return CrossPlayReference.from_json(json_data)
67+
elif json_data["type"] == CrossPlayObjectType.CALL.value:
68+
return CrossPlayMessage.from_json(json_data)
69+
else:
70+
raise CrossPlayException(f"Cannot decode CrossPlayObject from json: {json_data}")
71+
72+
class CrossPlayReference(CrossPlayObject):
73+
def __init__(self, object_type, object_id):
74+
super().__init__(object_type)
75+
self.object_id = object_id
76+
77+
@override
78+
def __str__(self):
79+
return f"CrossPlayReference(type={self.object_type}, oid={self.object_id})"
80+
81+
def to_json(self):
82+
json_data = super().to_json()
83+
json_data["oid"] = self.object_id
84+
return json_data
85+
86+
@classmethod
87+
def from_json(cls, json_data):
88+
object_type = CrossPlayObjectType(json_data["type"])
89+
object_id = json_data["oid"]
90+
return CrossPlayReference(object_type, object_id)
91+
92+
class CrossPlayLiteral(CrossPlayObject):
93+
def __init__(self, object_type, value):
94+
super().__init__(object_type)
95+
self.value = value
96+
97+
@override
98+
def __str__(self):
99+
return f"CrossPlayLiteral(type={self.object_type}, value={self.value})"
100+
101+
def reduce_literal(self):
102+
match self.object_type:
103+
case CrossPlayObjectType.INTEGER:
104+
return int(self.value)
105+
case CrossPlayObjectType.STRING:
106+
return str(self.value)
107+
case CrossPlayObjectType.BOOLEAN:
108+
return bool(self.value)
109+
case CrossPlayObjectType.DOUBLE:
110+
return float(self.value)
111+
case CrossPlayObjectType.NULL:
112+
return None
113+
case CrossPlayObjectType.ARRAY:
114+
arr = []
115+
116+
for item in self.value:
117+
if isinstance(item, CrossPlayLiteral):
118+
arr.append(item.reduce_literal())
119+
elif isinstance(item, CrossPlayReference):
120+
arr.append(item.object_id)
121+
else:
122+
raise CrossPlayException(f"Cannot reduce item of type {type(item)} in CrossPlayLiteral array.")
123+
124+
return arr
125+
case CrossPlayObjectType.TEAM:
126+
return Team(self.value)
127+
case _:
128+
raise CrossPlayException(f"Cannot reduce CrossPlayLiteral of type {self.object_type} to primitive.")
129+
130+
def to_json(self):
131+
json_data = super().to_json()
132+
133+
match self.object_type:
134+
case CrossPlayObjectType.INTEGER:
135+
json_data["value"] = int(self.value)
136+
case CrossPlayObjectType.STRING:
137+
json_data["value"] = str(self.value)
138+
case CrossPlayObjectType.BOOLEAN:
139+
json_data["value"] = bool(self.value)
140+
case CrossPlayObjectType.DOUBLE:
141+
json_data["value"] = float(self.value)
142+
case CrossPlayObjectType.NULL:
143+
json_data["value"] = 0
144+
case CrossPlayObjectType.ARRAY:
145+
json_data["value"] = [item.to_json() for item in self.value]
146+
case CrossPlayObjectType.TEAM:
147+
json_data["value"] = self.value.value
148+
case _:
149+
raise CrossPlayException(f"Cannot encode CrossPlayLiteral of type {self.object_type} to json.")
150+
151+
return json_data
152+
153+
@classmethod
154+
def from_json(cls, json_data):
155+
# print(f"Parsing CrossPlayLiteral from json: {json_data}")
156+
object_type = CrossPlayObjectType(json_data["type"])
157+
158+
match object_type:
159+
case CrossPlayObjectType.INTEGER:
160+
value = int(json_data["value"])
161+
case CrossPlayObjectType.STRING:
162+
value = str(json_data["value"])
163+
case CrossPlayObjectType.BOOLEAN:
164+
value = bool(json_data["value"])
165+
case CrossPlayObjectType.DOUBLE:
166+
value = float(json_data["value"])
167+
case CrossPlayObjectType.NULL:
168+
value = None
169+
case CrossPlayObjectType.ARRAY:
170+
value = [CrossPlayObject.from_json(item) for item in json_data["value"]]
171+
case CrossPlayObjectType.TEAM:
172+
value = Team(json_data["value"])
173+
case _:
174+
raise CrossPlayException(f"Cannot decode CrossPlayObject of type {object_type} as a literal.")
175+
176+
return CrossPlayLiteral(object_type, value)
177+
178+
class CrossPlayMessage(CrossPlayObject):
179+
def __init__(self, method, params):
180+
super().__init__(CrossPlayObjectType.CALL)
181+
self.method = method
182+
self.params = params
183+
184+
@override
185+
def __str__(self):
186+
return f"CrossPlayMessage(method={self.method}, params={self.params})"
187+
188+
def to_json(self):
189+
json_data = super().to_json()
190+
json_data["method"] = self.method.value
191+
json_data["params"] = [param.to_json() for param in self.params]
192+
return json_data
193+
194+
@classmethod
195+
def from_json(cls, json_data):
196+
if json_data["type"] != CrossPlayObjectType.CALL.value:
197+
raise CrossPlayException("Tried to parse non-call as CrossPlayMessage!")
198+
199+
method = json_data["method"]
200+
params = [CrossPlayObject.from_json(param) for param in json_data["params"]]
201+
return CrossPlayMessage(method, params)
202+
203+
# 0.1 ms timestep, 10 min timeout
204+
def wait(message: CrossPlayMessage, timeout=600, timestep=0.0001, message_dir=MESSAGE_DIR):
205+
try:
206+
read_file = os.path.join(message_dir, MESSAGE_FILE_JAVA)
207+
write_file = os.path.join(message_dir, MESSAGE_FILE_OTHER)
208+
java_lock_file = os.path.join(message_dir, LOCK_FILE_JAVA)
209+
other_lock_file = os.path.join(message_dir, LOCK_FILE_OTHER)
210+
211+
# if directory does not exist, create it
212+
if not os.path.exists(message_dir):
213+
os.makedirs(message_dir)
214+
215+
json_message = message.to_json()
216+
time_limit = time.time() + timeout
217+
218+
# print(f"Waiting to send message Python -> Java: {json_message}")
219+
220+
while os.path.exists(read_file) or os.path.exists(write_file) or os.path.exists(java_lock_file):
221+
time.sleep(timestep)
222+
223+
if time.time() > time_limit:
224+
raise CrossPlayException("Cross-play message passing timed out (Python waiting, Java busy).")
225+
226+
if not os.path.exists(other_lock_file):
227+
with open(other_lock_file, 'x') as f:
228+
f.write('')
229+
230+
# print("Created other lock file")
231+
232+
with open(write_file, 'w') as f:
233+
json.dump(json_message, f)
234+
235+
if os.path.exists(other_lock_file):
236+
os.remove(other_lock_file)
237+
238+
# print(f"Sent message Python -> Java: {json_message}")
239+
# print("Waiting for response Java -> Python...")
240+
time_limit = time.time() + timeout
241+
242+
while not os.path.exists(read_file) or os.path.exists(write_file) or os.path.exists(java_lock_file):
243+
time.sleep(timestep)
244+
245+
if time.time() > time_limit:
246+
raise CrossPlayException("Cross-play message passing timed out (Python waiting, Java not responding).")
247+
248+
if not os.path.exists(other_lock_file):
249+
with open(other_lock_file, 'x') as f:
250+
f.write('')
251+
252+
with open(read_file, 'r') as f:
253+
json_data = json.load(f)
254+
result = CrossPlayObject.from_json(json_data)
255+
256+
os.remove(read_file)
257+
258+
if os.path.exists(other_lock_file):
259+
os.remove(other_lock_file)
260+
261+
# print(f"Received message Java -> Python: {result}")
262+
263+
if isinstance(result, CrossPlayLiteral):
264+
return result.reduce_literal()
265+
else:
266+
return result
267+
except IOError as e:
268+
raise CrossPlayException("Cross-play message passing failed due to file I/O error: " + str(e))
269+
270+
def reset_files(message_dir=MESSAGE_DIR):
271+
read_file = os.path.join(message_dir, MESSAGE_FILE_JAVA)
272+
write_file = os.path.join(message_dir, MESSAGE_FILE_OTHER)
273+
java_lock_file = os.path.join(message_dir, LOCK_FILE_JAVA)
274+
other_lock_file = os.path.join(message_dir, LOCK_FILE_OTHER)
275+
java_started_file = os.path.join(message_dir, STARTED_FILE_JAVA)
276+
other_started_file = os.path.join(message_dir, STARTED_FILE_OTHER)
277+
278+
if not os.path.exists(message_dir) or not os.path.isdir(message_dir):
279+
os.makedirs(message_dir)
280+
elif os.path.exists(other_started_file):
281+
print("DEBUGGING: Detected existing crossplay_temp/started_other.txt file. " \
282+
"This indicates that a previous cross-play match did not terminate cleanly. " \
283+
"Deleting the old crossplay_temp files.")
284+
elif os.path.exists(java_started_file):
285+
print("DEBUGGING: Java cross-play runner already started. Using existing cross-play temp directory.")
286+
return
287+
288+
if os.path.exists(read_file):
289+
os.remove(read_file)
290+
291+
if os.path.exists(write_file):
292+
os.remove(write_file)
293+
294+
if os.path.exists(java_lock_file):
295+
os.remove(java_lock_file)
296+
297+
if os.path.exists(other_lock_file):
298+
os.remove(other_lock_file)
299+
300+
if not os.path.exists(os.path.join(message_dir, STARTED_FILE_OTHER)):
301+
with open(other_started_file, 'x') as f:
302+
f.write('')
303+
304+
def clear_temp_files(message_dir=MESSAGE_DIR):
305+
if not os.path.exists(message_dir) or not os.path.isdir(message_dir):
306+
return
307+
308+
read_file = os.path.join(message_dir, MESSAGE_FILE_JAVA)
309+
write_file = os.path.join(message_dir, MESSAGE_FILE_OTHER)
310+
java_lock_file = os.path.join(message_dir, LOCK_FILE_JAVA)
311+
other_lock_file = os.path.join(message_dir, LOCK_FILE_OTHER)
312+
java_started_file = os.path.join(message_dir, STARTED_FILE_JAVA)
313+
other_started_file = os.path.join(message_dir, STARTED_FILE_OTHER)
314+
315+
if os.path.exists(read_file):
316+
os.remove(read_file)
317+
318+
if os.path.exists(write_file):
319+
os.remove(write_file)
320+
321+
if os.path.exists(java_lock_file):
322+
os.remove(java_lock_file)
323+
324+
if os.path.exists(other_lock_file):
325+
os.remove(other_lock_file)
326+
327+
if os.path.exists(java_started_file):
328+
os.remove(java_started_file)
329+
330+
if os.path.exists(other_started_file):
331+
os.remove(other_started_file)

0 commit comments

Comments
 (0)