プリント基板設計・シミュレーション

TOP > アポロレポート > コラム > 第46回 プログラミングについて『芋虫』
コラム
2023/10/31

第46回 プログラミングについて『芋虫』

アポロレポート

 今回はちょっと息抜きということで、エスケープシーケンスのお話をしましょう。
パソコンのOSが Windows になってからはあまり縁がなくなってしまったエスケープシーケンスですが、周辺機器などを制御する際には必要になることもありますので、今回は画面上に芋虫が這うプログラムを例にして話を進めていきましょう。

 私が初めてエスケープシーケンスなるものを扱ったのはかれこれ15年程昔にさかのぼります。何度もお話していることですが、当時VAXに文字端末(VT100)、ペンプロッタ、ディジタイザ、グラフィック端末などを接続して基板設計のCADを扱っていたのですが、既存のプログラムを毎日使っているとだんだんと飽きてきてしまい、グラフィック端末に何か絵を描いてみたいと思う様になりました。確かそのグラフィック端末はJRC(日本無線)のNWX-235というものだったと記憶しています。
 当然、単なるアプリケーションユーザーとして機器を導入していましたので、グラフィックを簡単に行なうライブラリなどは購入もしていませんでした。しかしそのグラフィック端末のマニュアルにはエスケープシーケンスの説明書も含まれていましたので、それを使ってなんとかしてみよう思い立ったのでした。とは言っても、初めのころはエスケープシーケンスという言葉も知らなかったので、何気なくマニュアルをめくっていて、「この命令を使えば絵が描けるかもしれない」とぼんやりと感じた程度でした。
 FORTRANのことさえも良くわかっていなかった上に、バイナリのままでの入出力を行なったことなどなかったので、マニュアルには ESC [L と書いてあったのを、

   WRITE(6,100)'ESC[L'
100 FORMAT(1H,A)

   STOP
   END

と書いてやっていたのですから結果は、

 ,ESC[L

 Fortran stop

と表示されるだけで、何事も起こりませんでした。

 ちょっと余談になりますが、FORTRANの入出力についての不思議な部分を紹介しましょう。上の例のフォーマット文で、

100 FORMAT(1H,A)

 1H, のことをご存知ですか。多分学校などでFORTRANを習うときには、おまじないのように記述することを教えられるだけで、この3文字に含まれている歴史的な(ちょっと大袈裟かな)背景は教えてはくれないものです。多分学校の先生もあまりよく分かっていないのかも知れません。

 今ではFORTRAN77が当たり前なのですが、FORTRAN77よりも古い仕様のFORTRANでは文字列を表現するにはホレリス型というものしかありませんでした。例えば、文字列 abcdefg は、

 7Habcdefg

と表現するのです。ということは 1H, はコンマ1文字を表現したものなのです。それではなぜフォーマット文の最初にこれを記述するのでしょうか。これも歴史的 ... というよりもそれまでのFORTRANの使用された環境がそうさせたのかも知れません。そもそもFORTRANが誕生して成長の途中の頃は、タイムシェアリングで端末に向かってテキストエディタを操作してプログラムを作ることも、ファイルの内容を端末に表示することもなく、カードリーダーから読み込んだプログラムとデータを処理してプリンタに印刷することがその殆どを占めていました。そのような時代には面倒な行の制御などは必要ではなく簡単に表現できた方が良かったのです。
 そこで、FORTRANは入出力時には行の最初の1文字をその制御に使用することにした訳です。例えば行の最初の1文字が 1 のときには改ページ、/ のときには1行の空白行を開けるなどとしたのです。それではコンマはというと、これには何も意味がないのです。ですからコンマの代わりに別の意味のない文字であっても構わないということなのです。だからといって、
 
100 FORMAT(1HFA)

などとしてしまうと、理解しずらいものになってしまうので区切り記号でも使用されるコンマにしようということになり、それが現在でも生き残っている訳です。

 この行の先頭1文字を制御に使うことは、タイムシェアリングで対話的にプログラムやデータを扱えるようになると困った問題を生み出してしまいました。ファイルの中にもその制御用の文字を入れておくとなると通常のテキストファイルと違った解釈で画面に表示したり、印刷を行なわなければならなくなってしまうのです。VAX/VMSではこの問題をファイルヘッダーに改行制御用の属性を持たせることでなんとか解決していました。要するに、ファイルヘッダにFORTRAN用の改行制御を行なうものなのか、通常のテキストファイル用の改行制御を行なうものなのかを持たせるのです。

 私がFORTRANでエスケープシーケンスを使用したときにもう1つ困ったことがありました。上のフォーマット文の様に、

 100 FORMAT(1H,A)

と記述すると、必ず <CR> と <LF> が出力されてしまうのです。

 100 FORMAT(1H+,A)

はベタ印刷で <CR> のみが出力されるのですが、この <CR> も出力はしたくはないのです。都合のいいことに VAX FORTRANには最初の1文字がヌルだったら <CR>も <LF> も出力しないというように機能が拡張されていましたので、

   WRITE(6,100)0,'ABC'
100 FORMAT(A,A)

と記述すれば ABC のみが出力され、目的どおりに出力を行なえるようになりました。

 話を戻しましょう。エスケープシーケンスのマニュアルに書かれている ESC は、ESCという文字ではなくエスケープコードのことで、アスキー文字コードの表を見れば16進数で 1B なのです。実はこれが分かるのに私は半年ほどかかってしまいました。ですから、ESC [L を出力するには(これからはC言語)、

 printf("%c[L",0x1b);

と記述すればいいのです。

 @oooxooox
     o  o
     o  x
     xooxoo

 こんな芋虫が画面を這い回るのをボーッとして眺めてみたい。それが今回の目的です。
そこで、DOSのエスケープシーケンス表を調べてみると、

 画面の消去      ESC [2J

 カーソルの非表示   ESC [>5h

 カーソルの移動    ESC [pl;pcH

となっていますので、これを利用します。ちなみにこのエスケープシーケンスをどこかで見たことがあると思って VT100 のマニュアルを調べてみるとこれと同じなのです。
 またまた余談なのですが、 VT100 という名前は見たことがある方もいるかと思います。Windows95の Telnet の設定にもありますが、この VT100 はDECの文字端末の商品名なのです。どうしてこの名前が今でも亡霊のように生き残っているかというと、DECの PDPシリーズやVAXシリーズが世界的に売れまくってしまったのでいつの間にかこの端末が端末の標準のようになってしまったからなのです。ですからパソコンでもこのエスケープシーケンスをそのまま利用できるようにした方が得だということで採用されたのでした。

 またまた話を元に戻しましょう。まず上のエスケープシーケンスを実行する関数を作りましょう。

 #include <stdio.h>

 clear_screen()
 {
  printf("%c[2J",0x1b);
  fflush(stdout);
 }

 hide_cursor()
 {
  printf("%c[>5h",0x1b);
  fflush(stdout);
 }

 move_cursor(x,y)
 int x,y;
 {
  printf("%c[%d;%dH",0x1b,y,x);
  fflush(stdout);
 }

 特に難しい部分はないかと思いますが、1つ注意することはカーソルを移動する関数move_cursor で、5行目の3カラム目にカーソルを移動するときに最終的に出力する値は、

 ESC [5;3H

と、5行目の5は文字の5を出力しなければならないことです。また各関数でエスケープシーケンスを出力した後に fflush 関数を使用して標準出力のバッファ内のデータを強制的に出力することです。高水準入出力では入出力の操作が行われたときに素直にそれが行われるのではなくバッファリングを行ない、ある量が溜まったときに実際に入出力が行われるため今回作ろうとしているプログラムのときにはタイムラグが発生するので動きがぎこちなくなってしまうからです。

 次に芋虫のデータ構造を考えてみましょう。

 #define MAX_SIZE 30

 struct warm_stct{
  char c;       /* 節の形状を表示するための文字を保持します */
  int x,y;      /* 節の位置を保持します */
 };
 struct warm_stct warm[MAX_SIZE];

 できるだけ簡単な構造にしてみました。 MAX_SIZE は芋虫の長さです。この構造の配列の c には次のように文字を設定することにします。

 要素番号 0 1 2 3 4 5 6 .. 29
 設定文字 @ x o o o x o .. sp

 ここで29番目の要素、すなわち配列の一番最後の要素に sp (スペース)を設定するのは、芋虫が動き回るときに前のしっぽの末端の表示を消去するためです。
このようにするとプログラムが非常に簡単になります。

 #define SCREEN_XSIZE 80
 #define SCREEN_YSIZE 25

 #define MAX_SIZE 30

 struct warm_stct{
  char c;
  int x,y;
 };
 struct warm_stct warm[MAX_SIZE];

 main()
 {
  int drc,step;
  int i;
                        /* 芋虫の初期値の設定 */
  for(i=0;i<MAX_SIZE;i++){
      if(i==0     )warm[i].c='@';
   else if(i==MAX_SIZE-1)warm[i].c=' ';
   else if(i%4==1    )warm[i].c='x';
   else         warm[i].c='o';

   warm[i].x=SCREEN_XSIZE/2;
   warm[i].y=SCREEN_YSIZE/2;
  }

  clear_screen();              /* 画面を消去 */
  hide_cursor();               /* カーソルを非表示にする */

  while(1){
   drc =rand() % 4;            /* 芋虫の移動方向を設定 */
   step=rand() % 20;            /* 芋虫の移動距離を設定 */

   move_warm(drc,step);           /* 芋虫を移動する */
  }
 }

 芋虫の移動方向は上下左右の4方向としますので方向は乱数を発生する関数の値を4で割った余りを使用します。移動距離は最大20ステップ(実際には19)としますので20で割った余りを使用します。

 さらに芋虫を移動する関数 move_warm を作りましょう。

 move_warm(drc,step)
 int drc,step;
 {
  static int odrc= -1;         /* 以前に移動した方向を保持する変数 */

  int dx,dy;
  int i;

  if(step==0)return 1;         /* 移動距離が0のときには呼び出し側に
                      戻る */

  if((drc%2)==(odrc%2))return 1;    /* 移動方向が前と同じか反対のときには
                      呼び出し側に戻る */

                     /* 方向の応じてXとYの変化量を設定 */
     if(drc==0){ dx= 1; dy= 0; }
  else if(drc==1){ dx= 0; dy= 1; }
  else if(drc==2){ dx= -1; dy= 0; }
  else if(drc==3){ dx= 0; dy= -1; }

  while(--step){
                     /* 芋虫が画面の外に出てしまうときには
                      呼び出し側に戻る */
   if(warm[0].x+dx<= 0      )return 1;
   if(warm[0].x+dx> SCREEN_XSIZE)return 1;
   if(warm[0].y+dy<= 0      )return 1;
   if(warm[0].y+dy> SCREEN_YSIZE)return 1;

   odrc=drc;             /* 芋虫が移動したときにはその方向を
                      保持します */

   for(i=MAX_SIZE-1;i>=0;i--){    /* このループで芋虫を移動します */
    if(i==0){            /* 要素番号が0(頭)のよきには
                      変化量分移動 */
     warm[i].x+=dx;
     warm[i].y+=dy;
    }
    else{              /* 頭以外のときには、前の節の位置に
                      現在の節の位置を移動 */
     warm[i].x=warm[i-1].x;
     warm[i].y=warm[i-1].y;
    }
                     /* 各節を表示 */
    move_cursor(warm[i].x,warm[i].y);
    putchar(warm[i].c);
    fflush(stdout);
   }
  }
 }

 これだけの関数が揃えば実際に実行して表示することができます。

ところが、いざ実行してみると芋虫が飛び回ってしまいます。これは表示があまりにも速すぎるためです。そこで、表示を遅くするために芋虫を1ステップ移動した後に一定時間ループの実行を止めるようにしましょう。そこで、上の関数のループが次のようにdelay 関数を呼び出すようにします。

  while(--step){
   for(i=MAX_SIZE-1;i>=0;i--){
     :
     :
   }

   delay(0.1);
  }

 delay 関数は次のようにします。_ftime 関数はもしかするとマイクロソフト固有のものかも知れませんので、他のコンパイラを使っている方は 0.1 秒以下の時間を取得できる関数を使用して作成して下さい。

 #include <sys\timeb.h>
 #include <time.h>

 delay (delta)
 double delta;
 {
  struct _timeb to,tn;
  int dt;

  dt=(int)(delta*1000.0);

  _ftime(&to);

  while(1){
   _ftime(&tn);
   if(tn.millitm-to.millitm>dt)break;
  }
 }

 この遅延機能を組み込んだプログラムを実行すると、だいたい思った通りに芋虫が動き回ります。

 最後の問題はどうやってこのプログラムを終わらせるかです。
簡単にするために ctrl/c で終了することとします。ところが実際にctrl/cで終了させてみると、カーソルが非表示になったままになってしまいます。これでは後のパソコンの使用に差し支えます。

 この問題を解決するには特別な例外処理を行なう必要があります。C言語のライブラリには signal という便利な関数があります。この関数はOSがプログラムを実行する際に ctrl/c などの例外が発生したときに実行する関数をOSに教えるものです。
 signal 関数は、

 void (*signal(int sig,void (*func)(int sig)))(int sig);

の形式で、これを見るだけではすごく難しいのですが、実際の例で見ると分かりやすいと思います。いま ctrl/c が押されたときに、ctrlc_handler という関数を呼び出して欲しいとしたときに、

 #include <stdio.h>
 #include <signal.h>

 :
 :

  signal(SIGINT,ctrlc_handler);

と記述することで実現することができます。実際には signal 関数の戻り値も処理しなければなりません。この関数の戻り値は、この signal 関数で呼び出して欲しい関数を設定する前に設定されていた関数のアドレスを返すのです。このようになっているのは、複数の関数が独自に signal 関数で例外処理を行なっている場合、例外が発生したときにそれぞれの例外処理用の関数を呼び出さなければならないからです。例えば3つの関数がctrl/cの例外処理をOS側に設定したとすると、その戻り値は次のようになります。

 main()
 {
   signal(SIGINT,main_ctrlc_handler); /* この関数の前には何も設定されていな
                      いので NULL が返ります */

   sub1();

   sub2();
 }

 sub1()
 {
   signal(SIGINT,sub1_ctrlc_handler); /* main_ctrlc_handler のアドレスが返り
                      ます */
 }

 sub2()
 {
   signal(SIGINT,sub2_ctrlc_handler); /* sub1_ctrlc_handler のアドレスが返り
                      ます */
 }

 ですから、 signal 関数の戻り値を保持しておいて例外が発生したときには例外を処理する関数で、それらを呼び出すようにしなければなりません。実際には、

 #include <stdio.h>
 #include <signal.h>

 void main_ctrlc_prev_handler(int sig);
 void sub1_ctrlc_prev_handler(int sig);
 void sub2_ctrlc_prev_handler(int sig);

 void (*main_ctrlc_prev_handler)(int sig)=NULL;
 void (*sub1_ctrlc_prev_handler)(int sig)=NULL;
 void (*sub2_ctrlc_prev_handler)(int sig)=NULL;

 main()
 {
  main_ctrlc_prev_handler=signal(SIGINT,main_ctrlc_handler);

  sub1();

  sub2();
 }

 sub1()
 {
  sub1_ctrlc_prev_handler=signal(SIGINT,sub1_ctrlc_handler);
 }

 sub2()
 {
  sub2_ctrlc_prev_handler=signal(SIGINT,sub2_ctrlc_handler);
 }

とし、実際に例外処理を行なう関数では、

 void main_ctrlc_prev_handler(int sig)
 {
  :
  :

  if(main_ctrlc_prev_handler)main_ctrlc_prev_handler(sig);
 }

 void sub1_ctrlc_prev_handler(int sig)
 {
  :
  :

  if(sub1_ctrlc_prev_handler)sub1_ctrlc_prev_handler(sig);
 }

 void sub2_ctrlc_prev_handler(int sig)
 {
  :
  :

  if(sub2_ctrlc_prev_handler)sub2_ctrlc_prev_handler(sig);
 }

 のように処理をするようにします。

 最後にctrl/cの例外処理をする機能を加えたプログラム全体を示します。

 #include <stdio.h>
 #include <sys\timeb.h>
 #include <time.h>
 #include <signal.h>

 #define SCREEN_XSIZE 80
 #define SCREEN_YSIZE 25

 #define MAX_SIZE 30

 struct warm_stct{
  char c;
  int x,y;
 };
 struct warm_stct warm[MAX_SIZE];

 void ctrlc_handler(int sig);
 void (*ctrlc_prev_handler)(int sig)=NULL;

 main()
 {
  int drc,step;
  int i;
                        /* ctrl/c 例外処理関数の
                         設定 */
  ctrlc_prev_handler=signal(SIGINT,ctrlc_handler);

                        /* 芋虫の初期値の設定 */
  for(i=0;i<MAX_SIZE;i++){
      if(i==0     )warm[i].c='@';
   else if(i==MAX_SIZE-1)warm[i].c=' ';
   else if(i%4==1    )warm[i].c='x';
   else         warm[i].c='o';

   warm[i].x=SCREEN_XSIZE/2;
   warm[i].y=SCREEN_YSIZE/2;
  }

  clear_screen();              /* 画面を消去 */
  hide_cursor();               /* カーソルを非表示にする */

  while(1){
   drc =rand() % 4;            /* 芋虫の移動方向を設定 */
   step=rand() % 20;            /* 芋虫の移動距離を設定 */

   move_warm(drc,step);           /* 芋虫を移動する */
  }
 }

 move_warm(drc,step)
 int drc,step;
 {
  static int odrc= -1;         /* 以前に移動した方向を保持する変数 */

  int dx,dy;
  int i;

  if(step==0)return 1;         /* 移動距離が0のときには呼び出し側に
                      戻る */

  if((drc%2)==(odrc%2))return 1;    /* 移動方向が前と同じか反対のときには
                      呼び出し側に戻る */

                     /* 方向の応じてXとYの変化量を設定 */
     if(drc==0){ dx= 1; dy= 0; }
  else if(drc==1){ dx= 0; dy= 1; }
  else if(drc==2){ dx= -1; dy= 0; }
  else if(drc==3){ dx= 0; dy= -1; }

  while(--step){
                     /* 芋虫が画面の外に出てしまうときには
                      呼び出し側に戻る */
   if(warm[0].x+dx<= 0      )return 1;
   if(warm[0].x+dx> SCREEN_XSIZE)return 1;
   if(warm[0].y+dy<= 0      )return 1;
   if(warm[0].y+dy> SCREEN_YSIZE)return 1;

   odrc=drc;             /* 芋虫が移動したときにはその方向を
                      保持します */

   for(i=MAX_SIZE-1;i>=0;i--){    /* このループで芋虫を移動します */
    if(i==0){            /* 要素番号が0(頭)のよきには
                      変化量分移動 */
     warm[i].x+=dx;
     warm[i].y+=dy;
    }
    else{              /* 頭以外のときには、前の節の位置に
                      現在の節の位置を移動 */
     warm[i].x=warm[i-1].x;
     warm[i].y=warm[i-1].y;
    }
                     /* 各節を表示 */
    move_cursor(warm[i].x,warm[i].y);
    putchar(warm[i].c);
    fflush(stdout);
   }

   delay(0.1);
  }
 }

 clear_screen()
 {
  printf("%c[2J",0x1b);
  fflush(stdout);
 }

 hide_cursor()
 {
  printf("%c[>5h",0x1b);
  fflush(stdout);
 }

 move_cursor(x,y)
 int x,y;
 {
  printf("%c[%d;%dH",0x1b,y,x);
  fflush(stdout);
 }

 delay (delta)
 double delta;
 {
  struct _timeb to,tn;
  int dt;

  dt=(int)(delta*1000.0);

  _ftime(&to);

  while(1){
   _ftime(&tn);
   if(tn.millitm-to.millitm>dt)break;
  }
 }

 void ctrlc_handler(int sig)
 {
  printf("%c[>5l",0x1b);           /* カーソルを表示する */
  fflush(stdout);

  if(ctrlc_prev_handler)ctrlc_prev_handler(sig);

  exit(0);
 }

 今回はちょっと長くなってしまいましたが、エスケープシーケンスと例外処理の参考になるかと思います。

 それではまた次回。

そのお悩み、
アポロ技研に話してみませんか?

アポロ技研でワンランク上の物創りへ!
そのお悩み、アポロ技研に話してみませんか?