はじめに
Python の勉強のため簡単なゲームを作りましたので備忘録としてまとめておきます.
とりとめのない個人的な記事ではありますが、python を勉強するどなたかの参考になりましたならば幸いです.
環境は Python 3.13.1、Windows 11、コマンドプロンプトです.
スライドパズルを実装する中で私が python として学んだことは過去に書いた記事と重複しているので特筆しません.
よろしければ過去記事もご覧ください:
完成品
|>
の行が入力.
入力のしやすさを考え空きマスを移動させる形なので、操作感はパズドラに近い.
+-----------+
| 0| 1| 2| 3|
| 4| 5| 7| X|
| 8| 9| 6|11|
|12|13|10|14|
+-----------+
空白マス(X) をどの方向に動かすかを入力してください(up, down, left, right)
ゲームを終了する場合、quit
|> left
+-----------+
| 0| 1| 2| 3|
| 4| 5| X| 7|
| 8| 9| 6|11|
|12|13|10|14|
+-----------+
空白マス(X) をどの方向に動かすかを入力してください(up, down, left, right)
ゲームを終了する場合、quit
|> down
+-----------+
| 0| 1| 2| 3|
| 4| 5| 6| 7|
| 8| 9| X|11|
|12|13|10|14|
+-----------+
空白マス(X) をどの方向に動かすかを入力してください(up, down, left, right)
ゲームを終了する場合、quit
|> down
+-----------+
| 0| 1| 2| 3|
| 4| 5| 6| 7|
| 8| 9|10|11|
|12|13| X|14|
+-----------+
空白マス(X) をどの方向に動かすかを入力してください(up, down, left, right)
ゲームを終了する場合、quit
|> right
+-----------+
| 0| 1| 2| 3|
| 4| 5| 6| 7|
| 8| 9|10|11|
|12|13|14| X|
+-----------+
ゲームクリア!
ソースコード
from random import randrange
class Board:
numbers: list[int] # 盤面
blank: int # 空白マスの位置(インデックス)
# [[ 定数 ]]
# 空白マスを表現する値
BLANK = -1
# 移動方向
LEFT = (-1, 0)
RIGHT = ( 1, 0)
UP = ( 0, -1)
DOWN = ( 0, 1)
def __init__(self):
# 大きさは 4x4 固定とする
# ,-------- 0 列目
# | ,------ 1 列目
# | | ,---- 2 列目
# | | | ,-- 3 列目
# v v v v
# numbers [ a b c d e f g h i j k l m n o p q]
# ------- ********* ------- *******
# row 0 row 1 row 2 row 3
self.numbers = list(range(16))
self.blank = 15 # 右下
self.numbers[self.blank] = Board.BLANK
def shuffle(self):
directions = [Board.LEFT, Board.RIGHT, Board.UP, Board.DOWN]
count = 0
# 単純に numbers をシャッフルすると解けない盤面が出来上がる可能性があるため、
# 適当に動かすことでばらばらにする.
# 解けるスライドパズルであるということは偶置換であることと同値らしいのだが、
# スライドパズルではなく Python の練習が目的なので踏み込まない.
# 30 回も動かせば十分ばらばらになるはず.
# シャッフルの完了時に解けているのは望ましくないので、
# count >= 30 であっても self.is_solved() であれば動かす.
while count < 30 or self.is_solved():
if(self.move(directions[randrange(4)])):
count += 1 # 実際に空白マスが移動した場合だけカウントする
@staticmethod
def index(x, y):
return x + y * 4
@staticmethod
def coordinate(index):
y = index // 4
x = index - 4 * y
return (x, y)
def at(self, x, y):
return self.numbers[Board.index(x, y)]
def get_blank_position(self):
return Board.coordinate(self.blank)
def show(self):
print('+-----------+')
for y in range(4):
print('|' + '|'.join(
' X' if n == Board.BLANK else str(n).rjust(2)
for n in self.numbers[y * 4:(y + 1) * 4]
) + '|')
print('+-----------+')
def move(self, direction) -> bool:
'''
空白マス(X) を移動させる.
Args:
direction: (x: int, y: int) なる単位ベクトル.
Returns:
移動に成功したか?
Examples:
>> board.move(Board.UP)
>> board.move(Board.DOWN)
'''
def put(x, y, val):
index = Board.index(x, y)
self.numbers[index] = val
vx, vy = direction
if abs(vx) + abs(vy) != 1:
raise ValueError('direction must be a unit vector')
bx, by = self.get_blank_position()
# nx, ny: 移動後の空白マスの位置
nx = bx + vx
ny = by + vy
if nx < 0 or 4 <= nx or ny < 0 or 4 <= ny:
# 空白マスが範囲外に出た
return False
# 実際に移動させる; i.e. (bx, by) と (nx, ny) を交換する.
put(bx, by, self.at(nx, ny))
put(nx, ny, Board.BLANK)
self.blank = Board.index(nx, ny)
return True
def is_solved(self):
if self.blank != 15:
# 空白マスが右下にないので未解決
return False
for i in range(15):
if self.numbers[i] != i:
# 数字 i の位置が異なるので未解決
return False
# ここまでのチェックを通り抜けたならば解けている
return True
def process_message(board):
'''
メッセージループの処理
Returns:
処理を終了する場合 True
'''
board.show()
print('空白マス(X) をどの方向に動かすかを入力してください(up, down, left, right)')
print('ゲームを終了する場合、quit')
# 内部で input() を使っているのにメッセージループを名乗れるかは知らない.
# コマンドの表現には dict を使うのもよいが、大した数ではないので書き下してしまう.
match input('|> '):
case 'up':
board.move(Board.UP)
case 'down':
board.move(Board.DOWN)
case 'left':
board.move(Board.LEFT)
case 'right':
board.move(Board.RIGHT)
case 'quit':
return True
case _:
print('入力を読み取れませんでした')
return False
if board.is_solved():
board.show()
print('ゲームクリア!')
return True
return False
# メインの処理
board = Board()
board.shuffle()
while(True):
if process_message(board):
break
説明
メッセージループ
メインの処理はコードの一番下に描かれている関数 process_message と、それを呼び出す無限ループである.
process_message で行っていることを大まかに分ければ
print で入力形式などを表示する
input で入力を受け取る
入力に応じた処理
のみっつで、盤面の管理やクリア判定は Board クラスに任せている.
Board
クラス
内部表現
空きマスがどこにあるかを blank
として持っている.
盤面は二次元リストで問題はないのだが、何となく一次元リストを使って numbers: list[int]
としている.
中身はコンストラクタのコメントに書いてある通り、0 行目の情報、1 行目の情報、... となっている.
二次元的な x,y を使ってアクセスするには index(self, x, y)
を使うようにしている.
ゲームクリアの判定は is_solved(self)
で行っている.
判定方法としては、空きマスが右下にあり、numbers
が 0 から 14 まで昇順で持っていればよい.
BLANK = 15
としていればここで空きマスの位置を確かめる必要もなくせる.
空きマスの移動
move(self, direction)
で空きマスを方向 direction
に移動させる.
ここで、direction
は Board.LEFT
などとして定義している単位ベクトルである.
空きマスの移動は、その方向にある数字と空きマスの交換である.
中で補助関数 put
を用意したり get_blank_position
を使ったりしているが、index
/coordinate
の呼び出しが無駄に増えているので、すべて index
形式で考える方が簡潔だったかもしれない.