0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rescue Mazeのフィールドで遊ぶ②ビューアーを作る

0
Posted at

前回の記事では、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

これでビューアーを作ることができました!
こんな風に動きます
image.png

全体のコード
main.py

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上でロボットを動かしてみようと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?