From b86d2e0352e668f7cc451151a3f21925da6d563f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Nov 2025 18:48:26 +0000 Subject: [PATCH 01/21] =?UTF-8?q?Phase=200:=20Go=E7=A7=BB=E6=A4=8D?= =?UTF-8?q?=E3=83=97=E3=83=AD=E3=82=B8=E3=82=A7=E3=82=AF=E3=83=88=E3=81=AE?= =?UTF-8?q?=E6=BA=96=E5=82=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移植計画ドキュメントを作成 - MIGRATION_PLAN.md: 全体計画と手順 - PERL_TO_GO_MAPPING.md: Perl→Go変換対応表 - DATA_FORMAT.md: データフォーマット仕様 - API_REFERENCE.md: 内部API設計 - TESTING_STRATEGY.md: テスト戦略 - リポジトリ構造を整理 - 既存Perlコードを perl/ ディレクトリに移動 - Go実装用のディレクトリ構造を作成 (go/) - ドキュメントを docs/ 配下に整理 - Go版の初期設定 - go.mod を初期化 - Makefile を作成(ビルド、テスト用) - README.md を作成 Phase 1(逐語的移植)の準備が完了。 --- README.md | 100 ++- docs/migration/API_REFERENCE.md | 787 +++++++++++++++++ docs/migration/DATA_FORMAT.md | 459 ++++++++++ docs/migration/MIGRATION_PLAN.md | 456 ++++++++++ docs/migration/PERL_TO_GO_MAPPING.md | 851 +++++++++++++++++++ docs/migration/TESTING_STRATEGY.md | 621 ++++++++++++++ docs/original/README.md | 58 ++ docs/original/hako-readme.txt | 106 +++ docs/original/memo.txt | 90 ++ go/Makefile | 94 ++ go/README.md | 166 ++++ go/go.mod | 3 + .perltidyrc => perl/.perltidyrc | 0 app.psgi => perl/app.psgi | 0 {cgi => perl/cgi}/hako-main.cgi | 0 {cgi => perl/cgi}/hako-mente.cgi | 0 cpanfile => perl/cpanfile | 0 cpanfile.snapshot => perl/cpanfile.snapshot | 0 {lib => perl/lib}/Hako/Const.pm | 0 {lib => perl/lib}/Hako/Main.pm | 0 {lib => perl/lib}/Hako/Maintenance.pm | 0 {lib => perl/lib}/Hako/Map.pm | 0 {lib => perl/lib}/Hako/Top.pm | 0 {lib => perl/lib}/Hako/Turn.pm | 0 {lib => perl/lib}/Hako/Variable.pm | 0 {public => perl/public}/images/black.gif | Bin {public => perl/public}/images/f02.gif | Bin {public => perl/public}/images/land0.gif | Bin {public => perl/public}/images/land1.gif | Bin {public => perl/public}/images/land10.gif | Bin {public => perl/public}/images/land11.gif | Bin {public => perl/public}/images/land12.gif | Bin {public => perl/public}/images/land13.gif | Bin {public => perl/public}/images/land14.gif | Bin {public => perl/public}/images/land15.gif | Bin {public => perl/public}/images/land16.gif | Bin {public => perl/public}/images/land2.gif | Bin {public => perl/public}/images/land3.gif | Bin {public => perl/public}/images/land4.gif | Bin {public => perl/public}/images/land5.gif | Bin {public => perl/public}/images/land6.gif | Bin {public => perl/public}/images/land7.gif | Bin {public => perl/public}/images/land8.gif | Bin {public => perl/public}/images/land9.gif | Bin {public => perl/public}/images/monster0.gif | Bin {public => perl/public}/images/monster1.gif | Bin {public => perl/public}/images/monster2.gif | Bin {public => perl/public}/images/monster3.gif | Bin {public => perl/public}/images/monster4.gif | Bin {public => perl/public}/images/monster5.gif | Bin {public => perl/public}/images/monster6.gif | Bin {public => perl/public}/images/monster7.gif | Bin {public => perl/public}/images/monster8.gif | Bin {public => perl/public}/images/monument0.gif | Bin {public => perl/public}/images/monument1.gif | Bin {public => perl/public}/images/monument2.gif | Bin {public => perl/public}/images/prize0.gif | Bin {public => perl/public}/images/prize1.gif | Bin {public => perl/public}/images/prize10.gif | Bin {public => perl/public}/images/prize11.gif | Bin {public => perl/public}/images/prize2.gif | Bin {public => perl/public}/images/prize3.gif | Bin {public => perl/public}/images/prize4.gif | Bin {public => perl/public}/images/prize5.gif | Bin {public => perl/public}/images/prize6.gif | Bin {public => perl/public}/images/prize7.gif | Bin {public => perl/public}/images/prize8.gif | Bin {public => perl/public}/images/prize9.gif | Bin {public => perl/public}/images/space.gif | Bin {public => perl/public}/images/space0.gif | Bin {public => perl/public}/images/space1.gif | Bin {public => perl/public}/images/space10.gif | Bin {public => perl/public}/images/space11.gif | Bin {public => perl/public}/images/space2.gif | Bin {public => perl/public}/images/space3.gif | Bin {public => perl/public}/images/space4.gif | Bin {public => perl/public}/images/space5.gif | Bin {public => perl/public}/images/space6.gif | Bin {public => perl/public}/images/space7.gif | Bin {public => perl/public}/images/space8.gif | Bin {public => perl/public}/images/space9.gif | Bin {public => perl/public}/images/spacep.gif | Bin {public => perl/public}/images/xbar.gif | Bin {scripts => perl/scripts}/perltidy.sh | 0 {t => perl/t}/Main.t | 0 {t => perl/t}/Maintenance.t | 0 86 files changed, 3779 insertions(+), 12 deletions(-) create mode 100644 docs/migration/API_REFERENCE.md create mode 100644 docs/migration/DATA_FORMAT.md create mode 100644 docs/migration/MIGRATION_PLAN.md create mode 100644 docs/migration/PERL_TO_GO_MAPPING.md create mode 100644 docs/migration/TESTING_STRATEGY.md create mode 100644 docs/original/README.md create mode 100644 docs/original/hako-readme.txt create mode 100644 docs/original/memo.txt create mode 100644 go/Makefile create mode 100644 go/README.md create mode 100644 go/go.mod rename .perltidyrc => perl/.perltidyrc (100%) rename app.psgi => perl/app.psgi (100%) rename {cgi => perl/cgi}/hako-main.cgi (100%) rename {cgi => perl/cgi}/hako-mente.cgi (100%) rename cpanfile => perl/cpanfile (100%) rename cpanfile.snapshot => perl/cpanfile.snapshot (100%) rename {lib => perl/lib}/Hako/Const.pm (100%) rename {lib => perl/lib}/Hako/Main.pm (100%) rename {lib => perl/lib}/Hako/Maintenance.pm (100%) rename {lib => perl/lib}/Hako/Map.pm (100%) rename {lib => perl/lib}/Hako/Top.pm (100%) rename {lib => perl/lib}/Hako/Turn.pm (100%) rename {lib => perl/lib}/Hako/Variable.pm (100%) rename {public => perl/public}/images/black.gif (100%) rename {public => perl/public}/images/f02.gif (100%) rename {public => perl/public}/images/land0.gif (100%) rename {public => perl/public}/images/land1.gif (100%) rename {public => perl/public}/images/land10.gif (100%) rename {public => perl/public}/images/land11.gif (100%) rename {public => perl/public}/images/land12.gif (100%) rename {public => perl/public}/images/land13.gif (100%) rename {public => perl/public}/images/land14.gif (100%) rename {public => perl/public}/images/land15.gif (100%) rename {public => perl/public}/images/land16.gif (100%) rename {public => perl/public}/images/land2.gif (100%) rename {public => perl/public}/images/land3.gif (100%) rename {public => perl/public}/images/land4.gif (100%) rename {public => perl/public}/images/land5.gif (100%) rename {public => perl/public}/images/land6.gif (100%) rename {public => perl/public}/images/land7.gif (100%) rename {public => perl/public}/images/land8.gif (100%) rename {public => perl/public}/images/land9.gif (100%) rename {public => perl/public}/images/monster0.gif (100%) rename {public => perl/public}/images/monster1.gif (100%) rename {public => perl/public}/images/monster2.gif (100%) rename {public => perl/public}/images/monster3.gif (100%) rename {public => perl/public}/images/monster4.gif (100%) rename {public => perl/public}/images/monster5.gif (100%) rename {public => perl/public}/images/monster6.gif (100%) rename {public => perl/public}/images/monster7.gif (100%) rename {public => perl/public}/images/monster8.gif (100%) rename {public => perl/public}/images/monument0.gif (100%) rename {public => perl/public}/images/monument1.gif (100%) rename {public => perl/public}/images/monument2.gif (100%) rename {public => perl/public}/images/prize0.gif (100%) rename {public => perl/public}/images/prize1.gif (100%) rename {public => perl/public}/images/prize10.gif (100%) rename {public => perl/public}/images/prize11.gif (100%) rename {public => perl/public}/images/prize2.gif (100%) rename {public => perl/public}/images/prize3.gif (100%) rename {public => perl/public}/images/prize4.gif (100%) rename {public => perl/public}/images/prize5.gif (100%) rename {public => perl/public}/images/prize6.gif (100%) rename {public => perl/public}/images/prize7.gif (100%) rename {public => perl/public}/images/prize8.gif (100%) rename {public => perl/public}/images/prize9.gif (100%) rename {public => perl/public}/images/space.gif (100%) rename {public => perl/public}/images/space0.gif (100%) rename {public => perl/public}/images/space1.gif (100%) rename {public => perl/public}/images/space10.gif (100%) rename {public => perl/public}/images/space11.gif (100%) rename {public => perl/public}/images/space2.gif (100%) rename {public => perl/public}/images/space3.gif (100%) rename {public => perl/public}/images/space4.gif (100%) rename {public => perl/public}/images/space5.gif (100%) rename {public => perl/public}/images/space6.gif (100%) rename {public => perl/public}/images/space7.gif (100%) rename {public => perl/public}/images/space8.gif (100%) rename {public => perl/public}/images/space9.gif (100%) rename {public => perl/public}/images/spacep.gif (100%) rename {public => perl/public}/images/xbar.gif (100%) rename {scripts => perl/scripts}/perltidy.sh (100%) rename {t => perl/t}/Main.t (100%) rename {t => perl/t}/Maintenance.t (100%) diff --git a/README.md b/README.md index a90bb53..4961cfc 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,91 @@ -# これはなにか +# 箱庭諸島 -箱庭諸島2をベースに、今風のPerlで動くようにする版です。 +箱庭諸島 ver2.3 の現代的な実装プロジェクトです。 -# ライセンス +## 概要 -オリジナルの箱庭諸島2に準じます。 +本プロジェクトは以下の2つの実装を含みます: + +1. **Perl版** (`perl/`) - Plack/PSGIを使用した現代的なPerl実装 +2. **Go版** (`go/`) - Perl版からのポート(移植中) + +## プロジェクト構成 + +``` +hakoniwa/ +├── perl/ # Perl実装(現行版) +│ ├── cgi/ # CGIスクリプト +│ ├── lib/Hako/ # コアモジュール +│ ├── t/ # テスト +│ └── app.psgi # Plackアプリケーション +├── go/ # Go実装(移植版) +│ ├── cmd/ # エントリーポイント +│ ├── internal/ # 内部パッケージ +│ └── test/ # テスト +└── docs/ # ドキュメント + ├── migration/ # 移植関連ドキュメント + └── original/ # オリジナルドキュメント +``` + +## クイックスタート + +### Perl版を実行 + +```bash +cd perl +plackup app.psgi +# http://localhost:5000 でアクセス +``` + +### Go版を実行(移植完了後) + +```bash +cd go +make build +./bin/hako-main +# http://localhost:8080 でアクセス +``` + +## Go移植プロジェクト + +### 移植計画 + +詳細は [移植計画ドキュメント](docs/migration/MIGRATION_PLAN.md) を参照してください。 + +**フェーズ:** +- **Phase 0**: 準備・設計 ✅ +- **Phase 1**: 逐語的移植(進行中) +- **Phase 2**: Go慣用的リファクタリング(予定) +- **Phase 3**: 最適化・クリーンアップ(予定) + +### ドキュメント + +- [移植計画](docs/migration/MIGRATION_PLAN.md) - 全体計画と手順 +- [Perl→Goマッピング](docs/migration/PERL_TO_GO_MAPPING.md) - コード変換対応表 +- [データフォーマット仕様](docs/migration/DATA_FORMAT.md) - ファイル形式 +- [APIリファレンス](docs/migration/API_REFERENCE.md) - 内部API設計 +- [テスト戦略](docs/migration/TESTING_STRATEGY.md) - テスト方針 + +## ライセンス + +オリジナルの箱庭諸島 ver2.3 に準じます。 素晴らしいゲームを制作された原作者の方々に敬意を表します。 > 箱庭諸島 ver2.3 -> +> > 字: 徳岡宏樹 > 絵: 小川克人 > 題字: 稲葉修吾 > テストプレイ他協力: 井上友博、小澤武史、さかもと、ほえほえ、ありづか > 箱庭諸島のページ: http://www.bekkoame.ne.jp/~tokuoka/hakoniwa.html -## スクリプトについて +### スクリプトについて 以下、オリジナルのreadme.txtより。 > 箱庭諸島2のスクリプトを改変し、それを他人に譲渡、配布する場合には、 > 以下の制約を課します。 -> +> > ・無料配布であること。 > ・ゲーム画面のトップに表示される、スクリプトの配布元へのリンクを > 消すのを禁止すること。また、それ以外の改造は許可すること。 @@ -32,12 +96,12 @@ …といっても、オリジナルの作者である[徳岡宏樹さんのWebサイト](http://t.pos.to/hako/)は 現時点でアクセスできない状態になってしまっているため、4つめの制約はあまり意味のないものになってしまっています。 -## 画像ファイルについて +### 画像ファイルについて 以下、[徳岡宏樹さんのWebサイトサイトのアーカイブ](https://web.archive.org/web/20070113153728/http://t.pos.to/hako/)より。 > [Q3] オリジナルに付属していた画像については、再配布や改変は可能ですか? -> +> > [A3] 商用利用を除いて、許可するものとします。 > オリジナルスクリプトに付属していた文書では「箱庭諸島以外の用途に使用してはならない」と書いてありました。 > しかし、その後原作者より「商用でない限り、箱庭諸島以外でも配布・改変可」という許可を得ています。 @@ -45,7 +109,7 @@ > もちろん何らかの問題が発生したとしても画像の原作者は関知しません。 > 自己責任でお願いします。 -# 参考にさせていただいたWebサイト +## 参考にさせていただいたWebサイト * 再配布 * [箱庭諸島の保管庫](http://www.hakoniwa.net/hako/) @@ -53,6 +117,18 @@ * [Neo-INO](http://neo-sub.sakura.ne.jp/ino/hako/download.html) * 解説 * [箱庭解体新書](http://qqmh3psd.web.fc2.com/sadoga/) -* jcode.pl - * [ftp.iij.ad.jp](ftp://ftp.iij.ad.jp/pub/IIJ/dist/utashiro/perl/) +## 貢献 + +プルリクエストを歓迎します!移植プロジェクトへの貢献については、[移植計画](docs/migration/MIGRATION_PLAN.md)を参照してください。 + +## 開発環境 + +### Perl版 +- Perl 5.x +- Plack/PSGI +- 依存モジュール: `cpanfile` 参照 + +### Go版 +- Go 1.21以上 +- 依存モジュール: `go.mod` 参照 diff --git a/docs/migration/API_REFERENCE.md b/docs/migration/API_REFERENCE.md new file mode 100644 index 0000000..da5fc01 --- /dev/null +++ b/docs/migration/API_REFERENCE.md @@ -0,0 +1,787 @@ +# API リファレンス + +本ドキュメントは、箱庭諸島Go版の内部APIを定義します。Phase 1(逐語的移植)とPhase 2(リファクタリング後)の両方の設計を記載します。 + +--- + +## 目次 + +1. [Phase 1 API(逐語的移植)](#phase-1-api逐語的移植) +2. [Phase 2 API(リファクタリング後)](#phase-2-apiリファクタリング後) +3. [パッケージ一覧](#パッケージ一覧) +4. [共通型定義](#共通型定義) + +--- + +## Phase 1 API(逐語的移植) + +Phase 1では、Perlの構造をそのまま維持し、グローバル変数を使用します。 + +### パッケージ構成 + +``` +internal/hako/ +├── const/ # 定数 +├── variable/ # グローバル変数 +├── core/ # コアロジック +├── turn/ # ターン処理 +├── mapview/ # 島の表示 +├── top/ # トップページ +└── maintenance/ # メンテナンス +``` + +--- + +### const パッケージ + +**ファイル:** `internal/hako/const/const.go` + +#### 設定定数 + +```go +package const + +// ディレクトリ・パス設定 +const ( + BaseDir = "" + ImageDir = "/images" + DirName = "data" + DirMode = 0755 +) + +// 認証設定 +const ( + MasterPassword = "yourpassword" + SpecialPassword = "" + AdminName = "管理者" + Email = "admin@example.com" +) + +// ゲーム設定 +const ( + UnitTime = 21600 // 6時間(秒) + MaxIsland = 30 // 最大島数 + TopLogTurn = 30 // トップページに表示するログのターン数 + LogMax = 10 // ログファイルの最大数 + BackupTurn = 10 // バックアップ間隔(ターン) + BackupTimes = 4 // バックアップ世代数 + HistoryMax = 100 // 履歴の最大行数 + GiveupTurn = 24 // 放棄判定ターン数 + CommandMax = 30 // コマンドキューの最大数 + IslandSize = 11 // 島のサイズ(11x11) +) + +// 初期値 +const ( + InitialMoney = 100 // 初期資金(100億円単位) + InitialFood = 50 // 初期食料(100トン単位) +) + +// 単位 +const ( + UnitMoney = 100 // 資金の単位(億円) + UnitFood = 100 // 食料の単位(トン) + UnitPop = 100 // 人口の単位(人) + UnitArea = 100 // 面積の単位(平方km) +) +``` + +#### 地形定数 + +```go +// 地形タイプ +const ( + LandSea = 0 // 海 + LandWaste = 1 // 荒地 + LandPlains = 2 // 平地 + LandTown = 3 // 町 + LandForest = 4 // 森 + LandFarm = 5 // 農場 + LandFactory = 6 // 工場 + LandBase = 7 // ミサイル基地 + LandSbase = 8 // 海底基地 + LandDefence = 9 // 防衛施設 + LandMountain = 10 // 山 + LandMonster = 11 // モンスター + LandSbase2 = 12 // 海底都市 + LandOil = 13 // 海底油田 + LandMonument = 14 // 記念碑 +) +``` + +#### 災害・モンスター定数 + +```go +// 災害確率 +const ( + DisEarthquake = 10 // 地震 + DisTsunami = 10 // 津波 + DisTyphoon = 10 // 台風 + DisMeteo = 5 // 隕石 + DisHugeMeteo = 10 // 巨大隕石 + DisEruption = 5 // 噴火 + DisFire = 20 // 火災 +) + +// モンスター種類 +const ( + MonsterNumber = 9 +) + +// モンスター名 +var MonsterName = []string{ + "いのら", + "サンジラ", + "レッドいのら", + "ダークいのら", + "いのらゴースト", + "クジラ", + "キングいのら", + "メカいのら", + "くじらず", +} + +// モンスターHP +var MonsterBHP = []int{1, 1, 2, 3, 1, 10, 5, 3, 20} +var MonsterDHP = []int{1, 2, 2, 3, 1, 10, 5, 5, 30} + +// モンスター特殊能力 +var MonsterSpecial = []int{0, 0, 1, 2, 3, 4, 5, 6, 7} + +// モンスター経験値 +var MonsterExp = []int{100, 200, 300, 500, 10, 1000, 800, 500, 2000} +``` + +--- + +### variable パッケージ + +**ファイル:** `internal/hako/variable/variable.go` + +#### グローバル変数 + +```go +package variable + +import "bytes" + +// リクエストパラメータ +var ( + Mode string // 処理モード + CurrentName string // 島の名前(入力) + CurrentID string // 島のID(入力) + DefaultID string // デフォルトID + OldPassword string // 旧パスワード + InputPassword string // 新パスワード + InputPassword2 string // 新パスワード(確認) + Message string // メッセージ +) + +// コマンド関連 +var ( + CommandPlanNumber int // コマンド番号 + CommandKind int // コマンド種類 + CommandTarget string // ターゲットID + CommandX int // X座標 + CommandY int // Y座標 + CommandArg int // 引数 + CommandMode string // コマンドモード(write/insert/delete) +) + +// ゲーム状態 +var ( + Turn string // 現在のターン + IslandNumber string // 島の総数 + IslandLastID string // 最後のID + Islands []Island // 島データ + IDToName map[string]string // ID → 名前 + IDToNumber map[string]int // ID → 番号 +) + +// ログプール +var ( + LogPool []string // 通常ログ + LogPoolSecure []string // 機密ログ +) + +// 出力バッファ +var ( + OutputBuffer bytes.Buffer // HTML出力バッファ +) + +// 乱数座標配列 +var ( + Rpx [121]int // ランダムX座標 + Rpy [121]int // ランダムY座標 +) +``` + +--- + +### core パッケージ + +**ファイル:** `internal/hako/core/` + +#### core.go - エントリーポイント + +```go +package core + +// RunMain はメインエントリーポイント +// Ref: lib/Hako/Main.pm:65 +func RunMain(r *http.Request) +``` + +#### fileio.go - ファイルI/O + +```go +// ReadIslandsFile はメインデータファイルを読み込む +// Ref: lib/Hako/Main.pm:~200 +func ReadIslandsFile(id string) bool + +// ReadIsland は個別島データを読み込む +// Ref: lib/Hako/Main.pm:~300 +func ReadIsland(island *Island) bool + +// WriteIslandsFile はメインデータファイルに書き込む +// Ref: lib/Hako/Main.pm:~400 +func WriteIslandsFile() bool + +// WriteIsland は個別島データを書き込む +// Ref: lib/Hako/Main.pm:~450 +func WriteIsland(island *Island) bool +``` + +#### lock.go - ロック機構 + +```go +// hakoLock はファイルロックを取得 +// Ref: lib/Hako/Main.pm:~600 +func hakoLock() bool + +// unlock はファイルロックを解放 +// Ref: lib/Hako/Main.pm:~650 +func unlock() +``` + +#### utils.go - ユーティリティ + +```go +// htmlEscape はHTML特殊文字をエスケープ +// Ref: lib/Hako/Main.pm:~500 +func htmlEscape(str string) string + +// random は乱数を生成 [0, m) +// Ref: lib/Hako/Main.pm:~520 +func random(m int) int + +// encode はパスワードを暗号化 +// Ref: lib/Hako/Main.pm:~540 +func encode(password string) string + +// checkPassword はパスワードを検証 +// Ref: lib/Hako/Main.pm:~560 +func checkPassword(input, stored string) bool + +// expToLevel は経験値からレベルを計算 +// Ref: lib/Hako/Main.pm:~700 +func expToLevel(exp int) int + +// monsterSpec はモンスターの仕様を取得 +// Ref: lib/Hako/Main.pm:~720 +func monsterSpec(lv int) (name string, hp int, exp int) +``` + +#### template.go - HTMLテンプレート + +```go +// tempHeader はHTMLヘッダーを出力 +// Ref: lib/Hako/Main.pm:~800 +func tempHeader() + +// tempFooter はHTMLフッターを出力 +// Ref: lib/Hako/Main.pm:~850 +func tempFooter() + +// tempLockFail はロック失敗メッセージを出力 +// Ref: lib/Hako/Main.pm:~900 +func tempLockFail() + +// tempNoDataFile はデータファイルなしメッセージを出力 +// Ref: lib/Hako/Main.pm:~920 +func tempNoDataFile() + +// tempWrongPassword はパスワード誤りメッセージを出力 +// Ref: lib/Hako/Main.pm:~940 +func tempWrongPassword() +``` + +--- + +### turn パッケージ + +**ファイル:** `internal/hako/turn/` + +#### turn.go + +```go +package turn + +// NewIslandMain は新しい島を作成 +// Ref: lib/Hako/Turn.pm:~50 +func NewIslandMain() + +// TurnMain はターン処理を実行 +// Ref: lib/Hako/Turn.pm:~500 +func TurnMain() + +// ChangeMain は島の情報を変更 +// Ref: lib/Hako/Turn.pm:~200 +func ChangeMain() +``` + +--- + +### mapview パッケージ + +**ファイル:** `internal/hako/mapview/mapview.go` + +```go +package mapview + +// PrintIslandMain は島を観光表示 +// Ref: lib/Hako/Map.pm:~50 +func PrintIslandMain() + +// OwnerMain は島の開発画面を表示 +// Ref: lib/Hako/Map.pm:~200 +func OwnerMain() + +// CommandMain はコマンドを登録 +// Ref: lib/Hako/Map.pm:~400 +func CommandMain() + +// CommentMain はコメントを更新 +// Ref: lib/Hako/Map.pm:~600 +func CommentMain() +``` + +--- + +### top パッケージ + +**ファイル:** `internal/hako/top/top.go` + +```go +package top + +// TopPageMain はトップページを表示 +// Ref: lib/Hako/Top.pm:~50 +func TopPageMain() +``` + +--- + +### maintenance パッケージ + +**ファイル:** `internal/hako/maintenance/maintenance.go` + +```go +package maintenance + +// RunMaintenance はメンテナンスツールを実行 +// Ref: lib/Hako/Maintenance.pm:~50 +func RunMaintenance(r *http.Request) +``` + +--- + +## Phase 2 API(リファクタリング後) + +Phase 2では、グローバル変数を排除し、構造体ベースの設計に移行します。 + +### 設計方針 + +- グローバル変数 → 構造体フィールド +- 関数 → メソッド +- インターフェースの導入 +- エラーハンドリングの改善 +- コンテキスト対応 + +--- + +### コアインターフェース + +```go +// Storage はデータ永続化のインターフェース +type Storage interface { + ReadIslands(ctx context.Context) ([]Island, error) + WriteIslands(ctx context.Context, islands []Island) error + ReadIsland(ctx context.Context, id string) (*Island, error) + WriteIsland(ctx context.Context, island *Island) error + ReadHistory(ctx context.Context) ([]HistoryEntry, error) + WriteHistory(ctx context.Context, entries []HistoryEntry) error +} + +// LockManager はロック管理のインターフェース +type LockManager interface { + Lock(ctx context.Context) error + Unlock(ctx context.Context) error +} + +// Logger はログ出力のインターフェース +type Logger interface { + Info(msg string, fields ...Field) + Error(msg string, err error, fields ...Field) +} +``` + +--- + +### Game 構造体(Phase 2のコア) + +```go +package game + +import ( + "context" + "hakoniwa/internal/hako/const" +) + +// Game はゲームのメインロジックを管理 +type Game struct { + config *Config + storage Storage + lockMgr LockManager + logger Logger + rand *rand.Rand +} + +// NewGame は新しいGameインスタンスを作成 +func NewGame(cfg *Config, storage Storage, lockMgr LockManager, logger Logger) *Game { + return &Game{ + config: cfg, + storage: storage, + lockMgr: lockMgr, + logger: logger, + rand: rand.New(rand.NewSource(time.Now().UnixNano())), + } +} + +// HandleRequest はHTTPリクエストを処理 +func (g *Game) HandleRequest(ctx context.Context, req *Request) (*Response, error) { + // ロック取得 + if err := g.lockMgr.Lock(ctx); err != nil { + return nil, fmt.Errorf("failed to acquire lock: %w", err) + } + defer g.lockMgr.Unlock(ctx) + + // データ読み込み + islands, err := g.storage.ReadIslands(ctx) + if err != nil { + return nil, fmt.Errorf("failed to read islands: %w", err) + } + + // モード別処理 + var response *Response + switch req.Mode { + case "top": + response, err = g.handleTopPage(ctx, islands) + case "print": + response, err = g.handlePrintIsland(ctx, req, islands) + case "owner": + response, err = g.handleOwner(ctx, req, islands) + // ... + default: + response, err = g.handleTopPage(ctx, islands) + } + + if err != nil { + return nil, err + } + + return response, nil +} +``` + +--- + +### Request/Response 構造体 + +```go +// Request はHTTPリクエストを表現 +type Request struct { + Mode string + IslandName string + IslandID string + Password string + OldPassword string + Message string + Command *Command +} + +// Response はHTTPレスポンスを表現 +type Response struct { + StatusCode int + ContentType string + Body []byte + Cookies []*http.Cookie +} + +// Command はコマンドを表現 +type Command struct { + Number int + Kind int + Target string + X int + Y int + Arg int + Mode string // write/insert/delete +} +``` + +--- + +### Storage 実装 + +#### FileStorage(本番用) + +```go +package storage + +type FileStorage struct { + dataDir string + mu sync.RWMutex +} + +func NewFileStorage(dataDir string) *FileStorage { + return &FileStorage{dataDir: dataDir} +} + +func (fs *FileStorage) ReadIslands(ctx context.Context) ([]Island, error) { + fs.mu.RLock() + defer fs.mu.RUnlock() + + filepath := fmt.Sprintf("%s/hakojima.dat", fs.dataDir) + // 実装... +} + +func (fs *FileStorage) WriteIslands(ctx context.Context, islands []Island) error { + fs.mu.Lock() + defer fs.mu.Unlock() + + filepath := fmt.Sprintf("%s/hakojima.dat", fs.dataDir) + // 実装... +} +``` + +#### MemoryStorage(テスト用) + +```go +package storage + +type MemoryStorage struct { + islands map[string]*Island + mu sync.RWMutex +} + +func NewMemoryStorage() *MemoryStorage { + return &MemoryStorage{ + islands: make(map[string]*Island), + } +} + +func (ms *MemoryStorage) ReadIslands(ctx context.Context) ([]Island, error) { + ms.mu.RLock() + defer ms.mu.RUnlock() + + result := make([]Island, 0, len(ms.islands)) + for _, island := range ms.islands { + result = append(result, *island) + } + return result, nil +} +``` + +--- + +## 共通型定義 + +すべてのフェーズで使用される型定義。 + +### Island 構造体 + +```go +// Island は島のデータを表現 +type Island struct { + // 基本情報 + Name string `json:"name"` + ID string `json:"id"` + Prize int `json:"prize"` + Absent int `json:"absent"` + Comment string `json:"comment"` + Password string `json:"-"` // JSONには含めない + + // リソース + Money int `json:"money"` + Food int `json:"food"` + Pop int `json:"pop"` + Area int `json:"area"` + + // 施設 + Farm int `json:"farm"` + Factory int `json:"factory"` + Mountain int `json:"mountain"` + + // 地形データ + Land [][]int `json:"land"` // 11x11 + LandValue [][]int `json:"land_value"` // 11x11 + + // コマンド・掲示板 + Commands []Command `json:"commands"` + Lbbs []LbbsEntry `json:"lbbs"` +} + +// NewIsland は新しい島を作成 +func NewIsland(name, id, password string) *Island { + island := &Island{ + Name: name, + ID: id, + Password: password, + Money: const.InitialMoney, + Food: const.InitialFood, + Area: const.IslandSize * const.IslandSize, + Land: make([][]int, const.IslandSize), + LandValue: make([][]int, const.IslandSize), + } + + // 地形初期化 + for i := 0; i < const.IslandSize; i++ { + island.Land[i] = make([]int, const.IslandSize) + island.LandValue[i] = make([]int, const.IslandSize) + } + + return island +} +``` + +### LandInfo 構造体 + +```go +// LandInfo は地形情報を表現 +type LandInfo struct { + Type int // 地形タイプ + Value int // 地形の値 +} +``` + +### Command 構造体 + +```go +// Command はコマンドを表現 +type Command struct { + Kind int `json:"kind"` + Target string `json:"target"` + X int `json:"x"` + Y int `json:"y"` + Arg int `json:"arg"` +} +``` + +### LbbsEntry 構造体 + +```go +// LbbsEntry はローカル掲示板のエントリーを表現 +type LbbsEntry struct { + Name string `json:"name"` + Message string `json:"message"` + Time int64 `json:"time"` +} +``` + +### HistoryEntry 構造体 + +```go +// HistoryEntry は履歴エントリーを表現 +type HistoryEntry struct { + Turn int `json:"turn"` + Message string `json:"message"` +} +``` + +### LogEntry 構造体 + +```go +// LogEntry は詳細ログエントリーを表現 +type LogEntry struct { + SecretFlag int `json:"secret_flag"` // 0=公開, 1=当事者のみ, 2=機密 + Turn int `json:"turn"` + ID1 string `json:"id1"` // 当事者 + ID2 string `json:"id2"` // 相手 + Message string `json:"message"` +} +``` + +--- + +## 使用例 + +### Phase 1 の使用例 + +```go +import ( + "net/http" + "hakoniwa/internal/hako/core" +) + +func handler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=UTF-8") + core.RunMain(r) + w.Write(variable.OutputBuffer.Bytes()) +} +``` + +### Phase 2 の使用例 + +```go +import ( + "context" + "net/http" + "hakoniwa/internal/game" + "hakoniwa/internal/storage" +) + +func handler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // 依存性注入 + storage := storage.NewFileStorage("data") + lockMgr := lock.NewFileLock("data/hakojima.lock") + logger := logger.NewStdLogger() + cfg := LoadConfig() + + game := game.NewGame(cfg, storage, lockMgr, logger) + + // リクエストパース + req := parseRequest(r) + + // 処理実行 + resp, err := game.HandleRequest(ctx, req) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // レスポンス送信 + w.Header().Set("Content-Type", resp.ContentType) + w.WriteHeader(resp.StatusCode) + w.Write(resp.Body) +} +``` + +--- + +## 参考資料 + +- [PERL_TO_GO_MAPPING.md](./PERL_TO_GO_MAPPING.md) - Perl/Go対応表 +- [DATA_FORMAT.md](./DATA_FORMAT.md) - データフォーマット仕様 diff --git a/docs/migration/DATA_FORMAT.md b/docs/migration/DATA_FORMAT.md new file mode 100644 index 0000000..72f51cc --- /dev/null +++ b/docs/migration/DATA_FORMAT.md @@ -0,0 +1,459 @@ +# データフォーマット仕様 + +本ドキュメントは、箱庭諸島のデータファイル形式を詳細に記述します。Perl版とGo版で**完全な互換性**を維持する必要があります。 + +--- + +## 目次 + +1. [ファイル一覧](#ファイル一覧) +2. [hakojima.dat - メインデータファイル](#hakojimadat---メインデータファイル) +3. [island.{ID} - 個別島データ](#islandid---個別島データ) +4. [hakojima.his - 履歴ファイル](#hakojimahis---履歴ファイル) +5. [hakojima.log{N} - 詳細ログファイル](#hakojimalogn---詳細ログファイル) +6. [ロックファイル](#ロックファイル) +7. [バックアップファイル](#バックアップファイル) +8. [エンコーディング](#エンコーディング) +9. [Go実装の注意点](#go実装の注意点) + +--- + +## ファイル一覧 + +すべてのデータファイルは `cgi/data/` ディレクトリに格納されます(設定により変更可能)。 + +| ファイル名 | 説明 | 形式 | +|----------|------|-----| +| `hakojima.dat` | メインデータ(ターン情報、全島の基本情報) | テキスト(カンマ区切り) | +| `island.{ID}` | 個別島データ(地形、コマンド、掲示板) | テキスト(Hex + カンマ区切り) | +| `hakojima.his` | ゲーム履歴ログ | テキスト(カンマ区切り) | +| `hakojima.log{0-9}` | 詳細ログファイル(10ファイルローテーション) | テキスト(カンマ区切り) | +| `hakojima.lock` | ロックファイル | 空ファイル(存在チェックのみ) | +| `data.bak{0-3}` | バックアップディレクトリ | ディレクトリ(ローテーション) | + +--- + +## hakojima.dat - メインデータファイル + +### フォーマット + +``` +<ヘッダー行> +<島データ行1> +<島データ行2> +... +<島データ行N> +``` + +### ヘッダー行 + +カンマ区切りで以下の情報を含みます: + +``` +ターン数,最終更新時間,島の総数,次に割り当てるID +``` + +| フィールド | 変数名 | 型 | 説明 | +|----------|--------|---|------| +| ターン数 | `$HislandTurn` | 整数 | 現在のターン番号(0から開始) | +| 最終更新時間 | `$HislandLastTime` | UNIX時刻 | 最後にターンが更新された時刻 | +| 島の総数 | `$HislandNumber` | 整数 | 現在存在する島の数 | +| 次に割り当てるID | `$HislandNextID` | 整数 | 新しい島に割り当てるID(連番) | + +**例:** +``` +123,1699876543,5,6 +``` +- ターン123 +- 最終更新: 2023-11-13 12:34:03 (UNIX時刻) +- 島の総数: 5 +- 次ID: 6 + +### 島データ行 + +各島ごとに1行。カンマ区切りで以下の情報を含みます: + +``` +名前,ID,受賞,連続資金繰り数,コメント,暗号化パスワード,資金,食料,人口,広さ,農場,工場,採掘場 +``` + +| # | フィールド | 変数名 | 型 | 説明 | +|--|----------|--------|---|------| +| 0 | 名前 | `$island->[0]` | 文字列 | 島の名前(最大32文字) | +| 1 | ID | `$island->[1]` | 整数 | 島の一意なID | +| 2 | 受賞 | `$island->[2]` | 整数 | 受賞フラグ(ビットマスク) | +| 3 | 連続資金繰り数 | `$island->[3]` | 整数 | 連続で放置されているターン数 | +| 4 | コメント | `$island->[4]` | 文字列 | 島の説明(最大80文字) | +| 5 | 暗号化パスワード | `$island->[5]` | 文字列 | encode()関数で暗号化されたパスワード | +| 6 | 資金 | `$island->[6]` | 整数 | 現在の資金(100億円単位) | +| 7 | 食料 | `$island->[7]` | 整数 | 現在の食料(100トン単位) | +| 8 | 人口 | `$island->[8]` | 整数 | 現在の人口(100人単位) | +| 9 | 広さ | `$island->[9]` | 整数 | 島の面積(100平方km単位) | +| 10 | 農場 | `$island->[10]` | 整数 | 農場の数 | +| 11 | 工場 | `$island->[11]` | 整数 | 工場の数 | +| 12 | 採掘場 | `$island->[12]` | 整数 | 採掘場の数 | + +**例:** +``` +テスト島,0,0,0,平和な島です,xYz123,100,50,20,121,5,3,0 +``` + +### Go実装例 + +```go +type Island struct { + Name string // [0] 名前 + ID string // [1] ID + Prize int // [2] 受賞 + Absent int // [3] 連続資金繰り数 + Comment string // [4] コメント + Password string // [5] 暗号化パスワード + Money int // [6] 資金 + Food int // [7] 食料 + Pop int // [8] 人口 + Area int // [9] 広さ + Farm int // [10] 農場 + Factory int // [11] 工場 + Mountain int // [12] 採掘場 + + // island.{ID}ファイルから読み込まれるデータ + Land [][]LandInfo // 地形データ (11x11) + LandValue [][]int // 地形の値 (11x11) + Commands []Command // コマンドキュー + Lbbs []LbbsEntry // ローカル掲示板 +} +``` + +--- + +## island.{ID} - 個別島データ + +各島ごとに `island.0`, `island.1`, ... のファイルが作成されます。 + +### フォーマット + +``` +<地形データ(1行、363文字)> +<コマンド行1> +<コマンド行2> +... +<コマンド行N> (最大 HcommandMax 行) +<ローカル掲示板行1> +<ローカル掲示板行2> +... +``` + +### 地形データ(1行目) + +11x11 = 121ヘクスの地形情報を16進数で表現。**363文字**の固定長。 + +**フォーマット:** 各ヘクスは3文字 +``` + +``` + +- **land** (1桁): 地形タイプ(0-9, A-F) +- **landValue** (2桁): 地形の値(00-FF、16進数) + +**例:** +``` +000000000101010202030405060708090A0B0C0D0E0F0...(363文字) +``` + +| 位置 | 文字 | 意味 | +|-----|------|------| +| 0-2 | `000` | (0, 0) = 地形0, 値0 | +| 3-5 | `000` | (0, 1) = 地形0, 値0 | +| ... | ... | ... | +| 360-362 | `0F0` | (10, 10) = 地形0, 値F(15) | + +**読み取り順序:** 左上から右へ、行ごとに下へ(合計121ヘクス) + +**地形タイプ一覧:** + +| 値 | 定数名 | 地形 | +|----|--------|------| +| 0 | `$HlandSea` | 海 | +| 1 | `$HlandWaste` | 荒地 | +| 2 | `$HlandPlains` | 平地 | +| 3 | `$HlandTown` | 町 | +| 4 | `$HlandForest` | 森 | +| 5 | `$HlandFarm` | 農場 | +| 6 | `$HlandFactory` | 工場 | +| 7 | `$HlandBase` | 基地 | +| 8 | `$HlandSbase` | 海底基地 | +| 9 | `$HlandDefence` | 防衛施設 | +| A | `$HlandMountain` | 山 | +| B | `$HlandMonster` | モンスター | +| C | `$HlandSbase2` | 海底都市 | +| D | `$HlandOil` | 海底油田 | +| E | `$HlandMonument` | 記念碑 | + +### コマンド行(2行目以降) + +カンマ区切りで5フィールド: + +``` +kind,target,x,y,arg +``` + +| フィールド | 説明 | 型 | +|----------|------|---| +| kind | コマンドの種類 | 整数 | +| target | ターゲットID(援助・ミサイルなど) | 整数 | +| x | X座標(0-10) | 整数 | +| y | Y座標(0-10) | 整数 | +| arg | 引数(数量など) | 整数 | + +**例:** +``` +0,0,5,5,0 +1,2,3,4,10 +``` + +**最大行数:** `$HcommandMax`(デフォルト30) + +### ローカル掲示板行 + +実装により異なる(詳細は `$HuseLbbs` による) + +--- + +## hakojima.his - 履歴ファイル + +ゲームの公開イベント履歴を保存します。 + +### フォーマット + +``` +<ターン数>,<ログメッセージ> +<ターン数>,<ログメッセージ> +... +``` + +**例:** +``` +120,テスト島で大地震が発生! +121,モンスター「いのら」がサンプル島に接近! +122,テスト島がサンプル島に援助を実施 +``` + +**最大行数:** `$HhistoryMax`(デフォルト100) + +--- + +## hakojima.log{N} - 詳細ログファイル + +10個のログファイル(`hakojima.log0` ~ `hakojima.log9`)にローテーションで記録されます。 + +### フォーマット + +``` +<機密フラグ>,<ターン数>,,,<メッセージ> +``` + +| フィールド | 説明 | +|----------|------| +| 機密フラグ | 0=公開, 1=当事者のみ, 2=機密 | +| ターン数 | イベント発生ターン | +| id1 | 当事者の島ID | +| id2 | 相手の島ID(該当しない場合は空) | +| メッセージ | ログメッセージ | + +**例:** +``` +0,120,0,,テスト島: 人口が増加 +1,121,0,1,テスト島 → サンプル島: ミサイル発射 +2,122,1,,サンプル島: 極秘作戦実行 +``` + +--- + +## ロックファイル + +### hakojima.lock + +並行アクセスを防ぐためのロックファイル。 + +**ロック方式:** + +| モード | 方式 | 説明 | +|-------|------|------| +| 0 | flock | ファイルロック(推奨) | +| 1 | symlink | シンボリックリンク | +| 2 | mkdir | ディレクトリ作成 | +| 3 | no lock | ロックなし(開発用) | + +**Go実装:** +```go +import "github.com/gofrs/flock" + +fileLock := flock.New("data/hakojima.lock") +locked, err := fileLock.TryLock() +if err != nil || !locked { + return fmt.Errorf("failed to acquire lock") +} +defer fileLock.Unlock() +``` + +--- + +## バックアップファイル + +### data.bak{0-3} + +定期的に `data/` ディレクトリ全体をバックアップ。 + +**ローテーション:** +- `$HbackupTurn` ターンごとにバックアップ(デフォルト: 10ターン) +- `$HbackupTimes` 個のバックアップを保持(デフォルト: 4) + +**ディレクトリ構造:** +``` +data/ # 現在のデータ +data.bak0/ # 最新のバックアップ +data.bak1/ # 1つ前 +data.bak2/ # 2つ前 +data.bak3/ # 3つ前(最古) +``` + +--- + +## エンコーディング + +### 文字コード + +- **ファイル:** UTF-8 +- **Perl:** `use utf8; use open ':encoding(utf8)';` +- **Go:** すべて UTF-8(標準) + +### パスワード暗号化 + +**Perl実装 (`encode` 関数):** +```perl +sub encode { + my ($password) = @_; + if ($cryptOn) { + return crypt($password, $password); + } else { + return $password; + } +} +``` + +**Go実装:** +```go +import "golang.org/x/crypto/bcrypt" + +func encode(password string) (string, error) { + if const.CryptOn { + // crypt() 互換の実装が必要 + // Perl の crypt() は DES ベースなので注意 + return cryptCompat(password, password) + } + return password, nil +} +``` + +**注意:** Perlの `crypt()` は環境依存。互換性のため、同じアルゴリズム(DES)を使用する必要があります。 + +--- + +## Go実装の注意点 + +### 1. ファイルI/Oの順序保証 + +Perlでは順次処理されるため、Go実装でも**同じ順序**でファイルを読み書きする必要があります。 + +### 2. 数値のフォーマット + +**16進数地形データ:** +```go +// 読み込み +landType := int(hexStr[0]) // '0'-'9', 'A'-'F' +landValue, _ := strconv.ParseInt(hexStr[1:3], 16, 64) + +// 書き込み +hexStr := fmt.Sprintf("%X%02X", landType, landValue) +``` + +### 3. カンマ区切りのパース + +```go +parts := strings.Split(line, ",") +if len(parts) != expectedCount { + return fmt.Errorf("invalid format") +} +``` + +**注意:** コメントや名前にカンマが含まれる可能性があるため、適切にエスケープ処理が必要。 + +### 4. ファイルパーミッション + +```go +// ディレクトリ +os.MkdirAll(dirPath, 0755) + +// ファイル +os.WriteFile(filePath, data, 0644) +``` + +### 5. アトミック書き込み + +```go +// 一時ファイルに書き込み → リネーム +tmpFile := filepath + ".tmp" +if err := os.WriteFile(tmpFile, data, 0644); err != nil { + return err +} +if err := os.Rename(tmpFile, filepath); err != nil { + return err +} +``` + +### 6. テストデータ生成 + +互換性テスト用に、Perl版と同じデータを生成できるようにする: + +```go +func GenerateTestIsland() Island { + return Island{ + Name: "テスト島", + ID: "0", + Prize: 0, + Absent: 0, + Comment: "平和な島です", + Password: "xYz123", // encode("test") + Money: 100, + Food: 50, + Pop: 20, + Area: 121, + Farm: 5, + Factory: 3, + Mountain: 0, + } +} +``` + +--- + +## チェックリスト + +データフォーマット互換性を確保するために: + +- [ ] ファイルのエンコーディングがUTF-8 +- [ ] カンマ区切りのフィールド数が正しい +- [ ] 16進数地形データが363文字固定 +- [ ] パスワード暗号化がPerl版と一致 +- [ ] ファイルロックが正しく動作 +- [ ] バックアップローテーションが正しい +- [ ] Perl版が生成したデータをGo版で読み込める +- [ ] Go版が生成したデータをPerl版で読み込める + +--- + +## 参考資料 + +- [memo.txt](../../perl/memo.txt) - オリジナルのメモ +- [Hako::Main::readIslandsFile](../../perl/lib/Hako/Main.pm) - Perl読み込み実装 +- [Hako::Main::writeIslandsFile](../../perl/lib/Hako/Main.pm) - Perl書き込み実装 diff --git a/docs/migration/MIGRATION_PLAN.md b/docs/migration/MIGRATION_PLAN.md new file mode 100644 index 0000000..7ae1c7a --- /dev/null +++ b/docs/migration/MIGRATION_PLAN.md @@ -0,0 +1,456 @@ +# 箱庭諸島 Perl → Go 移植計画 + +## 概要 + +本ドキュメントは、箱庭諸島(Hakoniwa)のPerlコードベース(7,400行)をGoに移植するための詳細な計画書です。 + +### 移植方針 + +1. **段階的アプローチ**: 逐語的移植 → Go慣用的リファクタリング → 最適化 +2. **互換性維持**: データフォーマット・ゲームロジックの完全な互換性を保証 +3. **テスト駆動**: 各段階で既存のPerl版との出力比較テストを実施 + +--- + +## フェーズ概要 + +``` +Phase 0: 準備・設計(1-2日) + ↓ +Phase 1: 逐語的移植(2-3週間) + ↓ +Phase 2: Go的リファクタリング(1-2週間) + ↓ +Phase 3: 最適化・クリーンアップ(1週間) +``` + +--- + +## Phase 0: 準備・設計(1-2日) + +### 目標 + +- 移植の基盤となるドキュメントとリポジトリ構造を整備 +- Perl → Go の対応関係を明確化 +- テスト戦略を確立 + +### 成果物 + +1. **ドキュメント** + - [x] `MIGRATION_PLAN.md` - 本ドキュメント + - [ ] `PERL_TO_GO_MAPPING.md` - Perl/Go対応表 + - [ ] `DATA_FORMAT.md` - データフォーマット仕様 + - [ ] `API_REFERENCE.md` - 内部API設計 + - [ ] `TESTING_STRATEGY.md` - テスト戦略 + +2. **リポジトリ構造** + - [ ] `go/` ディレクトリ配下の標準レイアウト作成 + - [ ] `perl/` ディレクトリへの既存コード移動 + - [ ] テスト用ディレクトリ構造の準備 + +3. **開発環境** + - [ ] `go.mod` 初期化 + - [ ] `Makefile` 作成 + - [ ] CI/CD設定(後日) + +--- + +## Phase 1: 逐語的移植(2-3週間) + +### 目標 + +- Perlコードを機械的にGoへ変換 +- 構造・ロジックを可能な限り元のまま維持 +- 各モジュールごとに互換性テストを実施 + +### 基本方針 + +#### DO ✅ + +- Perlの構造をそのまま維持 +- 関数名を機械的に変換(`snake_case` → `camelCase`) +- グローバル変数をそのまま使用(`variable/variable.go`) +- Perlのロジックを1:1で翻訳 +- コメントで元のPerl行番号を記録 + +#### DON'T ❌ + +- Phase 1で設計を改善しようとしない +- グローバル変数を排除しない(Phase 2で実施) +- インターフェースを抽出しない(Phase 2で実施) +- エラーハンドリングを改善しない(Phase 2で実施) + +### 実装順序 + +1. **依存関係の少ないモジュールから開始** + +``` +1. internal/hako/const (依存: なし) +2. internal/hako/variable (依存: const) +3. internal/hako/core (依存: const, variable) + - core.go (エントリーポイント) + - fileio.go (ファイルI/O) + - lock.go (ロック機構) + - utils.go (ユーティリティ) + - template.go (HTMLテンプレート) +4. internal/hako/turn (依存: const, variable, core) + - turn.go (ターン処理) + - disaster.go (災害処理) + - monster.go (モンスター処理) +5. internal/hako/mapview (依存: const, variable, core) +6. internal/hako/top (依存: const, variable, core) +7. internal/hako/maintenance (依存: const, variable, core) +8. internal/web (依存: 上記すべて) + - handler.go (HTTPハンドラ) + - middleware.go (ミドルウェア) +9. cmd/hako-main (エントリーポイント) +10. cmd/hako-mente (メンテナンスツール) +``` + +2. **各モジュールの実装手順** + +``` +For each module: + 1. Perlコードを読み、関数一覧を作成 + 2. Go構造体・型定義を作成 + 3. 各関数を逐語的に移植 + 4. 単体テストを作成 + 5. Perl版との出力比較テストを実行 + 6. パスするまで修正 + 7. 次のモジュールへ +``` + +### 命名規則 + +#### モジュール名 + +| Perl | Go | +|------|-----| +| `Hako::Main` | `internal/hako/core` | +| `Hako::Const` | `internal/hako/const` | +| `Hako::Variable` | `internal/hako/variable` | +| `Hako::Turn` | `internal/hako/turn` | +| `Hako::Map` | `internal/hako/mapview` | +| `Hako::Top` | `internal/hako/top` | +| `Hako::Maintenance` | `internal/hako/maintenance` | + +#### 関数名 + +| Perl | Go | 可視性 | +|------|-----|--------| +| `sub readIslandsFile` | `func ReadIslandsFile` | 公開 | +| `sub htmlEscape` | `func htmlEscape` | 非公開 | +| `sub hakolock` | `func hakoLock` | 非公開 | +| `sub cgiInput` | `func cgiInput` | 非公開 | + +**ルール:** +- 公開関数: 大文字開始(`ReadIslandsFile`) +- 非公開関数: 小文字開始(`htmlEscape`) + +#### 変数名 + +| Perl | Go | +|------|-----| +| `$HunitTime` | `const.UnitTime` | +| `$mode` | `variable.Mode` | +| `@Hislands` | `variable.Islands` | +| `%HidToName` | `variable.IDToName` | + +### 型変換パターン + +```go +// Perlハッシュ → Goマップ +my %hash = (); → hash := make(map[string]string) + +// Perl配列 → Goスライス +my @array = (); → array := []string{} + +// Perlリファレンス → Goポインタ +my $ref = \%hash; → ref := &hash + +// Perl島データ → Go構造体 +my @island = (...); → type Island struct { ... } +``` + +### エラーハンドリング(Phase 1) + +```go +// Phase 1: Perlのdieをそのままpanic +sub foo { + die "error" if !$ok; +} +↓ +func foo() { + if !ok { + panic("error") // Phase 1ではこれでOK + } +} +``` + +**注**: Phase 2で適切な `error` 型に改善 + +### テスト方針(Phase 1) + +#### 1. 単体テスト + +```go +// 各関数の基本動作を確認 +func TestHtmlEscape(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {" +`, + hconst.TagBigBegin, hconst.TagNameBegin, variable.CurrentName, hconst.TagNameEnd, hconst.TagBigEnd, + hconst.TempBack, + )) + + islandInfo() + + // Phase 1: Simplified command form + out(fmt.Sprintf(`
+ + + + +
+
+
+ +
+パスワード
+ +
+計画フォーム(Phase 1: 簡略版) +
+
+
+
+`, + hconst.BgInputCell, + hconst.ThisFile, + variable.Islands[variable.CurrentNumber].ID, + variable.DefaultPassword, + )) + + islandMap(1) +} + +func tempRecent(mode int) { + out(fmt.Sprintf("

%s近況%s

\n", hconst.TagHeaderBegin, hconst.TagHeaderEnd)) + logPrintLocal(mode) +} + +func logPrintLocal(mode int) { + // Phase 1: Stub - would call logFilePrint for each log file + out("

(近況ログ表示は未実装)

\n") +} + +func tempLbbsHead() { + out(fmt.Sprintf("

%s%s島ローカル掲示板%s

\n", + hconst.TagHeaderBegin, variable.CurrentName, hconst.TagHeaderEnd)) +} + +func tempLbbsInput() { + out("

ローカル掲示板書き込み(観光者用)(Phase 1: 簡略版)

\n") +} + +func tempLbbsInputOW() { + out("

ローカル掲示板書き込み(所有者用)(Phase 1: 簡略版)

\n") +} + +func tempLbbsContents() { + out("

掲示板メッセージ一覧(Phase 1: 簡略版)

\n") +} + +func tempLbbsNoMessage() { + out("

名前かメッセージが入力されていません。

\n") +} + +func tempLbbsDelete() { + out("

メッセージを削除しました。

\n") +} + +func tempLbbsAdd() { + out("

メッセージを追加しました。

\n") +} + +func tempCommandDelete() { + out("

計画を削除しました。

\n") +} + +func tempCommandAdd() { + out("

計画を追加しました。

\n") +} + +func tempComment() { + out("

コメントを変更しました。

\n") } diff --git a/go/internal/hako/top/top.go b/go/internal/hako/top/top.go index eee41c3..cd878c3 100644 --- a/go/internal/hako/top/top.go +++ b/go/internal/hako/top/top.go @@ -4,9 +4,424 @@ // Ref: perl/lib/Hako/Top.pm package top +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" + + "github.com/neguse/hakoniwa/internal/hako/core" + "github.com/neguse/hakoniwa/internal/hako/hconst" + "github.com/neguse/hakoniwa/internal/hako/variable" +) + +// out outputs a string to the output buffer +func out(s string) { + variable.OutputBuffer.WriteString(s) +} + // TopPageMain displays the top page -// Ref: perl/lib/Hako/Top.pm:~50 +// Ref: perl/lib/Hako/Top.pm:32 func TopPageMain() { - // Phase 1: Stub implementation - // TODO: Implement top page display + // Release lock + core.Unlock() + + // Output template + tempTopPage() +} + +// tempTopPage outputs the top page HTML +// Ref: perl/lib/Hako/Top.pm:42 +func tempTopPage() { + // Title + out(fmt.Sprintf("%s%s%s\n", hconst.TagTitleBegin, hconst.Title, hconst.TagTitleEnd)) + + // Debug mode turn button + if hconst.Debug { + out(fmt.Sprintf(`
+ +
+`, hconst.ThisFile)) + } + + // Money column header (optional based on hide mode) + mStr1 := "" + if hconst.HideMoneyMode != 0 { + mStr1 = fmt.Sprintf("%s資金%s", + hconst.BgTitleCell, hconst.TagTHBegin, hconst.TagTHEnd) + } + + // Island list for select + islandList := getIslandList(variable.DefaultID) + + // Main form and table header + out(fmt.Sprintf(`

%sターン%d%s

+ +
+

%s自分の島へ%s

+
+あなたの島の名前は?
+
+ +パスワードをどうぞ!!
+
+
+
+ +
+ +

%s諸島の状況%s

+

+島の名前をクリックすると、観光することができます。 +

+ + + + + + +%s + + + + + +`, + hconst.TagHeaderBegin, variable.IslandTurn, hconst.TagHeaderEnd, + hconst.TagHeaderBegin, hconst.TagHeaderEnd, + hconst.ThisFile, + islandList, + variable.DefaultPassword, + hconst.TagHeaderBegin, hconst.TagHeaderEnd, + hconst.BgTitleCell, hconst.TagTHBegin, hconst.TagTHEnd, + hconst.BgTitleCell, hconst.TagTHBegin, hconst.TagTHEnd, + hconst.BgTitleCell, hconst.TagTHBegin, hconst.TagTHEnd, + hconst.BgTitleCell, hconst.TagTHBegin, hconst.TagTHEnd, + mStr1, + hconst.BgTitleCell, hconst.TagTHBegin, hconst.TagTHEnd, + hconst.BgTitleCell, hconst.TagTHBegin, hconst.TagTHEnd, + hconst.BgTitleCell, hconst.TagTHBegin, hconst.TagTHEnd, + hconst.BgTitleCell, hconst.TagTHBegin, hconst.TagTHEnd, + )) + + // Island rows + for ii := 0; ii < variable.IslandNumber; ii++ { + j := ii + 1 + island := variable.Islands[ii] + + id := island.ID + farm := island.Farm + factory := island.Factory + mountain := island.Mountain + + farmStr := "保有せず" + if farm != 0 { + farmStr = fmt.Sprintf("%d0%s", farm, hconst.UnitPop) + } + factoryStr := "保有せず" + if factory != 0 { + factoryStr = fmt.Sprintf("%d0%s", factory, hconst.UnitPop) + } + mountainStr := "保有せず" + if mountain != 0 { + mountainStr = fmt.Sprintf("%d0%s", mountain, hconst.UnitPop) + } + + // Island name with absent counter + name := "" + if island.Absent == 0 { + name = fmt.Sprintf("%s%s島%s", hconst.TagNameBegin, island.Name, hconst.TagNameEnd) + } else { + name = fmt.Sprintf("%s%s島(%d)%s", hconst.TagName2Begin, island.Name, island.Absent, hconst.TagName2End) + } + + // Parse prize field: "flags,monsters,turns" + prize := parsePrize(island.Prize) + + // Money display + mStr1 := "" + if hconst.HideMoneyMode == 1 { + mStr1 = fmt.Sprintf("", + hconst.BgInfoCell, island.Money, hconst.UnitMoney) + } else if hconst.HideMoneyMode == 2 { + mTmp := core.AboutMoney(island.Money) + mStr1 = fmt.Sprintf("", + hconst.BgInfoCell, mTmp) + } + + // Output island row + out(fmt.Sprintf(` + + + + +%s + + + + + + + + +`, + hconst.BgNumberCell, hconst.TagNumberBegin, j, hconst.TagNumberEnd, + hconst.BgNameCell, + hconst.ThisFile, id, + name, + prize, + hconst.BgInfoCell, island.Pop, hconst.UnitPop, + hconst.BgInfoCell, island.Area, hconst.UnitArea, + mStr1, + hconst.BgInfoCell, island.Food, hconst.UnitFood, + hconst.BgInfoCell, farmStr, + hconst.BgInfoCell, factoryStr, + hconst.BgInfoCell, mountainStr, + hconst.BgCommentCell, hconst.TagTHBegin, hconst.TagTHEnd, island.Comment, + )) + } + + out("
%s順位%s%s島%s%s人口%s%s面積%s%s食料%s%s農場規模%s%s工場規模%s%s採掘場規模%s
%d%s%s
%s%d%s + + +%s + +
+%s +
+%d%s%d%s%d%s%s%s%s
%sコメント:%s%s
\n\n
\n") + + // New island section + out(fmt.Sprintf("

%s新しい島を探す%s

\n", hconst.TagHeaderBegin, hconst.TagHeaderEnd)) + + if variable.IslandNumber < hconst.MaxIsland { + out(fmt.Sprintf(`
+どんな名前をつける予定?
+
+パスワードは?
+
+念のためパスワードをもう一回
+
+ + +
+`, hconst.ThisFile)) + } else { + out(" 島の数が最大数です・・・現在登録できません。\n") + } + + // Change name/password section + out(fmt.Sprintf(`
+

%s島の名前とパスワードの変更%s

+

+(注意)名前の変更には%d%sかかります。 +

+
+どの島ですか?
+ +
+どんな名前に変えますか?(変更する場合のみ)
+
+パスワードは?(必須)
+
+新しいパスワードは?(変更する時のみ)
+
+念のためパスワードをもう一回(変更する時のみ)
+
+ + +
+ +
+ +

%s最近の出来事%s

+`, + hconst.TagHeaderBegin, hconst.TagHeaderEnd, + hconst.CostChangeName, hconst.UnitMoney, + hconst.ThisFile, + islandList, + hconst.TagHeaderBegin, hconst.TagHeaderEnd, + )) + + // Recent events log + logPrintTop() + + // Discovery history + out(fmt.Sprintf("

%s発見の記録%s

\n", hconst.TagHeaderBegin, hconst.TagHeaderEnd)) + historyPrint() +} + +// parsePrize parses the prize field and returns HTML for prize display +// Prize format: "flags,monsters,turns" +// Ref: perl/lib/Hako/Top.pm:124-163 +func parsePrize(prizeField int) string { + // Convert to string for parsing + prizeStr := fmt.Sprintf("%d", prizeField) + + // Parse: flags,monsters,turns + parts := strings.Split(prizeStr, ",") + flags := 0 + monsters := 0 + turns := "" + + if len(parts) >= 1 { + flags, _ = strconv.Atoi(parts[0]) + } + if len(parts) >= 2 { + monsters, _ = strconv.Atoi(parts[1]) + } + if len(parts) >= 3 { + turns = parts[2] + } + + prize := "" + + // Turn prizes + turnParts := strings.Split(turns, ",") + for _, tp := range turnParts { + if tp != "" { + prize += fmt.Sprintf(`%s%s `, + tp, hconst.Prize[0]) + } + } + + // Flag prizes (prizes 1-9) + f := 1 + for i := 1; i < 10; i++ { + if (flags & f) != 0 { + prize += fmt.Sprintf(`%s `, + i, hconst.Prize[i]) + } + f *= 2 + } + + // Monster prizes + f = 1 + max := -1 + mNameList := "" + for i := 0; i < hconst.MonsterNumber; i++ { + if (monsters & f) != 0 { + mNameList += fmt.Sprintf("[%s] ", hconst.MonsterName[i]) + max = i + } + f *= 2 + } + if max != -1 { + prize += fmt.Sprintf(`%s `, + hconst.MonsterImage[max], mNameList) + } + + return prize +} + +// getIslandList generates island select options +// Ref: perl/lib/Hako/Main.pm:1079 +func getIslandList(selectID string) string { + list := "" + for i := 0; i < variable.IslandNumber; i++ { + name := variable.Islands[i].Name + id := variable.Islands[i].ID + selected := "" + if id == selectID { + selected = "SELECTED" + } + list += fmt.Sprintf("