プログラミング
記事内に商品プロモーションを含む場合があります

プログラムで回転するサイコロの向きを管理する方法【Python】

Aru

サイコロを上下左右に回転させて動かした場合、それぞれの面がどの方向を向いているかをプログラムで管理する方法を紹介します。このロジックは、競技プログラミング(AtCoderなど)で頻出するサイコロ問題のテンプレートとして使えるだけでなく、ルービックキューブのような3次元パズルのシミュレーションや、3Dゲームにおけるオブジェクトの簡易的な姿勢制御など、空間認識を必要とするプログラムの基礎となる重要な考え方です。

サイコロの回転問題とは

プログラミングにおける「サイコロ問題」とは、主に以下のような操作をシミュレートするものを指します。

  1. 初期状態の定義:どの数字がどの面(上、下、左、右、奥、手前)にあるかを設定する。
  2. 命令の実行:「右に転がす」「奥に転がす」「時計回りに90度回転させる」といった一連の命令を受け取る。
  3. 状態のクエリ:最終的に「上面」や「右面」にある数字は何かを答える。
図:サイコロ

「サイコロを転がしたときの状態」は、頭で考えるのは簡単ですが、プログラミングに慣れていない場合、「どう実装してよいのかわからない」問題です。私自身もこの手の実装は、毎回悩みます(図に書いたりします)。

つまり、直感的には簡単だけど、いざコードに落とし込もうとすると、「サイコロの状態(6つの面の向き)」をどのようなデータ構造で保持するかに悩む問題です。

もし、「上に回転した場合は・・・」などと考えながら、if文でコーディングすると、混乱してバグの温床になりやすいです。

この記事では、Pythonのクラスと多重代入(タプルアンパッキング)の機能を活用して、この複雑な状態遷移をスマートに、かつバグりにくい書き方で記述してみます。

配列を使って管理する(要素0は上など)方法もありますが、今回はわかりやすくするため、top, leftなどの名前で定義しています。

サイコロをどう定義する?

基本機能

サイコロは6つの面を持つ立方体です。プログラムで扱う際は、サイコロの「数字そのもの」と「面の位置(向き)」を明確に区別する必要があります。 ここでは、現在のサイコロの空間的な向きを基準に、以下の6つの変数で状態を管理することにします。

  • top: 上面(天) – 今、天井を向いている面
  • bottom: 底面(地) – 今、床に接している面
  • left: 左側面(西)
  • right: 右側面(東)
  • up: 奥側面(北)
  • down: 手前側面(南)

このクラスには、回転処理の前に基本機能として以下の3つのメソッドを実装します。それぞれの向きのイメージは以下の図のようになります。

図:サイコロを展開した図(左と右はbottomの位置が異なるが同じ)
  1. 初期化 (__init__)
    6つの面の初期状態を受け取ってインスタンス変数に格納します。引数の順番は競技プログラミングの問題形式(例:上、下、左、右、奥、手前)に合わせて調整可能ですが、ここでは各面をup, down, left, right, top, bottomの順で受け取る設計にしています。各引数に名前をつけているので、どの値がどの面に対応するかを混乱なく設定できます。
  2. 状態の取得 (get)
    現在のサイコロの状態(6つの面の数字)をリスト形式で返します。最終的な答えを出力したり、テストコードで正誤判定を行ったりする際に使用します。戻り値は、先頭から順にup, down, left, right, top, bottomに書かれている値です。
  3. デバッグ出力 (print)
    開発中に「今どうなっているか」を確認するため、展開図のような配置でコンソールに出力するメソッドです。単に数字を一列に並べるよりも、位置関係(上下左右)が直感的に把握できるため、ロジックのミスやバグの発見が早くなります。

クラス定義は以下のようになります。

class dice:
    def __init__(self, up, down, left, right, top, bottom):
        self.up = up
        self.down = down
        self.left = left
        self.right = right
        self.top = top
        self.bottom = bottom

    def get(self):
        # 現在の状態をリストとして取得
        return [self.up, self.down, self.left, self.right, self.top, self.bottom]
    
    def print(self):
        # 展開図のように表示して確認しやすくする
        # 上段:奥、中段:左・上・右、下段:手前、最下段:裏(底)のように配置しても良いが
        # ここでは直感的な位置関係でprintする
        print(f"  {self.bottom} (bottom)")
        print(f"  {self.up} (up/back)")
        print(f"{self.left} {self.top} {self.right} (left top right)")
        print(f"  {self.down} (down/front)")

回転に対する処理

回転処理を実装していきます。実際のサイコロを思い浮かべてもらえばわかりますが、 サイコロを転がすと4つの面が入れ替わり、残りの2つの面(回転軸となる面)は変わりません

つまり、4面を回転に合わせて入れ替えて、残りの2つは操作しないことになります。これを実現するために、Pythonの a, b = b, a という記法を使い、一括更新を行います。 これにより、「代入順序を間違えて値が消えてしまった」というミスを防ぐことができます。

右(Right)への回転

サイコロを右(東方向)にコロンと転がす動作をイメージしてください。 この時、回転の軸となる奥(up)と手前(down)の面は固定されたまま動きません。変化するのは、横方向にぐるりと一周する4つの面だけです。

具体的な遷移は以下のようになります:

  • 左面にあった数字 $\rightarrow$ 上面へ移動
  • 上面にあった数字 $\rightarrow$ 右面へ移動
  • 右面にあった数字 $\rightarrow$ 底面へ移動
  • 底面にあった数字 $\rightarrow$ 左面へ移動

これをコードで書くと、たった1行で表現できます。

図:サイコロを右に回転
    def Right(self):
        self.left, self.top, self.right, self.bottom = self.bottom, self.left, self.top, self.right

左(Left)への回転

左(西方向)への回転は、右回転の逆動作です。 右回転と同様に、奥(up)と手前(down)を軸として固定し、横方向の4面を逆順にスライドさせます。

    def Left(self):
        self.left, self.top, self.right, self.bottom = self.top, self.right, self.bottom, self.left

上(Up/奥)への回転

ここではメソッド名をUpとしていますが、これはサイコロを**奥側(北方向)**に転がす操作に対応させます。 この場合、回転軸が変わります。左右(left/right)の面は固定され、縦方向の面がぐるりと入れ替わります。

  • 上面 $\rightarrow$ 奥面へ
  • 奥面 $\rightarrow$ 底面へ
  • 底面 $\rightarrow$ 手前面へ
  • 手前面 $\rightarrow$ 上面へ
図:サイコロを奥に回転
    def Up(self):
        self.up, self.top, self.down, self.bottom = self.top, self.down, self.bottom, self.up

下(Down/手前)への回転

手前側(南方向)へ転がす操作です。Upの逆回転となります。 手前に転がすと、奥にあった面が上に現れます。

    def Down(self):
        self.up, self.top, self.down, self.bottom = self.bottom, self.up, self.top, self.down

動作確認

実装したクラスを使って、実際にサイコロを転がしてみましょう。 初期状態として [3, 4, 2, 5, 1, 6] (up, down, left, right, top, bottom)を持つサイコロを生成します。 これは、標準的なサイコロにおいて「上が1、手前が4」の時、他の面がどうなっているかを定義したものです。

# 初期値の設定: up=3, down=4, left=2, right=5, top=1, bottom=6
init = [3, 4, 2, 5, 1, 6]
d = dice(*init)

print("初期状態:")
d.print()

# 個別に転がしてみる
print("-" * 10)
print("左へ回転:")
d.Left()
d.print()

print("-" * 10)
print("右へ回転(元に戻す):")
d.Right()
d.print()

print("-" * 10)
print("奥(上)へ回転:")
d.Up()
d.print()

print("-" * 10)
print("手前(下)へ回転:")
d.Down()
d.print()
実行結果
初期状態:
  6 (bottom)
  3 (up/back)
2 1 5 (left top right)
  4 (down/front)
----------
左へ回転:
  2 (bottom)
  3 (up/back)
1 5 6 (left top right)
  4 (down/front)
----------
右へ回転(元に戻す):
  6 (bottom)
  3 (up/back)
2 1 5 (left top right)
  4 (down/front)
----------
奥(上)へ回転:
  3 (bottom)
  1 (up/back)
2 4 5 (left top right)
  6 (down/front)
----------
手前(下)へ回転:
  6 (bottom)
  3 (up/back)
2 1 5 (left top right)
  4 (down/front)

連続した回転のシミュレーション

コーディングテストなどでは、「”UURRDL” の順に転がした後の状態を求めよ」といった形式で入力が与えられることがよくあります。 このクラスを使えば、文字列をループで回すだけでシミュレーションが完了します。

例えば "UURRDL" (上、上、右、右、下、左)という順で転がしてみます。

# seqに応じて転がしたときの面を出力
seq = "UURRDL"
print(f"\n連続回転: {seq}")

for i, e in enumerate(seq):
    if e == "U":
        d.Up()
    elif e == "D":
        d.Down()
    elif e == "L":
        d.Left()
    elif e == "R":
        d.Right()
    
    # 途中経過を確認
    print(f"Step {i+1} [{e}]の操作後 --- Topは {d.top}")
    # 必要に応じて全体を表示
    # d.print()
実行結果
連続回転: UURRDL
Step 1 [U]の操作後 --- Topは 4
Step 2 [U]の操作後 --- Topは 6
Step 3 [R]の操作後 --- Topは 2
Step 4 [R]の操作後 --- Topは 1
Step 5 [D]の操作後 --- Topは 4
Step 6 [L]の操作後 --- Topは 2

全コード

最後にコード全体をまとめておきます。

class dice:
    def __init__(self, up, down, left, right, top, bottom):
        self.up = up
        self.down = down
        self.left = left
        self.right = right
        self.top = top
        self.bottom = bottom

    def get(self):
        # 現在の状態をリストとして取得
        return [self.up, self.down, self.left, self.right, self.top, self.bottom]
    
    def print(self):
        # 展開図のように表示して確認しやすくする
        # 上段:奥、中段:左・上・右、下段:手前、最下段:裏(底)のように配置しても良いが
        # ここでは直感的な位置関係でprintする
        print(f"  {self.bottom} (bottom)")
        print(f"  {self.up} (up/back)")
        print(f"{self.left} {self.top} {self.right} (left top right)")
        print(f"  {self.down} (down/front)")

    def Right(self):
        self.left, self.top, self.right, self.bottom = self.bottom, self.left, self.top, self.right

    def Left(self):
        self.left, self.top, self.right, self.bottom = self.top, self.right, self.bottom, self.left

    def Up(self):
        self.up, self.top, self.down, self.bottom = self.top, self.down, self.bottom, self.up

    def Down(self):
        self.up, self.top, self.down, self.bottom = self.bottom, self.up, self.top, self.down



# 初期値の設定: up=3, down=4, left=2, right=5, top=1, bottom=6
init = [3, 4, 2, 5, 1, 6]
d = dice(*init)

print("初期状態:")
d.print()

# 個別に転がしてみる
print("-" * 10)
print("左へ回転:")
d.Left()
d.print()

print("-" * 10)
print("右へ回転(元に戻す):")
d.Right()
d.print()

print("-" * 10)
print("奥(上)へ回転:")
d.Up()
d.print()

print("-" * 10)
print("手前(下)へ回転:")
d.Down()
d.print()

# seqに応じて転がしたときの面を出力
seq = "UURRDL"
print(f"\n連続回転: {seq}")

for i, e in enumerate(seq):
    if e == "U":
        d.Up()
    elif e == "D":
        d.Down()
    elif e == "L":
        d.Left()
    elif e == "R":
        d.Right()
    
    print(f"Step {i+1} [{e}]の操作後 --- Topは {d.top}")

まとめ

サイコロの回転シミュレーションは、複雑に見えますが、データ構造と処理を適切に設計すれば非常にシンプルに記述できます。

  1. 状態の保持
    6つの面(top, bottom, left, right, up, down)を変数として持つ。
  2. 回転の実装
    Pythonの多重代入機能を活用し、一時変数なしでスマートに値をスワップする。
  3. 不変部分の特定
    回転ごとに「変わらない2つの面(軸)」と「回転する4つの面」を意識する。

このクラス設計は汎用性が高く、サイコロの目の和を求める問題や、特定の面が上に来るまでの最短手数を幅優先探索(BFS)で求める問題などにもそのまま応用できます。

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

ABOUT ME
ある/Aru
ある/Aru
IT&機械学習エンジニア/ファイナンシャルプランナー(CFP®)
専門分野は並列処理・画像処理・機械学習・ディープラーニング。プログラミング言語はC, C++, Go, Pythonを中心として色々利用。現在は、Kaggle, 競プロなどをしながら悠々自適に活動中
記事URLをコピーしました