連携処理3:端末制御

端末(terminal)とは,コンピュータの基本的な機能 (キーボードから命令を受け取り,画面に文字を描く機能) を実現するためのハードウェアやソフトウェアのことである.

コマンドラインの入力に普段使っているアプリケーション, たとえば, Linux の「GNOME 端末(gnome-terminal)」や MacOSX の「ターミナル」等は, 端末ソフトウェアの一種.

これまでのC言語プログラムでは,標準ライブラリの入出力関数 printf( )scanf( ) 等を利用して, 端末の入出力を実行してきた. しかし,これらの関数では,一行単位でしか入出力できないし, 白黒でしか表示できない,等の制約が多かった. 「なーんだC言語では,まったく面白味のないプログラムしか作れないのかー」 いや,それは誤解だ.

端末自身は,キーボードや画面をより自由自在に制御する機能をもっており, C言語の標準ライブラリでも,それらの機能の一部を利用できる. さらに,curses ライブラリを利用すれば, エディタやビデオゲームのような高機能な端末制御プログラム効率的に作成できるようになる.

教科書の該当範囲:第7.8.7節,付録B10
参考書の該当範囲:第13.5節,第13.6節
参考資料:cursesライブラリの超てきとー解説

エスケープシーケンスによる端末制御

端末画面上の任意の位置にカーソルを移動したり, 表示する文字の色を変更したりするには, エスケープシーケンス(escape sequence) と呼ばれる特殊な文字列を利用する.

たとえば,次のようなコマンドを実行すると, 文字の色を変更できる:

$ echo "^[[31m Red"		# '^['の入力方法は下記の通り
Red
$ echo "^[[0m Reset"
Reset
$
端末やシェルの設定を独自にカラフルに変更している場合, 上のコマンドの効果が見えないかもしれない. 次のコマンドで色設定を無効にできるかもしれない:
$ PS1="$ "

ただし,文字列の先頭の記号 '^[' は, [Esc] キーを表わす特殊な文字である. 2 文字の文字列 "^[" ではない. この文字をコマンドラインで入力するためには, 次のように操作する:

[Ctrl]+[v] キー → [Esc] キー

これら Esc 文字から始まる文字列 ( "^[[31m""^[[0m") がエスケープシーケンスである. 代表的なエスケープシーケンスを紹介しておく. なお,この表では,エスケープ文字を「Esc」と表記している.

文字色の変更
Esc[30m
Esc[31m
Esc[32m
Esc[33m
Esc[34m
Esc[35mマゼンタ
Esc[36mシアン
Esc[37m
背景色の変更
Esc[40m
Esc[41m
Esc[42m
Esc[43m
Esc[44m
Esc[45mマゼンタ
Esc[46mシアン
Esc[47m
カーソルの移動
Esc[nAn 行上へ
Esc[nBn 行下へ
Esc[nCn 列右へ
Esc[nDn 列左へ
Esc[n;mHn 行目 m 列目へ
その他
Esc[0m色のリセット
Esc[2J画面のクリア
本授業では端末モデルとして VT100 のみを想定している. 実際には,複数種の端末モデルがあり, エスケープシーケンスは端末モデルにより異なる. なお,MacOSX,Linux,Windows 用のほぼ全ての端末ソフトでは, VT100 がサポートされている...ハズなんだが,完全ではない. (利用できない機能もある.)

これらのエスケープシーケンスは,端末自身がもっている機能なので, C言語でも利用できる. エスケープシーケンスを printf( ) 関数などで出力するだけだ. ここでは,関数ではなく,printf コマンドで試してみよう:

$  printf "クリアして \e[2J"
$  printf "\e[3;0H 3行目で \e[31m 赤にしたり\n"
$  printf "\e[1;10H 1行目へ戻ってみたり\n"
$  printf "\e[5;0H\e[0m 5行目で元通りに\n"
...
これまでと何がちがう? 以前作成のプログラムでは, コマンド行の下の方向にしか実行結果を表示できなかった. 一方,このプログラム(コマンド)では,行に囚われず, 画面上の好きな位置に出力できている.

ここで '\e'Esc を表わす特殊文字である. 改行記号 '\n' などと同様,2文字で1文字を表わす.

実際には '^[' でも OK. (ただし,'^[' を入力できない, とほほなエディタもあるようだ.)

curses ライブラリによる端末制御

エスケープシーケンスの文字列は, 人間が読んでもほとんど意味不明な「呪文(curse)」のようなものであり, それを直接利用してプログラムを作成するなんてことは非常に面倒だ. そこで,この面倒な処理を簡単化するための関数群が curses ライブラリとして用意されている.

現代では,このライブラリは ncurses(new curses) と呼ばれるバージョンに置き換えられている. (元々の curses は Unix 創世記から利用されている.)

このライブラリの関数を利用すると, 端末画面上の任意の位置にカーソルを移動したり, 表示する文字の色を変更したりするプログラム (いわゆるフルスクリーンアプリケーション) が簡単に実現可能になる.

概要

単純な curses プログラム hello.c を作ってみよう:

#include <ncurses.h>

int main(void)
{
	initscr();	// 端末制御の開始

	start_color();	// カラーの設定
	init_pair(1, COLOR_RED, COLOR_BLUE);	// 色番号1を赤文字/青地とする
	bkgd(COLOR_PAIR(1));			// 色1をデフォルト色とする

	erase();	// 画面表示
	move(10, 20);
	addstr("Hello World");
	refresh();

	timeout(-1);
	getch();	// キー入力

	endwin();	// 端末制御の終了
	return (0);
}

これをコンパイルするには,-lncurses オプションを指定し, ライブラリをリンクする必要がある:

$ cc  hello.c  -lncurses  -o hello

実行すると,メッセージが表示され,何かキーを押すと終了する.

ソースを読んでみよう. これまでのプログラムとは全然違うものに見えるだろう. とりあえず今は,細かいことについては,気にしないでよい.

まず,おなじみのヘッダファイル stdio.h とか 標準ライブラリの入出力関数 printf( )scanf( ) とかは, まったく使われていない. その代わり,ncurses.h をインクルードし, curses ライブラリの関数だけを使っている.

画面表示やキー入力のために, 標準ライブラリの標準入出力関数(printf() 等)と curses ライブラリの端末入出力関数(addstr( ) 等)とを 混ぜて使わないこと. 一緒に使ってもエラーにはならないが, 多分,期待した結果にはならない. 注意せよ.

ま,curses の入出力関数があれば, 標準入出力関数を使う必要はないので, 心配する必要もない. 端末入出力には,curses の関数だけを使うこと. ただし,ファイルの入出力の場合には, 端末制御とは無関係なので, 標準ライブラリの入出力関数(fprintf( ) 等)のお世話にもなる.

curses の関数についての詳しくは, 参考資料を参照せよ.

次のセクションから, curses ライブラリの使い方を段階的に学習して行く. ある程度まで理解できたら,自由に改造してみよう.

沢山のサンプルソースコードがあり,面倒臭そうだが, 実際にソースを編集し,実行すること. そして,「何をどうして,どうなったのか?」確認すること. さらに,「どんなことが,どうすればできそうか?」推測しよう.

Step 1. 画面への出力

基本プログラムとして,端末画面に文字列を表示してみよう. 何らかのキーを押すと終了する.

hello-1.c

#include <ncurses.h>

int main(void)
{
	initscr();

	move(10, 20);
	addstr("Hello World");
	getch();

	endwin();
	return (0);
}

これは,端末画面の上から 10 行目,左から 20 桁目の位置に 文字列 "Hello World" を表示するだけのものであり, 何かキーを押すと終了する.

行と列の位置については,0 から数える. つまり,端末的な 10 行目は,世間一般的には 11 行目に相当.

ここで利用されている関数の意味は次の通り:

なお,画面表示の関数としては, addstr() の他に,次のようなものもある:

Step 2. 画面サイズの取得

文字列表示をセンタリングしてみよう.

hello-2.c

#include <ncurses.h>
#include <string.h>

int main(void)
{
	int  x, y, w, h;
	char *str = "Hello World";

	initscr();
	getmaxyx(stdscr, h, w);
	y = h/2;
	x = (w - strlen(str))/2;

	move(y, x);
	addstr(str);
	getch();

	endwin();
	return (0);
}

まず,一度,実行してみよう. そして,端末ウィンドウのサイズを変更してから, 再度,実行してみよう. サイズに応じて,表示位置が自動的に調整されるハズだ.

ただし,この単純すぎるプログラムでは, 実行中にサイズを変えても追従しない. 実行前にサイズを変えること.

次の関数が登場した:

ここで,stdscr は端末画面を表わす定数である. 標準出力 stdout の curses 版だと考えればよい.

ところで,Cの基本がわかっている人は疑問に思うハズだ. なぜ,利用方法が getmaxyx(stdscr, &h, &w) ではなく, getmaxyx(stdscr, h, w) なのか?

関数プロトタイプは getmaxyx(stdscr, ポインタ1, ポインタ2) とすべきではないのか? との思いが胸を去来することもあります.

それは,getmaxyx( ) が実は,関数ではなくマクロであるからだ. 次のような感じに引数付きマクロとして定義されている:

#define getmaxyx(画面, y, x) { y=画面の行数; x=画面の桁数; }

ポインタがわからないドシロートさんでもわかるような親切設計? しかし,Cについて生半可な知識があると,かえって混乱してしまいそうだ. 注意せよ. (このマクロは良い設計なのだろうか?良い子はマネしないこと!)

Step 3. 画面の書き換え

次に,何かキーを押すたびに表示位置を変化するようにしてみよう. [Q]キーを押すと終了する.

hello-3.c

#include <ncurses.h>
#include <string.h>

int main(void)
{
	int  x, y, w, h;
	char *str="Hello World";
	int	key;

	initscr();
//	noecho();		// echo();
//	cbreak();		// nocbreak();
	getmaxyx(stdscr, h, w);
	y = h/2;
	x = (w - strlen(str))/2;

	while (1) {
//		erase();
		move(y, x);
		addstr(str);
//		refresh();

		key = getch();
		if (key == 'q') break;

		y++; if (y >= h) y = 0;
	}
	endwin();
	return (0);
}

無効化(コメント化)されている部分(// 以降)を 有効化しないと(// を外さないと)うまく行かないだろう. まず,コメントのまま実行し,その後,有効化して実行し, 動作を比較してみよう.

次の関数が出てきた:

いくつかの関数は,省略しても変わらない.

ここで,refresh() の意味がよくわからないかもしれない. curses では, 実際の表示用の画面(物理画面,実画面)の他に, 内部的な作業用の画面(論理画面,裏画面)を利用している. 通常,出力の処理は,論理画面だけに対して作用する. つまり,出力しただけでは,実際の画面上には表示されない(場合がある).

表示したい場合には,出力処理の後で,refresh() しよう. これで,論理画面の内容が物理画面へ確実にコピーされ, 実際に見えるようになるハズだ.

つまり,refresh() しないと, 表示したつもりの内容が実際には表示されない可能性がある. ありがちなミスなので注意しよう.

Step 4. アニメーション

今度は,キー入力しなくても自動的に動くようにしてみよう. hello-3.c のコピーを作り,次のように変更しよう. (また後で使うので,hello-3.c は書き換えずに残しておこう.)

hello-4.c

...
#include <unistd.h>		// usleep()

int main(void)
{
	...
	initscr();
	noecho();
	cbreak();
	timeout(0);
	...

	while (1) {
		...
		usleep(100000);
	}
	endwin();
	return (0);
}

次の関数を追加しただけ:

Step 5. キーボードによる操作

前のコードでは,下方向にしか移動できなかった. 今度は,カーソルキーの入力によって上下左右に移動できるようにする. hello-3 を元にして,次のように書き換えよう.

hello-5.c

...

int main(void)
{
	...
	initscr();
	noecho();
	cbreak();
	keypad(stdscr, TRUE);
	...
	while (1) {
		...
		key = getch();
		if (key == 'q') break;
		switch (key) {
		case KEY_UP:	y--; break;
		case KEY_DOWN:	y++; break;
		case KEY_LEFT:	x--; break;
		case KEY_RIGHT:	x++; break;
		}
	}
	endwin();
	return (0);
}

新たな関数はひとつだけ:

さてここで, getch( ) の戻り値の型が char ではなく int だったことの理由を明かそう. ASCII コード(1バイトの数値,0x00 〜 0xFF) だけでは表わしきれない程に多数ある特殊キーにも 番号(4バイトの数値,0x00000100 〜 0xFFFFFFFF)が割り当てられている. また,各特殊キーには, わかり易いマクロ名も定義されている:

Step 6. カラーの利用

今度は,カラーを使ってみる. ソースファイルをゼロから作成しよう.

color.c

#include <ncurses.h>

int main(void)
{
	// 端末の準備
	initscr();

	// 色の準備
	start_color();
	init_pair(1, COLOR_RED, COLOR_BLUE);	// 色1 は青地に赤文字
	init_pair(2, COLOR_GREEN, COLOR_BLUE);	// 色2 は青地に緑文字
	init_pair(3, COLOR_YELLOW, COLOR_BLUE);	// 色3 は青地に黄文字
	init_pair(10, COLOR_WHITE, COLOR_BLUE);	// 色10 は青地に白文字
	bkgd(COLOR_PAIR(10));			// 背景は色10

	// 表示
	attrset(COLOR_PAIR(1));			// 色1 を使う
	mvaddstr(5,  5, "Hello World");
	attrset(COLOR_PAIR(2));			// 色2 を使う
	mvaddstr(5, 25, "Hello World");
	attrset(COLOR_PAIR(3));			// 色3 を使う
	mvaddstr(5, 45, "Hello World");

	attrset(COLOR_PAIR(1) | A_BOLD);		// 色&強調表示
	mvaddstr(6,  5, "Hello World");
	attrset(COLOR_PAIR(2) | A_BOLD);
	mvaddstr(6, 25, "Hello World");
	attrset(COLOR_PAIR(3) | A_BOLD);
	mvaddstr(6, 45, "Hello World");

	attrset(COLOR_PAIR(1) | A_REVERSE);		// 色&反転表示
	mvaddstr(7,  5, "Hello World");
	attrset(COLOR_PAIR(2) | A_REVERSE);
	mvaddstr(7, 25, "Hello World");
	attrset(COLOR_PAIR(3) | A_REVERSE);
	mvaddstr(7, 45, "Hello World");

	attrset(COLOR_PAIR(1) | A_REVERSE | A_BOLD);	// 色&反転&強調
	mvaddstr(8,  5, "Hello World");
	attrset(COLOR_PAIR(2) | A_REVERSE | A_BOLD);
	mvaddstr(8, 25, "Hello World");
	attrset(COLOR_PAIR(3) | A_REVERSE | A_BOLD);
	mvaddstr(8, 45, "Hello World");

	// 終了
	getch();
	endwin();
	return (0);
}
ここで,attrset() の引数の数式にある記号「|」は, 論理和(OR)演算子であり, 複数の属性値を1個の引数にまとめるためのものだ.

端末の種類や設定によっては,うまく働かない機能もあるかもしれないが, あまり気にしないでおこう.

補足:日本語の利用

全角日本語文字列(というかマルチバイト文字列)も利用できる:

#include <ncurses.h>
#include <locale.h>	// setlocale()

int main(,..)
{
	setlocale(LC_ALL, "");		// システム側の言語環境(日本語)を利用するよ
	initscr();
	...
	addstr("こんにちWorld");	// 全角・半角の混合文字列も使えるよ
	...
}

コンパイル方法: (ncurses ではなく,ncursesw ライブラリを使用)

$ cc ... -lncursesw

このように,文字列全体を表示するだけなら,とても簡単だ. しかし,全角文字では,2バイト以上で1文字を表現していたり, 画面上で半角2文字分の幅を使ったりするので, 下の練習問題2のように, 1文字ずつとか1バイトずつに分解して処理することは難しい.

無理ではない.しかし,文字コードに関する高度な知識が必要. したがって,とりあえず, 文字列分解処理が必要な場合には,全角文字列を使わない ってことで妥協しよう. たとえば,今回の練習問題2では, 文字列を日本語に変えると動作保証対象外としてよい.

上級者向け情報: ワイド文字(日本語文字というか英語以外の文字)用のデータ型 wchar_t, マルチバイト列(Cの標準の文字列)とワイド文字列の変換関数 mbtowc(), etc. で検索してみよう.


練習問題

hello-5 のプログラムで左右に移動しているとき, 文字列の一部が画面からハミ出すと表示が乱れてしまう. この対策を施せ.

  1. 画面内からハミ出さないように, 文字列の移動範囲を制限するように改良せよ.
  2. ヒント:画面右側の範囲チェックでは, 文字列の末尾の座標を調べる必要があるだろう.

  3. [上級者用問題]ハミ出した文字が画面の反対側から (右にハミ出ると左から,左にハミ出ると右から) 出てくるように改良せよ.
  4. ヒント:addstr( ) ではなく, addch( ) または mvaddch( ) を利用するとよい. つまり,文字列を一度に出力するのではなく, 一文字ずつ位置を指定しながら出力するとよい. このとき,当然,各文字の出力位置が画面内に収まるようにすること.

範囲のチェックの際,座標を 0 から数えることに注意. たとえば画面の「幅」が w の場合, 画面内の「座標」の範囲は 0wー1 となる. 0 w ではない!!

時間

PCに内蔵されているタイマを利用し, 時計アプリを作ってみよう.

Step 0. 標準ライブラリの時間関数の利用

基本プログラムとして,時刻を標準出力してみる.

clock-0.c

#include <stdio.h>
#include <time.h>	// time(), localtime(), strftime()

/*
現在の時刻文字列を生成する関数
buf:時刻文字列バッファ(参照渡し)
n:文字列バッファのサイズ
*/
void GetTimeStr(char *buf, int n)
{
	time_t		t;	// 現在の unix時刻が入るよ
	struct tm	*tm;	// 時刻要素構造体へのポインタが入るよ

	t = time(NULL);		// 現在の unix時刻を取得
	tm = localtime(&t);	// unix時刻を時刻要素(年月日時分秒)へ分解
	strftime(buf, n, "%H:%M:%S", tm);	// 時刻文字列(時:分:秒)を生成
}

#define BUFLEN	10	// 文字列バッファの配列サイズ

int main(void)
{
	char	buf[BUFLEN];	// 時刻文字列が入るよ

	GetTimeStr(buf, BUFLEN);	// 時刻文字列を取得
	printf("%s\n", buf);		// 時刻文字列を表示

	return (0);
}
static変数の場合, 関数終了時にもメモリ領域が解放されず,データが温存される.
一方,通常の auto変数の場合,データは破棄される. (ゴミとして残る場合もある.)

新出の関数:(#include <time.h>

Step 1. cursesライブラリとの連携

フルスクリーン化してみる.

clock-1.c

// #include <stdio.h>
#include <ncurses.h>
#include <string.h>
#include <time.h>

/*
デジタル時計を表示する関数
s:時刻文字列
*/
void DrawClock(char *s)
{
	int	x, y;		// 表示位置
	int	w, h;		// 画面サイズ

	getmaxyx(stdscr, h, w);	// 画面サイズを取得
	x = (w - strlen(s))/2;	// 中央表示のための位置を算出
	y = h/2;

	mvaddstr(y, x, s);	// 時刻を表示
}

...
void GetTimeStr(char *buf, int n)
{
	...
}

...

int main(void)
{
	char	buf[BUFLEN];
	int	key;		// 入力キー文字が入るよ

	// 端末の初期化
	initscr();
	curs_set(0);		// カーソルは表示しないよ
	noecho();
	cbreak();
	timeout(0);		// キー入力は待たないよ

	// 時計の表示
	while (1) {
		GetTimeStr(buf, BUFLEN);

		erase();
		DrawClock(buf);			// 時刻文字列を表示
		refresh();

		key = getch();
		if (key == 'q') break;		// [Q]キーで終了
	}

	// 終了
	endwin();

	return (0);
}

Step 2. 表示方法の改善

表示方法をお洒落(レトロな7セグメント表示器)にしてみる.

clock-2.c

#include <ncurses.h>
#include <ctype.h>
#include <string.h>
#include <time.h>

#define SEG_W	4	// 7セグ1文字分の幅
#define SEG_H	5	// 7セグ1文字分の高さ

/*
コロンを描く関数
y, x:表示位置
*/
void DrawColon(int y, int x)
{
	mvaddch(y+1, x+1, '#');
	mvaddch(y+3, x+1, '#');
}

/*
7セグメントの文字を表示する関数
y, x:表示位置
seg:発光パターン文字列(7文字+終端記号)
*/
void Draw7SegChar(int y, int x, char *seg)
{
	if (seg[0] == '1') mvaddstr(y+0, x+0, "###");

	if (seg[1] == '1') {
		mvaddstr(y+0, x+0, "#");
		mvaddstr(y+1, x+0, "#");
		mvaddstr(y+2, x+0, "#");
	}

	if (seg[2] == '1') {
		mvaddstr(y+0, x+2, "#");
		mvaddstr(y+1, x+2, "#");
		mvaddstr(y+2, x+2, "#");
	}

	if (seg[3] == '1') mvaddstr(y+2, x+0, "###");

	if (seg[4] == '1') {
		mvaddstr(y+2, x+0, "#");
		mvaddstr(y+3, x+0, "#");
		mvaddstr(y+4, x+0, "#");
	}

	if (seg[5] == '1') {
		mvaddstr(y+2, x+2, "#");
		mvaddstr(y+3, x+2, "#");
		mvaddstr(y+4, x+2, "#");
	}

	if (seg[6] == '1') mvaddstr(y+4, x+0, "###");
}

/*
セグメントの配置:
	0 ━
	1|  |2
	3 ━
	4|  |5
	6 ━
*/

/*
7セグメントのデジタル時計を描く関数
s:時刻文字列
*/
void Draw7SegClock(char *s)
{
	int	x, y;		// 表示位置
	int	w, h;		// 画面サイズ

	static char	*seg[] = {	// 7セグの発光パターン文字列
		"1110111",	// 文字0
		"0010010",	// 文字1
		"1011101",	// 文字2
		"1011011",	// 文字3
		"0111010",	// 文字4
		"1101011",	// 文字5
		"1101111",	// 文字6
		"1010010",	// 文字7
		"1111111",	// 文字8
		"1111011"	// 文字9
	};

	getmaxyx(stdscr, h, w);		// 画面サイズを取得
	x = (w - strlen(s)*SEG_W)/2;	// 中央表示のための位置を算出
	y = (h - SEG_H)/2;

	while (*s != '\0') {
		if (isdigit(*s)) {
			Draw7SegChar(y, x, seg[*s - '0']);	// 数字を描く
		} else {
			DrawColon(y, x);			// コロンを描く
		}
		x += SEG_W;	// 表示位置を次の桁へ
		s++;		// フラグ文字列を次の桁へ
	}
}

...
void GetTimeStr(char *buf, int n)
{
	...
}

...

int main(void)
{
	...
	while (1) {
		...
		erase();
		Draw7SegClock(buf);		// 時刻を7セグ表示
		refresh();
		...
	}
	...
}
なぜ,static変数にしたのか? 通常の変数(auto変数)だと,同じ内容の初期化が何度も (この関数を呼び出す度に)繰り返され,非効率. static なら,初期化が1度だけで済み,効率的.

乱数

最も単純なゲームプログラムの例として,サイコロを作ってみよう.

dice.c

#include <stdio.h>
#include <stdlib.h>	// srand(), rand(), RAND_MAX
// #include <time.h>		// time()

// min 以上,max 以下の整数乱数
int Rand(int min, int max)
{
	return (min + (int)((max - min + 1.0)*rand()/(RAND_MAX + 1.0)));
	// ↑ 最良版:可能な限り等確率な乱数.

	// ↓ 簡易版:等確率ではない(イカサマ),お遊び用ならこれでもOK.
//	return (min + rand()%(max - min + 1));
}

int main(void)
{
	int  i;

//	srand(123);		// 乱数のシャッフル(一定回数→ 再現性のある乱数)
//	srand(time(NULL));	// 乱数のシャッフル(時間的に変化→ 再現性のない乱数)

	for (i = 0; i < 3; i++) {
		printf("%d\n", Rand(1, 6));	// サイコロの目を表示
	}
	return (0);
}
rand() の使用方法(最良版と簡易版)の違いについて... そもそも整数乱数の場合,完璧に等確率とすることは数学的に不可能であり, 各数値の出現確率に多少の凹凸は避けられない.
例:範囲 1〜10 の均等乱数に対する範囲 1〜6 への変換について 10回だけ試行することを考えよう. どのように均等化しようとしても,4個の数値が2回, 2個の数値が1回となり,不均等となるハズである.
簡易版(剰余)の方法では,シワ寄せが最大値方向へ集中し, 最大値側の範囲 5〜6 の出現確率が小さくなる.
最良版(乗除算)の方法では,シワ寄せを全範囲 1〜6 にほぼ均等に分散できる.

まずは,このプログラムを複数回実行してみよう. 何度実行しても,同じ値が出てきてしまうだろう. コンピュータは実際にサイコロを振ったりはできないので, 既定の不規則的な数列から数値を順番に取り出しているだけなのだ.

コンピュータの乱数列は,完全な不規則ではない. 実は規則的に生成されている. 教科書 p.57 を参照せよ.

そこで,シャッフルの処理が必要になる. ただし,シャッフルの回数が同じだと, これまた,同じ値になってしまう. srand(123) を有効化し, 引数の値を色々と変えて, 複数回実行してみよう.

結局,現在時刻でシャッフルするのが 常套手段となっている. srand(time(NULL)) を有効化してみよう. これで,本物のサイコロらしくなったハズだ.

新出の関数:(#include <stdlib.h>

さて,乱数を2個使えば「丁半博打」とか, 3個使えば「スロットマシン」とか,開発できそうだ. curses と組み合わせて実現してみては?


(c) 2018, yanagawa@kushiro-ct.ac.jp