Skip to content

New environment cabt #370

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions kaggle_environments/envs/cabt/cabt.js
Original file line number Diff line number Diff line change
@@ -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<br>" + 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])
}
}
}
36 changes: 36 additions & 0 deletions kaggle_environments/envs/cabt/cabt.json
Original file line number Diff line number Diff line change
@@ -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"]
}
}
83 changes: 83 additions & 0 deletions kaggle_environments/envs/cabt/cabt.py
Original file line number Diff line number Diff line change
@@ -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)
Empty file.
Binary file added kaggle_environments/envs/cabt/cg/cg.dll
Binary file not shown.
66 changes: 66 additions & 0 deletions kaggle_environments/envs/cabt/cg/game.py
Original file line number Diff line number Diff line change
@@ -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()
Binary file added kaggle_environments/envs/cabt/cg/libcg.so
Binary file not shown.
37 changes: 37 additions & 0 deletions kaggle_environments/envs/cabt/cg/sim.py
Original file line number Diff line number Diff line change
@@ -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 = [[], []]