お絵かきアルゴロジック のような プログラミング学習のための エデュテイメントアプリを開発してみよう.
2つのプログラムを連携動作させる:
構成図:
[スクリプト] ⇨ [pdcl] ⇨ [キーシーケンス] ⇦⇨ [pendraw] ⇦⇨ [ユーザ]
スクリプト言語 PDCL(pendraw control language)の仕様:
とりあえず,ペン(カーソル)をキーボードで手動操作し, 線画を描くプログラムを作成しよう. ふつうの 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;
// 2回目以降は入力行内の残りの部分を対象とする
if (tok == NULL) break;
if (tok[0] == '#') break; // コメントを無視
argv[i] = tok;
}
if (i == 0) break;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);
}
...
}
実行・操作:
$ vim script.txt ... # スクリプトファイルを作成 $ ./pendraw ... # [P]キーでスクリプトを実行
基本的に,ファイル入出力処理では, データは,ファイルの先頭から末尾の方向へ順番に連続的に読み書きされる. このような方式は, シーケンシャルアクセス(sequential access)と呼ばれている.
一方,ファイル内でのデータの読み書き位置が不連続的な入出力の方式は, ランダムアクセス(random access)と呼ばれている. これはデータを再読み込みしたり,読み飛ばしたりする場合に必要となる.
Cの標準ライブラリには, ランダムアクセスを実現するための ファイル位置関数が用意されている:
これらの関数についての詳しくは, 教科書 K&R p.311 を参照せよ.