アルゴロジック や タートルグラフィックス を真似して, ゲーム感覚でプログラミングを学習するための エデュテイメントアプリを開発してみよう. また,この作業を通じて,スクリプト処理と複数プログラム連携の技を修得しよう.
自動お絵描きシステムを実現するため, 次の2つのプログラムを作成し, 連携動作させる:
システム構成図:
【ユーザ】⇦⇨【端末】⇦⇨【pendraw】 ⇧⇩ 【スクリプト】⇨【pdcl】⇨【キーシーケンス】
スクリプト言語 PDCL(pendraw control language)の仕様:
スクリプトの例:(階段を描くスクリプト)
on repeat 5 right 2 down 2 end
基本的に,ファイル入出力処理では, データは,ファイルの先頭から末尾の方向へ順番に連続的に読み書きされる. このような方式は, シーケンシャルアクセス(sequential access)と呼ばれている.
一方,ファイル内でのデータの読み書き位置が不連続的な入出力の方式は, ランダムアクセス(random access)と呼ばれている. これはデータを再読み込みしたり,読み飛ばしたりする場合に必要となる. 今回,スクリプトの反復処理などのために,この技を利用する.
Cの標準ライブラリには, ランダムアクセスを実現するための ファイル位置関数が用意されている:
これらの関数についての詳しくは, 参考書 K&R p.311 やオンラインマニュアル等を参照せよ.
$ man fseek
Cのプログラムの中から他のプログラム(unix コマンド)を実行することもできる.
コマンドとプログラムとの間でのデータ交換は,できなくはないが,面倒. (一時的なデータファイルを作り,データを書き込んでおき, そのファイル名をコマンド文字列にも付けておく,等の手間が必要...)
使用例:
printf("Cソースファイルのリスト:\n"); system("ls *.c"); // lsコマンドを実行.実行結果は標準出力される.
使用例:
FILE *pp; char buf[256]; pp = popen("ls *.c", "r"); // lsコマンドを実行.実行結果は入力パイプへ出力される. if (pp == NULL) return (1); while (1) { if (fscanf(pp, "%255s", buf) == EOF) break; // 入力パイプからファイル名を1個だけ読む. printf("Cソースファイル:%s\n", buf); } pclose(pp); return (0);
これらの関数についての詳しくは, オンラインマニュアル等を参照せよ.
$ man system $ man popen
今回,2つのプログラムの連携動作のために,パイプを利用する.
とりあえず,ペン(カーソル)をキーボードで手動操作し, 線画を描くプログラムを作成しよう. ふつうの curses プログラムです.
ソース pendraw.c:(ver.1,ダウンロード)
#include <ncurses.h> #include <string.h> // strlen() /* ペンをキー入力でコントロールする. pen:ペンの状態(参照渡し) x, y:ペンの位置(参照渡し) w, h:画面サイズ key:入力キー */ void Ctrl(int *pen, int *x, int *y, int w, int h, int key) { switch (key) { case '-': *pen = 0; break; // on case '+': *pen = 1; break; // off case ' ': *pen = (*pen + 1)%2; break; // on/off切替 case 'h': (*x)--; if (*x < 0) *x = 0; break; // ← case 'j': (*y)++; if (*y >= h) *y = h - 1; break; // ↓ case 'k': (*y)--; if (*y < 0) *y = 0; break; // ↑ case 'l': (*x)++; if (*x >= w) *x = w - 1; break; // → } } /* メッセージを画面中央に表示し,キー入力を待つ. msg:メッセージ文字列 w, h:画面サイズ return:入力キー */ int Info(char *msg, int w, int h) { mvaddstr(h/2, (w - strlen(msg))/2, msg); refresh(); return (getch()); } /* ゲーム本体 */ void Game(void) { int pen; // ペンの状態(0:off,1:on)が入るよ int x, y; // ペンの位置が入るよ int w, h; // 画面のサイズが入るよ int key; // 入力キーの文字が入るよ // 初期設定 erase(); // 画面をクリア getmaxyx(stdscr, h, w); // 画面サイズを取得 pen = 0; x = w/2; y = h/2; // ペンの状態・位置を初期化 while (1) { // 描画 // erase(); // 画面を消去しなければ軌跡が残るよ if (pen) mvaddch(y, x, '#'); // ペンのキャラクタを描画 refresh(); // 画面を表示 // 入力 key = getch(); // 端末からキーを入力 // ペンの操作 Ctrl(&pen, &x, &y, w, h, key); if (key == 'q') break; // [Q]で終了 } Info("Done. Hit any key!", w, h); // 終了確認 } /* タイトル画面を表示し,キー入力を待つ. return:入力キー */ int Title(void) { erase(); mvaddstr( 1, 3, "PenDraw"); mvaddstr( 3, 3, "Key:"); mvaddstr( 4, 3, " [ ][+][-] - pen on/off"); mvaddstr( 5, 3, " [H][J][K][L] - move"); mvaddstr( 6, 3, " [Q] - quit"); refresh(); return (getch()); } int main(void) { int key; // 入力キーの文字が入るよ // 初期設定 initscr(); // 端末制御を開始 noecho(); // 入力キーは表示するなよ cbreak(); // 入力バッファは使うなよ curs_set(0); // カーソルは表示するなよ timeout(-1); // キー入力は来るまで待てよ // メインループ while (1) { key = Title(); // タイトル if (key == 'q') break; Game(); // ゲーム本体 } // 終了 endwin(); // 端末制御を終了 return (0); }
コンパイルと実行:
$ cc pendraw.c -lncurses -o pendraw $ ./pendraw
操作方法:
問題点:pen off状態だとペン位置が見えない... 上級者は解決してみては?
自動操作への準備として, 手動操作のキーシーケンスの記録(ファイル出力)機能を追加しよう.
pendraw.c:(ver.2)
... /* ゲーム本体 fout:操作記録用ファイルポインタ */ void Game(FILE *fout) // 引数の追加 { ... while (1) { .. Ctrl(&pen, &x, &y, w, h, key); // 操作の記録 if (fout != NULL) fputc(key, fout); // ファイルにキー文字を出力 if (key == 'q') break; } ... } ... int main(void) { FILE *fout; // 操作記録用ファイルポインタ int key; ... // メインループ while (1) { key = Title(); if (key == 'q') break; fout = fopen("key.txt", "w"); // 操作記録用ファイルの準備 // とりあえずエラー処理は省略 Game(fout); // ゲームの本体(引数の追加) if (fout != NULL) fclose(fout); // 操作記録用ファイルの後始末 } ... }
実行・操作:
$ ./pendraw ... # 適当にキー操作し,終了 $ cat key.txt ... # 操作したキー文字列が記録された?
記録したキーシーケンスファイルの入力機能を追加しよう. これで,自動操作が可能になる.
pendraw.c:(ver.3)
#include <ncurses.h> #include <string.h> #include <unistd.h> // usleep() ... /* ゲーム本体 fin:操作入力用ファイルポインタ fout:操作記録用ファイルポインタ */ void Game(FILE *fin, FILE *fout) { ... while (1) { ... refresh(); // 入力 if (fin != NULL) { key = fgetc(fin); // ファイルからキーを入力 usleep(100000); // 100ms停止(再生速度の調整) } else { key = getch(); // 端末からキーを入力 } if (key == EOF) break; // EOFで終了 // ペンの操作 Ctrl(&pen, &x, &y, w, h, key); ... } ... } /* タイトル画面を表示し,キー入力を待つ. return:入力キー */ int Title(void) { ... mvaddstr( 6, 3, " [Q] - quit"); mvaddstr( 8, 3, "Input from:"); mvaddstr( 9, 3, " [K] - keyboard"); mvaddstr(10, 3, " [F] - file (key sequence 'key.txt')"); refresh(); return (getch()); } int main(void) { FILE *fin; // 操作入力用ファイルポインタ FILE *fout; int key; ... while (1) { key = Title(); if (key == 'q') break; // ファイルの準備 switch (key) { case 'f': // 自動操作(ファイル入力) fin = fopen("key.txt", "r"); fout = NULL; break; default: // 手動操作(キー入力,ファイル記録) fin = NULL; fout = fopen("key.txt", "w"); break; } Game(fin, fout); // ゲーム本体(引数の再追加) // ファイルの後始末 if (fin != NULL) fclose(fin); // 入力用 if (fout != NULL) fclose(fout); // 記録用 } ... }
操作:タイトル画面で [F]キー.
これで直前に作成した軌跡がアニメーションとして再生されるハズ. または,キーシーケンスファイルkey.txt を手動作成しておけば, その内容通りの自動操作も可能.
長いキーシーケンスをすべて手動で作成するのは大変です. スクリプト言語を利用して, キーシーケンスを効率的に生成できるようにしたい.
とりあえず,ペン状態切替コマンド(on,off)と ペン移動コマンド(up,down,left,right)を 実装してみよう.
ソース pdcl.c:(ver.1,ダウンロード)
#include <stdio.h> #include <stdlib.h> // atoi() #include <string.h> // strtok() #define BUFLEN 256 // バッファ文字列のサイズ #define ARGS 16 // コマンド引数の個数の最大値 int proc(FILE *fp) { int key; // 出力キーの文字 char line[BUFLEN]; // 入力行(コマンドライン)文字列のバッファ int argc; // コマンド引数の個数 char *argv[ARGS]; // コマンド引数の配列 char *cmd; // コマンド名(argv[0] のコピー) char *tok, *p; // strtok()の作業用ポインタ int n; // コマンドの反復回数 int i; // カウンタ while (1) { // スクリプトの入力 if (fgets(line, BUFLEN, fp) == NULL) break; // スクリプトのトークン分解 p = line; // strtok()の1回目は入力行の先頭を対象とする for (i = 0; i < ARGS; i++) { tok = strtok(p, " \t\n"); // 空白,タブ,改行を区切としてトークンを抽出 // 注意:lineの内容が改変される.区切文字 → 終端記号 p = NULL; // strtok()の2回目以降はline内の残りの部分を対象とする // 残り部分の先頭アドレスが必要なのでは? // はい,strtok()自身がstatic変数として記憶してくれている if (tok == NULL) break; if (tok[0] == '#') break; // コメントを無視 argv[i] = tok; } if (i == 0) continue; argc = i; cmd = argv[0]; // スクリプトの解釈 // ペン状態切替コマンド if (strcmp(cmd, "off") == 0) key='-'; else if (strcmp(cmd, "on") == 0) key='+'; // ペン移動コマンド else if (strcmp(cmd, "up") == 0) key='k'; else if (strcmp(cmd, "down") == 0) key='j'; else if (strcmp(cmd, "left") == 0) key='h'; else if (strcmp(cmd, "right") == 0) key='l'; // その他...シンタックスエラー else continue; // とりあえず無視しておく // pendraw操作用キーシーケンスの出力 if (argc < 2) { // 引数なしの場合 n = 1; // 1回だけ } else { // ありの場合 n = atoi(argv[1]); // 反復回数 } for (i = 0; i < n; i++) { fputc(key, stdout); } } return (EOF); } int main(int argc, char *argv[]) { FILE *fp; int i; for (i = 1; i < argc; i++) { fp = fopen(argv[i], "r"); if (fp == NULL) break; proc(fp); fclose(fp); } fputc('q', stdout); return (EXIT_SUCCESS); }
スクリプトファイルの例 script-1.txt:
down 5 on right 10 up 10 left 10 down 10
コンパイル・実行・操作:
$ cc pdcl.c -o pdcl $ ./pdcl script-1.txt jjjjj+llllllllllkkkkkkkkkkhhhhhhhhhhjjjjjjjjjjq # キーシーケンスが生成された $ ./pdcl script-1.txt > key.txt # キーシーケンスファイルへ保存 $ ./pendraw ... # [F]キーで再生してみよう
よりプログラミング言語らしく, 制御構造コマンド(repeat,end)も追加しよう.
pdcl.c:(ver.2)
#include <stdio.h> // ftell(),fseek() ... int proc(FILE *fp) { ... int n; // コマンドの反復回数 int i; // カウンタ long int pos; // repeatコマンドのファイル内位置 while (1) { ... // スクリプトの解釈 // 制御構造コマンド repeat-end if (strcmp(cmd, "repeat") == 0) { if (argc < 2) { n = 1; } else { n = atoi(argv[1]); } pos = ftell(fp); // ファイル内の現在位置(repeat直後)を記録 for (i = 0; i < n; i++) { if (fseek(fp, pos, SEEK_SET)) goto ERROR; // ファイルを巻き戻す(repeat直後へ) if (proc(fp)) goto ERROR; // repeat-end間を再帰処理(n回反復) } // ... n <= 0 だとバグるのは仕様です continue; } else if (strcmp(cmd, "end") == 0) return (0); // 再帰終了(for内へ戻る) // ペン状態切替コマンド if (strcmp(cmd, "off") == 0) key='-'; ... } ERROR: // エラー処理は未実装 return (EOF); } ...
スクリプトの例 script-2.txt:
up 5 left 10 repeat 4 on right 3 off right 2 end
実行・操作:
$ ./pdcl script-2.txt kkkkkhhhhhhhhhh+lll-ll+lll-ll+lll-ll+lll-llq $ ./pdcl script-2.txt > key.txt $ ./pendraw ...
いちいちキーシーケンスファイルを事前作成するという二度手間は面倒くさい. そこで,スクリプトをフロントエンドから直接実行できるようにしたい. 完全に直接ではないが... ファイル入力ではなくパイプ入力を使えば実現できる.
フロントエンド pendraw 内から バックエンド pdcl を間接的に実行し, その出力データをファイルを介さず, パイプを通して入力してみよう.
pendraw.c:(ver.4)
... /* タイトル画面を表示し,キー入力を待つ. return:入力キー */ int Title(void) { ... mvaddstr( 8, 3, "Input from:"); mvaddstr( 9, 3, " [K] - keyboard"); mvaddstr(10, 3, " [F] - file (key sequence 'key.txt')"); mvaddstr(11, 3, " [P] - pipe (PDCL script 'script.txt')"); refresh(); return (getch()); } int main(void) { ... while (1) { ... switch (key) { case 'f': // 自動操作(ファイル入力) fin = fopen("key.txt", "r"); fout = NULL; break; case 'p': // 自動操作(パイプ入力) fin = popen("./pdcl script.txt", "r"); // pdclの出力キーシーケンスを入力するよ fout = NULL; break; default: // 手動操作(キー入力,ファイル記録) fin = NULL; fout = fopen("key.txt", "w"); break; } Game(fin, fout); // ゲーム本体 if (fin != NULL) { if (key == 'p') { pclose(fin); // パイプの後始末 } else { fclose(fin); // ファイルの後始末 } } if (fout != NULL) fclose(fout); } ... }
なお,ファイルオープン fopen() でファイルとの入出力が可能となるように, パイプオープン popen() では 他のプログラムとの入出力が可能となる. そして,入出力完了後には,後始末として, ファイルクローズ fclose() と同様に, パイプクローズpclose() が必要となる.
実行・操作:
$ vim script.txt ... # スクリプトファイルを作成 $ ./pendraw ... # [P]キーでスクリプトを実行
pendraw ver.4 および pdcl ver.2 を元にして, さらに機能を追加せよ. 次の内から1つ以上に取り組むこと:
オリジナルの図案を生成する PDCL スクリプトを作成せよ.
質問 Q1〜Q4 に回答し,電子メールで提出せよ.
メールの送信形式を必ず「テキスト形式 or プレーンテキスト」に変更せよ.