PowerShellスクリプトの最適化によるマインクラフト地形生成の高速化（アレフガルド in マインクラフト Part14）

# はじめに
　マインクラフト Education Edition上でアレフガルドを再現する取り組みにおいて、Excel形式で用意した地形データをマインクラフトの世界に反映させる処理の高速化に成功しました。本記事では、PowerShellスクリプトの最適化によって処理時間を10分の1以下に短縮できた過程を紹介します。

# 課題：大規模な地形生成の処理時間
　マインクラフト上にアレフガルドを再現するにあたっては、PowerShellスクリプトでExcelに記録した地形データを読み取り、マインクラフトのCodeBuilderで動作するJavascriptのコマンド群を生成するというアプローチを取っていました。Excelファイルの各セルには:

セルの値：地形の高さ
セルの背景色：使用するブロックの種類

という形でデータが格納されています。しかし、再現する地形の範囲が広くなるにつれ、Excelファイルからコマンド群を生成する処理に膨大な時間がかかるようになり、開発効率が著しく低下していました。

# Claude 3.7に相談：高速化のアプローチ
　コード改善の具体的な方向性を見つけるため、Claude 3.7に既存のPowerShellスクリプトを解析してもらいました。最初は、現在使用しているスクリプトをファイルとしてUploadしたうえで、「添付のPowerShellプログラムの処理を高速化したい。高速化に有効な改善ポイントを箇条書きでまとめてほしい。」とリクエストしました。このやり取りで、以下のような高速化ポイントが提案されました：

1. Excel COMオブジェクトの代替手段の使用
1. ループ最適化
1. 一括処理の導入
1. 不要な変数変換の削減
1. パイプラインの活用
1. 並列処理の導入
1. 条件分岐の最適化
1. メモリ使用量の最適化
1. 出力処理の効率化
1. 事前計算とキャッシュの活用

一度に取り組むには改善項目が多く挙げられたので、続くプロンプトで、特に改善効果が大きいと期待できるポイントを3つ挙げてもらいました。

- Excel COMオブジェクトの代替手段を使用する
- ルックアップテーブルと事前計算の活用
- 出力処理の最適化と一括処理

# 最適化の実装と効果
## 第1段階：ImportExcelモジュールとCOMオブジェクトのハイブリッド
　まずExcel COMオブジェクトを用いた読み込みに変えて、ImportExcelモジュールを用いてデータをメモリに一括読み込みする形にコードを書き換えてもらいました。が、ImportExcelを用いた読み込みでは、セルの値しか取得できず背景色の取得ができないという問題が発生しました（この問題自体も、スクリプト実行時に発生したエラーからClaudeが特定してくれました）。そこで折衷案として：

- セルの値：ImportExcelモジュールで一括取得
- セルの背景色：COMオブジェクトを使用

というハイブリッドアプローチを採用しました。加えて以下の最適化も実装しました：

- データの一括キャッシュ（セルの値と色を最初に一度で読み込み）
- ルックアップテーブルの活用（色からブロックへの変換をハッシュテーブルで高速化）
- StringBuilderを使用した文字列連結の最適化

## 第2段階：デュアルシート方式による完全な最適化
　第1段階の改善のみでも、かなりの性能改善が得られそうでしたが、背景色の読み取りにCOMオブジェクトを利用する処理が残っていたので、背景色のデータもセルの値として取得できるように、Excelファイルの構造自体を変更する「デュアルシート方式」を考案しました。具体的には、以下のように背景色（使用するブロック）を値として格納するシートを追加で作成したのです。

- 高さを格納するシート
- 色の値（RGB Hex）を格納するシート

色情報をセルの値として保存する方法も、Claudeに相談してVBAマクロを作成し、背景色のRGB Hex値を自動的に抽出・保存できるようにしました。これにより、COMオブジェクトを完全に排除し、全てのデータをImportExcelモジュールで一括取得できるようになったのです。

# 成果：処理時間の比較
　ちょうど、前回のメルキドに続き、ドムドーラ周辺の地形データの補間を終えたところだったので、ドムドーラ周辺の地形データ（640セル x 528セル）を対象に、3つのバージョンで処理時間を測定してみました。結果は以下の通りとなっています。

| バージョン | 実行時間(秒) |改善率|
|:---:|:---:|:---:|
|元のバージョン<br>(COMオブジェクト)|1,791.72|-|
|第一段階 改善版<br>(ImportExcel + COMハイブリッド)|740.64|2.4倍高速化|
|第二段階 デュアルシート版 <br> (COMオブジェクト不使用)|171.63|10.4倍高速化|

最終的に、元のスクリプトと比較して10倍以上の高速化を実現することができました。

# まとめ
　今回、Claudeと対話しつつ改善版のスクリプト作成に要した時間は約3時間程度でした。高速化を通じて、今後の地形生成作業において得られる作業時間の短縮を考えると、非常に価値のある改善となりました。この経験から学んだポイントは以下の通りです。

- Excel COMオブジェクトの操作は極めて遅い
- データ構造の見直しにより劇的な速度向上が可能
- ルックアップテーブルやデータキャッシュの活用が効果的
- 生成AI（Claude 3.7）を活用することでコードの改善ポイントの検討や実際の改善を効率的に進められる

　今回の高速化では技術的負債を解消するだけでなく、今後の取り組み全体の進行速度も向上させることができました。同様の課題を抱えている方の参考になれば幸いです。

# 今後の展望
　今回はPowerShellスクリプトの最適化に焦点を当てましたが、さらなる改善として以下のようなアプローチもClaudeに提言されました。すぐに取り組めるかどうかはわかりませんが、今後の参考にしていきたいと思います。

- 並列処理の導入
- マインクラフトコマンドの生成アルゴリズムの最適化
- より大規模な地形データにも対応できるデータ構造の検討

アレフガルド in マインクラフトプロジェクトの進捗については、今後も随時報告していきたいと思います。

# Appendix1
高速化されたスクリプトを用いて生成したコマンド群で再現したドムドーラ周辺
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/297527/de3cd507-a487-4f43-84ca-611ccbf2cd4c.png)

# Appendix2
Claudeを用いて改善したPowerShellスクリプト
<details><summary>元のコード(COMオブジェクト)</summary>

``` Powershell
$hostname = hostname
Add-Type -AssemblyName System.Drawing
$Excel = New-Object -ComObject Excel.Application 
$Excel.Visible = $false

$Workbook = $Excel.Workbooks.Open("<excel_path>")
$Worksheet = $Workbook.Sheets.Item(1)


$c_max = $Worksheet.Cells.Item(2,2).Value()
$r_max = $Worksheet.Cells.Item(3,2).Value()
$x_base = $Worksheet.Cells.Item(5,2).Value()
$y_base = $Worksheet.Cells.Item(6,2).Value()
$z_base = $Worksheet.Cells.Item(7,2).Value()
$bottom = $Worksheet.Cells.Item(8,2).Value()

$x_s = $x_base
$x_e = $x_base + $c_max
$y_s = $bottom - 1
$y_e = $y_s
$z_s = $z_base
$z_e = $z_base + $r_max
$line_count = 1
$k = 0
echo "player.onChat(""run$k"", function () {"

for ($i = 1; $i -le $r_max; $i++) {
    $x_start = $x_base
    $x_end = $x_base -1
    $z = $z_base + $i - 1
    echo "blocks.fill(BEDROCK,world($x_s, $y_s, $z),world($x_e, $y_e, $z),FillOperation.Replace)"
    echo "blocks.fill(AIR,world($x_s, -33, $z),world($x_e, -31, $z),FillOperation.Replace)"
    for ($j = 1; $j -le $c_max; $j++) {

        $Value = $Worksheet.Cells.Item($i,$j+2).Value()

            $Color = $Worksheet.Cells.Item($i,$j+2).Interior.Color
            $Color = ([System.Drawing.ColorTranslator]::FromOle($Color)).Name
            $Value_next = $Worksheet.Cells.Item($i,$j+3).Value()
            $Color_next = $Worksheet.Cells.Item($i,$j+3).Interior.Color
            $Color_next = ([System.Drawing.ColorTranslator]::FromOle($Color_next)).Name
            $x = $x_base + $j - 1
            if ($Value -eq "x"){
                $y = $y_base
                $block = "WATER"
            }else{
                $y = $y_base + $Value
                if ($Color -eq "ff92d050") {
                    $block = "GRASS"
                } elseif ($Color -eq "fffffbd5") {
                    $block = "SAND"
                } elseif ($Color -eq "ffddebf7") {
                    $block = "DIAMOND_BLOCK"
                } elseif ($Color -eq "ffbfbfbf") {
                    $block = "STONE"
                } elseif ($Color -eq "ff996600") {
                    $block = "COARSE_DIRT"
                } elseif ($Color -eq "ffed7d31") {
                    $block = "BRICKS"
                } elseif ($Color -eq "ffa6a6a6") {
                    $block = "STONE_BRICKS"
                } elseif ($Color -eq "ff00b050") {
                    $block = "GRASS"
                } elseif ($Color -eq "ffffd966") {
                    $block = "COARSE_DIRT"
                } elseif ($Color -eq "White") {
                    $block = "AIR"
                } elseif ($Color -eq "ff002060") {
                    $block = "MUD"
                } elseif ($Color -eq "ff806000") {
                    $block = "SANDSTONE"
                } elseif ($Color -eq "Red") {
                    $block = "BRICKS"
                } elseif ($Color -eq "ff0070C0") {
                    $block = "WATER"
                } else {
                    $block = $Color
                }
                if ($Color_next -eq "ff92d050") {
                    $block_next = "GRASS"
                } elseif ($Color_next -eq "fffffbd5") {
                    $block_next = "SAND"
                } elseif ($Color_next -eq "ffddebf7") {
                    $block_next = "DIAMOND_BLOCK"
                } elseif ($Color_next -eq "ffbfbfbf") {
                    $block_next = "STONE"
                } elseif ($Color_next -eq "ff996600") {
                    $block_next = "COARSE_DIRT"
                } elseif ($Color_next -eq "ffed7d31") {
                    $block_next = "BRICKS"
                } elseif ($Color_next -eq "ffa6a6a6") {
                    $block_next = "STONE_BRICKS"
                } elseif ($Color_next -eq "ff00b050") {
                    $block_next = "GRASS"
                } elseif ($Color_next -eq "ffffd966") {
                    $block_next = "COARSE_DIRT"
                } elseif ($Color_next -eq "White") {
                    $block_next = "AIR"
                } elseif ($Color_next -eq "ff002060") {
                    $block_next = "MUD"
                } elseif ($Color_next -eq "ff806000") {
                    $block_next = "SANDSTONE"
                } elseif ($Color_next -eq "Red") {
                    $block_next = "BRICKS"
                } elseif ($Color_next -eq "ff0070C0") {
                    $block_next = "WATER"
                } else {
                    $block_next = $Color_next
                }
            }
            if ($Value -ne $Value_next -or $block -ne $block_next -or $j -eq $c_max) {
                $x_end = $x
\#                echo "blocks.fill(AIR,world($x_start, -33, $z),world($x_end, -31, $z),FillOperation.Replace)"
                echo "blocks.fill($block,world($x_start, $bottom, $z),world($x_end, $y, $z),FillOperation.Replace)"
                $line_count = $line_count + 1
                $x_start = $x + 1
            }
    }
}

echo "})"

$Workbook.Close()
$Excel.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($Excel)
Remove-Variable Excel
```
</details>

<details><summary>第一段階 改善版(ImportExcel + COMハイブリッド)</summary>

``` Powershell
# 実行時間計測の開始
$totalStopwatch = [System.Diagnostics.Stopwatch]::StartNew()

# まず、ImportExcelモジュールがインストールされていない場合はインストールする
if (-not (Get-Module -ListAvailable -Name ImportExcel)) {
    Write-Host "ImportExcelモジュールをインストールしています..."
    Install-Module -Name ImportExcel -Scope CurrentUser -Force
}

# モジュールをインポート
Import-Module ImportExcel

# パラメータとファイルパスの設定
$hostname = hostname
$excelPath = "<excel_path>"
$sheetName = "<sheet_name>"




# パラメータ読み込みのタイミング計測開始
$paramStopwatch = [System.Diagnostics.Stopwatch]::StartNew()

# パラメータを先に取得する必要があるので、一時的にExcelを開く
$tempExcel = New-Object -ComObject Excel.Application
$tempExcel.Visible = $false
$tempWorkbook = $tempExcel.Workbooks.Open($excelPath)
$tempWorksheet = $tempWorkbook.Sheets.Item($sheetName)

# パラメータの読み込み
$c_max = $tempWorksheet.Cells.Item(2,2).Value()
$r_max = $tempWorksheet.Cells.Item(3,2).Value()
$x_base = $tempWorksheet.Cells.Item(5,2).Value()
$y_base = $tempWorksheet.Cells.Item(6,2).Value()
$z_base = $tempWorksheet.Cells.Item(7,2).Value()
$bottom = $tempWorksheet.Cells.Item(8,2).Value()

# 一時的なExcelを閉じる
$tempWorkbook.Close($false)
$tempExcel.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($tempWorksheet) | Out-Null
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($tempWorkbook) | Out-Null
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($tempExcel) | Out-Null

$paramStopwatch.Stop()
Write-Host "パラメータ読み込み時間: $($paramStopwatch.Elapsed.TotalSeconds) 秒"

Write-Host "パラメータ: c_max=$c_max, r_max=$r_max, x_base=$x_base, y_base=$y_base, z_base=$z_base, bottom=$bottom"

# 色とブロックのマッピング - ルックアップテーブルを作成
$colorToBlockMap = @{
    "ff92d050" = "GRASS"
    "fffffbd5" = "SAND"
    "ffddebf7" = "DIAMOND_BLOCK"
    "ffbfbfbf" = "STONE"
    "ff996600" = "COARSE_DIRT"
    "ffed7d31" = "BRICKS"
    "ffa6a6a6" = "STONE_BRICKS"
    "ff00b050" = "GRASS"
    "ffffd966" = "COARSE_DIRT"
    "White"    = "AIR"
    "ff002060" = "MUD"
    "ff806000" = "SANDSTONE"
    "Red"      = "BRICKS"
    "ff0070C0" = "WATER"
}

# 出力用の StringBuilder を作成（効率的な文字列連結用）
$output = New-Object System.Text.StringBuilder
[void]$output.AppendLine("player.onChat(""run0"", function () {")

$x_s = $x_base
$x_e = $x_base + $c_max
$y_s = $bottom - 1
$y_e = $y_s
$z_s = $z_base
$z_e = $z_base + $r_max

# データ読み込み時間の計測開始
$dataLoadStopwatch = [System.Diagnostics.Stopwatch]::StartNew()

# ImportExcelを使ってExcelデータを読み込む - シート名を指定
Write-Host "ImportExcelを使用してExcelデータを読み込みます..."
# 高度な設定で開くために-NoHeaderパラメータを使用、シート名も指定
$excelData = Import-Excel -Path $excelPath -WorksheetName $sheetName -NoHeader -DataOnly

# 色情報を取得するためにCOMオブジェクトを使用（ImportExcelでは色情報が取得できないため）
Add-Type -AssemblyName System.Drawing
$colorExcel = New-Object -ComObject Excel.Application
$colorExcel.Visible = $false
$colorWorkbook = $colorExcel.Workbooks.Open($excelPath)
$colorWorksheet = $colorWorkbook.Sheets.Item($sheetName)

# データ処理用のキャッシュを作成
$cellCache = @{}

for ($i = 1; $i -le $r_max; $i++) {
    $rowIndex = $i # ImportExcelでは1から始まるインデックス
    
    for ($j = 1; $j -le $c_max; $j++) {
        $colIndex = $j + 2 # 3列目から始まるデータ
        
        # ImportExcelでは値を取得
        $cellValue = $excelData[$rowIndex-1].("P$colIndex") # PowerShellのインデックスは0から

        # 色情報はCOMオブジェクトから取得
        $cellColor = $colorWorksheet.Cells.Item($i, $colIndex).Interior.Color
        $cellColor = if ($cellColor -ne $null) { ([System.Drawing.ColorTranslator]::FromOle($cellColor)).Name } else { "White" }
        
        $cellCache["$i,$j"] = @{
            Value = $cellValue
            Color = $cellColor
        }
    }
}

# 色情報取得用のCOMオブジェクトを閉じる
$colorWorkbook.Close($false)
$colorExcel.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($colorWorksheet) | Out-Null
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($colorWorkbook) | Out-Null
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($colorExcel) | Out-Null

$dataLoadStopwatch.Stop()
Write-Host "データ読み込み時間: $($dataLoadStopwatch.Elapsed.TotalSeconds) 秒"

# データ処理時間の計測開始
$processingStopwatch = [System.Diagnostics.Stopwatch]::StartNew()
Write-Host "データ処理を開始します..."

# 実際の処理部分
for ($i = 1; $i -le $r_max; $i++) {
    $z = $z_base + $i - 1
    
    # AIR行の設定
    [void]$output.AppendLine("blocks.fill(AIR,world($x_s, -33, $z),world($x_e, -31, $z),FillOperation.Replace)")
    
    $x_start = $x_base
    $x_end = $x_base - 1
    
    for ($j = 1; $j -le $c_max; $j++) {
        $cellData = $cellCache["$i,$j"]
        $Value = $cellData.Value
        $Color = $cellData.Color
        
        # 次のセルのデータを取得（境界チェック）
        if ($j -lt $c_max) {
            $next_cellData = $cellCache["$i,$($j+1)"]
            $Value_next = $next_cellData.Value
            $Color_next = $next_cellData.Color
        } else {
            $Value_next = 999
            $Color_next = "null"
        }
        
        $x = $x_base + $j - 1
        
        if ($Value -eq "x") {
            $y = $y_base
            $block = "WATER"
        } else {
            $y = if ($Value -eq $null) { $y_base } else { $y_base + $Value }
            
            # 色からブロックを決定（ルックアップテーブルを使用）
            if ($colorToBlockMap.ContainsKey($Color)) {
                $block = $colorToBlockMap[$Color]
            } else {
                $block = $Color  # マップにない場合は色をそのまま使用
            }
        }
        if ($Value_next -eq "x"){
            $block_next = "WATER"
        } else {
            if ($colorToBlockMap.ContainsKey($Color_next)) {
                $block_next = $colorToBlockMap[$Color_next]
            } else {
                $block_next = $Color_next  # マップにない場合は色をそのまま使用
            }
        }
        
        # ブロックの変更を検出して出力
        if ($Value -ne $Value_next -or $block -ne $block_next -or $j -eq $c_max) {
            $x_end = $x
            [void]$output.AppendLine("blocks.fill($block,world($x_start, $bottom, $z),world($x_end, $y, $z),FillOperation.Replace)")
            $x_start = $x + 1
        }
    }
}

[void]$output.AppendLine("})")

$processingStopwatch.Stop()
Write-Host "データ処理時間: $($processingStopwatch.Elapsed.TotalSeconds) 秒"

# ファイル出力時間の計測開始
$outputStopwatch = [System.Diagnostics.Stopwatch]::StartNew()

# 結果をファイルに書き出す
$outputPath = [System.IO.Path]::GetDirectoryName($excelPath) + "\generated_commands2.js"
$output.ToString() | Out-File -FilePath $outputPath -Encoding utf8

$outputStopwatch.Stop()
Write-Host "ファイル出力時間: $($outputStopwatch.Elapsed.TotalSeconds) 秒"

# 結果を表示
$totalStopwatch.Stop()
Write-Host "処理が完了しました。"
Write-Host "生成されたコマンドは $outputPath に保存されました。"
Write-Host "総実行時間: $($totalStopwatch.Elapsed.TotalSeconds) 秒"

# 詳細な内訳を表示
$timingResults = @"
==== パフォーマンス計測結果 ====
パラメータ読み込み時間: $($paramStopwatch.Elapsed.TotalSeconds) 秒
データ読み込み時間: $($dataLoadStopwatch.Elapsed.TotalSeconds) 秒
データ処理時間: $($processingStopwatch.Elapsed.TotalSeconds) 秒
ファイル出力時間: $($outputStopwatch.Elapsed.TotalSeconds) 秒
総実行時間: $($totalStopwatch.Elapsed.TotalSeconds) 秒
=============================
"@

Write-Host $timingResults

# タイミング情報をファイルに追記
$timingPath = [System.IO.Path]::GetDirectoryName($excelPath) + "\timing_results.txt"
$timingResults | Out-File -FilePath $timingPath -Encoding utf8

# 最後に結果を返す
$output.ToString()

```

</details>

<details><summary>第二段階 デュアルシート版 (COMオブジェクト不使用)</summary>

``` Powershell
# まず、ImportExcelモジュールがインストールされていない場合はインストールする
if (-not (Get-Module -ListAvailable -Name ImportExcel)) {
    Write-Host "ImportExcelモジュールをインストールしています..."
    Install-Module -Name ImportExcel -Scope CurrentUser -Force
}

# モジュールをインポート
Import-Module ImportExcel

# パラメータとファイルパスの設定
$hostname = hostname
$excelPath = "<excel_path>"
$codeSheetName = "<value_sheet>"
$colorSheetName = "<colo_sheet>"


# 実行時間計測の開始
$totalStopwatch = [System.Diagnostics.Stopwatch]::StartNew()

# パラメータ読み込みのタイミング計測開始
$paramStopwatch = [System.Diagnostics.Stopwatch]::StartNew()

# パラメータを取得するためにExcelを開く（ImportExcelでもパラメータ取得は可能ですが、
# 元のスクリプトの構造に合わせるために一時的にCOMオブジェクトを使用）
$tempExcel = New-Object -ComObject Excel.Application
$tempExcel.Visible = $false
$tempWorkbook = $tempExcel.Workbooks.Open($excelPath)
$tempWorksheet = $tempWorkbook.Sheets.Item($codeSheetName)

# パラメータの読み込み
$c_max = $tempWorksheet.Cells.Item(2,2).Value()
$r_max = $tempWorksheet.Cells.Item(3,2).Value()
$x_base = $tempWorksheet.Cells.Item(5,2).Value()
$y_base = $tempWorksheet.Cells.Item(6,2).Value()
$z_base = $tempWorksheet.Cells.Item(7,2).Value()
$bottom = $tempWorksheet.Cells.Item(8,2).Value()

# 一時的なExcelを閉じる
$tempWorkbook.Close($false)
$tempExcel.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($tempWorksheet) | Out-Null
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($tempWorkbook) | Out-Null
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($tempExcel) | Out-Null

$paramStopwatch.Stop()
Write-Host "パラメータ読み込み時間: $($paramStopwatch.Elapsed.TotalSeconds) 秒"

Write-Host "パラメータ: c_max=$c_max, r_max=$r_max, x_base=$x_base, y_base=$y_base, z_base=$z_base, bottom=$bottom"

# 色とブロックのマッピング - ルックアップテーブルを作成
$colorToBlockMap = @{
    "#92D050" = "GRASS"
    "#FFFBD5" = "SAND"
    "#DDEBF7" = "DIAMOND_BLOCK"
    "#BFBFBF" = "STONE"
    "#996600" = "COARSE_DIRT"
    "#ED7D31" = "BRICKS"
    "#A6A6A6" = "STONE_BRICKS"
    "#00B050" = "GRASS"
    "#FFD966" = "COARSE_DIRT"
    "#FFFFFF"    = "AIR"
    "#002060" = "MUD"
    "#806000" = "SANDSTONE"
    "#FF0000"      = "BRICKS"
    "#0070C0" = "WATER"
}

# 出力用の StringBuilder を作成（効率的な文字列連結用）
$output = New-Object System.Text.StringBuilder
[void]$output.AppendLine("player.onChat(""run0"", function () {")

$x_s = $x_base
$x_e = $x_base + $c_max - 1
$y_s = $bottom - 1
$y_e = $y_s
$z_s = $z_base
$z_e = $z_base + $r_max

# データ読み込み時間の計測開始
$dataLoadStopwatch = [System.Diagnostics.Stopwatch]::StartNew()

# コード生成シートからデータを読み込む（値）
Write-Host "コード生成シートからデータを読み込みます..."
$valueData = Import-Excel -Path $excelPath -WorksheetName $codeSheetName -NoHeader -DataOnly

# 色データシートから色情報を読み込む（Hexコード）
Write-Host "色データシートから色情報を読み込みます..."
$colorData = Import-Excel -Path $excelPath -WorksheetName $colorSheetName -NoHeader -DataOnly

# データ処理用のキャッシュを作成
$cellCache = @{}

for ($i = 1; $i -le $r_max; $i++) {
    $rowIndex = $i - 1 # PowerShellのインデックスは0から
    
    for ($j = 1; $j -le $c_max; $j++) {
        $colIndex = $j + 2 # 3列目から始まるデータ
        $colLetter = [char]([int][char]'A' + $colIndex - 1) # A, B, C...
        
        # 値データを取得
        $cellValue = $valueData[$rowIndex].("P$colIndex")
        
        # 色データを取得（同じ位置にあると仮定）
        $cellColor = $colorData[$rowIndex].("P$colIndex")
        if ([string]::IsNullOrEmpty($cellColor)) {
            $cellColor = "#FFFFFF" # 色情報がない場合はWhiteとする
        }
        
        $cellCache["$i,$j"] = @{
            Value = $cellValue
            Color = $cellColor
        }
    }
}

$dataLoadStopwatch.Stop()
Write-Host "データ読み込み時間: $($dataLoadStopwatch.Elapsed.TotalSeconds) 秒"

# データ処理時間の計測開始
$processingStopwatch = [System.Diagnostics.Stopwatch]::StartNew()
Write-Host "データ処理を開始します..."

# 実際の処理部分
for ($i = 1; $i -le $r_max; $i++) {
    $z = $z_base + $i - 1
    
    # AIR行の設定
    [void]$output.AppendLine("blocks.fill(AIR,world($x_s, -33, $z),world($x_e, -31, $z),FillOperation.Replace)")
    
    $x_start = $x_base
    $x_end = $x_base - 1
    
    for ($j = 1; $j -le $c_max; $j++) {
        $cellData = $cellCache["$i,$j"]
        $Value = $cellData.Value
        $Color = $cellData.Color
        
        # 次のセルのデータを取得（境界チェック）
        if ($j -lt $c_max) {
            $next_cellData = $cellCache["$i,$($j+1)"]
            $Value_next = $next_cellData.Value
            $Color_next = $next_cellData.Color
        } else {
            $Value_next = 999
            $Color_next = "null"
        }
        
        $x = $x_base + $j - 1
        
        if ($Value -eq "x") {
            $y = $y_base
            $block = "WATER"
        } else {
            $y = if ($Value -eq $null) { $y_base } else { $y_base + $Value }
            
            # 色からブロックを決定（ルックアップテーブルを使用）
            if ($colorToBlockMap.ContainsKey($Color)) {
                $block = $colorToBlockMap[$Color]
            } else {
                $block = $Color  # マップにない場合は色をそのまま使用
            }
        }
        if ($Value_next -eq "x"){
            $block_next = "WATER"
        } else {
            if ($colorToBlockMap.ContainsKey($Color_next)) {
                $block_next = $colorToBlockMap[$Color_next]
            } else {
                $block_next = $Color_next  # マップにない場合は色をそのまま使用
            }
        }
        
        # ブロックの変更を検出して出力
        if ($Value -ne $Value_next -or $block -ne $block_next -or $j -eq $c_max) {
            $x_end = $x
            [void]$output.AppendLine("blocks.fill($block,world($x_start, $bottom, $z),world($x_end, $y, $z),FillOperation.Replace)")
            $x_start = $x + 1
        }
#            [void]$output.AppendLine("$x,$z,$block,$block_next,$Value,$Color")

    }
}

[void]$output.AppendLine("})")

$processingStopwatch.Stop()
Write-Host "データ処理時間: $($processingStopwatch.Elapsed.TotalSeconds) 秒"

# ファイル出力時間の計測開始
$outputStopwatch = [System.Diagnostics.Stopwatch]::StartNew()

# 結果をファイルに書き出す
$outputPath = [System.IO.Path]::GetDirectoryName($excelPath) + "\generated_commands3.js"
$output.ToString() | Out-File -FilePath $outputPath -Encoding utf8

$outputStopwatch.Stop()
Write-Host "ファイル出力時間: $($outputStopwatch.Elapsed.TotalSeconds) 秒"

# 結果を表示
$totalStopwatch.Stop()
Write-Host "処理が完了しました。"
Write-Host "生成されたコマンドは $outputPath に保存されました。"
Write-Host "総実行時間: $($totalStopwatch.Elapsed.TotalSeconds) 秒"

# 詳細な内訳を表示
$timingResults = @"
==== パフォーマンス計測結果 ====
パラメータ読み込み時間: $($paramStopwatch.Elapsed.TotalSeconds) 秒
データ読み込み時間: $($dataLoadStopwatch.Elapsed.TotalSeconds) 秒
データ処理時間: $($processingStopwatch.Elapsed.TotalSeconds) 秒
ファイル出力時間: $($outputStopwatch.Elapsed.TotalSeconds) 秒
総実行時間: $($totalStopwatch.Elapsed.TotalSeconds) 秒
=============================
"@

Write-Host $timingResults

# タイミング情報をファイルに追記
$timingPath = [System.IO.Path]::GetDirectoryName($excelPath) + "\timing_results.txt"
$timingResults | Out-File -FilePath $timingPath -Encoding utf8

# 最後に結果を返す
$output.ToString()

```

</details>