2025年10月10日金曜日

Automatic Semicolon Insertion; ASIで沼った話

Automatic Semicolon Insertion; ASIで沼った話

久々に原因特定に時間が掛かったので備忘録。
今回問題になったのが「Automatic Semicolon Insertion」、通称ASIによる影響です。
直訳すれば「自動セミコロン挿入」ですが、簡単に言うと「セミコロン(;)で処理を自動的に終了位置として判定してくれるよ」という仕組み。
コード自体が勝手に書き換わるわけではなく、JavaScriptエンジンがセミコロンを「挿入したつもり」で文の境界を解釈します。
便利な機能ではあるのですがちょっとした「落とし穴」になりえるのでJavaScript弄っている人は暇つぶしにでも読んでみてください。

ASIって何?(=処理末端解釈)

ASIの最大の目的は、開発者の利便性向上です。JavaScriptはインタープリタ系言語の代表格で、型指定が不要、変数の自動型変換が可能という「ゆるさ」が売り。
数千行のコードを書くときに、毎文の終わりにセミコロンを手動で付けるなんて面倒くさいですよね? ASIがあれば、エンジンが自動でセミコロンを「仮想挿入」して文を区切ってくれるので、セミコロン漏れの心配が減ります。

JavaScriptのようなスクリプト言語は、C++やJavaのようなコンパイル言語と比べて、型の指定が不要であったり、変数の型が自動変換されたりと、開発の自由度やスピードが重視される傾向にあります。ASIもまた、開発者の負担を減らし、柔軟な記述を可能にする自然な流れと言えます。

ASIの落とし穴

ASIの問題は、文の境界が曖昧になる点にあります。
簡単に言えば「自動判定」が誤作動する…いやまぁ機械的に処理してるわけだから正確にはユーザーがASIが意識の外にある場合があるってだけですが。
最も有名なのがreturn文の例です。

return // ここで改行
a + 1
本来は a + 1 の結果を返したい意図で書いたとしても、JavaScriptは改行された時点で「ここで処理が終わったな」と判断し、内部的に以下のように解釈します。
return; //  ASIによってセミコロンが挿入
a + 1;  //  別の処理として解釈

結果、returnは何も値を返さず処理が終了し、期待した動作になりません。JavaScriptは変数名やメソッド名の途中ではない限り、基本的にどこでも改行可能なので、この自由さがASIと衝突することで、予期せぬ挙動を生み出すのです。

今回、ASIで沼った話

さて、本題の「ASIで沼った話」。これは単純なイベントリスナー登録の話です。
と、その前に、通常はこの様に書きますが
document.getElementById(id).addEventListener(type, listener);
個人コーディングの時はBSCオリジナルラッパーを使っているので
$i('aaa').ael('change', bbb);
こんな感じに圧縮記述出来ます。

普段、可読性を重視して、リスナーは次のように列挙して記述しています。
$i('aaa1').ael('change', bbb)
$i('aaa2').ael('change', bbb)
$i('aaa3').ael('change', bbb)
これは何の問題もありません。ASIによって、各行の末尾にセミコロンが補完され、それぞれ独立した処理として実行されます。まさに「ASI様様」という状態です。

リスナーをまとめてみた

今回は同じ関数を呼ぶ処理があったので可読性と効率を考えて、forEachでまとめて登録することにしました。
// リスナーまとめエリアでの記述
['aaaAbc','aaaDef','aaaGhi'].forEach(id=>{$i(id).ael('change', aaaAAA);});
このコード自体はテストして問題もありません。
DOMも読み込み済みでこれといったエラーはありません。

ところが、このコードを「リスナーまとめエリア」に移動した瞬間、aaaGhiの要素でエラーが発生! 原因不明で頭を抱えました。DOMはOK、個別記述ならOK、隔離してもOK、上部移動してもOK。でも、このエリアに置くとダメ…。

沼の原因:配列の誤解釈

forEachをばらせば正常になるのでバラで登録しても良かったんですが今後の為にも一応調査した事で一応特定は出来ました。
今回の具体的な配置は以下の通りです。
$i('aaa1').ael('change', bbb) // 1行目
$i('aaa2').ael('change', bbb) // 2行目
$i('aaa3').ael('change', bbb) // 3行目
['aaaAbc','aaaDef','aaaGhi'].forEach(id=>{$i(id).ael('change', aaaAAA);}); // 4行目
実は、内部的に発生していたのは、3行目と4行目の結合でした。
$i('aaa3').ael('change', bbb)['aaaAbc','aaaDef','aaaGhi'].forEach(id=>{$i(id).ael('change', aaaAAA);});
JavaScriptのインタプリタはこの様に解釈しました。

具体的には:
  1. $i('aaa3').ael('change', bbb) の実行結果(addEventListenerは通常 undefined などの値を返します)
  2. その結果に対して、次の行の配列をインデックスとしてアクセスしようとする
  3. undefinedなどの値に配列アクセス([ ])を試みたためエラーが発生
  4. 結果的に「undefined.addEventListener」のような状態になり、処理全体が破綻した

改行によって処理の継続と判断される箇所で、たまたま次の行が(や[など、前の行の処理の続きと解釈できる記号で始まっていたため、ASIが発動せず、予期せぬ文法解釈(配列アクセス)が行われてしまったという事ですね。

利便性 vs リスク:JavaScriptのジレンマ

結局、ASIでセミコロンから解放されたはずなのに、「セミコロンを明示的に付けた方が安全」
という話になりますが、そうなると「ASIの存在意義は?」という根本的な問いに行き着きます

①自由さと厳密さのトレードオフ
JavaScriptの設計思想は元々「柔軟な記述」を重視していますが、現在のコーディングの潮流はより厳密な方向に向かっています。

②TypeScriptという名のJavaScript
TypeScriptという言葉を聞いたことがある人も居るかもしれませんが、JavaScriptに静的型付けの概念を持ち込み、コンパイル時に厳密なチェックを可能することでコーディングを厳格堅牢にできます。
ですが・・・結局のTypeScriptはJavaScriptに過ぎません。コンパイルという表現を使いますが一般的なプログラムのように機械語にするわけでは無い為、処理が高速化するわけでもありません。
JavaScriptは結局ブラウザ依存です。つまり、折角型を付けたコードを削除して低級のJavaScriptに戻すというここだけ聞くと馬鹿らしい処理をしています。

個人的に最近はモジュール形式(.mjs)で書くことが増えましたが、これはローカルスコープやstrict modeの適用など、より厳格なコーディングを促しますがASIは有効です。

JavaScriptの未来:柔軟性と厳格性の狭間

正直、現状のJavaScriptがどこを目指しているのか、掴みどころがありません。
柔軟性を保ち続けたいのか、それともTypeScriptのような厳格な方向へ完全にシフトしてしまうのか。言語には通常、何らかの明確な「信念」がありますよね。
例えば、C言語系は高速実行を、Javaはプラットフォーム非依存を、Pythonは読みやすさと生産性を、PerlやRubyは簡潔さを、それぞれの強みとして進化させてきました。一方、JavaScriptは「どっちつかず」の状態が長すぎて、信念のなさが逆にその独自の魅力——いや、ブレブレの個性——を生んでいる気がします。
もちろん、見守るしかない部分もありますが、心から願うのは、この「ゆるさ」が失われないことくらいでしょうか・・・。


まとめ
  • セミコロンを積極的に付ける:ASIに頼りすぎず、明示的に。
  • 改行前に一文完結を確認:returnやメソッドチェーンの後、特に注意。
  • ツール活用:PrettierやESLintで自動整形・警告を。
  • テストを忘れず:個別動作OKでも、文脈で壊れる可能性あり。

2025年9月28日日曜日

liタグをドラッグアンドドロップで入れ替える話

 タグの入替方法

交換自体はそれほど難しくないが、アイテム交換にアクションを付けるとちょっと面倒臭い。
交換処理はいかの手順を踏む

  1. 入替対象タグ内に「draggable="true"」を追加してドラッグ可能にする
  2. ドラッグを開始時の対象idを取得
  3. 交換対象をドラッグオーバーで特定し交換対象がわかる表示
  4. ドロップして実際に交換

処理的にはこうなります。

親タグの<ul>にはidを付与します。「<ul id="draggable-list">」
子タグの<li>にdraggableとidを付与「<li id="item-xxx" draggable="true" >」
親を取得してその中で入れ替えます
リスナーは「item.addEventListener(event, handler)」イベント別に分けます
イベントは「dragstart、dragover、dragend」この三つが必須

  • dragstart(ドラッグ中)
  • dragover(交換可能位置上)
  • dragend(マウスボタンを放した)

挙動抑止として「drop、dragenter、dragleave、dragend」

  • dragenter(ドラッグ対象先判定)
  • dragleave(対象外選択範囲)

抑止処理

「e.preventDefault();」でブラウザ上のデフォルト挙動表示を抑止します。
なので処理の頭に指定します。over中とenterの最初に記述します。
dragleaveは何もしないので処理を空で用意。

処理開始

dragstartにやる事は現在のidが何か?を取得します。
「e.dataTransfer.setData('text/plain', e.target.id);」を使えばdataTransferに掴んでいる対象を保存できます。
変数を使わないのでコード的には綺麗ですが問題はover中は中身を取得出来ません。

なので「draggedItem = e.target;」で対象別途保存します

over中

つまりは交換可能対象の上でドラッグ中の状態です
overされてる対象と現在掴んでる対象を比較交換するかの状態を表示させます
ただ、そのためには現在掴んでいる対象を知る必要があります。
const draggedId = e.dataTransfer.getData('text/plain');if (!draggedId) return;」
const dragitem=document.getElementById(draggedId )
とすれば、ドラッグ対象を得られるのですが、over中はdataTransferから空文字しか返ってきません。
つまり、放した瞬間しか取得出来ません。「if (!draggedId) return;」でその間無視する事ができるものの、それは交換できる状態の視覚効果の処理に到達出来ません。
一応放した瞬間に取得出来るのでAとBの対象交換自体は出来ます。

先に言った通り「交換可能視覚効果」を得るには「ドラッグ先の対象」と「ドラッグ中の対象」両方を知ってる必要があり、ドラッグ中の対象を事前に得るならば処理開始時に「draggedItem = e.target;」をしておいてdraggedItem を参照するようにすればOK
ただ、draggedItem は処理開始時とover中どちらからもアクセスできる必要があります
それはつまりdraggedItem をグローバル変数にせざる得ないという事です。

「 if (targetItem && targetItem !== draggedItem) {」空の場合と交換対象がドロップ中のアイテム自身ではいけないのでこんな感じの分岐になります。

ドロップ

対象にマウスボタンを放した時
ただ、over中に処理が完結する。

動的にリストを追加

function addlist(n){
    const smna = getSecondaryMaterialNames();
    const list = $i('draggable-list');
    const newItem = Object.assign(document.createElement('li'), {id: `item-${crypto.randomUUID()}`,draggable: true,innerHTML: `<span>${smna[n]}</span><span class="delete-btn">×</span>`});
    newItem.dataset.smc = n;
    list.appendChild(newItem); // add tag
    setlistael(newItem);       // set ael(drag)
}

リストに×ボタンを付けてリストを消す

✕ボタンのイベントリスナーは一度だけ定義して親のイベントから継承させる
なのでリスナー登録するのは✕ボタンではない

 $i('draggable-list').ael('click', (e) => {
if (e.target.classList.contains('delete-btn')) {const itemToRemove = e.target.closest('li');if (itemToRemove) {itemToRemove.remove();getUpdatedOrder();}}
});

 親のidにクリックイベントで登録。
クラスのdelete-btnに継承してliの現ターゲットを削除させる。

2025年9月7日日曜日

LOMの全イベント管理シミュレーターを作った話

LOM全イベントの完全管理 

前回構想だけ話しましたが結局作りました
LOMでは全68イベントが存在します。

実の所イベントだけなら(固定の初期範囲と初期位置)だけならそこまで複雑になりません。
所がシミュレーションを好きなランドメイクをしつつイベントも好きな様に組むとなると爆発的に難しくなります。

個々の仕組みはそんなに難しくないのですがこれが組み合わさってそれをシステムに落とすとなると難易度がヤバい(語彙力低下)。
でももっと深く遊ぶのにランドメイクのシミュレーションだけではイベントを別途メモ帳とか別の場所で管理する必要があって面倒臭い。
そのためページ1つで完結させたくなった。

ファイル構成を考える

今回は処理とデータを分離しました。
とは言え、DBの使えるサーバーではないのでDB用JSを作ってそれをインポートで引っ張ります。
仕様が変更されることが無いのでシステム的な拡張性を考慮する必要がないので名前付きDBのような仕組みは不要で、欲しいのは配列の塊だけです。

今回は普通のjavascriptじゃなくてmjsで作成しました。
拡張子をmjsにしようと思ったら、サーバーが対応してないという根本的な問題があった(サーバー側がファイル形式をホワイトリストで処理してるようでアクセスが出来なくなる)のですがそもそもmjsは拡張子jsでも動くので結局拡張子はjsにする事になりました。

さておき、構成としては
・メイン処理JS
・DOM用オリジナルラッパーJS
・DB-JS
この3つ。
検索時に静的に埋め込まれていた方が良いかなという視点でAFとイベント名のリストはHTML側に記述しています。管理面を考えると動的に生成した方が楽なんだけどSEO的にはどっちが良いのか未だ分からない。最近のクローラーは動的な内容もある程度判定するようなんだけどベタ記述されてれば確実に読み込みはするはず。

操作部分を静的にするとデメリットもあって、描画を待たないといけない。
対処方法は3つ
・body下でjavascriptを読み込ませる
・window.onloadで発火させる
・DOMContentLoaded で発火させる

body下の場合htmlは上から順に処理されるのでボタンやリストのテーブルなどの描画が終わった後にjavascriptが実行されます(必要最低限の描画直後で最速だが、場合によっては描画が変な遅れ方するとエラーする可能性はある)。

onloadはスクリプト含めた描画に必要な全てを読み込んだ後に実行されます(一番確実だが一番遅い)

DOMContentLoaded はHTML の文書が完全に読み込まれ構文解析され、すべての遅延スクリプト(deferの遅延読み込み、module読み込み)がダウンロードされ、実行されたときに発火。
※画像、サブフレーム、非同期スクリプトは待たない。

DOM用オリジナルラッパーJSは過去に紹介しているbscラッパーです。
elementをラップして短縮記述出来ます。
例えば、document.getElementById()は$i()に圧縮出来ます。

処理を考える

ランドメイクのアンドゥをする際に前回のものは全工程を再処理してたので遅いのが少し不満だったため今回は各工程をスナップショットする様に変更。これによって今回はアンドゥがだいぶ高速化出来ています。

1.初期化

■初期値1(初回ロード)
・マップカラー、全マップ、初期マップインデックスを設定。
・イベントリスナーを設定。
・ログを初期化し、アーティファクト、6x6エリア、6x6マップをセットアップ。
・URLにクエリがある場合、保存状態を復元し、ない場合はデフォルト。

全体のマップの色や1度だけの処理部分。初期化2も呼ぶ。

■初期化2(随時リセット)
・イベントを初期化(非表示、無効、チェック解除)。
・イベント状態を0で初期化(68要素)。
・保存スナップショットを初期化(可変)。
・アーティファクトを初期化(26要素)。
・6x6エリアのマナを初期化し、初期マナ値を設定。
・ログに初期マップインデックスを記録。

エリアの変更等で呼ばれる
リセット用の初期化

2.操作

順序的には
マップ処理→配置処理→イベント処理
マップが無ければランドが配置できずランドが無ければイベントをする場所がない
大枠はこの3工程をループ

AFには基本イベントがある。
街以外はランドロックの解除が必要なので基本1つはイベントがある。
AF配置でも、イベント終了でもイベントが解除される

■初期操作

・25x25の全体マップから初期エリアの選択→6x6エリア更新
・ポスト取得
・ポストの配置(ポストだけは陸なら何処でも置けるため専用の配置)
・ドミナの取得(以降隣接配置に変わる)

■AF

・アーティファクトを選択し、配置可能なエリアを表示。
・指定されたAFを配置可能な6x6エリアを計算し、ハイライト。
・キャンセル時はハイライトとAFを戻す

・配置→位置の記憶→マナ計算(周囲4マスの変更)→イベント解放
・町の場合無条件でランドロック解除

■イベント

・イベントの開始
・まいごのプリンセス等は開始後にAFを取得
・イベントの終了
・ランドロック解除
・次イベント解放

■例外

・果樹園の60個採取は単独判定
・丙子椒林剣をサブイベント処理

■失敗・消滅
・失敗はその時点でゲーム内でも失敗が表示される
・消滅は条件が満たせなかった場合で結果的に発生しない

■ステップ

・ステップが変動するたびに処理
・各変数を全て保存
・リドゥ、アンドゥで変数を復元
・ステップ数上限を超える場合は変数保存、以内であれば復元
・状態をリアルタイムで復元用url生成

■その他描画関係

シミュレーションに関係ない部分の描画
・リストの表示切替等
・全体マップの表示切替等
・復元urlの𝕏でのポスト機能

3.処理の関数化

実処理はもう少し細分化。関数ベース。

関数名 処理内容
setdata ページロード時の初期化。マップカラー、全マップ、初期マップインデックス(gmi=97)を設定。リスナーを設定し、ログ、AF、6x6エリア/マップを初期化。URLクエリがあれば状態を復元、なければlms()を呼び出す。
setstartaf AF、イベント、マナ、ログの初期状態を設定。イベントを非表示/無効化、gevsを0で初期化、AF配列(gafo, gafc, gafs)を初期化、6x6エリアのマナ(l36m, l36a)を設定、ログに初期マップを記録。
set6x6area 6x6マップの表示位置を調整。マップインデックスから座標を取得し、box6x6のleft/topをピクセル単位で設定。
set6x6map 6x6マップを表示。マップカラーと6x6マップデータを取得し、各セル(box65)の背景色を設定。
get6x6map 6x6マップデータを生成。有効ポイントとフルマップから6x6領域を切り出し、山データを適用、座標ラベルを設定してデータを返す。
set6x6coordinates 6x6マップの座標ラベル(行:数値、列:アルファベット)を設定。インデックスを座標に変換して表示。
m6x6del 6x6エリアとマナ表示をクリア。area36とmana36のinnerHTMLを空にする。
get6x6mana 6x6マップのマナデータを取得。有効ポイントとマナマップから6x6領域のマナを抽出。
setfullmap 25x25の全体マップを表示。マップカラーとフルマップデータを用いて各セル(box25)の背景色を設定。
highlightmap 25x25マップにハイライトを適用。ハイライト用マップデータで背景色を更新。
boxclick 25x25マップのセルクリック処理。クリックされたインデックスが有効ポイントに含まれる場合、setmapを呼び出してマップを切り替え。
setmap 指定されたマップインデックスに切り替え。mapselectを更新し、6x6座標を設定後、lms()を呼び出す。
mapmove マップを上下左右に移動。方向(左/右/上/下)に応じてインデックスを変更し、lms()を呼び出す。
choiceaf AFを選択し、配置可能なエリアを表示。選択したAFのボタンを非表示、情報を表示、配置可能エリアをハイライト。
availableareas AFを配置可能な6x6エリアを計算。地形条件と隣接AFの状態を基に、配置可能なセルを透明度0.5でハイライト。
m6x6p100 6x6エリアの透明度をリセット(1.0に設定)。
afcancel AF選択をキャンセル。AF情報/ステータスを非表示、AFボタンを再表示、透明度をリセット。
setaf 6x6マップの指定位置にAFを配置。配置可能かチェックし、ログ、AF状態、エリア、マナを更新。必要に応じてAF解放や自動アンロックを実行し、lms()を呼び出す。
setafdirect 指定されたAFを指定位置に直接配置。UI操作を介さず、setafと同様の処理を行い、lms()を呼び出す。
getbuildingblocks AF「ビルディングブロック」を解放。gafs[1]=1を設定し、ボタンを表示。
checkmana AF配置時のマナを計算。隣接セルのマナを加算し、上限3に制限。隣接セルのマナも更新。
defaultunlock 特定ランド(街など)を自動アンロック。固定リストに基づき、gafs[n]=3を設定。
autounlock ランドのアンロックイベントを自動終了。アンロックリストからイベント番号を取得し、eventclearを呼び出す。
getaf 指定されたAFを解放。未解放の場合、gafs[n]=1を設定し、ボタンを表示。
manazero 特定ランド(Lucemia)のマナをゼロにリセット。l36mを更新し、UIに反映。
eventstart イベントを開始。gevs[n]=2を設定、UIを更新(チェックボックス有効、背景白)。競合イベントをチェックし、ログを記録、lms()を呼び出す。
eventclear イベントをクリア。開始状態を確認し、gevs[n]=3を設定、UIを更新(背景緑)。関連AFやアンロックイベントを実行、ログを記録、lms()を呼び出す。
subevent1 サブイベント(果実60個)を処理。チェックボックスを更新、AFを解放、ログを記録、lms()を呼び出す。
subevent2 サブイベント(武器「丙子椒林剣」)を処理。チェックボックスを更新、ログを記録、lms()を呼び出す。
eventcheck イベントの解放条件をチェック。AF、マナ、イベント状態を基に開始可能イベントを判定、UIを更新。進行不能イベントを失敗状態(gevs=4)に設定。
eventdisplay イベントリストの表示を制御。ラジオボタンの選択に応じて、開始済み/進行中/全イベントを表示/非表示。サブイベントの表示も制御。
relogw ログを圧縮(sX-eXをwXに変換)。glog[0]を圧縮し、glog[1]に保存。
compressLog ログ文字列を圧縮。sX-eXをwXに置換。
logconvert ログをHTML形式に変換。AF配置、イベント開始/終了、サブイベントをリスト形式で表示。最後のログをlaststepに表示。
log2txt ログエントリをテキストに変換。AF配置、イベント、サブイベントの内容を文字列化。
savestep 状態を保存し、URLを更新。ログと履歴を比較し、スナップショットを保存、URLに圧縮ログを付加、ステップ数を更新。
savesnapshot 現在のゲーム状態をスナップショットとして保存。グローバル変数とUI状態を保存。
loadset URLや履歴から状態を復元。ログをパースし、AF配置、イベント、サブイベントを再現、lms()を呼び出す。
undo 1ステップ前の状態に復元。ログの最後のエントリを削除し、スナップショットを復元。
redo 1ステップ後の状態に復元。履歴の次のエントリを適用。
undoall 初期状態(ステップ1)に復元。スナップショット1を復元。
redoall 最新の状態に復元。履歴の最終ステップを復元。
restoreSnapshot 指定されたスナップショットを復元。グローバル変数とUI状態を復元し、lms()を呼び出す。
add1step 1ステップ分のログを適用。AF配置、イベント、サブイベントを再現。
makecheck チェックリストを更新。特定ランドのマナや配置状況をチェックし、条件達成(OK)を表示。ランドレベルを計算。
convertlandlevel ランドのレベルを計算。基準点(AF0)とのマンハッタン距離と配置数を加算。
mana2tag マナ配列をHTML形式(色付きスパン)に変換。マナ値に応じて背景色を設定。
setcolor4 マップカラーの説明を表示。マップカラーを取得し、color4要素に背景色を設定。
openmyurl 保存URLを新しいタブで開く。saveurlの値をwindow.openで開く。
urlpost 進行状況をXに投稿。チェックリストを基にテキストを生成し、xpostで投稿。
setlistener イベントリスナーを設定。マップクリック、AF選択、イベント操作、Undo/Redo、表示切り替えなどのリスナーを登録。
iddisplayswitch 指定された要素の表示/非表示を切り替え。displayスタイルをトグル。
arrayaddition マナ配列を要素ごとに加算。
arrayaddition3max マナ配列を加算し、上限3に制限。
index2coordinates インデックスを座標(行, 列)に変換。

こんな感じ。

2025年8月25日月曜日

VBSでUTF-8で読み書きする

■ ADODB.StreamのUTF-8読み書き

VBS自体は使えなくなるかもしれないんですがVBAや他の言語でも使えるので一応備忘
基本的にテキストの読み書きはFileSystemObjectを使うのですがShift-JISしか使えません。

今回はWebページのヘッダーのメタタグの機械的な変換をしたいなぁと思った時にWebページは全部UTF-8に移行済みだったので、じゃあ「ADODB.Stream」という事ですね。

ADODB.Streamで出来る事は主に2つ、文字コード指定してのテキストの読み書きとバイナリデータの読み書きです。

■UTF-8を読み込む

まずは読み込みから
htmlpath="c:\makewebsite\test.html"

With CreateObject("ADODB.Stream"):.Type = 2:.Charset = "UTF-8":.Open:.LoadFromFile htmlpath:html = .ReadText:.Close:End With

  1. 通常通りCreateObjectでオブジェクトを作ります。
  2. Typeは1がバイナリモード、2がテキストモードです。
  3. Charsetは文字コード、今回はUTF-8のhtmlを読み込むので UTF-8です。
  4. Openでストリームを開きます
  5. LoadFromFile にファイルパスを指定
  6. ReadTextでhtmlの内容を全て取得
  7. Closeでストリームを閉じます

■UTF-8で書き込み①

次に書き込み
htmlpath="c:\makewebsite\test.html"
retext="書き込む文字"

With CreateObject("ADODB.Stream"):.Type = 2:.Charset = "UTF-8" :.Open:.WriteText retext:.SaveToFile htmlpath, 2:.Close:End With

これでOK!単純に先ほど逆順に処理します

  1. CreateObject(ストリームオブジェクト)
  2. Type(2:テキストモード)
  3. Charset(文字コード:UTF-8)
  4. Openでストリームを開きます
  5. WriteText でストリームに書き込む
  6. SaveToFile でストリームを全て出力、2は上書き
  7. Closeでストリームを閉じます

■Byte Order Mark/バイトオーダーマーク

ただし、1つだけ問題があります。
ADODB.Streamの仕様上書き込みにはBOM(ばいとおーだーまーく)が入ります
これが何かというとファイル先頭の3byteを使ってテキストの種類を書き込みます。

通常あっても基本問題ありませんが、現在の主流はテキストの内容を見て自動判断する方式です。windows10以降のデフォルト保存形式もUTF-8BOMなしです。

そしてwebで扱うHTMLもBOMは不要です。
普通のテキスト同じく基本的には問題ないものの、頭に3byteがクローラーなどの邪魔になる可能性がありBOMは消す必要があります。

■UTF-8で書き込み②(BOMなし)

With CreateObject("ADODB.Stream")
.Type = 2:.Charset = "UTF-8":.Open:.WriteText retext:.Position = 0:.Type = 1:.Position=3:tb=.Read:.Close
.Type = 1:.Open:.Write tb:.SaveToFile htmlpath, 2:.Close
End With

  1. CreateObject(ストリームオブジェクト)
  2. Type(2:テキストモード)
  3. Charset(文字コード:UTF-8)
  4. Openでストリームを開きます
  5. WriteText でストリームに書き込む
  6. テキストの状態でストリームの位置を0に
  7. Type(1:バイナリモードに変更)
  8. バイナリモードで開始位置を3byte移動
  9. Readで変数に4byte以降代入
  10. Closeでストリームをリセット
  11. Type(1:バイナリモード)
  12. Openでストリームを開きます
  13. Writeでストリームを書き込む
  14. SaveToFile でストリームを全て出力(2は上書き)
  15. Closeでストリームを閉じます

公式ドキュメントを見る限りCloseで閉じた後に再度Openで開いても問題なさそうなので1つのストリームでBOMを消しています。
処理が複雑そうになるならストリームは読み込みと書き込みで分けてください。

Closeメソッド
https://learn.microsoft.com/ja-jp/office/client-developer/access/desktop-database-reference/close-method-ado

処理としては文字列情報をテキストストリームに変換
UTF-8でテキストストリームに書き込むとBOMが入る
テキストストリームの状態でポジションを0で先頭に戻す
バイナリモードにして先頭からBOM分の3byteをスキップさせ変数にbyteで代入
ストリームを切断後に開きなおし、ストリームにbyteを代入あとはSaveToFileで出力して終了。

■UTF-8で書き込み③(BOMなし)

With CreateObject("ADODB.Stream")
.type=2:.charset="UTF-8":.open:.writetext retext
.position=0:.type=1:.Position=3:tb=.Read
.position=0:.seteos:.write tb:.SaveToFile htmlpath, 2:.Close
End With

もう少し詰めるならこう。
BOM3byteを飛ばすところまでは同じ。
ストリームの位置を先頭に戻し、seteosでストリーム内容を完全消去します。
そこにストリームを書き込みなおして出力。

コードの評価をジェミニなどで通したときにBOMが必ずつくか分からないと言われて調べたけれど、BOMが付かない状況を探しても見つかりませんでした。

WriteText メソッド (ADO)
https://learn.microsoft.com/ja-jp/office/client-developer/access/desktop-database-reference/writetext-method-ado

Charset プロパティ (ADO)
https://learn.microsoft.com/ja-jp/office/client-developer/access/desktop-database-reference/charset-property-ado

そもそもBOMについての記載がなく、仕様説明ないまま使われてる技術怖い・・・。

2025年8月15日金曜日

WindowsPowerShellのGUI版を作るメモ

 さて、今回はhtaとvbsを使ったローカルスクリプトをちょっとGUIのパワーシェルにして行こうかなって話。

パワーシェルでハローワールド

まずパワーシェルでハロワしましょう。
今回はファイルを作りたいのでパワーシェルに直打ちではないです。
文字を表示するだけならWrite-Hostを使います。
「Write-Host "ハローワールド";pause」とメモ帳に記載して「はろわ.ps1」UTF-8BOM付きで保存。


とりまOSデフォのパワーシェル5.1を使います。
デフォルトのエンコードがsjisなのでBOM付き保存が必要です。


※6以降を使う場合は別途インストールが必要。新しい方を使う場合は起動のexeが変わります。デフォの場合「powershell~」新しい方の場合は「pwsh~」になります。
※pwshの方のファイルはデフォがUTF-8BOMなしです。気になる場合はパワーシェルのコンソールで「[System.Text.Encoding]::Default」を打てば分かります。


セキュリティが高くてデフォルトでは実行出来ないので右クリックショートカット作成で「powershell -ExecutionPolicy Bypass -File C:\t\はろわ.ps1」パスは保存先のパスを指定。「-ExecutionPolicy」が実行ポリシーを現セッションのみ変更する指定で「Bypass」が全ての許可指定。「-File」はファイルから実行って意味。


GUIウィンドウでハローワールド

HTAの代わりにウィンドウを作らないといけないので次はウィンドウ表示

  Add-Type -AssemblyName System.Windows.Forms
  $form=New-Object System.Windows.Forms.Form
  $form.ShowDialog()

これだけ。
1行目は「.NET Framework」ライブラリのウィンドウ(フォーム)を使える様にする指定
2行目はオブジェクトを変数に代入
3行目で表示
ファイルを上書きして実行(実行方法はさっきと同じ)
そうするとパワーシェルウィンドウとなんも表示されないウィンドウが表示されます。
これはパワーシェルが本体で、そこからウィンドウを表示してるから。
ウィンドウを✕閉じするとパワーシェルも一緒に閉じます。

次にウィンドウのタイトルとウィンドウ内でもハロワしてみましょう
オブジェクトを代入する時に「 -Property @{プロパティ設定}」でプロパティを設定しながら代入出来ます。

Add-Type -AssemblyName System.Windows.Forms
$form = New-Object System.Windows.Forms.Form -Property @{
    Text = "ハローワールド"
    Size = New-Object System.Drawing.Size(600, 400)
}
$label = New-Object System.Windows.Forms.Label -Property @{
    Text = "ハローワールド"
}
$form.Controls.Add($label)
$form.ShowDialog()

htaと違ってウィンドウ内には直接文字を打てません。
テキストを入力するにはテキスト入力エリア=ラベルを作ってそこに文字を指定します。
更にラベルを作ったら、それをウィンドウに追加「$form.Controls.Add($label)」しないとウィンドウ上にラベルが配置されません。変更したら上書き保存。

今回から起動ショートカットのコマンドに「-windowstyle hidden」を追加します。
具体的には「C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -windowstyle hidden -ExecutionPolicy Bypass -File C:\t\はろわ.ps1」
これで表示メインじゃない起動元のパワーシェルを非表示(=バックグラウンド)にできます。


これでGUIハローワールドも終了

ボタン等の配置方法

htaはhtmlと同様の仕組みなので簡単にinputboxやtextarea、ボタンなどを配置出来ていましたが今回は違います。
テキストを表示するだけでもラベルが必要なようにhtmlなどで言えば全てのコントロール(テキストボックスやボタン等)がz-indexのように個々がレイヤー別にあり、コントロールに追加した順番に(後追加ほど手前に)表示されます。
<input type="button" value="フォルダリスト取得" onClick="getfolderlist()" >
としてた場合は   

$btnFolderList = New-Object System.Windows.Forms.Button -Property @{
    Text = "フォルダリスト取得"
    Location = New-Object System.Drawing.Point(680, 10)
    Size = New-Object System.Drawing.Size(100, 25)
}

こんな感じで必ず座標と基本的にはサイズの指定も必要になる
htaで「onClick="getfolderlist()"」と関数へキックしていたが、処理する関数を呼ぶのと違って自身のコントロール(変数)のクリックに処理を記述します
$btnFolderList.Add_Click({処理})
で記述する事が出来ます。ただ毎回「New-Object System.Windows.Forms.Button -Property @{~」と書くのは面倒なのでコントロールの操作を関数化しましょうか

function newbutton{
    param($tx,$x,$y,$w,$h)    
    $setbt = New-Object System.Windows.Forms.Button -Property @{
        Text = $tx
        Location = New-Object System.Drawing.Point($x, $y)
        Size = New-Object System.Drawing.Size($w, $h)
    }
    return $setbt
}

例えば、この様に関数を作ります
$btnFolderList =newbutton "フォルダリスト取得" 680 10 100 25
そうすれば、この様に関数で設定出来ます。
関数を呼ぶ時は「,」不要です。コンソールのコマンド系に倣ってこういう形です。
一応プログラム的な書き方にも対応していて
$btnFolderList =newbutton( "フォルダリスト取得",680, 10, 100, 25)
という記述方法も可能です。括弧を付ける場合、区切りは「,」に変更する必要があります。

テキスト入力コントロールの生成

次はテキスト入力エリア、htaでは以下通りですが
<input type="text" name="ifo" size="100" value="C:\">
<textarea name="txa" cols="120"  rows="25">リスト編集エリア</textarea>

パワーシェル(System.Windows.Forms)ではテキストエリアとインプットテキストとような区分けはありません。
テキストを入力するエリアに行入力(Multiline)を許すかどうかが違うだけで機能的にはTextBoxの挙動の差を設定するだけです。

function newtextbox{
    param($tx,$x,$y,$w,$h,$ml = $false , $sb = "none",$ww=$true)    
    $settb= New-Object System.Windows.Forms.TextBox -Property @{
        Text = $tx
        Location = New-Object System.Drawing.Point($x, $y)
        Size = New-Object System.Drawing.Size($w, $h)
        Multiline = $ml
        ScrollBars = $sb #Vertical/Both
        WordWrap = $ww
    }
    return $settb
}

こんな感じですね。
$txtReadPath =newtextbox $desktoppath 70 10 600 20
$txtArea=newtextbox "リスト編集エリア" 10 70 760 400 $true "Both" $false
呼ぶ時はこの様に記述し、デフォルト値を設定すれば引数のありなしも制御可能です。

HTAツールをパワーシェルGUIツール化

さて、概ねパワーシェルのGUI化が把握できたので実際に過去に作ったHTAツールをパワーシェルでGUI化してみます

Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName Microsoft.VisualBasic

# 定義
$desktoppath=[Environment]::GetFolderPath("Desktop")

# フォームの作成
$form = New-Object System.Windows.Forms.Form -Property @{
    Text = "一括フォルダ生成"
    Size = New-Object System.Drawing.Size(800, 600)
    StartPosition = "CenterScreen"
}

# コントロール生成関数
function newlabel{
    param($tx,$x,$y,$w,$h)    
    $setlabel = New-Object System.Windows.Forms.Label -Property @{
        Text = $tx
        Location = New-Object System.Drawing.Point($x, $y)
        Size = New-Object System.Drawing.Size($w, $h)
    }
    return $setlabel
}
function newtextbox{
    param($tx,$x,$y,$w,$h,$ml = $false , $sb = "none",$ww=$true)    
    $settb= New-Object System.Windows.Forms.TextBox -Property @{
        Text = $tx
        Location = New-Object System.Drawing.Point($x, $y)
        Size = New-Object System.Drawing.Size($w, $h)
        Multiline = $ml
        ScrollBars = $sb #Vertical/Both
        WordWrap = $ww
    }
    return $settb
}
function newbutton{
    param($tx,$x,$y,$w,$h)    
    $setbt = New-Object System.Windows.Forms.Button -Property @{
        Text = $tx
        Location = New-Object System.Drawing.Point($x, $y)
        Size = New-Object System.Drawing.Size($w, $h)
    }
    return $setbt
}

# コントロールの作成
# tx
$lblReadPath = newlabel "読込パス:" 10 10 60 20
$lblOutputPath = newlabel "出力パス:" 10 480 60 20
# txbx
$txtReadPath =newtextbox $desktoppath 70 10 600 20
$txtArea  =newtextbox "リスト編集エリア" 10 70 760 400 $true "Both" $false
$txtOutputPath =newtextbox $desktoppath 70 480 600 20
# btn
$btnFolderList =newbutton "フォルダリスト取得" 680 10 100 25
$btnFileList =newbutton "ファイルリスト取得" 680 40 100 25
$btnMakeFolders =newbutton "フォルダ生成" 680 480 100 25
$btnNumberSequence =newbutton "連番入力" 10 510 100 25

#ボタン処理
$btnFolderList.Add_Click({
    if ($txtReadPath.Text -eq "") {
        $txtArea.Text = "ディレクトリを入力して!"
        return
    }
    if (-not (Test-Path $txtReadPath.Text -PathType Container)) {
        $txtArea.Text = "ディレクトリが正しくありません"
        return
    }
    $folders = Get-ChildItem -Path $txtReadPath.Text -Directory | ForEach-Object { $_.Name }
    $txtArea.Text = $folders -join "`r`n"
})
$btnFileList.Add_Click({
    if ($txtReadPath.Text -eq "") {
        $txtArea.Text = "ディレクトリを入力して!"
        return
    }
    if (-not (Test-Path $txtReadPath.Text -PathType Container)) {
        $txtArea.Text = "ディレクトリが正しくありません"
        return
    }
    $files = Get-ChildItem -Path $txtReadPath.Text -File | ForEach-Object { $_.Name }
    $txtArea.Text = $files -join "`r`n"
})

$btnMakeFolders.Add_Click({
    if (-not (Test-Path $txtOutputPath.Text -PathType Container)) {
        [System.Windows.Forms.MessageBox]::Show("出力先ディレクトリが正しくありません", "エラー", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error)
        return
    }
    $outputPath = $txtOutputPath.Text
    if (-not $outputPath.EndsWith("\")) { $outputPath += "\" }
    $folderNames = $txtArea.Text -split "`r`n" | Where-Object { $_ -ne "" }
    foreach ($folder in $folderNames) {
        try {
            New-Item -Path ($outputPath + $folder) -ItemType Directory -ErrorAction Stop | Out-Null
        } catch {
            # エラーは無視(既存フォルダなど)
        }
    }
})

$btnNumberSequence.Add_Click({
    $myput = [Microsoft.VisualBasic.Interaction]::InputBox("テキストエリアに数字連番生成するだけ`n数字を入力してください。", "連番入力")
    if ($myput -eq ""){return}
    if (-not ($myput -match '^\d+$')) {
        [System.Windows.Forms.MessageBox]::Show("数値(整数)を入力してください", "エラー", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error)
        return
    }
    $n = [int]$myput
    $len = $myput.Length
    $result = 1..$n | ForEach-Object { $_.ToString("D$len") }
    $txtArea.Text = $result -join "`r`n"
})

# コントロールをフォームに追加
$form.Controls.AddRange(@($lblReadPath, $txtReadPath, $btnFolderList, $btnFileList, $txtArea, $lblOutputPath, $txtOutputPath, $btnMakeFolders, $btnNumberSequence))

# フォームの表示
$form.ShowDialog()

1つずつ処理確認しつつほぼそのまま実装出来ました

2025年8月7日木曜日

Astria Ascending(アストリア アセンディング)をやり込んだ感想

Astria Ascending(以降AA)個人的かつ 最終的な感想としては「面白かった」です。
ただ、おそらく自分がやりこみの深いコアゲーマーであるので一般的ユーザーと感想が一致しないような気はします。特にライトユーザーとかにはゲームとして難しいかもしれない。
まぁ、ゲーマーの定義についはまた今度考えるとしましょうか。

バグが多いし不親切

とは言え、「2025年(ver1.07)時点バグ」で纏めた通りまぁまぁバグが残っていますし、とにかくこのゲームシステムの説明とかが不足してるし、クエストなど不親切な所が多いので不満が並ぶのも分かります。

ただ、余り古いサイトの情報ばかりみてあげて欲しい。
古い記事をみるとトロコン出来なかったとかあるので昔の状態ではそうとうヤバそうではありますが少なからず、現行のバグは基本的に進行不能とかはなく、普通にプレイする分には大丈夫です。
感想を検索すると古い情報が多いので進行不能にはならないはず・・・とだけ。

ライトユーザーは強制されなければ理解しようとしない

さて、LOMでも思ったけれど強制的じゃなければゲームシステムなんてたぶん理解しようと思わない。かなりチュートリアルを念入りにやらないと無理だし、なんならチュートリアルを入れても強制的(それを理解しなければ先に進めない)じゃなければ理解しない。
LOMはやりたい人だけやればいいシステム部分だけど、AAのフォーカスシステムは理解しなくても一先ずなんとかなるせいで難易度がノーマルでもかなり難易度が高めに感じる。
二周目ではシステムの理解が深まった事でノーマルだとほぼ楽に進める様になったが、そもそもライトユーザー・・・というか一般ユーザーはゲームを二周しない。

ジョブについて説明不足

このゲームの良くない所としてはまず第一にジョブのやり直しがきかないのに全然説明がない所。普通に進めるとどのジョブがどんなアビリティを使えるかすら分からない。

これを補足するとゲーム上全く説明がないのだが、「日誌」>「ジョブ」からジョブの覚えるアビリティなどを事前に確認出来る。不親切すぎる!!教えてよそれ!!!気づいたのだいぶ後だよ!!


色々あったがゲーム自体は面白い

さて、色々悪い所を先に記述したがトータルでは面白かったのでその部分について

アクションパート、謎解きパート、ショートカット

この手のアクションゲームパートはいかにシステムを悪よ・・・げふん。
いかにシステムを上手く使うかが攻略として面白い。
例えば「みずがめ座神殿」などはどこのサイトも滑空を取得しないと攻略出来ないと書いているが、彫像転移でもクリア出来る。

こういった攻略を考えるのも面白い。
更に滑空延長を使えば本来強制される「やぎ座神殿」の4つか5つほど攻略フロアをスキップ出来る

滑空延長や多段ジャンプ等は「特殊操作テクニック」にまとめてあるのでそちらを見てもらうとしてこういった事が出来るのも面白かった。
特殊操作テクニックを使わなくてもクリア出来るのだが、アクションゲームが苦手な方やパズルが苦手な人には感想を見るに一部では不評。ゲームを作るのって難しいね。
ヴァルキリープロファイルとかが面白い人にはおそらくアクションパートも面白いと思う。

戦闘システム、フォーカスシステム、属性システム

二周目ではかなり上手く使えて面白かった。
弱点を突いてフォーカスポイント(以降:FP)を貯めてそれを使って大ダメージを与える。
属性が重要なのでヒューズやウィークネスが非常に役に立つ。

戦闘の根幹をなすシステムなのだが、色々な感想をみると戦闘メンバーが(主にダグマに)固定されるみたいな事が書いてあって、シャーマンやハンターの弱点付与や味方の属性変更をほぼ理解してない故の問題に感じました。
先に述べた通り、ジョブの説明不足が招いてる問題に感じる。
シャーマンもハンターも・・・というかどのジョブも必ず二人だれかが取得できるので固定しなくても戦えるシステムになっている。

ただまぁこれも一周回ってケイディンの【ハンターのアビリティ「ウィークネス(敵の弱点上書き)」+アサシンのサポートアビリティ「波及(異常全体化)」】が便利でアルパジョでウィークネスとってもなぁ・・・という一歩先を行ったメンバー固定になる可能性はある。

「ウィークネス+波及」を紹介してる所もまぁまぁあるが、ウィークネスは大半のボスには効かないのでほぼ雑魚戦用であり、やり込んだ結果実は雑魚戦用ならもっと良いアビリティが存在する。それは組み合わせなのでジョブの方で紹介します。

ジョブシステム、アセンションツリー

「ウィークネス+波及」の様にジョブを上手く選択すればかなり戦闘が有利になる
ジョブの組み方によってかなり色々な事が出来て面白かった。

ただ・・・頭に述べている通り戦闘システムと関連して根幹をなすシステムなのに説明不足すぎる。
大半のプレーヤーは手探り過ぎて上手くアビリティを使えていない。
先ほど述べた通り、シャーマンとハンターが上手く使えてないライトユーザーが殆ど。
更に言えば、アビリティ説明も結構嘘が多いので困る。

例えば時間術師のリブートの説明は「ターゲットを戦闘開始時の状態に戻す」だがこれは全くの嘘で、正しくは「全てのバフを消し去りバフカウンタをリセット、HPMPを最大値に回復」する。

例えば戦闘開始時にHPが1でMP0であっても、リブートをするとHPとMPを全回復する。
つまり、戦闘開始時のキャラの状態なんて保存しておらず、単純に最大HPと最大MP分の回復をする。
更に言えば、ガーディアンの転移でHPとMPで入れ替えてる状態でリブートすると元々HPとMPになる。この処理の見る限り本当に単純に通常時の最大HPと最大MPを現在のHP欄とMP欄に代入してるだけなのです。
サイトの方では書いてませんがバフカウンタリセットがこれまた特殊な利便性をはらんでいるのですがまぁその利便性はゲームをつまらなくするので省略。

まぁとにかく、リブートが超便利な神アビリティなわけです。

次にガーディアンの「累積」です。これは3ターンダメージを保留し4ターン目開始時にまとめてダメージを受けるアビリティです。
これだけ見るとそうなんだー程度なのですが、この累積は上書きできます
これを理解してるかどうかで価値が全く違います。
つまり、「1:累積発動ターン」「2:自由行動ターン」「3:再累積ターン」と繰り返す事で累積を行うキャラクターは一切のダメージを受けません。
戦闘終了まで累積をした場合、ダメージの借金を踏み倒します
ただしジョブの所で説明している通りターン処理は自分にターンが来た時に消費されます。
再行動系のサポートアビリティを付けてるとターン調整に失敗します。

更にこの累積は即死を無効化します
どういうことかというと即死条件スキルのデメリットを無効化します
1つ目は鉄壁です。本来は味方の全ダメージを無効化して自身がそのダメージを受けて受けきっても即死するという使い勝手悪いアビリティなのですが累積があると話が変わってきます。
自身もダメージを受けないし即死も無効。その上味方のダメージは0になる。最強防御になります。
2つ目は戦士の犠牲。自身の死亡を無効化しつつ、味方一人復活。
3つ目は戦士の殉職。自身の死亡を無効化しつつ、味方全員復活。
4つ目は黒騎士のディスパース。
自身を指定すれば死亡する事なく味方全員のHPMPを回復できます
5つ目は黒騎士の消耗。自身の即死を無効にしつつ、敵を100%即死

そうです。サイト側で神アビリティ認定していますが黒騎士の「消耗」は自身を即死させて即死免疫のない敵を必ず即死させます
即死アビリティはどれも確率(アサシンの暗殺や時間術師の死後の世界など)なのにこの消耗だけは確率の記載がない通り必ず敵を即死させます。
つまり、クレスで累積を行い消耗を使えばノーリスクで敵を100%即死出来るという事です。


続けて紹介するのはハーネッサーのオムニキャストこのアビリティは対象を指定するアビリティの対象を全体化します(ランダム対象や自身固定アビリティは不可)

指定さえできるアビリティなら全体化するというのが超絶便利効果で、少し前に「ウィークネス+波及」よりいい方法があると記載しましたが、それは「オムニキャスト+消耗」です。
効果は全ての敵を100%即死です。超ぶっ壊れアビリティの完成です。

このゲームにおける全ての通常戦闘、バトルチャレンジも含めボス以外なら全ての敵を1ターンで即死可能です。属性も何も関係ありません。

バトルチャレンジはAGIさえちゃんと確保できればレベルが低くてもボスを除き各チャレンジが1・2分で終わります。

属性の強弱やFPで大ダメージの概念はどうした・・・はさておき。
とまぁ、こんな感じにシステムを理解するほど色々やり込みが出来るわけです。


ミニゲーム:J-STER、シューティング

J-STERはまぁまぁ楽しめました。
トークン値の暴力すぎるという意見もあるけどそれはFF8やFF9のカードゲームもそうなのでとくには気になりませんでした。
基本的にトークン値前後±1か2くらいだとよく考えて返す必要があり面白かったです。
トークンコンバーターがFF8のカード変化とかの位置なのですがXPもSPも変換量が微妙で不要カード減らしてランダム戦を楽にする以外の使い道がないのが勿体ない。

あと、致命的ではないもののカードの強さでフィルタして交換するとフィルタのタブに居るのに内容だけが全体リストに戻されるという超面倒臭いバグが残ったままです。
他にもカードゲームする時に出すカードを選択する画面で位置をある程度移動した状態でカードの強さでフィルタしてタブを動かすと選択してるトークンと画面左の選択画面の位置情報がずれるという絶対に気付くだろうバグも残ったままです。

トークンの選択周りはバグが多すぎる。
放置されたまま直りませんし、たぶん致命的な問題にならないので今後も直らない気がします。

ランダム戦で全体からランダムなのも地獄な所で、もう少し調整するべきだったんじゃないかなぁ・・・。敵も弱いカードをランダムで選ぶならまだしも敵だけ強く不満が溜まるし、トークンを減らしたくてもトークンコンバーターが得られる所までチャプターが進まないといけない。
さらに追い打ちで先ほど述べたバグ、トークンの強さでフィルターを掛けて交換しても交換するたびに全体リストになって一番上に戻るバグによって交換もかなり時間が掛かってしまう。
トークン選択周りはデバッグしてないとしか思えない。

シューティングゲーム

ミニゲームとしてダメとは言わないのですが、本編で強制するならもう少し緩めで良かったかもしれない。
キャラの当たり判定がデカすぎ。敵の攻撃当たり判定も広い。
属性を使いたかったのも分かりますが、斑鳩でも2属性なのに8種類も属性あったら切り替えが間に合いません。

これに至ってはプレイすると移動してしまって連続プレイも面倒。

2025年5月5日月曜日

JSON.parse(JSON.stringify())で深いコピーをしないでという話

javascriptで深いコピーをする方法について

前回は浅いコピーの話をしたので今回は深いコピーの話

■JSON.parse(JSON.stringify(配列))

let originalarr= Array(36).fill().map(() => Array(8).fill(0));
let copiedarr= JSON.parse(JSON.stringify(originalarr));
copiedarr[0][0] = 1; console.log(originalarr[0][0]); // 出力: 0 (影響を受けない)

 この方法は有名で簡潔に深いコピーを実現できます
ただし、JSON.parse(JSON.stringify(配列)) は、手軽に深いコピーを実現できる反面、undefined、関数、Date オブジェクト、RegExp オブジェクト、Map、Set、循環参照などの特定のデータ型を正しく扱えないという大きな制約があります。


この方法でコピーするのがおすすめのパターン

以下のような比較的単純な構造のオブジェクトや配列を深いコピーする場合には手軽で有効です。
・プリミティブな値 (number, string, boolean, null) のみを含む配列やオブジェクト
・プレーンな JavaScript オブジェクト (POJO - Plain Old JavaScript Object) で、上記のプリミティブな値と基本的なオブジェクト、配列のみで構成されている場合


そもそもこれは何をやっているのか

使い方と使う場面を知ったら次は仕組みの理解。

■そもそもJSONってなに?
JSON 「JavaScript Object Notation」の略です。
JavaScript のオブジェクトを軽量なテキスト形式で表現するための標準的なデータ形式です。その名前が示す通り、JavaScript のオブジェクトの記法をベースにしていますが、言語に依存しないため、様々なプログラミング言語やシステム間でデータを交換する際に広く利用されています。


JSON.stringify() メソッド

まずは内部から理解しましょう。
構文は以下パターン。
JSON.stringify(value)
JSON.stringify(value, replacer)
JSON.stringify(value, replacer, space)

「JSON 文字列に変換する」というのが根本的な機能です。
例外のTypeErrorが発生するのは「value が循環参照を含む場合・長整数値に遭遇した場合」です。
上記に記載している通り色々な言語間のデータをやり取りなどが可能な文字列形式にするという事です。ですので、JSON.stringify(配列)を行うとstringを出力します。 


JSON.parse() 静的メソッド

JSON 文字列を JavaScript のオブジェクトや配列に変換します。
つまり、逆の事を行います。JSON 文字列を解析して、元の配列やオブジェクトにします。


■つまり?

つまり、「JSON.parse(JSON.stringify(配列))」というのは配列を一旦文字列にしてそれを再度配列に戻すという事を行っています。
一旦文字列になる事で配列ではなくなるので新たに再変換されて出力される時に新しい配列になるので多次元配列がディープコピーできるのです。
また、この処理を行う時に文字列に上手く変換できない値達は欠損するという事です。


■素直に配列でコピーする

関数化する

function deepCopyMultiDimensionalArray(arr) {
if (!Array.isArray(arr)) {
return arr; // 配列でなければそのまま返す (プリミティブな値やオブジェクトなど)
} const newArray = [];
for (let i = 0; i < arr.length; i++) {
newArray[i] = deepCopyMultiDimensionalArray(arr[i]); // 要素が配列なら再帰的にコピー
}
return newArray;
} //ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー // 使用例 let originalMulti = [1, [2, [3, 4]], 5, { a: 6 }];
let copiedMulti = deepCopyMultiDimensionalArray(originalMulti); copiedMulti[1][1][0] = 99;
copiedMulti[3].a = 100; console.log("オリジナル:", originalMulti);
// 出力: オリジナル: [ 1, [ 2, [ 3, 4 ] ], 5, { a: 6 } ]
console.log("コピー:", copiedMulti);
// 出力: コピー: [ 1, [ 2, [ 99, 4 ] ], 5, { a: 100 } ] let originalWithObject = [1, [2, { b: 7 }]];
let copiedWithObject = deepCopyMultiDimensionalArray(originalWithObject); copiedWithObject[1][1].b = 101;
console.log("オリジナル (オブジェクトあり):", originalWithObject);
// 出力: オリジナル (オブジェクトあり): [ 1, [ 2, { b: 7 } ] ]
console.log("コピー (オブジェクトあり):", copiedWithObject);
// 出力: コピー (オブジェクトあり): [ 1, [ 2, { b: 101 } ] ]

こんな感じ


■二次元配列のコピーでいい場合

まぁでも多次元配列なんてあんまり使う物でもないと思いますので(多次元で深くネストすると人間も何処に何があるか分かり難くなるので)、大半は二次元配列とかが多い(セルや座標)と思います。

その程度であれば1次配列を[...配列]で展開して回すだけでも十分です。
function deepcopy2d(oa) {return oa.map(ar=>[...ar]);}


■structuredClone(obj)

実はもっと簡単な方法があります。

let original = {
  date: new Date(),
  regex: /abc/g,
  map: new Map([['key', 'value']]),
  set: new Set([1, 2, 3]),
  arrayBuffer: new ArrayBuffer(8),
  circular: {}
}; original.circular.self = original;
let cloned = structuredClone(original);
console.log(cloned.date); // 元の Date オブジェクトとは異なる新しい Date オブジェクト console.log(cloned.regex); // 元の RegExp オブジェクトとは異なる新しい RegExp オブジェクト
console.log(cloned.map);  // 元の Map オブジェクトとは異なる新しい Map オブジェクト
console.log(cloned.set);  // 元の Set オブジェクトとは異なる新しい Set オブジェクト
console.log(cloned.arrayBuffer); // 元の ArrayBuffer とは異なる新しい ArrayBuffer
console.log(cloned.circular.self === cloned); // true (循環参照も正しくコピー)
cloned.date.setTime(0);
console.log(original.date.getTime() === cloned.date.getTime()); // false (深いコピー)

こんな感じです。
見ての通り、より多くのデータ型をサポート: Date オブジェクト、RegExp オブジェクト、Map、Set、ArrayBuffer、DataView、ImageBitmap、ImageData など、JSON.stringify() では正しく扱えなかった多くの型を適切にコピーできます。

循環参照を処理: structuredClone() は循環参照のあるオブジェクトグラフも正しくコピーできます。内部的にコピー済みのオブジェクトを追跡し、無限ループを防ぎます。

Transferable オブジェクトの転送: ArrayBuffer などの Transferable オブジェクトは、コピーではなく所有権が移動するため、より効率的なデータの受け渡しが可能です(元のオブジェクトでは使用できなくなります)。

より自然なコピー: JSON.stringify() のように一旦文字列にシリアライズするステップがないため、より直接的で自然なコピー処理が行われます。


しかもwindow.のグローバル関数なのでこれでオールOK!
実装されたのは2020年頃でnodeも2021年頃実装しています。