from __future__ import annotations
from dataclasses import dataclass
from enum import IntEnum
from types import MappingProxyType
from typing import Callable, Iterable, Iterator, Mapping
# ============================================================
# 位置:値オブジェクト
# ============================================================
@dataclass(frozen=True)
class Position:
x: int
y: int
def __add__(self, other: "Position") -> "Position":
return Position(self.x other.x, self.y other.y)
# ============================================================
# Entity:世界に存在するもの
# ============================================================
class Entity:
"""すべての存在の基底クラス"""
def char(self) -> str:
return " "
def is_passable(self) -> bool:
return True
class Ground(Entity):
"""地形レイヤーに置かれるもの"""
pass
class Floor(Ground):
pass
class Wall(Ground):
def char(self) -> str:
return "#"
def is_passable(self) -> bool:
return False
class Gimmick(Entity):
"""ゴールなど、地形の上に重なる仕掛け"""
pass
class Goal(Gimmick):
def char(self) -> str:
return "."
class Occupant(Entity):
"""
マスを占有する存在。
PlayerもBlockも、WorldQueryから見ると
「そのマスを塞いでいるもの」として同じ概念。
"""
def is_passable(self) -> bool:
return False
def can_act(self) -> bool:
"""自分の意思で行動できるか"""
return False
def can_be_pushed(self) -> bool:
"""他者に押されて動けるか"""
return False
class Player(Occupant):
def char(self) -> str:
return "p"
def can_act(self) -> bool:
return True
class Block(Occupant):
def char(self) -> str:
return "o"
def can_be_pushed(self) -> bool:
return True
# ============================================================
# Layer:世界を重ね合わせで表現する
# ============================================================
class LayerLevel(IntEnum):
GROUND = 0
GIMMICK = 1
OCCUPANT = 2
Layer = Mapping[Position, Entity]
def freeze_layer(layer: dict[Position, Entity]) -> Layer:
"""
dictを読み取り専用にする。
GameStateはfrozen=Trueでも、
中のdictがmutableだと本当の不変ではない。
そこでMappingProxyTypeで外部変更を防ぐ。
"""
return MappingProxyType(dict(layer))
def freeze_layers(layers: Iterable[dict[Position, Entity]]) -> tuple[Layer, ...]:
return tuple(freeze_layer(layer) for layer in layers)
# ============================================================
# GameState:世界の構造だけを持つ
# ============================================================
@dataclass(frozen=True)
class GameState:
"""
GameStateは「今の世界」を表すだけ。
ここに移動ロジック、描画、クリア判定を入れない。
単一責務を徹底する。
"""
layers: tuple[Layer, ...]
width: int
height: int
# ============================================================
# WorldQuery:世界を読む専用
# ============================================================
class WorldQuery:
"""
GameStateを観測するためのクラス。
状態は変更しない。
「この座標に何がある?」
「通れる?」
「行動できるOccupantはどこ?」
だけを答える。
"""
def __init__(self, state: GameState):
self.state = state
def in_bounds(self, pos: Position) -> bool:
return 0 <= pos.x < self.state.width and 0 <= pos.y < self.state.height
def get_at(self, pos: Position) -> list[Entity]:
if not
self.in_bounds(pos):
return [Wall()]
return [layer[pos] for layer in self.state.layers if pos in layer]
def occupant_at(self, pos: Position) -> Occupant | None:
entity = self.state.layers[LayerLevel.OCCUPANT].get(pos)
if isinstance(entity, Occupant):
return entity
return None
def is_passable(self, pos: Position) -> bool:
if not
self.in_bounds(pos):
return False
return all(
entity.is_passable() for entity in self.get_at(pos))
def actors(self) -> list[Position]:
return [
pos
for pos, entity in self.state.layers[LayerLevel.OCCUPANT].items()
if isinstance(entity, Occupant) and entity.can_act()
]
def goals(self) -> list[Position]:
return [
pos
for pos, entity in self.state.layers[LayerLevel.GIMMICK].items()
if isinstance(entity, Goal)
]
# ============================================================
# Transition:状態遷移関数
# ============================================================
Transition = Callable[[GameState], GameState]
def move_occupants(*moves: tuple[Position, Position]) -> Transition:
"""
ここがSSS の核。
この関数は、すぐに状態を変更しない。
代わりに、
GameState -> GameState
という関数を返す。
つまり「処理を実行する」のではなく、
「未来の状態変換」を値として返す。
"""
def transition(state: GameState) -> GameState:
new_layers = [dict(layer) for layer in state.layers]
occupants = new_layers[LayerLevel.OCCUPANT]
for src, dst in moves:
occupants[dst] = occupants.pop(src)
return GameState(
layers=freeze_layers(new_layers),
width=state.width,
height=state.height,
)
return transition
# ============================================================
# Rule:遷移候補を遅延生成する
# ============================================================
Rule = Callable[[WorldQuery, Position, Position], Iterator[Transition]]
def walk_rule(
query: WorldQuery,
actor_pos: Position,
direction: Position,
) -> Iterator[Transition]:
"""
歩行ルール。
条件を満たすときだけ、
「歩く状態遷移関数」をyieldする。
"""
target = actor_pos direction
if query.occupant_at(target) is None and
query.is_passable(target):
yield move_occupants((actor_pos, target))
def push_rule(
query: WorldQuery,
actor_pos: Position,
direction: Position,
) -> Iterator[Transition]:
"""
箱押しルール。
PlayerがBlockを直接知るのではなく、
can_be_pushed() という契約だけを見る。
"""
target = actor_pos direction
target_occupant = query.occupant_at(target)
if target_occupant is None:
return
if not target_occupant.can_be_pushed():
return
pushed_target = target direction
if
query.is_passable(pushed_target):
yield move_occupants(
(target, pushed_target),
(actor_pos, target),
)
def default_rules() -> tuple[Rule, ...]:
"""
ルールの優先順位。
先にpush_ruleを見るので、
箱があれば押す。
箱がなければwalk_ruleで歩く。
"""
return (
push_rule,
walk_rule,
)
# ============================================================
# TransitionGenerator:可能な遷移を生成する
# ============================================================
class TransitionGenerator:
"""
Rule群からTransitionを遅延生成する。
ここではまだ状態を変えない。
あくまで「可能な状態遷移関数」を列挙するだけ。
"""
def __init__(self, rules: Iterable[Rule]):
self.rules = tuple(rules)
def generate(
self,
state: GameState,
actor_pos: Position,
direction: Position,
) -> Iterator[Transition]:
query = WorldQuery(state)
actor = query.occupant_at(actor_pos)
if actor is None:
return
if not actor.can_act():
return
for rule in self.rules:
yield from rule(query, actor_pos, direction)
# ============================================================
# GameEngine:遷移を1つ選んで適用するだけ
# ============================================================
class GameEngine:
"""
Engineは司令塔。
ただし賢くしすぎない。
やることは、
1. 行動者を選ぶ
2. Transitionを生成する
3. 最初のTransitionを適用する
だけ。
"""
def __init__(self, generator: TransitionGenerator):
self.generator = generator
def update(
self,
state: GameState,
direction: Position,
actor_index: int = 0,
) -> GameState:
query = WorldQuery(state)
actors = query.actors()
if not (0 <= actor_index < len(actors)):
return state
actor_pos = actors[actor_index]
transition = next(
self.generator.generate(state, actor_pos, direction),
None,
)
if transition is None:
return state
return transition(state)
# ============================================================
# ClearRule:クリア判定だけ
# ============================================================
class ClearRule:
def is_cleared(self, state: GameState) -> bool:
query = WorldQuery(state)
goals = query.goals()
if not goals:
return False
return all(isinstance(query.occupant_at(goal), Block) for goal in goals)
# ============================================================
# Renderer:描画だけ
# ============================================================
class Renderer:
def render(self, state: GameState) -> None:
query = WorldQuery(state)
for y in range(state.height):
line = ""
for x in range(state.width):
pos = Position(x, y)
entities = query.get_at(pos)
top = entities[-1]
char = top.char()
has_goal = any(isinstance(entity, Goal) for entity in entities)
if has_goal:
if isinstance(top, Block):
char = "O"
elif isinstance(top, Player):
char = "P"
elif isinstance(top, Goal):
char = "."
line = char
print(line)
↓↓↓