|
| 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