diff --git a/kaggle_environments/envs/cabt/cabt.js b/kaggle_environments/envs/cabt/cabt.js new file mode 100644 index 00000000..9e26d9b3 --- /dev/null +++ b/kaggle_environments/envs/cabt/cabt.js @@ -0,0 +1,152 @@ + +function renderer(context) { + const step = context.step + const visList = context.environment.steps[0][0].visualize + const energyText = "CGRWLPFDM A" + + const info = context.environment.info + const players = [ + info?.TeamNames?.[0] || "Player 0", + info?.TeamNames?.[1] || "Player 1" + ] + + let canvas = context.parent.querySelector("canvas") + if (!canvas) { + container = document.createElement("div") + container.style.position = "relative" + context.parent.appendChild(container) + + canvas = document.createElement("canvas") + canvas.width = 750 + canvas.height = 700 + container.appendChild(canvas) + + for (let k = 0; k < 2; k++) { + const button = document.createElement("button") + button.style.width = "120px" + button.style.height = "50px" + button.style.left = k == 0 ? "240px" : "380px" + button.style.top = "10px" + button.style.position = "absolute" + button.style.zIndex = 1 + button.innerHTML = "Open Visualizer
" + players[k] + button.addEventListener("click", (e) => { + for (let i = 0; i < visList.length; i++) { + for (let j = 0; j < 2; j++) { + visList[i].current.players[j].ramainingTime = context.environment.steps[i][j].observation.remainingOverageTime + } + } + visList[0].ps = players + + const input = document.createElement("input") + input.type = "hidden" + input.name = "json" + input.value = JSON.stringify(visList) + + const form = document.createElement("form") + form.method = "POST" + form.action = "https://ptcgvis.heroz.jp/Visualizer/Replay/" + if (info.EpisodeId == null) { + form.action += k + } else { + form.action += info.EpisodeId + "/" + k + } + form.target = "_blank" + form.appendChild(input) + + document.body.appendChild(form) + form.submit() + }) + container.appendChild(button) + } + } + + if (visList.length <= step) { + return + } + const vis = visList[step] + const state = vis.current + + const ctx = canvas.getContext("2d") + ctx.clearRect(0, 0, canvas.width, canvas.height) + + ctx.strokeStyle = "#ccc" + ctx.fillStyle = "#fff" + ctx.lineWidth = 2 + + ctx.font = "20px sans-serif" + if (state.result >= 0) { + if (state.result == 2) { + ctx.fillText("Draw", 330, 70) + } else { + ctx.fillText(players[state.result] + " Win", 310, 120) + } + } + + ctx.font = "12px sans-serif" + + const drawCard = (x, y, card) => { + ctx.beginPath() + ctx.rect(x, y, 80, 60) + ctx.stroke() + nm = card.name + nm2 = null + if (nm.length >= 13) { + for (let i = 0; i < nm.length; i++) { + if (nm[i] == " ") { + nm2 = nm.substring(i + 1) + nm = nm.substring(0, i) + break + } + } + } + ctx.fillText(nm, x + 5, y + 13) + if (nm2 != null) { + ctx.fillText(nm2, x + 5, y + 27) + } + } + const drawField = (x, y, card) => { + drawCard(x, y, card) + ctx.fillText("HP " + card.hp, x + 5, y + 41) + energy = "" + for (let e of card.energies) { + energy = energy + energyText[e] + } + ctx.fillText(energy, x + 5, y + 55) + } + const posY = (index, len) => { + const center = 290 + let height + if (len <= 8) { + height = 35 * len + } else { + height = 280 + } + return center + height * (2 * index + 1 - len) / len + } + + for (let j = 0; j < state.stadium.length; j++) { + drawCard(330, 420, state.stadium[j]) + } + + for (let i = 0; i < 2; i++) { + const ps = state.players[i] + + ctx.fillText("Active", i == 0 ? 245 : 425, 270) + ctx.fillText("Bench", i == 0 ? 145 : 525, 10) + ctx.fillText("Hand", i == 0 ? 15 : 655, 10) + ctx.fillText("Deck " + ps.deckCount, i == 0 ? 258 : 438, 150) + ctx.fillText("Discard " + ps.discard.length, i == 0 ? 245 : 425, 170) + ctx.fillText("Prize " + ps.prize.length, i == 0 ? 258 : 438, 200) + + for (let j = 0; j < ps.active.length; j++) { + drawField(i == 0 ? 240 : 420, posY(j, ps.active.length), ps.active[j]) + } + for (let j = 0; j < ps.bench.length; j++) { + drawField(i == 0 ? 140 : 520, posY(j, ps.bench.length), ps.bench[j]) + } + for (let j = 0; j < ps.hand.length; j++) { + drawCard(i == 0 ? 10 : 650, posY(j, ps.hand.length), ps.hand[j]) + } + } +} diff --git a/kaggle_environments/envs/cabt/cabt.json b/kaggle_environments/envs/cabt/cabt.json new file mode 100644 index 00000000..c2636058 --- /dev/null +++ b/kaggle_environments/envs/cabt/cabt.json @@ -0,0 +1,36 @@ +{ + "name": "cabt", + "title": "Card Battle", + "description": "Limited Card Battle.", + "version": "1.0.0", + "agents": [2], + "configuration": { + "episodeSteps": 10000, + "actTimeout": 0, + "runTimeout": 3000, + "decks": { + "description": "Both player decks.", + "type": "array", + "default": [ + [5,5,5,5,5,5,5,5,5,5,9,9,77,77,77,77,156,156,156,156,157,157,157,157,331,331,331,331,408,408,408,408,474,474,474,474,528,528,528,528,530,530,530,530,532,554,554,554,576,576,576,576,585,585,585,585,630,630,630,630], + [5,5,5,5,5,5,5,5,5,5,9,9,77,77,77,77,156,156,156,156,157,157,157,157,331,331,331,331,408,408,408,408,474,474,474,474,528,528,528,528,530,530,530,530,532,554,554,554,576,576,576,576,585,585,585,585,630,630,630,630] + ] + } + }, + "reward": { + "description": "Lost:-1, Won:1, Draw:0", + "enum": [-1, 0, 1], + "default": 0 + }, + "observation": { + "remainingOverageTime": 600 + }, + "action": { + "description": "List of option index.", + "type": "array", + "default": [] + }, + "status": { + "defaults": ["INACTIVE", "INACTIVE"] + } +} diff --git a/kaggle_environments/envs/cabt/cabt.py b/kaggle_environments/envs/cabt/cabt.py new file mode 100644 index 00000000..61df49b6 --- /dev/null +++ b/kaggle_environments/envs/cabt/cabt.py @@ -0,0 +1,83 @@ +import random +import os +import json + +from .cg.sim import Battle +from .cg.game import battle_start, battle_finish, battle_select, visualize_data + + +def random_agent(obs: dict) -> list[int]: + return random.sample(list(range(len(obs["select"]["option"]))), obs["select"]["maxCount"]) + +def first_agent(obs: dict) -> list[int]: + return list(range(obs["select"]["maxCount"])) + +agents = {"random": random_agent, "first": first_agent} + + +def finish(env): + if len(env.steps) > 0: + env.steps[0][0]["visualize"] = json.loads(visualize_data()) + battle_finish() + +def interpreter(state, env): + if env.done: + decks = env.configuration.decks + battle_start(decks[0], decks[1]) + else: + error = False + select_player = Battle.obs["current"]["yourIndex"] + if state[select_player].status == "TIMEOUT": + error = True + else: + try: + battle_select(state[select_player].action) + except: + state[select_player].status = "INVALID" + error = True + + if error: + state[select_player].reward = -1 + state[1 - select_player].status = "DONE" + state[1 - select_player].reward = 1 + finish(env) + return state + + obs = Battle.obs + s = obs["current"] + if s["result"] >= 0: + state[0].status = "DONE" + state[1].status = "DONE" + if s["result"] == 0: + state[0].reward = 1 + state[1].reward = -1 + elif s["result"] == 1: + state[0].reward = -1 + state[1].reward = 1 + else: + state[0].reward = 0 + state[1].reward = 0 + finish(env) + else: + index = s["yourIndex"] + state[index].status = "ACTIVE" + state[1 - index].status = "INACTIVE" + o = state[index].observation + o["select"] = obs["select"] + o["logs"] = obs["logs"] + o["current"] = obs["current"] + o["search_begin_input"] = obs["search_begin_input"] + return state + +def renderer(state, env): + return json.dumps(Battle.obs) + +def html_renderer(): + jspath = os.path.abspath(os.path.join(os.path.dirname(__file__), "cabt.js")) + with open(jspath, encoding="utf-8") as f: + return f.read() + + +jsonpath = os.path.abspath(os.path.join(os.path.dirname(__file__), "cabt.json")) +with open(jsonpath) as f: + specification = json.load(f) diff --git a/kaggle_environments/envs/cabt/cg/__init__.py b/kaggle_environments/envs/cabt/cg/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kaggle_environments/envs/cabt/cg/cg.dll b/kaggle_environments/envs/cabt/cg/cg.dll new file mode 100644 index 00000000..98d24325 Binary files /dev/null and b/kaggle_environments/envs/cabt/cg/cg.dll differ diff --git a/kaggle_environments/envs/cabt/cg/game.py b/kaggle_environments/envs/cabt/cg/game.py new file mode 100644 index 00000000..761da3fd --- /dev/null +++ b/kaggle_environments/envs/cabt/cg/game.py @@ -0,0 +1,66 @@ +import ctypes +import json + +from .sim import lib, Battle + +def _get_battle_data() -> dict: + """Retrieve the current state. + + Returns: + dict: Current observation. + """ + sd = lib.GetBattleData(Battle.battle_ptr) + Battle.obs = json.loads(sd.json.decode()) + Battle.obs["search_begin_input"] = ctypes.string_at(sd.data, sd.count).decode('ascii') + return Battle.obs + +def battle_start(deck0: list[int], deck1: list[int]) -> dict: + """Start the battle. + + Args: + deck0: List of card IDs included in the first player’s deck. + deck1: List of card IDs included in the second player’s deck. + + Returns: + dict: First observation. + """ + if len(deck0) != 60 or len(deck1) != 60: + raise ValueError("The deck must contain 60 cards.") + cards = deck0 + deck1 + arg = (ctypes.c_int*len(cards))(*cards) + Battle.battle_ptr = lib.BattleStart(arg) + if Battle.battle_ptr == 0: + raise ValueError("Invalid deck.") + return _get_battle_data() + +def battle_finish(): + """End the battle and free the memory used during it.""" + lib.BattleFinish(Battle.battle_ptr) + +def battle_select(select_list: list[int]) -> dict: + """Select option. + + Args: + select_list: + + Returns: + dict: Next observation. + """ + if not isinstance(select_list, list) or not all(isinstance(i, int) for i in select_list): + raise ValueError("select_list is not list[int]") + arg = (ctypes.c_int*len(select_list))(*select_list) + err = lib.Select(Battle.battle_ptr, arg, len(select_list)) + if err != 0: + if err == 30: + raise ValueError("battle_ptr broken.") + else: + raise IndexError() + return _get_battle_data() + +def visualize_data() -> str: + """Retrieve the data to be used by the visualizer. + + Returns: + str: The data to be used by the visualizer. + """ + return lib.VisualizeData(Battle.battle_ptr).decode() diff --git a/kaggle_environments/envs/cabt/cg/libcg.so b/kaggle_environments/envs/cabt/cg/libcg.so new file mode 100644 index 00000000..d2b10644 Binary files /dev/null and b/kaggle_environments/envs/cabt/cg/libcg.so differ diff --git a/kaggle_environments/envs/cabt/cg/sim.py b/kaggle_environments/envs/cabt/cg/sim.py new file mode 100644 index 00000000..bd05904a --- /dev/null +++ b/kaggle_environments/envs/cabt/cg/sim.py @@ -0,0 +1,37 @@ +import ctypes +import os + +class SerialData(ctypes.Structure): + _fields_ = [ + ("json", ctypes.c_char_p), + ("data", ctypes.POINTER(ctypes.c_ubyte)), + ("count", ctypes.c_int), + ("selectPlayer", ctypes.c_int) + ] + +if os.name == 'nt': + lib_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cg.dll") +else: + lib_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "libcg.so") +lib = ctypes.cdll.LoadLibrary(lib_path) + +lib.GameInitialize() + +lib.BattleStart.restype = ctypes.c_void_p +lib.BattleStart.argtypes = [ctypes.POINTER(ctypes.c_int)] + +lib.BattleFinish.argtypes = [ctypes.c_void_p] + +lib.GetBattleData.restype = SerialData +lib.GetBattleData.argtypes = [ctypes.c_void_p] + +lib.Select.restype = ctypes.c_int +lib.Select.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_int), ctypes.c_int] + +lib.VisualizeData.restype = ctypes.c_char_p +lib.VisualizeData.argtypes = [ctypes.c_void_p] + +class Battle: + battle_ptr = None + obs = None + raminingTime = [[], []]