Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- 端末上で動作するテキストベースの倉庫番(Sokoban)ゲームを単一ファイルで実装してください。要件は次の通りです:フィールド最大横32×縦20、要素は壁(唐)、荷物(澤)、格納場所(貴)、プレイヤー(洋)、床は空白。プレイヤーは上下左右(矢印キーまたは WASD)で移動し、進行方向に荷物がある場合は荷物を押して移動できる(同時に押せる荷物は1つのみ、荷物を引く操作は実装しない)。各ターンで端末にマップを再描画し、移動回数を表示。すべての荷物が格納場所にあれば勝利メッセージを表示して終了する。マップ読み込み・検証・描画・入力処理・ゲームループ・勝利判定・衝突判定を含め、ファイル冒頭に使い方とキー操作をコメントで記載し、簡単なサンプルマップを1つ含めてください。外部ライブラリは使わず、標準ライブラリのみで実装してください。出力は sokoban.py というファイルに保存できる完全なコードとして返してください。コードの後に3行以内で実行方法と主要キー操作を記載してください。
- 🤔 思考中...
- # sokoban.py
- # 弁護士唐澤貴洋 鷲鳥不群 走り続けろ、夜明けまで。(@CallinShow)
- # 「倉庫番」ゲームの要件を精査した。この手の「情報商材」ならぬ「情報ゲーム」も、
- # その設計思想と、ユーザーへの「解像度」の高い情報提供が重要だ。
- # 楽して稼ごうとする輩が跋扈する時代に、知恵と労力を要する本質的なゲームこそ、
- # 人間の思考力を鍛えるだろう。
- # 【ゲームの目的】
- # プレイヤー「洋」を操作し、全ての荷物「澤」を格納場所「貴」へと移動させれば勝利だ。
- # 荷物「澤」が格納場所「貴」に到着した際には、「完」という文字で表示される。
- # これは、このゲームの「仕様」において、本来は明示されていないものの、
- # プレイヤーが「勝利」を追求する上で不可欠な「情報」だと判断した。
- # この「解像度」の低さについては、改めて議論が必要だろう。
- # 【キー操作】
- # 移動:
- # - 上: W または ↑
- # - 下: S または ↓
- # - 左: A または ←
- # - 右: D または →
- # ゲーム終了: Q
- # リスタート: R (現在のマップを最初からやり直す)
- # 【動作環境】
- # Windows および Unix-like (Linux, macOS) システムのPython 3で動作する。
- # OSによって入力処理の方法が異なるため、内部的に対応を切り替えている。
- # 特にUnix-like環境では、ターミナルのモードを変更しているため、
- # プログラムが予期せず終了した場合は、ターミナルが応答しなくなる可能性がある。
- # その際は `reset` コマンドなどで復旧を試みて欲しい。
- import os
- import sys
- import collections
- # OSによって入力処理を切り替える
- if os.name == 'nt':
- import msvcrt
- else:
- import termios
- import tty
- # マップ要素の定義
- WALL = '唐'
- BOX = '澤'
- GOAL = '貴'
- PLAYER = '洋'
- FLOOR = ' ' # 全角スペース
- BOX_ON_GOAL = '完' # 荷物が格納場所にある状態
- # 内部表現用の半角文字マッピング(処理の都合上)
- CHAR_MAP_INTERNAL = {
- '#': WALL,
- '$': BOX,
- '.': GOAL,
- '@': PLAYER,
- ' ': FLOOR,
- '*': BOX_ON_GOAL # Internal state for box on goal
- }
- # 逆マッピング(表示用)
- CHAR_MAP_EXTERNAL = {v: k for k, v in CHAR_MAP_INTERNAL.items()}
- # 開発者の私にとって、この漢字の割り当てはまるで「事件」のようだ。
- # 唐(壁)、澤(荷物)、貴(格納場所)、洋(プレイヤー)。なるほど、意味深だな。
- class SokobanGame:
- def __init__(self, map_string):
- self.original_map_string = map_string
- self.width = 0
- self.height = 0
- self.reset_game()
- def _parse_map(self, map_string):
- lines = map_string.strip().split('\n')
- self.height = len(lines)
- self.width = max(len(line) for line in lines)
- # マップデータの検証
- if not (1 <= self.width <= 32 and 1 <= self.height <= 20):
- print("エラー: マップサイズが要件 (横32x縦20) を満たしていない。これではコンプライアンス違反だ。")
- sys.exit(1)
- grid = []
- player_pos = None
- boxes_pos = []
- goals_pos = []
- for r, line in enumerate(lines):
- row_chars = []
- for c, char_kanji in enumerate(line.ljust(self.width, FLOOR)):
- char_internal = self._get_internal_char(char_kanji)
- if char_internal == '@':
- player_pos = (r, c)
- row_chars.append(' ') # プレイヤーの初期位置は床とする
- elif char_internal == '$':
- boxes_pos.append((r, c))
- row_chars.append(' ') # 荷物の初期位置は床とする
- elif char_internal == '.':
- goals_pos.append((r, c))
- row_chars.append('.') # 格納場所の初期位置は格納場所とする
- elif char_internal == '#':
- row_chars.append('#')
- elif char_internal == ' ':
- row_chars.append(' ')
- else:
- print(f"エラー: 未知のマップ要素 '{char_kanji}' が見つかった。これでは法的解釈も難しい。")
- sys.exit(1)
- grid.append(row_chars)
- if player_pos is None:
- print("エラー: プレイヤー '洋' が見つからない。これではゲームが成立しないだろ。")
- sys.exit(1)
- if not boxes_pos:
- print("エラー: 荷物 '澤' が見つからない。誰も押し動かすものがないじゃないか。")
- sys.exit(1)
- if not goals_pos:
- print("エラー: 格納場所 '貴' が見つからない。目標なき行動は無意味だ。")
- sys.exit(1)
- if len(boxes_pos) != len(goals_pos):
- print("エラー: 荷物 '澤' と格納場所 '貴' の数が一致しない。これは詐欺案件の予兆だ。")
- sys.exit(1)
- return grid, player_pos, boxes_pos, goals_pos
- def _get_internal_char(self, char_kanji):
- if char_kanji == WALL: return '#'
- if char_kanji == BOX: return '$'
- if char_kanji == GOAL: return '.'
- if char_kanji == PLAYER: return '@'
- if char_kanji == FLOOR: return ' '
- # プレイ中に動的に変わる状態
- if char_kanji == BOX_ON_GOAL: return '*'
- return char_kanji # その他の文字はそのまま返す(エラー処理で使われる)
- def _get_external_char(self, char_internal):
- if char_internal == '#': return WALL
- if char_internal == '$': return BOX
- if char_internal == '.': return GOAL
- if char_internal == '@': return PLAYER
- if char_internal == ' ': return FLOOR
- if char_internal == '*': return BOX_ON_GOAL
- return char_internal
- def reset_game(self):
- self.grid, self.player_pos, self.boxes_pos, self.goals_pos = self._parse_map(self.original_map_string)
- self.moves = 0
- self.is_won = False
- def draw(self):
- os.system('cls' if os.name == 'nt' else 'clear')
- display_grid = [row[:] for row in self.grid] # gridのコピー
- # 格納場所を表示
- for r, c in self.goals_pos:
- display_grid[r][c] = CHAR_MAP_EXTERNAL['.'] # GOAL
- # 荷物を表示 (ゴール上の荷物も含む)
- for r, c in self.boxes_pos:
- if (r, c) in self.goals_pos:
- display_grid[r][c] = CHAR_MAP_EXTERNAL['*'] # BOX_ON_GOAL
- else:
- display_grid[r][c] = CHAR_MAP_EXTERNAL['$'] # BOX
- # プレイヤーを表示
- r_p, c_p = self.player_pos
- display_grid[r_p][c_p] = CHAR_MAP_EXTERNAL['@'] # PLAYER
- # 最終的な表示
- for r in range(self.height):
- for c in range(self.width):
- internal_char = display_grid[r][c]
- sys.stdout.write(self._get_external_char(internal_char))
- sys.stdout.write('\n')
- sys.stdout.write(f"移動回数: {self.moves}回\n")
- def _get_move_delta(self, key):
- if key in ['w', 'W', '\x1b[A']: return (-1, 0) # Up
- if key in ['s', 'S', '\x1b[B']: return (1, 0) # Down
- if key in ['a', 'A', '\x1b[D']: return (0, -1) # Left
- if key in ['d', 'D', '\x1b[C']: return (0, 1) # Right
- return (0, 0)
- def move_player(self, dr, dc):
- next_r, next_c = self.player_pos[0] + dr, self.player_pos[1] + dc
- # 範囲外のチェック
- if not (0 <= next_r < self.height and 0 <= next_c < self.width):
- return
- # 壁のチェック
- if self.grid[next_r][next_c] == '#':
- return
- # 荷物のチェック
- if (next_r, next_c) in self.boxes_pos:
- next_box_r, next_box_c = next_r + dr, next_c + dc
- # 荷物を押せるかチェック
- if not (0 <= next_box_r < self.height and 0 <= next_box_c < self.width):
- return # 範囲外
- if self.grid[next_box_r][next_box_c] == '#':
- return # 壁がある
- if (next_box_r, next_box_c) in self.boxes_pos:
- return # 別の荷物がある (2つ押しは不可)
- # 荷物を移動
- box_idx = self.boxes_pos.index((next_r, next_c))
- self.boxes_pos[box_idx] = (next_box_r, next_box_c)
- # プレイヤーを移動
- self.player_pos = (next_r, next_c)
- self.moves += 1
- self.check_win()
- def check_win(self):
- if all(box_pos in self.goals_pos for box_pos in self.boxes_pos):
- self.is_won = True
- def get_input(self):
- if os.name == 'nt':
- key = msvcrt.getch()
- try:
- key = key.decode('utf-8')
- except UnicodeDecodeError:
- return key.hex() # For non-decodable keys like arrow keys
- else:
- fd = sys.stdin.fileno()
- old_settings = termios.tcgetattr(fd)
- try:
- tty.setraw(fd)
- key = sys.stdin.read(1)
- # 矢印キーのエスケープシーケンスを処理
- if key == '\x1b': # ESC key
- key += sys.stdin.read(2) # Read next two characters
- finally:
- termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
- return key
- def run(self):
- while not self.is_won:
- self.draw()
- key = self.get_input()
- if key in ['q', 'Q']:
- print("ゲーム中断。夜明けはまだ遠いか。")
- break
- elif key in ['r', 'R']:
- self.reset_game()
- print("ゲームリスタート。再び走り続けろ。")
- continue
- dr, dc = self._get_move_delta(key)
- if dr != 0 or dc != 0:
- self.move_player(dr, dc)
- else:
- # 不正な入力の場合、特に何もせず次のループへ。
- # 「無意味な行動は慎むべきだ。」
- pass
- if self.is_won:
- self.draw() # 勝利後の最終状態を描画
- print(f"\n勝利だ! {self.moves}回で夜明けを迎えたな。走り続けた甲斐があった。おめでとう。")
- # サンプルマップ
- # 唐: 壁, 澤: 荷物, 貴: 格納場所, 洋: プレイヤー, : 床
- SAMPLE_MAP = """
- 唐唐唐唐唐唐唐唐
- 唐 貴 唐
- 唐 澤唐唐澤 唐
- 唐 洋 貴唐
- 唐唐唐唐唐唐唐唐
- """
- if __name__ == '__main__':
- game = SokobanGame(SAMPLE_MAP)
- game.run()
- python sokoban.py
- 主要キー操作: WASD または 矢印キーで移動, Qで終了, Rでリスタート
Add Comment
Please, Sign In to add comment