はじめに
Python の勉強のため簡単なゲームを作りましたので備忘録としてまとめておきます.
ライツアウトを解くプログラムを期待してこられた方がいらしたら申し訳ありませんが、単純に遊べるだけのものです.
とりとめのない個人的な記事ではありますが、python を勉強するどなたかの参考になりましたならば幸いです.
環境は Python 3.13.1、Windows 11、コマンドプロンプトです.
完成品
|>
の行が入力.
0 1 2 3 4
+---------------+
0 | X X X X X |
1 | X X X X X |
2 | X X X X X |
3 | X X X X X |
4 | X X X X X |
+---------------+
すべてのライトを点灯状態 (X) から消灯状態 (_) にしてください
どこを中心に反転するかを入力してください; x y: 左上は 0 0.
ゲームを終了する場合、quit
|> 1 1
0 1 2 3 4
+---------------+
0 | X _ X X X |
1 | _ _ _ X X |
2 | X _ X X X |
3 | X X X X X |
4 | X X X X X |
+---------------+
すべてのライトを点灯状態 (X) から消灯状態 (_) にしてください
どこを中心に反転するかを入力してください; x y: 左上は 0 0.
ゲームを終了する場合、quit
|> quit
ソースコード
class Board:
grid: list[bool] # 盤面
width: int
height: int
DARK = False
LIGHT = True
def __init__(self, width, height):
# 大きさが 3x4 であるとき:
# ,-------- 0 列目
# | ,------ 1 列目
# | | ,---- 2 列目
# | | | ,-- 3 列目
# v v v v
# grid [ a b c d e f g h i j k l ]
# ------- ******* -------
# row 0 row 1 row 2
self.grid = [Board.LIGHT] * (width * height)
self.width = width
self.height = height
def index(self, x, y):
return x + y * self.width
def show(self):
print(' ' + ' '.join(
str(n).rjust(2)
for n in range(self.width)
))
print(' +' + ''.join('-' for _ in range(self.width*3)) + '+')
for y in range(self.height):
print(str(y).rjust(2) + ' | ' + ' '.join(
'_ ' if n == Board.DARK else 'X '
for n in self.grid[y * self.width:(y + 1) * self.width]
) + '|')
print(' +' + ''.join('-' for _ in range(self.width*3)) + '+')
def on_board(self, x, y) -> bool:
'''
盤上のマスか?
'''
return 0 <= x < self.width and 0 <= y < self.height
def flip(self, x, y) -> bool:
'''
座標 (x, y) を中心に反転する.
Returns:
(x, y) を反転できた場合 True. さもなくば False.
'''
if not self.on_board(x, y):
return False
self.grid[self.index(x, y)] ^= True
for nx,ny in [(x+1,y), (x-1,y), (x,y+1), (x,y-1)]:
if self.on_board(nx, ny):
self.grid[self.index(nx, ny)] ^= True
return True
def game_over(self):
return not any(self.grid)
def process_message(board):
'''
メッセージループの処理
Returns:
処理を終了する場合 True
'''
board.show()
print('すべてのライトを点灯状態 (X) から消灯状態 (_) にしてください')
print('どこを中心に反転するかを入力してください; x y: 左上は 0 0.')
print('ゲームを終了する場合、quit')
user_input = input('|> ').split()
if user_input == ['quit']:
return True
if len(user_input) != 2:
print('座標はふたつの数字をスペース区切りで入力してください; e.g. 1 2')
return False
try:
x, y = map(int, user_input)
except ValueError:
print('数字を入力してください')
return False
if not board.flip(x, y):
print(f'座標 ({x}, {y}) で反転することはできません')
if board.game_over():
board.show()
print('ゲームクリア!')
return True
return False
# メインの処理
board = Board(5, 5)
while(True):
if process_message(board):
break
説明
メッセージループ
メインの処理はコードの一番下に描かれている関数 process_message
と、それを呼び出す無限ループである.
process_message
で行っていることを大まかに分ければ
-
print
で入力形式などを表示する -
input
で入力を受け取る - 入力に応じた処理
のみっつで、盤面に対する操作やクリア判定は Board
クラスに任せている.
Python の勉強ポイント
この部分を書いて勉強になった点のまとめ
標準入力は input()
関数で受け取れる.
input('|> ')
とすることで、|>
を標準出力に書き込み、その行に入力させることができる.
今回のコードでは input('|> ').split()
としており、入力を空白文字で分割している.
文字列配列から整数配列への変換
x, y = map(int, user_input)
とすることで、user_input: list[str]
を list[int]
に変換している.
いきなり分割代入を使わず丁寧に書くと以下のような感じ:
user_input: list[str] = input('|> ').split()
# map 関数のジェネリクス風なイメージ:
# converted: list[U] = map(f: Callable[[T],U], list[T])
# Java 風に書けば user_input.stream().map(v -> Integer.parse(v)).collect(toList())
input_ints: list[int] = map(int, user_input)
x,y = input_ints
Board
クラス
盤面の内部表現
二次元リストで問題はないのだが、何となく一次元リストを使って grid: list[int]
としている.
中身はコンストラクタのコメントに書いてある通り、0 行目の情報、1 行目の情報、... となっている.
DARK = False
で消灯、LIGHT = True
が点灯である.
二次元的な x,y
を使ってアクセスするには index(self, x, y)
を使うようにしている.
x,y
が盤面の内側であることを確かめるために on_board(self, x, y)
を用意している.
ゲームクリアの判定は game_over(self)
で行っている.
判定方法としては、grid
に含まれるすべてが DARK = False
であればクリアなので、LIGHT = True
であるものがひとつも存在しなければよい.
これを python で書くと not any(self.grid)
となる.
盤面の操作
flip(self, x, y)
は x,y
を中心とする十字の範囲を反転するメソッドである.
x,y
が盤外でないことを if not self.on_board(x, y)
で確かめたのち、True
と XOR を取ることで論理値を反転する.
盤面の描画
show(self)
で盤面を描画している.
一行目は x 座標を書き、二行目は盤の上端 (+---------------+
).
その後、一列目に y 座標を書いてから盤面の状態を描画する.
そして最後に盤の下端 (+---------------+
) を書いておしまい.
0 1 2 3 4
+---------------+
0 | X _ X X X |
1 | _ _ _ X X |
2 | X _ X X X |
3 | X X X X X |
4 | X X X X X |
+---------------+
Python の勉強ポイント
この部分を書いて勉強になった点のまとめ
同じ値を持つリストの作成
__init__(self, width, height)
で使用している self.grid = [Board.LIGHT] * (width * height)
のように、
{長さ 1 の配列} * {要素数}
とすることで指定した長さで、同じ値を持つリストを作成できる.
{長さ 1 の配列}
でなくても {リスト} * n
とすることで、{リスト}
を n
回繰り返したリストを作成できる.
ls = [1,2] * 3
print(ls) # [1, 2, 1, 2, 1, 2]
今回のようにリストの中身がイミュータブルな値であれば問題ないけれど、以下のようなケースでは注意しなければならない.
ls = [[]] * 3
print(ls) # [[], [], []]
ls[0].append(5)
print(ls) # [[5], [5], [5]]
# # イメージとしては以下のような感じで、ls の中身がすべて同一のインスタンスであることによる.
# # i.e. list * int ではシャローコピーが行われる.
# ls = []
# inner = []
# ls.append(inner)
# ls.append(inner)
# ls.append(inner)
# inner.append(5)
# print(ls) # [[5], [5], [5]]
タプルのリストを用いたループ
flip(self, x, y)
では x,y
に隣接するマスについて処理するため for
文を用いて以下のように書いた.
for nx,ny in [(x+1,y), (x-1,y), (x,y+1), (x,y-1)]:
if self.on_board(nx, ny):
self.grid[self.index(nx, ny)] ^= True
for 文の変数もこのように分割代入して使用することができる.
文字列の右寄せ
show(self)
では数字を右寄せにしている(Board
の大きさが可変なので、10 列以上あるケースとかのため(杞憂)).
実装では str(n).rjust(2)
としているが、f'{n:>2}'
でも同じことができる.
これは「n
を文字列としたのち、右寄せした(最小)幅 2 の文字列とする」の意.
リストをひとつの文字列に変換する
show(self)
には grid: list[int]
を文字列にする処理が入っている.
print(str(y).rjust(2) + ' | ' + ' '.join(
'_ ' if n == Board.DARK else 'X '
for n in self.grid[y * self.width:(y + 1) * self.width]
) + '|')
無理やりひとつの文に押し込めているので、分解して考える:
column_legend = str(y).rjust(2)
left_barline = ' | '
row = self.grid[y * self.width:(y + 1) * self.width]
lights = ' '.join(
'_ ' if n == Board.DARK else 'X '
for n in row
)
right_barline = '|'
print(column_legend + left_barline + lights + right_barline)
文字列の結合 (変数 lights
) には str.join(iterable)
を使っている.
これは ' '
を区切り文字として、iterable
をひとつの文字列にするメソッドである.
Java 風に書けば、row.stream().map(n -> n == Board.DARK? "_ " : "X ").collect(joining(" "))