前回の記事では、RoboCupJunior Rescue MazeのフィールドのJSONファイルの形式について見てみました。
今回は、そのJSONファイルから迷路をGUIに表示するプログラムをPythonで作っていきます。
この記事は Tuton Advent Calender 2025 の16日目の記事です。
はじめに
前回のこちらの記事で紹介した、RCJ Rescue MazeのCMSのマップのJSONファイルを読み込んで、マップを描画するプログラムを作成します。
次回以降の記事でこちらのソフトウェア上でロボットを動かして探索させます。
つくる。
今回は、PythonのTkinterを使ってGUIを作成します。
TkinterはPythonの標準ライブラリのため追加でインストールする必要はありません。
まずは、マップのJSONファイルに一致した構造体を用意します。
from dataclasses import dataclass
from typing import List, Optional, Dict, Any
@dataclass
class JsonMapDataTilePosition:
x: int
y: int
z: int
@dataclass
class JsonMapDataTile:
changeFloorTo: Optional[int] = None
victims: Optional[Dict[str, str]] = None
blue: Optional[bool] = False
reachable: Optional[bool] = True
checkpoint: Optional[bool] = False
speedbump: Optional[bool] = False
black: Optional[bool] = False
ramp: Optional[bool] = False
steps: Optional[bool] = False
red: Optional[bool] = False
@dataclass
class JsonMapDataCell:
isWall: bool = False
halfWall: Optional[int] = None
isTile: bool = False
tile: Optional[JsonMapDataTile] = None
x: Optional[int] = None
y: Optional[int] = None
z: Optional[int] = None
isLinear: Optional[bool] = None
ignoreWall: Optional[bool] = None
virtualWall: Optional[bool] = None
@dataclass
class JsonMapData:
name: str
length: int
height: int
width: int
leagueType: str
duration: int
finished: bool
startTile: JsonMapDataTilePosition
cells: Dict[str, JsonMapDataCell]
次に、JSONから読み取った情報を、プログラムの中で扱いやすくするためにフィールドデータを保存しておくクラスを作ります。
class Field:
def __init__(self, name):
self.name = name
self.jsonMapData = None
self.mapData = None
self.size = (0, 0) # (length, width)
# mapDataについて補足
# mapDataは2次元配列で、各要素は以下のように表される
# 0: 空白
# 1: 壁, 柱
# 2: タイル
# 3: 沼
# 壁とタイルをそれぞれ1つのセルとして扱うため、配列のサイズは(2*width+1) x (2*length+1)となる
# 奇数,奇数はタイル。他は壁。
def readJson(self, json_data):
self.jsonMapData = JsonMapData(**json_data)
self.jsonMapData.cells = {
key: JsonMapDataCell(**value)
for key, value in self.jsonMapData.cells.items()
}
self.jsonMapData.startTile = JsonMapDataTilePosition(
**self.jsonMapData.startTile)
# StartTileのx,yを入れ替える
self.jsonMapData.startTile.x, self.jsonMapData.startTile.y = (
self.jsonMapData.startTile.x-1)//2, (self.jsonMapData.startTile.y-1)//2
self.name = self.jsonMapData.name
self.mapData = [[0 for _ in range(2*self.jsonMapData.width+1)] for _ in range(
2*self.jsonMapData.length+1)]
self.size = (self.jsonMapData.length, self.jsonMapData.width)
for key, cell in self.jsonMapData.cells.items():
y, x, z = map(int, key.split(","))
if z != 0:
continue
if cell.isWall:
self.mapData[x][y] = 1
if x % 2 == 0 and y % 2 == 1: # 縦壁
self.mapData[x][y-1] = 1
self.mapData[x][y+1] = 1
elif y % 2 == 0 and x % 2 == 1: # 横壁
self.mapData[x-1][y] = 1
self.mapData[x+1][y] = 1
else:
pass
# print("Warning: Wall at unexpected position", x, y)
if cell.isTile:
cell.tile = JsonMapDataTile(
**cell.tile) if cell.tile is not None else None
if cell.tile is None:
continue
if cell.tile.reachable is False:
continue
#print("Tile at", x, y, cell.tile)
#print(len(self.mapData), len(self.mapData[0]))
self.mapData[x][y] = 2
if (cell.tile is not None) and cell.tile.blue:
self.mapData[x][y] = 4
#print("Swamp at", x, y)
for i in range(self.size[0]):
for j in range(self.size[1]):
# self.mapData[2*i+1][2*j+1] = 2
for _x, _y in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
if self.mapData[2*i+1+_x][2*j+1+_y] != 1:
break
else:
self.mapData[2*i+1][2*j+1] = 1
# StartTileは3
sx, sy, sz = (self.jsonMapData.startTile.x,
self.jsonMapData.startTile.y, self.jsonMapData.startTile.z)
self.mapData[sy*2+1][sx*2+1] = 3
def __str__(self):
mapDataStr = "\n".join(
["".join([str(cell) for cell in row]) for row in self.mapData])
return f"Field(name={self.name}, mapData=\n{mapDataStr})"
そして、GUIとして描画するクラスを作ります。
今回は、壁とタイル(色タイル含む)のみを描画しています。
maps/フォルダ内のマップをプルダウンメニューから選べるようにしています。
class MapViewer:
def __init__(self):
self.map_scale = 0.6
self.root = Tk()
self.root.title("Map Viewer")
self.root.geometry("800x1000")
TitleLabel = Label(self.root, text="Map Viewer",
font=("Helvetica", 10, "bold"))
TitleLabel.pack(pady=10)
self.packMapSelector()
self.canvas = Canvas(self.root, width=int(800*0.4),
height=int(800*0.4), bg="white")
self.canvas.pack()
self.small_cell_size = int(15 * self.map_scale)
self.big_cell_size = int(45 * self.map_scale)
self.root.mainloop()
def draw_from_field(self, fieldData: Field):
self.fieldData = fieldData
self.canvas.delete("all")
for i, row in enumerate(self.fieldData.mapData):
for j, cell in enumerate(row):
x0 = (j//2)*(self.small_cell_size+self.big_cell_size) + \
self.small_cell_size * (j % 2)
y0 = (i//2)*(self.small_cell_size+self.big_cell_size) + \
self.small_cell_size * (i % 2)
x1 = x0 + (self.big_cell_size if (j %
2 == 1) else self.small_cell_size)
y1 = y0 + (self.big_cell_size if (i %
2 == 1) else self.small_cell_size)
color = self.get_color(cell)
self.canvas.create_rectangle(
x0, y0, x1, y1, fill=color, outline="black", tag=f"cell_{i}_{j}")
if cell == 3:
self.canvas.create_text((x0+x1)//2, (y0+y1)//2, text="S",
fill="blue", font=("Helvetica", int(16*self.map_scale), "bold"))
self.pos = (self.fieldData.jsonMapData.startTile.x,
self.fieldData.jsonMapData.startTile.y)
self.robot_dir = 90
self.robot_isRun = False
def packMapSelector(self):
# mapフォルダにあるものを自動でリストとして取得
map_files = [
"maps/" + f for f in os.listdir("maps/") if f.endswith(".json")]
selected_map = StringVar()
selected_map.set(map_files[0])
map_dropdown = OptionMenu(self.root, selected_map, *
map_files, command=self.load_map_from_file)
map_dropdown.pack(pady=10)
def load_map_from_file(self, file_path):
with open(file_path, "r") as f:
json_data = json.load(f)
field = Field("loaded_map")
field.readJson(json_data)
self.draw_from_field(field)
def convertTileToCanvasCoords(self, tile_x, tile_y):
canvas_x = (tile_x * 2 + 1) // 2 * \
(self.small_cell_size + self.big_cell_size) + \
self.big_cell_size//2+self.small_cell_size
canvas_y = (tile_y * 2 + 1) // 2 * \
(self.small_cell_size + self.big_cell_size) + \
self.big_cell_size//2+self.small_cell_size
return canvas_x, canvas_y
def get_color(self, cell_value):
if cell_value == 0:
return "#FCFCFC" # empty
elif cell_value == 1:
return "#000000" # wall
elif cell_value == 2:
return "#FFFFFF" # tile
elif cell_value == 3:
return "#FFFFFF" # start tile
elif cell_value == 4: # swamp
return "#0048FF"
else:
return "#FF0000" # unknown
これでビューアーを作ることができました!
こんな風に動きます

全体のコード
from tkinter import IntVar
import json
from dataclasses import dataclass, field, asdict
from typing import List, Optional, Dict, Any
from tkinter import *
from tkinter import ttk
from collections import defaultdict
import os
from dataclasses import dataclass
from typing import List, Optional, Dict, Any
@dataclass
class JsonMapDataTilePosition:
x: int
y: int
z: int
@dataclass
class JsonMapDataTile:
changeFloorTo: Optional[int] = None
victims: Optional[Dict[str, str]] = None
blue: Optional[bool] = False
reachable: Optional[bool] = True
checkpoint: Optional[bool] = False
speedbump: Optional[bool] = False
black: Optional[bool] = False
ramp: Optional[bool] = False
steps: Optional[bool] = False
red: Optional[bool] = False
@dataclass
class JsonMapDataCell:
isWall: bool = False
halfWall: Optional[int] = None
isTile: bool = False
tile: Optional[JsonMapDataTile] = None
x: Optional[int] = None
y: Optional[int] = None
z: Optional[int] = None
isLinear: Optional[bool] = None
ignoreWall: Optional[bool] = None
virtualWall: Optional[bool] = None
@dataclass
class JsonMapData:
name: str
length: int
height: int
width: int
leagueType: str
duration: int
finished: bool
startTile: JsonMapDataTilePosition
cells: Dict[str, JsonMapDataCell]
class Field:
def __init__(self, name):
self.name = name
self.jsonMapData = None
self.mapData = None
self.size = (0, 0) # (length, width)
# mapDataについて補足
# mapDataは2次元配列で、各要素は以下のように表される
# 0: 空白
# 1: 壁, 柱
# 2: タイル
# 3: 沼
# 壁とタイルをそれぞれ1つのセルとして扱うため、配列のサイズは(2*width+1) x (2*length+1)となる
# 奇数,奇数はタイル。他は壁。
def readJson(self, json_data):
self.jsonMapData = JsonMapData(**json_data)
self.jsonMapData.cells = {
key: JsonMapDataCell(**value)
for key, value in self.jsonMapData.cells.items()
}
self.jsonMapData.startTile = JsonMapDataTilePosition(
**self.jsonMapData.startTile
)
# StartTileのx,yを入れ替える
self.jsonMapData.startTile.x, self.jsonMapData.startTile.y = (
self.jsonMapData.startTile.x - 1
) // 2, (self.jsonMapData.startTile.y - 1) // 2
self.name = self.jsonMapData.name
self.mapData = [
[0 for _ in range(2 * self.jsonMapData.width + 1)]
for _ in range(2 * self.jsonMapData.length + 1)
]
self.size = (self.jsonMapData.length, self.jsonMapData.width)
for key, cell in self.jsonMapData.cells.items():
y, x, z = map(int, key.split(","))
if z != 0:
continue
if cell.isWall:
self.mapData[x][y] = 1
if x % 2 == 0 and y % 2 == 1: # 縦壁
self.mapData[x][y - 1] = 1
self.mapData[x][y + 1] = 1
elif y % 2 == 0 and x % 2 == 1: # 横壁
self.mapData[x - 1][y] = 1
self.mapData[x + 1][y] = 1
else:
pass
# print("Warning: Wall at unexpected position", x, y)
if cell.isTile:
cell.tile = (
JsonMapDataTile(**cell.tile) if cell.tile is not None else None
)
if cell.tile is None:
continue
if cell.tile.reachable is False:
continue
# print("Tile at", x, y, cell.tile)
# print(len(self.mapData), len(self.mapData[0]))
self.mapData[x][y] = 2
if (cell.tile is not None) and cell.tile.blue:
self.mapData[x][y] = 4
# print("Swamp at", x, y)
for i in range(self.size[0]):
for j in range(self.size[1]):
# self.mapData[2*i+1][2*j+1] = 2
for _x, _y in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
if self.mapData[2 * i + 1 + _x][2 * j + 1 + _y] != 1:
break
else:
self.mapData[2 * i + 1][2 * j + 1] = 1
# StartTileは3
sx, sy, sz = (
self.jsonMapData.startTile.x,
self.jsonMapData.startTile.y,
self.jsonMapData.startTile.z,
)
self.mapData[sy * 2 + 1][sx * 2 + 1] = 3
def __str__(self):
mapDataStr = "\n".join(
["".join([str(cell) for cell in row]) for row in self.mapData]
)
return f"Field(name={self.name}, mapData=\n{mapDataStr})"
class MapViewer:
def __init__(self):
self.map_scale = 0.6
self.root = Tk()
self.root.title("Map Viewer")
self.root.geometry("800x1000")
TitleLabel = Label(self.root, text="Map Viewer", font=("Helvetica", 10, "bold"))
TitleLabel.pack(pady=10)
self.packMapSelector()
self.canvas = Canvas(
self.root, width=int(800 * 0.4), height=int(800 * 0.4), bg="white"
)
self.canvas.pack()
self.small_cell_size = int(15 * self.map_scale)
self.big_cell_size = int(45 * self.map_scale)
self.root.mainloop()
def draw_from_field(self, fieldData: Field):
self.fieldData = fieldData
self.canvas.delete("all")
for i, row in enumerate(self.fieldData.mapData):
for j, cell in enumerate(row):
x0 = (j // 2) * (
self.small_cell_size + self.big_cell_size
) + self.small_cell_size * (j % 2)
y0 = (i // 2) * (
self.small_cell_size + self.big_cell_size
) + self.small_cell_size * (i % 2)
x1 = x0 + (self.big_cell_size if (j % 2 == 1) else self.small_cell_size)
y1 = y0 + (self.big_cell_size if (i % 2 == 1) else self.small_cell_size)
color = self.get_color(cell)
self.canvas.create_rectangle(
x0, y0, x1, y1, fill=color, outline="black", tag=f"cell_{i}_{j}"
)
if cell == 3:
self.canvas.create_text(
(x0 + x1) // 2,
(y0 + y1) // 2,
text="S",
fill="blue",
font=("Helvetica", int(16 * self.map_scale), "bold"),
)
self.pos = (
self.fieldData.jsonMapData.startTile.x,
self.fieldData.jsonMapData.startTile.y,
)
self.robot_dir = 90
self.robot_isRun = False
def packMapSelector(self):
# mapフォルダにあるものを自動でリストとして取得
map_files = ["maps/" + f for f in os.listdir("maps/") if f.endswith(".json")]
selected_map = StringVar()
selected_map.set(map_files[0])
map_dropdown = OptionMenu(
self.root, selected_map, *map_files, command=self.load_map_from_file
)
map_dropdown.pack(pady=10)
def load_map_from_file(self, file_path):
with open(file_path, "r") as f:
json_data = json.load(f)
field = Field("loaded_map")
field.readJson(json_data)
self.draw_from_field(field)
def convertTileToCanvasCoords(self, tile_x, tile_y):
canvas_x = (
(tile_x * 2 + 1) // 2 * (self.small_cell_size + self.big_cell_size)
+ self.big_cell_size // 2
+ self.small_cell_size
)
canvas_y = (
(tile_y * 2 + 1) // 2 * (self.small_cell_size + self.big_cell_size)
+ self.big_cell_size // 2
+ self.small_cell_size
)
return canvas_x, canvas_y
def get_color(self, cell_value):
if cell_value == 0:
return "#FCFCFC" # empty
elif cell_value == 1:
return "#000000" # wall
elif cell_value == 2:
return "#FFFFFF" # tile
elif cell_value == 3:
return "#FFFFFF" # start tile
elif cell_value == 4: # swamp
return "#0048FF"
else:
return "#FF0000" # unknown
v=MapViewer()
次回以降の記事ではこのGUI上でロボットを動かしてみようと思います。