<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Reducer on 怠惰技術ブログ</title>
    <link>/tags/reducer/</link>
    <description>Recent content in Reducer on 怠惰技術ブログ</description>
    <generator>Hugo -- 0.147.7</generator>
    <language>ja</language>
    <lastBuildDate>Sat, 28 Mar 2026 15:00:00 +0900</lastBuildDate>
    <atom:link href="/tags/reducer/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>React で作る堅牢なゲームセーブシステム：localStorage と Reducer を疎結合に保つ設計</title>
      <link>/posts/2026-03-28-save-load-system-reducer/</link>
      <pubDate>Sat, 28 Mar 2026 15:00:00 +0900</pubDate>
      <guid>/posts/2026-03-28-save-load-system-reducer/</guid>
      <description>&lt;h1 id=&#34;react-で作る堅牢なゲームセーブシステムlocalstorage-と-reducer-を疎結合に保つ設計&#34;&gt;React で作る堅牢なゲームセーブシステム：localStorage と Reducer を疎結合に保つ設計&lt;/h1&gt;
&lt;p&gt;ゲーム開発において、プレイヤーの進行状況を保存する「セーブ / ロード」は最も重要な機能の一つです。Web ブラウザで動作する React アプリケーションの場合、最も手軽な保存先は &lt;code&gt;localStorage&lt;/code&gt; です。&lt;/p&gt;
&lt;p&gt;しかし、単純に &lt;code&gt;localStorage.setItem&lt;/code&gt; をコードのあちこちに散りばめてしまうと、テスタビリティが低下し、データ構造の変更（スキーマ変更）に弱いシステムになってしまいます。&lt;/p&gt;
&lt;p&gt;本記事では、架空の RPG 『React Odyssey』を例に、&lt;code&gt;SavePort&lt;/code&gt; インターフェースと &lt;code&gt;Reducer&lt;/code&gt; の初期化関数を組み合わせた、クリーンで堅牢なセーブ / ロードの実装方法を解説します。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;1-概要なぜ直接-localstorageを避けるのか&#34;&gt;1. 概要：なぜ「直接 localStorage」を避けるのか&lt;/h2&gt;
&lt;p&gt;React の &lt;code&gt;useReducer&lt;/code&gt; を使ったゲーム開発では、ゲームの状態（State）は一つの大きなオブジェクトとして管理されます。これを &lt;code&gt;JSON.stringify&lt;/code&gt; して &lt;code&gt;localStorage&lt;/code&gt; に保存するのは簡単です。&lt;/p&gt;
&lt;p&gt;しかし、以下の理由から、ロジックの中に直接 &lt;code&gt;localStorage&lt;/code&gt; を書くべきではありません。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;副作用の分離&lt;/strong&gt;: Reducer は純粋関数であるべきです。セーブ処理（副作用）を Reducer の中に入れることはできません。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;環境非依存&lt;/strong&gt;: 将来的に保存先を &lt;code&gt;IndexedDB&lt;/code&gt; やクラウド（Firebase 等）に変更したくなったとき、コードを大幅に書き換える必要が出てきます。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;テストのしやすさ&lt;/strong&gt;: &lt;code&gt;localStorage&lt;/code&gt; が存在しない Node.js 環境（Vitest 等）でロジックのテストを行う際、モック化が容易である必要があります。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;これらを解決するために、&lt;strong&gt;Dependency Inversion Principle（依存性逆転の原則）&lt;/strong&gt; に基づいた設計を採用します。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;2-saveport-設計抽象化の定義&#34;&gt;2. SavePort 設計：抽象化の定義&lt;/h2&gt;
&lt;p&gt;まずは、ゲームエンジン側が必要とする「セーブ機能」のインターフェースを定義します。これを &lt;code&gt;SavePort&lt;/code&gt; と呼びます。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// domain/save/save-port.ts
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;/** 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; * 保存されるデータの構造（スキーマ）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; * ゲームの現在の状態に加え、メタ情報を付与する
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;SaveData&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;version&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;number&lt;/span&gt;;        &lt;span style=&#34;color:#75715e&#34;&gt;// セーブデータのバージョン
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;timestamp&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;;      &lt;span style=&#34;color:#75715e&#34;&gt;// 保存日時
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;state&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;GameState&lt;/span&gt;;       &lt;span style=&#34;color:#75715e&#34;&gt;// 実際のゲーム状態
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;/**
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; * セーブ/ロードに関する抽象インターフェース
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;SavePort&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;/** データを保存する */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;save&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;SaveData&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Promise&lt;/span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;void&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;/** データを読み込む。存在しない場合は null を返す */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;load&lt;/span&gt;()&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Promise&lt;/span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;SaveData&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;null&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;/** セーブデータが存在するか確認する */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;exists&lt;/span&gt;()&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Promise&lt;/span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;boolean&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;/** セーブデータを削除する */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;clear&lt;/span&gt;()&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Promise&lt;/span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;void&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;なぜインターフェースにするのか？&lt;/strong&gt;
ゲームのメインロジック（UseCase）は、この &lt;code&gt;SavePort&lt;/code&gt; を通じて読み書きを行います。この時点では「どうやって保存するか」は知らなくてよく、「保存できる」という事実だけに依存させます。&lt;/p&gt;</description>
    </item>
    <item>
      <title>Reducer パターンだけでゲーム状態を管理する：副作用ゼロのアーキテクチャ</title>
      <link>/posts/2026-03-28-reducer-pure-state/</link>
      <pubDate>Sat, 28 Mar 2026 14:00:00 +0900</pubDate>
      <guid>/posts/2026-03-28-reducer-pure-state/</guid>
      <description>&lt;h1 id=&#34;reducer-パターンだけでゲーム状態を管理する副作用ゼロのアーキテクチャ&#34;&gt;Reducer パターンだけでゲーム状態を管理する：副作用ゼロのアーキテクチャ&lt;/h1&gt;
&lt;h2 id=&#34;概要&#34;&gt;概要&lt;/h2&gt;
&lt;p&gt;現代のフロントエンド開発において、Redux や React の &lt;code&gt;useReducer&lt;/code&gt; を通じて「Reducer パターン」は広く浸透しました。しかし、このパターンを本格的なゲーム開発、特に複雑なロジックが絡み合う RPG やシミュレーションゲームに適用しようとすると、多くの開発者が「副作用」の壁にぶつかります。&lt;/p&gt;
&lt;p&gt;「ダメージ計算に乱数を使いたい」「ゲーム内の経過時間を管理したい」「マスターデータを参照したい」……。&lt;/p&gt;
&lt;p&gt;これらの要素を素直に実装すると、Reducer の外側にある状態や関数に依存してしまい、純粋関数としての美しさとテストのしやすさが失われてしまいます。&lt;/p&gt;
&lt;p&gt;本記事では、すべての副作用を引数として注入し、&lt;strong&gt;&lt;code&gt;gameReducer(state, action, masters, random)&lt;/code&gt;&lt;/strong&gt; という純粋関数のみでゲームの全ロジックを完結させる「副作用ゼロ」のアーキテクチャについて解説します。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;課題なぜゲーム開発で-reducer-は敬遠されるのか&#34;&gt;課題：なぜゲーム開発で Reducer は敬遠されるのか&lt;/h2&gt;
&lt;p&gt;一般的な Web アプリケーションの Reducer は単純です。「ボタンを押したらフラグを反転させる」「入力された文字列を状態に保存する」といった操作がメインだからです。&lt;/p&gt;
&lt;p&gt;しかし、ゲームでは以下のような「非決定的な要素」や「巨大な静的データ」が頻繁に登場します。&lt;/p&gt;
&lt;h3 id=&#34;1-乱数-randomness-の扱い&#34;&gt;1. 乱数 (Randomness) の扱い&lt;/h3&gt;
&lt;p&gt;クリティカルヒットの判定、モンスターのドロップアイテム、マップの自動生成など、ゲームは乱数の塊です。Reducer の中で &lt;code&gt;Math.random()&lt;/code&gt; を呼んだ瞬間、その関数は「同じ入力に対して同じ出力を返す」という純粋性を失います。&lt;/p&gt;
&lt;h3 id=&#34;2-静的データ-master-data-の参照&#34;&gt;2. 静的データ (Master Data) の参照&lt;/h3&gt;
&lt;p&gt;モンスターのステータス表、アイテムの定義、スキル効果など、ゲームには膨大な「変わらないデータ」があります。これを Reducer の外にあるグローバル変数から参照すると、テスト時にそのグローバル変数の状態も気にしなければならなくなります。&lt;/p&gt;
&lt;h3 id=&#34;3-時刻-time-と経過の管理&#34;&gt;3. 時刻 (Time) と経過の管理&lt;/h3&gt;
&lt;p&gt;「3時間経過したらスタミナが回復する」「夜になるとモンスターが強くなる」といった時間依存の処理です。&lt;code&gt;new Date()&lt;/code&gt; を Reducer で使うのは、乱数と同様に純粋性を破壊します。&lt;/p&gt;
&lt;h3 id=&#34;4-非同期処理-asyncfetch&#34;&gt;4. 非同期処理 (Async/Fetch)&lt;/h3&gt;
&lt;p&gt;サーバーから最新のイベント情報を取得したり、セーブデータをロードしたりする処理です。Reducer は同期的に実行される必要があるため、非同期処理をそのまま中に書くことはできません。&lt;/p&gt;
&lt;h3 id=&#34;oopオブジェクト指向との比較&#34;&gt;OOP（オブジェクト指向）との比較&lt;/h3&gt;
&lt;p&gt;クラスベースの OOP では、&lt;code&gt;player.attack(monster)&lt;/code&gt; のようにメソッドを呼び出します。これは直感的ですが、内部で &lt;code&gt;this.hp -= damage&lt;/code&gt; のように状態を直接書き換えます（ミュータブル）。
小規模なら良いですが、プロジェクトが大きくなり「攻撃時にスキルが発動し、その効果で回復し、さらにログに記録し……」と連鎖が始まると、どこで何が起きたかを追跡するのが不可能になります。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;設計副作用を引数に封じ込める&#34;&gt;設計：副作用を「引数」に封じ込める&lt;/h2&gt;
&lt;p&gt;副作用を排除するための解決策はシンプルです。&lt;strong&gt;「必要なものはすべて外から渡す」&lt;/strong&gt;、つまり依存性の注入 (DI) です。&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
