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年頃実装しています。

2025年4月28日月曜日

ランドメイク&全イベント統合シミュレーターを作ろうかなって話

「 Hey!Scripting Cat!全イベントのシミュレーションもしたいよ!」

HAHAHA!なかなか無理をいいますね。この令和の時代に需要が何処にあるんですか。
ひとまずは「ランドメイクシミュレーター」は作ってあるからそれを使ってね
現状でもAFイベントはセットになってるからプレイ時に可能な配置をシミュレーションできるよ。
とは言え確かにあそこまでやるならもう全部のイベントの消滅条件とかまで管理しちゃえばいいんじゃない?という気もする。あと連続(アンドゥ・リドゥ)描画をした時にちょっと遅いのでそこを解消したいですね。

ランドメイク&全イベント管理を考える

まずは現在のランドメイクシミュレーターの機能と追加したい機能

■現在のランドメイクシミュレーターの機能
・全体マップを選択する
・ランドメイクマップを生成6x6(地形と土地マナ)
・ポスト入手→配置→積み木入手→配置
積み木まではイベントがない為配置と同時にAF取得
以下ループ
┏┓
┃┃
┃■イベント開始
┃┃
┃■イベント終了
┃┃
┃■AF入手
┃┃
┗┛
・常に工程を復元用ログに保存

■現在サブ機能
・全体マップの表示非表示
・全体マップ選択範囲クリック選択
・全体マップ選択範囲矢印で操作
・推奨エリアの直接選択
・配置とイベント両方のログ
・配置とイベント両方のアンドゥリドゥ

ーーーーーーーーーーー

■初期化
・Fマップ生成
・Lマップ生成
・リスナー設置
 ┗Fマップ 25x25
 ┗Lマップ 6x6
 ┗ボタン類 12個前後
 ┗AF 26 
 ┗イベント 約68x2
 ┗チェック 2程度

■操作
Fマップ選択>インデックス変更判定>変更時再初期化
繰り返し
 ┗イベント開始or終了をチェック
 ┗AFを選択
 ┗AFを設置
 ┗イベントの取得

■イベント
・開始または終了チェック時にAF取得判定
・終了チェックでランドロック解除
・履歴加算

■AF設置時
・AF選択時 他の操作を停止、AF情報表示、配置可能エリア光らせる
・(キャンセル時:各操作停止解除、AF情報を閉じる、配置エリアを戻す)
・AF配置 AFがランドになる際に周囲4つのマナ合計÷2を加算して配置、周囲4つのランドはAFのマナを直加算
・配置終了時(各操作停止解除、AF情報を閉じる、配置エリアを戻す)
・設置終了時イベントを解放
・設置終了時履歴加算
 
■履歴操作
・各操作でイベント操作を記憶
・アンドゥ、肯定を1つ戻す
・リドゥ、履歴範囲内で履歴を一つ進める
・履歴操作中に履歴加算がある場合現在の位置を履歴の末端に変更

■状態の保存復元
SaveURLに現在の状態をパラメータで保存
URLにアクセスするだけで復元可能

鶏が先か卵が先か

・イベントが終了するとAF取得(ない時もある)
・AFを設置してランド生成でイベント生成

ランドが無いとイベントがそもそも発生しないのでAFが先
ポストを配置→ホーム生成→ナンバリング0イベント草人→AF入手

2025年4月27日日曜日

onclickを廃止した話

「Hey!Scripting Cat!モジュール読み込みにしたらボタンのonclickで関数が実行できなった!どうすればいい?」

なるほど良くある話ですね。HTML の onclick 属性は、イベント発生時にグローバルスコープで評価されるため、モジュールスコープ内で定義された関数を直接参照しようとすると、その関数が見つからず実行できません。

じゃあonclickが使えなくなるのかと言えばそうではありません。

モジュール読み込みでボタンから関数を実行する方法

web上のjavascriptにはグローバル領域に直接スクリプトを読み込む方法とモジュール(module)で読み込む方法の主に二種類があります。
今回の目標はボタンを押して関数を実行する事が目的です。
工程は2つあります「ボタンを押す」「関数実行」。これを実現する方法は2つ存在します。


1.window.領域に関数を持ってくる(非推奨)

今までの感覚を変更しないのであれば一番簡単です。
変更するのは関数側です。

■js
function moduleFunction() {console.log('関数実行');}
window.moduleFunction = moduleFunction;

これだけです。htmlも変更不要で簡単ですね。
現在onclickが動かないというのはつまりグローバル領域に関数が居ない為です。
じゃあ何処にいるのかというとモジュールスコープ領域に居ます。

つまり、window.関数名に現在の関数名を代入すればモジュール内の関数をhtml側からonclickで実行できます。折角モジュール読み込みするのであれば(スコープを気にし始めたのなら)一応非推奨ではありますが他の関数等はモジュールにいるので今までの通りの方法「<script src="script.js"></script>」でただjsを読み込むだけより全然良いと思います。
onclickが出来ないならレガシーな通常読み込みに戻そうと考えてしまうより一旦はこの方法で良いと思います。モダンへの入り口は広い方がいい。


モジュールに移行する理由

個人や小規模なサイトであれば必ずしもモジュール読み込みの形式にする必要はありません。ただ、そもそもモジュールで読み込む方法が生まれたという事は理由が、つまり利点があるから実装されました。

最大の理由はおそらく名前の衝突です。グローバルスコープに全ての変数や関数があると衝突して正しく動かなくなる可能性高くなります。

<script type="module" ></script>で直接モジュール領域を作ったり
<script type="module" src="script.js"></script>モジュール読み込みを行うとグローバルスコープを侵さずに衝突しない領域を作ることができます。

次に個々に領域を分断した事でそれをつなぐインポートとエクスポートが便利だからです。
これも結局はスコープの制御がしやすいという話に帰結します。


2.addEventListenerでボタン待ちをする

さて、次はよりモダンな方法です。
まずはボタンにidを付けます(他にも方法はある後述)。
■html
<button id="mybutton">ボタン</button>
■js
function moduleFunction() {console.log('関数実行');}
document.getElementById("mybutton").addEventListener('click', moduleFunction)

これだけです。それほど難しくはなりません。


モジュールのトップレベルのコードは、モジュールが最初に読み込まれ、評価される際に一度だけ実行されます。このタイミングで addEventListener を呼び出すことで、ボタン要素にクリックイベントのリスナーが登録され、ボタンがクリックされるのを待機する状態になります。

これによってボタンが押されるとidで対象のボタンが特定されクリックが実行されたことでmoduleFunctionを実行できます。
少し注意点としては「addEventListener('click', moduleFunction)」の関数名部分(この場合「moduleFunction」)には「()」をつけてはいけません。
関数登録に括弧が付くとその位置で即時関数が実行されてしまいます。

※ボタンを特定する他の方法
id 以外に、nameやclassやタグ名で要素を取得できます。
・class(例: document.querySelector('.mybutton'))
・タグ(例: document.querySelectorAll('button'))


引数を渡したい場合

括弧が無ければじゃあ引数渡せないのかというとそうではなく
document.getElementById("mybutton").addEventListener('click', ()=>{moduleFunction(1)})
とすれば引数を渡す事が出来ます。
これでonclickとはさよならできますね。

関数に飛ばさず直接処理する場合

document.getElementById("mybutton").addEventListener('click', ()=>{ここに処理を記述する})

こうするだけでOK。
さらっと流してるアロー関数部分は前の記事を参照してください。(https://blackstraycatreboot.blogspot.com/2025/04/blog-post.html)
要するに先ほどアロー関数に関数名を記述してるのは無名関数にする事で処理領域を確保して、この領域で改めて関数にキックしてたわけです。
ですから、ここに関数を記述せずに処理を記述すればOK!



余談

ちなみに「addEventListener」は実行時にイベントオブジェクトを渡します。
この場合、'click' イベントに対するリスナーなので、event は クリックイベント に関する情報を保持します。
今回のclick イベントの場合はeventの型はMouseEvent オブジェクト。
MouseEvent は Event クラスの派生型で、クリック固有のプロパティ(例: マウス座標)を含みます。


function moduleFunction(arg, event) {
  console.log(`引数: ${arg}`);
  // イベントオブジェクトの詳細を確認
  console.log(`クリックされた要素:`, event.target);
  console.log('イベントオブジェクト:', event);
  console.log('type:', event.type); // "click"
  console.log('target:', event.target); // <button id="mybutton">...
  console.log('clientX/Y:', event.clientX, event.clientY); // 例: 150, 200
  console.log('ctrlKey:', event.ctrlKey); // 例: false
}
document.getElementById('mybutton').addEventListener('click', (event) => {
  moduleFunction(1, event); // event オブジェクトを渡す
});

こういった情報取得も可能です。
ただまぁ今回は「ボタンを押す」「関数実行」が主題なので今回はこれで終了。

2025年4月24日木曜日

location.search/href/hash:urlから複数のデータを取得する方法

「Hey,Scripting Cat!URLパラメータの取得の仕方を教えて!」


はい、urlからパラメータを取得する方法はいくつかあります
location.href フル URL(プロトコル、ホスト、パス、クエリ、ハッシュ
location.search 「?」以降の全てクエリ名を取得
location.hash 「#」以降の値を取得

URLの指定方法と取り出し方

http://url/a.htm?q=z3z7z9z9z9z9z0z9z8z34z0z9z9z14z1#1z2z3
上記の場合取得できるのは

【href】http://url/a.htm?q=z3z7z9z9z9z9z0z9z8z34z0z9z9z14z1#1z2z3
【search】?q=z3z7z9z9z9z9z0z9z8z34z0z9z9z14z1
【hash】#1z2z3

ハッシュは必ず最後にする必要があります。ハッシュの後にクエリ―パラメータを入れるとハッシュの内容として処理される為クエリを取得出来ません
ちなみに、処理でもハッシュ(hash)ですが、フラグメントと呼んだりもします。

クエリとハッシュの扱い方の違い

さて、取り出し方の前にそもそものurlパラメータに指定方法について
既に上に書いてはいますが2種類あります。

■クエリ(?クエリ名=aaa&クエリ名=bbb)

基本的にはクエリ指定は別のページとして扱います
つまりサーバーや検索エンジンは別々のURL(コンテンツが違うものとして)判定しようとします。
もっと言うとクエリが指定されているのにページの内容が全く同じか少ししか変化しない場合グーグル等検索エンジンなどからは複数のページで同じ内容のコンテンツが提供されていると判断されてランキング低下やインデックス優先度低下のリスクが上がります。
phpなのでページ内容が直接変更されるものは良いですが、javascriptのみで内容を変更するような場合はjavascriptが無い状態で基本ページを判定されるので注意が必要です(同じページなのにurlパラメータ違いで全てが別ページ扱いになり、重複ページのペナルティの可能性が高まる)
他には、次に説明するハッシュと違いリアルタイムでシームレスなURL変更は出来ません。
「location.search = "?q=ppp"」みたいにするとページをリロードをしてパラメータが更新されます。

■ハッシュ(#aaa)

クエリとは逆に同じページ内のリンクとして扱います
アンカーなどでページを上下させる方法などが一般的です。
ハッシュを使ってコンテンツをタブなどで切り替えるなどもありです。
ブラウザの履歴などには一応urlの履歴が残りますが、クライアント側(ブラウザ側)で処理している為、サーバー側からはハッシュの遷移が取得出来ません。
そのためページ遷移の判定を得るためにクエリを指定するパターンも結構あります。
しかし先に説明した通りグーグルなどはクエリ違いの同じページを嫌います。
また、ハッシュはページをリロードせずにリアルタイムでURLハッシュを変更可能です。

■URLの正規化(canonical)

canonicalタグ(カノニカルタグ)を使う事で(基本的にはパラメータなしを指定)パラメータの有無についてこのページが(コンテンツが重複してるページの)正規のページです。と、知らせるタグがあります。
ただし、完全な指定をするというものではないのでurl違いの同じ内容のページはない方が良いです。検索エンジンに知らせるだけで検索エンジンが申告通り判定するかは別問題)。


データの取り出し方

まぁなんも考えず上記のurlをの指定方法をみれば想像つくと思いますが「str.split(”z”)」すれば良いです。もちろん取り出したいデータが数値じゃなくアルファベットなら固定数値でsplitすれば良いです。
ちなみにhrefからパラメータを取り出す事も出来ますが、基本的にはsearch/hashで取り出しましょう。
そうしないとクエリを取り出したかったのにハッシュが取り込まれたりする可能性があります。

■パラメータの頭に区切りを入れる手法
ちなみに上記で区切りの頭にzを入れてますがこれは「q=z3z7z9z9z9z9z0」この「クエリ名=」が邪魔だからです。パラメータの一つ目[0]を必ず捨てる事でパラメータを直接splitして1番目から配列を回すだけで良い。必要なら0番目の要素を破棄も可能。
普通は「q=3z7z9z9z9z9z0」ですが、こうした場合は「=」以降で切り出しした後にsplitをするか配列後0番目の要素から要素[0].split("=")[1]とかでもこの要素0が空指定だった場合、要素[0].split("=")[1]ではエラーするので有無を判定してから切り出す必要がでたりします。
下記で推奨する正しいパラメータの抜き出し方もありますがクエリ名を知ってる必要があります(普通は知ってる)。今回の方法であればクエリ名が何であっても0番目を捨てるので雑にデータを抜き出せます。


正しいURLパラメータと正しい値の取り出し方

正直手間なのですが正しい指定と取り出し方もあります。
ちゃんするならこうした方が良いです。
http://url/a.htm?q1=aaa&q2=bbb&q3=10&q4=30&q5=200#abcd

データの取り出し方

// 現在の URL のクエリ文字列を取得
const url = new URL(location.href);
// 例: http://url/a.htm?q1=aaa&q2=bbb&q3=10&q4=30&q5=200#abcd
// クエリパラメータを取得
const params = new URLSearchParams(url.search);
// 各パラメータの値を取得
const q1 = params.get("q1"); // "aaa"
const q2 = params.get("q2"); // "bbb"
const q3 = params.get("q3"); // 10 (数値)
const q4 = params.get("q4"); // 30 (数値)
const q5 = params.get("q5"); // 200 (数値)
// フラグメントを取得
const fragment = url.hash.slice(1); // "abcd" (# を除去)
// 確認
console.log({ q1, q2, q3, q4, q5, fragment });

厳密な指定や取得が必要な場合はこうしてください。SEO的にもこちらが正しいです。
ただ、やはり少し手間なんですよね。20個以上パラメータが必要だったりするときに全てクエリ名を入れる必要があるし。

ちなみにクエリは重複指定も可能です
http://url/a.htm?q1=aaa&q1=bbb
みたいなパターンです。
const params = new URLSearchParams(url.search);
const q1 = params.get("q1"); // "aaa"
これだと実は1つ目しか取得出来ません。二つ目以降は無視されます。
勿論対処方法もあります。

const q1 = params.getAll("q1"); // ["aaa","bbb"]
手動で切り出す方法はいくらでもありますが基本的にはこの取り出し方が一番きれいだと思います。
なお、getAll は常に配列を返します。1つの中身でも配列です["要素値"]、無ければ[](空配列)です。

非推奨な話

一番最初に説明している通り実は「location.search」は「「?」以降の全てクエリ名を取得します。なので実は「?クエリ名=」って記述しなくてもパラメータを指定出来るし、urlからパラメータを取り出す事も可能です。
例えば上部ではでは「z」で区切っていたけど数字もアルファベットも使い切りたい時に区切り文字がない。そんな時に実はURLに「a.html?aaa?BBB?ddd?123?saf?777777?ss」みたいなURLを作っても別にページにアクセス出来なくなったりはしない。
さらに「location.search.split("?")」みたいな形でデータを取り出す事も可能。

まぁでも「やろうと思えば出来る」だけであって勿論非推奨です。
当然ながら一般的なURLパラメータ以外は検索エンジンにも嫌がられます。