SVX日記
2025-05-01(Thu) デスクトップでF1のラップタイム計測ごっこ
ラップタイムの計測をしたいなぁ、ということで、実装してみることにした。
計測には、いわゆる「当たり判定」が必要になる。以前に、2Dシューティングにおける「箱同士」の当たり判定は書いたことがあるのだが、同じ2Dでも任意の角度に回転する物体同士の当たり判定には別の方法が必要になる。
確かベクトル演算の手法が使えたような。ちょっと前に読んだ線形代数の本を引っ張り出してきて調べる。そうだった、外積だ。座標pが、座標a, bを通る直線のどちら側に位置するのかを判定できる。今回は、コース上のラップ計測ラインの通過を判定したいだけだから、それ一発で済む。まずは、理解するためのサンプルコードを書く。
#!/usr/bin/env ruby
# 座標 p が、座標 a, b を通る直線のどちら側に位置するかを調べる
def vec(i, j) # 座標 i->j をベクトル化
{ :x => j[:x] - i[:x],
:y => j[:y] - i[:y] }
end
def vcross(i, j) # ベクトル i, j の外積を求める
i[:x] * j[:y] - i[:y] * j[:x]
end
a = { :x => 5, :y => 3 } # 座標 a
b = { :x => 15, :y => 10 } # 座標 b
ab = vec(a, b) # 直線ベクトル a->b
20.times {|y|
20.times {|x|
p = { :x => x, :y => y } # 座標 p
ap = vec(a, p)
print(vcross(ab, ap) < 0 ? ' -' : ' +')
}
puts
}
+ - - - - - - - - - - - - - - - - - - -
+ + + - - - - - - - - - - - - - - - - -
+ + + + - - - - - - - - - - - - - - - -
+ + + + + a - - - - - - - - - - - - - -
+ + + + + + + - - - - - - - - - - - - -
+ + + + + + + + - - - - - - - - - - - -
+ + + + + + + + + + - - - - - - - - - -
+ + + + + + + + + + + - - - - - - - - -
+ + + + + + + + + + + + + - - - - - - -
+ + + + + + + + + + + + + + - - - - - -
+ + + + + + + + + + + + + + + b - - - -
+ + + + + + + + + + + + + + + + + - - -
+ + + + + + + + + + + + + + + + + + - -
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
判定処理は毎フレームしこたま繰り返すので、計算を軽くしつつ、オブジェクト化して書き直す。a→bベクトルは一度計算するだけでいいので、TimingLineクラスの初期化時に行うようにし、判定をover?メソッドにまとめる。より、直感的な記述になりつつ、同じ実行結果が得られる。
#!/usr/bin/env ruby
# 座標 p が、座標 a, b を通る直線のどちら側に位置するかを調べる
class TimingLine
def initialize(a, b)
@a = a
@ab = { :x => b[:x] - a[:x],
:y => b[:y] - a[:y] }
end
def over?(p)
@ab[:x] * (p[:y] - @a[:y]) - @ab[:y] * (p[:x] - @a[:x]) < 0
end
end
a = { :x => 5, :y => 3 } # 座標 a
b = { :x => 15, :y => 10 } # 座標 b
tline = TimingLine.new(a, b)
20.times {|y|
20.times {|x|
p = { :x => x, :y => y } # 座標 p
print(tline.over?(p) ? ' -' : ' +')
}
puts
}
思索しつつ試作したコードを、CoffeeScriptで書かれたゲームのコードに落とし込む。それなりにオブジェクト化してあるので、どのオブジェクトに、どう落とし込むか、非常に考えどころである。とりあえず、地球上の位置(Wpos)クラス、コース(Course)クラス、メインプログラムに落とし込んでみた。
$ diff ../topdrivin.org/wpos.bean wpos.bean
85a86,92
> to_vec: (wpos) -> # 終点座標を渡し、ベクトル情報を生成
> @vec_x = wpos.wpx - @wpx
> @vec_y = wpos.wpy - @wpy
>
> vec_over: (wpos) -> # 座標を渡し、ベクトル線を超えたか返す
> @vec_x * (wpos.wpy - @wpy) - @vec_y * (wpos.wpx - @wpx) < 0
>
$ diff ../topdrivin.org/course.bean course.bean
19c19
< constructor: (_s, x, y, car) ->
---
> constructor: (_s, x, y, car, sectors) ->
21a22
> @sectors = sectors
54a56,58
> @sector = 0 # 開始セクタ(ラップタイム計測)
> @dir = _s['DIRECTOR']
>
80a85,91
> # ラップタイム描画
> @context.font = '24px sans-serif'
> @context.fillStyle = 'white'
> @context.textAlign = 'right'
> @context.fillText(@sectors[@sector]['name'], 100, 24)
> @context.fillText(@dir.tsc1000, 100, 48)
>
95a107,108
> if(@sectors[@sector]['vec'].vec_over(@car.wpos)) # セクタ計測ラインを超えた?
> @sector = (@sector + 1) % @sectors.length
$ diff ../topdrivin.org/sample_course_view.bean sample_course_view.bean
80a81,88
> sectors = [
> { l: [25.957223, -80.244210], r: [25.957362, -80.244212], name: 'dummy 05' },
> { l: [25.956204, -80.243633], r: [25.956089, -80.243657], name: 'sector 1' },
> { l: [25.958981, -80.229886], r: [25.958922, -80.229795], name: 'dummy 15' },
> { l: [25.960090, -80.230708], r: [25.960203, -80.230716], name: 'sector 2' },
> { l: [25.960523, -80.242873], r: [25.960578, -80.243020], name: 'dummy 25' },
> { l: [25.959922, -80.238718], r: [25.959788, -80.238811], name: 'finish line' },
> ]
83a92,96
> @tsc1000 = 0; @tsc1000inc = [17, 16, 17] # 1/1000時計を初期化
> for sector in sectors
> sector['vec'] = Wpos.deg(sector['l'][1], sector['l'][0])
> sector['vec'].to_vec(Wpos.deg(sector['r'][1], sector['r'][0]))
>
87c100
< @objs['COURSE'].push(new Course(@_s, 0, 0, mycar))
---
> @objs['COURSE'].push(new Course(@_s, 0, 0, mycar, sectors))
95a109
> @tsc1000 += @tsc1000inc[tsc % 3] # 1/1000時計を加算
マイアミサーキットの場合、各セクタの計測ラインが、いずれもUターンのようなコーナーの先にあるため、計測ラインの手前にダミーの計測ラインを設けている。そうしないと、各セクタの計測ラインを超える前に、超えたという判定になってしまうためである。いったんアッチに行ってからね、って感じ。
ちなみに、計測に使うタイムは、ゲームの固定FPS(1/60タイマ)に同期する仕様とした。ただし、1/60秒は割り切れない値なので、1000分の17, 16, 17を順に加算することで作り出している。これは逆に言うと、60FPSのゲームなので16/1000秒以下の計測粒度はない、ということなのだが、サウジアラビアの予選でポールのフェルスタッペン、ピアストリのタイム差は10/1000秒。それは、優に格ゲーの1フレーム以下の戦いだったってことである。マジかよ……。
それにしても、今回、最終的に追加したコードは意外なほど少ない。実は丸2日かかっているのだけれど。というのも、上述したように「どのオブジェクトに、どう落とし込むか」が非常に考えどころであり、楽しいところでもあるのである。「ラップタイム計測」をするのは誰(どのオブジェクト)であるべきか? 自車(MyCar)クラスか? 今回はコース(Course)クラスに追加したが、それで正しいのだろうか。今回は、ラップタイムの描画もコース(Course)クラスにやらせているが、ラップタイムの管理も含めて、ラップタイムクラスを新設するべきだし、それを駆動するのは自車(MyCar)クラスであるべきのようにも思える。
前にも書いたが、やっぱりプログラミングは盆栽だなぁ、と思う。処理をどこに足すべきか。それは、どの枝を伸ばすか、みたいなものなのではないか。間違ったら剪定して、また違う枝を伸ばしてみたり。
2025-05-15(Thu) 青タイヤ買い
以前なら、車検のタイミングをみながら、タイヤ屋に行くスケジュールを考えるトコなのだが、最近は通販で買って、近所のピットサービスで組み換えてもらうという選択肢がある。とはいえ、前回は自分で組み換えて、ヒドい目にあったのだが。
なので、事前に買っておくのもアリだ。と、いうのも、いま履いているファルケンのジークス(ZIEX)のZE914Fが気に入ってるんだよね。乗り心地に違和感はないし、回転方向の指定がないし、安物感はないのに9千円弱とやたらと安い。だいぶ前からカタログ落ちしてて、Amazonの扱いも終わっていると思ったのだが、いつの間にか復活しているのを発見したのだ。
2025-05-17(Sat) Emacsの中からツンデレAIに話を振ってみる
以前に「業務チャットの中からツンデレAIに話を振ってみる」として、自作のチャットツールにAIに話しかけられる機能を実装したのだが、ぼちぼち、Emacsの中からも話しかけたくなってきた。
自分は割とケチな方であるが、いくつかはサブスク契約をしている。そのうちのひとつが「池田直渡氏の『ぜんぶクルマが教えてくれる』」だ。クルマは好きなものの経済への興味は薄いのだが、どうにも記事が面白すぎるので有料会員になっている。月に1,000円と安くはないのだが……あ、カードの期限が切れるって? えー、更新……ん? カードのセキュリティコードを入れろって? 毎月の引き落としだから、それの登録が必要なのはわかるが、セキュリティコードってショップ側が保持してはいけないのではなかったっけ?
気になってググってみたが、思いついた検索ワードでは情報が引き出せなかった。あ。こういう時こそAIではないか? いや、そんなに期待はしてないけれども……でも、以前に「あのぉ、あれ、なんつうの? ゼロもの?」って訊いたら「あぁ、前日譚(プリクエル)ね」と、スパッと答えてくれたのだよなぁ。試してみよう……
Q: クレジットカードのセキュリティコードは、ショップ側が保持しないルールでしょうか?
定期購読の申込みの際などには、入力が求められますが、それは例外ですか?
A: とても良い質問です。
定期購読の申込み時にセキュリティコード(CVC)を入力させること自体はOKですが、やはり保持(保存)することはNGです。
定期購読の初回申込み時にセキュリティコードを入力 → その都度、決済代行会社でトークン化して保存
→ 加盟店側ではCVCを保持しないという形が正しいです。
個人的に、あまりAIは好きではないといいつつ「サンプルコード的なものを書かせると、AIはちゃんとしたものを出してくる」傾向も確認しているし、もう少し積極活用してもよい気がしてきた。そうなると、ブラウザから使うのがかったるい。毎度のログインが必要になるし、なぜか手元の環境だとパスワードが保持されずに手入力が必要だし……ここは、エディタ上からサッと使えるべきだ。
require 'libngs'
load 'ngs.config'
ngs = []; ngs << NGS.new(@configs, params)
job = nil
lina = @configs[:lina]
it = $stdin.read
prompt = (lina[:def] || 'あなたは「リナ」という名前で%sす。') % lina[:jobs][job] \
+ (lina[:ask] || '「%s」さんから%sと問いかけられました。') % [lina[:you] || 'ご主人', it.enbracket] \
+ (lina[:com] || 'それに対しての助言を%sお願いします。') % lina[:tones][0]
response = ngs[0].ask(prompt)
it.split(/\n/).each {|line|
puts('>> %s' % line)
}
puts
response.each_wline(lina[:wrap] || 78) {|line|
puts('> %s' % line)
}
Emacsからは「M-|」で呼んでもいいんだが「C-x l l」で呼べるようにキーバインドを追加した。lを被らせているのは「C-x l c」でコード書いて、とか「C-x l t」で翻訳して、とか、後に用途ごとに使い分けられるようにするためである。
(defun ask-lina (start end)
"ask lina"
(interactive "r")
(shell-command-on-region start end "lina" t t))
(define-prefix-command 'ctl-xl-map)
(define-key ctl-x-map (kbd "l") 'ctl-xl-map)
(define-key ctl-xl-map (kbd "l") 'ask-lina)
shell-command-on-region相当の動作って、viでもできる?
>> shell-command-on-region相当の動作って、viでもできる?
> べ、別にあんたのために教えてあげるわけじゃないんだから!でも、どうせ知りたいん
> でしょ?仕方ないから教えてあげるわ。
>
> viでは、`:!`コマンドを使えば、選択した範囲に対してシェルコマンドを実行すること
> ができるの。たとえば、ビジュアルモードで範囲を選んでから、`:!コマンド`って打つ
> と、選択したテキストにコマンドが適用されるのよ。ちゃんと覚えておきなさいよね!
>
> ああ、別に期待してるわけじゃないんだから、勘違いしないでよね!
考えてみれば「AIへの問い合わせ」は「ウェブ検索、閲覧、取捨選択」に置き換わるものとも言える。これまでエディタからブラウザを呼び出して検索するなんて考えもしなかったが、エディタからAIを呼び出すことができれば、検索が必要な状況をエディタ上のみで解決できてしまえるってことだ。悪くないよなぁ。
2025-05-25(Sun) 中古ボロードスターの注意点
先日、NDロードスターを中古を買う場合のチェックポイント、とかなんとかいう動画で、幌を開ける時に見えるフェルトのような部分が取り上げられていた。なんでも、これがすり減っている場合は、幌の開閉回数が多いのでダメ、なんだとか。
ふーん、と思いつつ自分のクルマを見ると、だいぶすり減っている。そりゃ、幌を開けないでコロがすことのが少ないんだから当然だ。別に売るつもりなんてないからどうでもいいのだが、中古車としてはダメなボロードスターということになるらしい。
2025-05-26(Mon) F1モナコグランプリ観戦ツアー、中止
先日発生した瞬間再燃F1ブームが続いている。なにしろ、カミさんまで強く興味を持ち始めたんで相乗効果が止まらない。各ドライバのキャラクタがわかってくるにつれて、毎レース盛り上がるのなんの。最初のフリー走行から全部を観る始末。当時ですらそこまで観てなかったぞ。そもそも観られなかったしね。
で、ついに伝統のモナコ。開発中の「TopDrivin'」でコースの下見(トンネルは上を走るので超絶難しい)を済ませ、各コーナの名前を記したコースの見取図のプリントアウトまで手元に用意して、イザ決勝ッ!
……て、なんなんですかぁ……これは……こういうのはアリなんですかね? 最初の角田の奇襲にヌカ喜びした後、通せんぼが起きてからはイライラしかない。結局、チームオーダって、アリなの? ナシなの? アリにしたって、こんなのまでアリなの? スポーツマンシップっつうか、なんつうか。唯一、オモシロチャレンジしたラッセルは怒られちゃうし。
いっそ来年はモナコに観に行くかッ! ……なんて、割とマジで考えていたのに、決勝を観ながらふたりしてドッチラケ状態。こんなレースなんてゼッテェ観に行くかよ。つまらない必勝法のあるクソゲー。セガのモナコGPのが100倍は面白いわ。アホか。
とはいえ、一晩明けてニュースを観ると、ドライバも、チームも、みんながしゃーなかったわなぁ、と苦笑いしているっぽい。たぶん、2回ストップを発案した主催者もそうなんだろうな。そう考えると、まぁ、誰が悪いというわけでもない。だからといってあれがレースだとは認めたくないけれど、まぁ、珍しい出し物を見せてもらいました、ってトコか。
2025-05-28(Wed) 勝手にモナコを改修、勝手にスペインでFP1
先日、自製の「TopDrivin'」でモナコを走れるようにしたのだが、問題はトンネルだ。航空写真では、コースはホテルの屋根にしか見えないので、トンネルの出口にたどり着くことができない。まぁ、そんなコースはモナコだけだし、別にマジメに考える必要はないのだが……データを用意して左右のトラックを描画するとか? それだと「TopDrivin'」の「ほぼコースデータを作る必要がない」という特長がスポイルされるんだよなぁ……などと考えていたら、名案が浮かんでしまった。
当該の部分だけ、地図データを表示するようにしてはどうか? 地図データならトンネルが描かれているはず……ん、まてよ? それなら「TopDrivin'」側に手を加えずとも、マップデータへのアクセスを減らすための、専用プロキシ側に実装できるのではないか。
qs = cgi.query_string
exctiles[qs] and qs = qs.gsub(/lyrs=s/, 'lyrs=m')
2025-05-29(Thu) NDロードスター 燃料タンクギリギリチャレンジ
……などとググると、走行可能距離が0kmになったので冷や汗かいてスグ給油した、というような記事はたくさん出てくるのだが……いや、まったくそれで正しいのだけれど、自分はだいぶ攻めたことがある。自慢じゃないが。
NDロードスターの整備書を見ると「燃料残量が約9リットル(フューエル・ゲージが2指針表示)のときに、燃料残量警告灯を点灯します」という記述がある。経験上、フューエル・ゲージのゼロ指針表示と、走行可能距離の0km表示は、ほぼ同時。なので、燃料残量警告灯が点灯するのは、そのタイミングよりだいぶ前だ。改めて整備書を見たら「走行可能距離は、燃料残量が6リットルになると0kmを表示」という記述も見つけた。
つまり、残9リットルでオレンジ点灯、残6リットルで0km表示、ということになる。これまで5, 6回、0km表示になってから給油しているが、決まって34リットル以上入る。カタログにはタンク容量は40リットルとあるので、残6リットルで0km表示という記述は正しそうだ。
以前に四国の室戸岬から、徳島市のすぐ手前の小松島市まで、国道55号(ただし南阿波サンライン経由)を走ったことがある。約120km。まるまる四国の右辺とも言えよう。別にギリギリチャレンジをしたかったわけではないが、給油をしそびれて、室戸岬から走り出してほどなく0kmになってしまった。
道路はほとんど信号のない快走路なのだが、ガソリンスタンドもほとんどない。たまにあっても営業してなかったりする。結局、0km表示になってから、100kmくらい走るハメになってしまった。さすがに最後の方はちょっとドキドキしたが。
給油は翌朝。38.02リットル入った。その時の平均燃費は19.20km/l。当該の区間は平均車速50km/h強と極めて快調に走ったので優に20km/lを超えているはず。100km弱を4リットルで走ったと考えると矛盾しない。残りは2リットル前後だったということになるが、そう考えると、まだ余裕があったのだな。
さらにもうひとつ。自分が取り付けているレーダーには、OBD2経由でCAN通信を読む機能があり、それでも燃料残量を知ることができるのだが、こっちは燃料タンク内のフロートが伝えてくる角度情報そのままのようだ。というのも、車の姿勢(主にピッチ)で上下数%をフラフラするからである。