IchigoJam で電車運転シミュレーション？

[IchigoJam](https://ichigojam.net/) で電車の運転のようなことができるプログラムを作ってみた。

※IchigoJamはjig.jpの登録商標です。

# 電車の加減速の観察

仕様決定の参考にするため、[TRAIN CREW](https://acty-soft.com/traincrew/) における電車の加減速の様子を観察した。
なお、観察は晴天時、なるべく勾配がゼロの状況で行った。

## 減速の観察

「[いい感じにブレーキをかけたい](https://github.com/mikecat/ii_kanzi_ni_brake_kaketai)」を用いてブレーキの状態と加速度の関係を観察した。
その結果、ブレーキの状態ごとに、速度にかかわらず加速度はだいたい一定のようであった。

いくつかの車種について観察を行った結果、ブレーキの状態と加速度 [$\textrm{m}/\textrm{s}^2$] の関係は以下のようになっていた。

|ブレーキ|5300系|4000系|3020系|
|:-:|--:|--:|--:|
|EB|-1.09|-1.13|-1.17|
|400kPa|-|-|-1.18|
|B7 / 350kPa|-|-1.09|-0.95|
|B6 / 300kPa|-1.05|-0.94|-0.79|
|B5 / 250kPa|-0.93|-0.81|-0.69|
|B4 / 200kPa|-0.78|-0.64|-0.58|
|B3 / 150kPa|-0.65|-0.49|-0.49|
|B2 / 100kPa|-0.48|-0.34|-0.35|
|B1 / 50kPa|-0.31|-0.17|-0.33|
|N / ユルメ|-0.08|-0.06|-0.06|

![ブレーキの状態と加速度の関係](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/c092bef0-d60a-ea3f-d310-850b643524ff.png)

多少の前後はあるものの、ブレーキの段数を上げると加速度も直線的に変化していくことがわかる。

## 加速の観察

電車の加速の様子を観察すると、同じノッチでも速度によって加速度が変化する様子がみられた。
そこで、以下のコードを用いて時間と速度の関係を記録した。

```csharp:SpeedLog.cs
using System;
using System.Windows.Forms;
using TrainCrew;

class SpeedLog: Form
{
	public static void Main()
	{
		Application.Run(new SpeedLog());
	}

	public SpeedLog()
	{
		bool trainCrewActive = false;
		this.Text = "SpeedLog";

		TrainCrewInput.Init();
		trainCrewActive = true;
		Timer timer = new Timer();
		timer.Interval = 15;
		timer.Tick += (s, e) => {
			if (!trainCrewActive) return;
			TrainState trainState = TrainCrewInput.GetTrainState();
			Console.WriteLine(
				"{0}\t{1}\t{2}\t{3}",
				trainState.NowTime.TotalSeconds,
				trainState.Pnotch,
				trainState.Bnotch,
				trainState.Speed
			);
		};
		timer.Start();
		FormClosed += (s, e) => {
			trainCrewActive = false;
			timer.Stop();
			TrainCrewInput.Dispose();
		};
	}
}
```

さらに、以下のコードで、記録したデータをもとに速度と加速度の関係を求めた。

```perl:get_accel.pl
#!/usr/bin/perl

use strict;
use warnings;

my $prev_time = 0;
my $prev_speed = 0;
my $prev_valid = 0;

while (my $line = <STDIN>) {
	my ($time, $notch, $brake, $speed) = split("\t", $line);
	my $valid = $notch > 0;
	if ($valid && $prev_valid && $time > $prev_time) {
		printf("%g\t%g\n", $prev_speed, ($speed - $prev_speed) / ($time - $prev_time));
	}
	$prev_time = $time;
	$prev_speed = $speed;
	$prev_valid = $valid;
}
```

すると、5300系では、以下のような関係が得られた。

![5300系の力行における速度と加速度の関係](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/b28e5c93-16aa-74e7-d3a0-16493fb315e0.png)
![5300系のP1における速度と加速度の関係](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/4027b242-9d38-1413-c289-dcd56d32faae.png)
![5300系の力行における速度と加速度の関係 (低速域)](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/809983a4-dff1-addc-e1da-890860b7ea3d.png)

以下のことが読み取れる。

* 0～3 [km/h] 程度 (P1 では 0～0.5 [km/h] 程度) では、速度が上がるにつれてだんだん加速度も上がっていく
* その後は、ある速度までは加速度はだいたい横ばいである
* ある速度を超えると、速度が上がるにつれてだんだん加速度が下がっていく

# 加減速の仕様の決定

観察した挙動をもとに、今回のプログラムでは以下の仕様に決定した。

* 力行では、ある速度までは一定の加速度を用い、その速度を超えると加速度が速度が上がるにつれて線形に減衰する
  * 低速域で加速度が下がる挙動は、影響が少なく重要ではないと判断し、採用しないこととした
  * 観察した挙動では、速度を上げたときの加速度の下がり方は曲線を描いていたが、今回は簡単のため線形のモデルを使用することにした
  * ある程度まで加速すると加速度が0になるので、速度のオーバーフローを防ぐことができる
* ブレーキでは、加速度と段数が比例する
  * 完全に比例させることで、全ての段数を1個の式で表すことができ、コード量を削減できる

# 路線の仕様の決定

今回のプログラムで運転する路線 (区間) は、以下の仕様とした。

* 1,000 m の直線
  * 長さは適当なキリの良い数値
  * 勾配・カーブ・信号・速度制限は面倒なのでなし
* 停車位置の 130 m 手前～10 m 奥に駅のホームがある
  * ホームの長さは、TRAIN CREW における仕様を参考にした
* 停車位置の 160 m 手前まで、40 m 間隔で架線用の柱がある
  * 柱の間隔は、TRAIN CREW における仕様を参考にした
  * 停車位置の奥にもあるべきだが、面倒なのでなし

# 3D風描画

シンプルな仕組みで3Dの世界を表現するため、3次元空間内の物体を目の前の仮想的なスクリーンに映る映像で表現することを考える。
すると、スクリーンの中心を中心に、見える場所に目からの距離に反比例する係数を掛ければ良さそうだということがわかる。
これは、目を頂点、視線に垂直な平面を底面とする、視界を表す四角錐を考えることで説明できる。
この四角錐の高さ (すなわち、目からスクリーンや物体の位置の平面までの距離) を変えると相似な四角錐ができ、これらの相似な四角錐上で「同じ長さ」ということは、スクリーンに映したとき (すなわち、同じ高さの四角錐に変換したとき) は長さにもとの四角錐の相似比の逆がかかるということになる。

![今回用いる3D風描画の原理](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/9ef7666c-b6fb-c67d-72c0-88462e9d47ae.png)

# 位置・速度・加速度の表現

IchigoJam BASIC の変数は、-32,868 ～ 32,767 の整数しか扱えない。
限られた範囲の整数でなるべくいい感じに位置・速度・加速度を表現するため、今回は以下の表現を用いた。

|変数|意味|
|:-:|---|
|`M`|停止位置から手前方向への変位 [m]|
|`P`|`M` が示す位置からの奥方向への変位 [mm]|
|`O`|速度を位置へ反映させる際の余り|
|`S`|奥方向への速度 [mm/s]|
|`A`|奥方向への加速度 [mm/s/frame]|

`A` は1**フレーム**あたりの加速度なので毎フレーム速度に足すことができる一方、`S` は1**秒**あたりの速度なので毎フレーム変位に足すには1秒あたりのフレーム数で割らなければならない。
このとき、割った余りを単純に捨ててしまうと誤差に繋がるため、余りを `O` に溜め、これが1秒あたりのフレーム数に達したら変位に反映させるようにした。

速度が 1 [km/h] のとき、3,600 秒間で 1,000,000 mm 進むので、今回用いる速度は 277.77… [mm/s] である。
よって、`S` を `277` で割った値を速度 [km/h] として表示することにした。

# プログラム

```text
10 ' ﾃﾞﾝｼｬ ｳﾝﾃﾝ
20 LET[3],1000,500,5,8000,1400,10,16000,1200,10,16000,1000,13,16000,1280,13:M=1000:P=0:O=0:S=0:T=0:N=-4:CLT
30 K=INKEY():IFK=49N=-9
40 K=K&#DF:N=N-(K=81ANDN>-8)+(K=90ANDN<5):IFK=83N=0
50 IFN>0L=N*3:A=[L+2]-(S>[L])*(S-[L])/[L+1]:A=A*(A>0):ELSEA=3*N-(N=0)
60 S=S+A:S=S*(S>0):O=O+S%60:P=P+S/60+O/60:O=O%60:M=M-P/1000:P=P%1000:IFM<6IFS=0GOTO160
70 CLS:LC22,0:?DEC$(80-T/60,5);"s";:LC22,1:IFM>0?DEC$(M-(P>0),5);".";DEC$(2000-P,3);:GOTO90ELSEIFM<0?DEC$(M,5);ELSE?"   ";:IFP?"-0";ELSE?" 0";
80 ?".";DEC$(1000+P,3);
90 ?"m";:LC22,2:?DEC$(S/277,5);"km/h";:LC25,3:IFN=0?"N ";ELSEIFN=-9?"EB";ELSEIFN<0?"B";-N;ELSE?"P";N;
100 DRAW32,24,32,48:DRAW32,24,44,48:IF0<MIFM<120X=34+180/M:DRAWX,24+240/M,X,24+120/M
110 IF-10<MIFM<250Q=(M-131)*(M>130)+1:R=M+10:W=30-60/Q:X=30-60/R:Y=24+120/Q:Z=24+120/R:DRAW30-300/Q,Y,W,Y:DRAWW,Y,X,Z:DRAW30-300/R,Z,X,Z
120 FORI=0TO80STEP40:Q=M%40+I+1:IF160+I<MR=30-120/Q:DRAWR,24+240/Q,R,24-240/Q
130 NEXT
140 T=T+1:IFTICK()<T:WAIT1
150 GOTO30
160 IFINKEY()=32GOTO20ELSEGOTO160
```

* [OneFiveCrowd で実行する](https://onefivecrowd.mikecat.work/#text=RUN%0A&prg=CgAMJyDD3t28rCCz3cPdABQAaExFVFszXSwxMDAwLDUwMCw1LDgwMDAsMTQwMCwxMCwxNjAwMCwxMjAwLDEwLDE2MDAwLDEwMDAsMTMsMTYwMDAsMTI4MCwxMzpNPTEwMDA6UD0wOk89MDpTPTA6VD0wOk49LTQ6Q0xUAB4AFEs9SU5LRVkoKTpJRks9NDlOPS05ACgAMEs9SyYjREY6Tj1OLShLPTgxQU5ETj4tOCkrKEs9OTBBTkROPDUpOklGSz04M049MAAyAEJJRk4-MEw9TiozOkE9W0wrMl0tKFM-W0xdKSooUy1bTF0pL1tMKzFdOkE9QSooQT4wKTpFTFNFQT0zKk4tKE49MCkAPABUUz1TK0E6Uz1TKihTPjApOk89TytTJTYwOlA9UCtTLzYwK08vNjA6Tz1PJTYwOk09TS1QLzEwMDA6UD1QJTEwMDA6SUZNPDZJRlM9MEdPVE8xNjAAAEYAjENMUzpMQzIyLDA6P0RFQyQoODAtVC82MCw1KTsicyI7OkxDMjIsMTpJRk0-MD9ERUMkKE0tKFA-MCksNSk7Ii4iO0RFQyQoMjAwMC1QLDMpOzpHT1RPOTBFTFNFSUZNPDA_REVDJChNLDUpO0VMU0U_IiAgICI7OklGUD8iLTAiO0VMU0U_IiAwIjsAAFAAFD8iLiI7REVDJCgxMDAwK1AsMyk7AFoAYj8ibSI7OkxDMjIsMjo_REVDJChTLzI3Nyw1KTsia20vaCI7OkxDMjUsMzpJRk49MD8iTiAiO0VMU0VJRk49LTk_IkVCIjtFTFNFSUZOPDA_IkIiOy1OO0VMU0U_IlAiO047AGQAUERSQVczMiwyNCwzMiw0ODpEUkFXMzIsMjQsNDQsNDg6SUYwPE1JRk08MTIwWD0zNCsxODAvTTpEUkFXWCwyNCsyNDAvTSxYLDI0KzEyMC9NAG4AhElGLTEwPE1JRk08MjUwUT0oTS0xMzEpKihNPjEzMCkrMTpSPU0rMTA6Vz0zMC02MC9ROlg9MzAtNjAvUjpZPTI0KzEyMC9ROlo9MjQrMTIwL1I6RFJBVzMwLTMwMC9RLFksVyxZOkRSQVdXLFksWCxaOkRSQVczMC0zMDAvUixaLFgsWgB4AEpGT1JJPTBUTzgwU1RFUDQwOlE9TSU0MCtJKzE6SUYxNjArSTxNUj0zMC0xMjAvUTpEUkFXUiwyNCsyNDAvUSxSLDI0LTI0MC9RAACCAARORVhUAIwAFlQ9VCsxOklGVElDSygpPFQ6V0FJVDEAlgAGR09UTzMwAKAAHklGSU5LRVkoKT0zMkdPVE8yMEVMU0VHT1RPMTYw) (高速、非公式)
* [IchigoJam web で実行する](https://fukuno.jig.jp/app/IchigoJam/#10%20%27%20%EF%BE%83%EF%BE%9E%EF%BE%9D%EF%BD%BC%EF%BD%AC%20%EF%BD%B3%EF%BE%9D%EF%BE%83%EF%BE%9D%0A20%20LET%5B3%5D%2C1000%2C500%2C5%2C8000%2C1400%2C10%2C16000%2C1200%2C10%2C16000%2C1000%2C13%2C16000%2C1280%2C13%3AM%3D1000%3AP%3D0%3AO%3D0%3AS%3D0%3AT%3D0%3AN%3D-4%3ACLT%0A30%20K%3DINKEY%28%29%3AIFK%3D49N%3D-9%0A40%20K%3DK%26%23DF%3AN%3DN-%28K%3D81ANDN%3E-8%29%2B%28K%3D90ANDN%3C5%29%3AIFK%3D83N%3D0%0A50%20IFN%3E0L%3DN%2A3%3AA%3D%5BL%2B2%5D-%28S%3E%5BL%5D%29%2A%28S-%5BL%5D%29%2F%5BL%2B1%5D%3AA%3DA%2A%28A%3E0%29%3AELSEA%3D3%2AN-%28N%3D0%29%0A60%20S%3DS%2BA%3AS%3DS%2A%28S%3E0%29%3AO%3DO%2BS%2560%3AP%3DP%2BS%2F60%2BO%2F60%3AO%3DO%2560%3AM%3DM-P%2F1000%3AP%3DP%251000%3AIFM%3C6IFS%3D0GOTO160%0A70%20CLS%3ALC22%2C0%3A%3FDEC%24%2880-T%2F60%2C5%29%3B%22s%22%3B%3ALC22%2C1%3AIFM%3E0%3FDEC%24%28M-%28P%3E0%29%2C5%29%3B%22.%22%3BDEC%24%282000-P%2C3%29%3B%3AGOTO90ELSEIFM%3C0%3FDEC%24%28M%2C5%29%3BELSE%3F%22%20%20%20%22%3B%3AIFP%3F%22-0%22%3BELSE%3F%22%200%22%3B%0A80%20%3F%22.%22%3BDEC%24%281000%2BP%2C3%29%3B%0A90%20%3F%22m%22%3B%3ALC22%2C2%3A%3FDEC%24%28S%2F277%2C5%29%3B%22km%2Fh%22%3B%3ALC25%2C3%3AIFN%3D0%3F%22N%20%22%3BELSEIFN%3D-9%3F%22EB%22%3BELSEIFN%3C0%3F%22B%22%3B-N%3BELSE%3F%22P%22%3BN%3B%0A100%20DRAW32%2C24%2C32%2C48%3ADRAW32%2C24%2C44%2C48%3AIF0%3CMIFM%3C120X%3D34%2B180%2FM%3ADRAWX%2C24%2B240%2FM%2CX%2C24%2B120%2FM%0A110%20IF-10%3CMIFM%3C250Q%3D%28M-131%29%2A%28M%3E130%29%2B1%3AR%3DM%2B10%3AW%3D30-60%2FQ%3AX%3D30-60%2FR%3AY%3D24%2B120%2FQ%3AZ%3D24%2B120%2FR%3ADRAW30-300%2FQ%2CY%2CW%2CY%3ADRAWW%2CY%2CX%2CZ%3ADRAW30-300%2FR%2CZ%2CX%2CZ%0A120%20FORI%3D0TO80STEP40%3AQ%3DM%2540%2BI%2B1%3AIF160%2BI%3CMR%3D30-120%2FQ%3ADRAWR%2C24%2B240%2FQ%2CR%2C24-240%2FQ%0A130%20NEXT%0A140%20T%3DT%2B1%3AIFTICK%28%29%3CT%3AWAIT1%0A150%20GOTO30%0A160%20IFINKEY%28%29%3D32GOTO20ELSEGOTO160%0ARUN%0A) (公式、**超低速**)

実機で実行する場合、従来機種だと処理落ちや画面のちらつきが発生するので、IchigoJam R または IchigoJam P (もしくはそれ以上に高速な (後継？) 機種) で実行すること。
また、IchigoJam R で実行すると、描画の乱れがみられた。
以下のコードを追加することで、描画の乱れを改善できることがある。

```text
65 IFT%2GOTO140
```

このコードを追加した際、正常に描画できることもあれば、描画が乱れることもあった。
以下のコードを追加したほうが、さらに乱れにくくなるかもしれない。

```text
65 IFT%4GOTO140
```

# 操作方法

|キー|役割|
|:-:|---|
|1|非常ブレーキ (EB) をかける|
|Q|マスコンをブレーキ方向に1段進める|
|S|マスコンを N にする|
|Z|マスコンを力行方向に1段進める|
|スペース|停車時、次のゲームを開始する|

停車位置までの残り距離が5m以下 (オーバーランを含む) の状態で速度がゼロ (0km/h表示だけでなく、小数点以下までゼロ) になると停車と判定し、時間のカウントを停止する。

# 実行例

<iframe width="560" height="420" src="https://www.youtube.com/embed/Ej0ZHXqVUjw?si=fXDaE7waptiBREuW" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

IchigoJam R において描画が乱れた状態。
画面の一部の描画が抜けてしまっている。

![描画が乱れた状態](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/3d0c7a79-2095-f108-dd7a-780a54fe6bd5.png)

ゲームを開始した状態。

![ゲームを開始した状態](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/7dcc6897-51d0-4a21-632f-bf8e3f1bdf16.png)

発車。
今回戸閉め灯などは実装していないので、ゲーム開始後すぐに発車してよい。

![発車](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/e8f30da6-0c98-c18c-45aa-306c1e712ac2.png)

加速。

![加速](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/4c4f528b-54ef-d489-605a-4170d97bf429.png)

減速。駅が見えてきた。

![減速](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/49e70d52-cdff-9d4e-9d04-dfc0be6c79bc.png)

停車位置目標が見えてきた。
視野の関係で、正しい停車位置は停車位置目標が画面上少し過ぎたところであることに注意。

![停車位置目標](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/4ab5d27b-a2a7-8818-fdde-ba4370d8a3bd.png)

停車。ちょっと遅かったようだ。
なお、今回は停車すると操作の受け付けは終了し、転動防止のためにブレーキを強めることはできない。

![停車](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/4a4dcc61-3378-c7d1-62dd-cdd48b9f5887.png)

# まとめ

IchigoJam R 上で、電車の運転のようなことができるプログラムを作ることができた。