前回が終わった時点でのソースコードを確認しましょう.
#include <stdio.h> #include <stdlib.h> #include <string.h> #define PLAYER_MAX 20 enum result { /* 勝敗情報を表す列挙型 */ RSLT_NOTPLAYED, /* 未対戦 */ RSLT_WIN, /* 勝 */ RSLT_LOSE, /* 負 */ RSLT_NA /* (同一プレイヤー同士なので)対戦しない */ }; typedef struct { char player[PLAYER_MAX + 1]; int table[PLAYER_MAX][PLAYER_MAX]; int player_number; } rrtable; rrtable *initialize(char *player) { rrtable *tab; int i; if (strlen(player) < 2) { printf("Too few players.\n"); exit(1); } if (strlen(player) > PLAYER_MAX) { printf("Too many players.\n"); exit(1); } if ((tab = (rrtable *)malloc(sizeof(rrtable))) == NULL) { printf("Malloc error.\n"); exit(1); } strcpy(tab->player, player); tab->player_number = strlen(player); memset((void *)tab->table, RSLT_NOTPLAYED, PLAYER_MAX * PLAYER_MAX); for (i = 0; i < tab->player_number; i++) { tab->table[i][i] = RSLT_NA; } return tab; } void print_table(rrtable *tab) { int i, j; printf(" %s\n", tab->player); for (i = 0; i < tab->player_number; i++) { putchar(tab->player[i]); for (j = 0; j < tab->player_number; j++) { switch (tab->table[i][j]) { case RSLT_NOTPLAYED: putchar(' '); break; case RSLT_WIN: putchar('o'); break; case RSLT_LOSE: putchar('x'); break; case RSLT_NA: putchar('-'); break; default: putchar('?'); } } putchar ('\n'); } } /* 標準入力から1行読み出して,lineの参照する配列に格納する */ char *get_line(char *line, int max_byte) { int c; int i; /* 1文字ずつ読み出して,lineの配列に格納する */ for (i = 0; i < max_byte && (c = getchar()) != EOF && c != '\n'; i++) { line[i] = c; } line[i] = '\0'; /* 改行になるまで読み飛ばす */ if (i == max_byte) { while ((c = getchar()) != EOF && c != '\n') ; } /* 1文字も読み出せなければNULLを返す */ if (line[0] == '\0') { return NULL; } return line; } /* 文字列playerから文字cを探してその位置を返す */ int search_player_index(rrtable *tab, char c) { int i; char *player = tab->player; for (i = 0; i < strlen(player); i++) { if (c == player[i]) { return i; } } /* 見つからなければ負の数を返す */ return -1; } /* 対戦結果を追加する */ void add_result(rrtable *tab, char *line) { int winner, loser; if (strlen(line) != 3 || line[1] != '>') { printf("%s: ignored.\n", line); return; } winner = search_player_index(tab, line[0]); loser = search_player_index(tab, line[2]); if (winner < 0 || loser < 0) { printf("%s: ignored.\n", line); return; } tab->table[winner][loser] = RSLT_WIN; tab->table[loser][winner] = RSLT_LOSE; } int main(void) { #define LINE_BYTE_MAX 25 char line[LINE_BYTE_MAX + 1]; rrtable *tab; /* 最初の行を読み出し,構造体を初期化する */ get_line(line, LINE_BYTE_MAX + 1); tab = initialize(line); print_table(tab); /* 各行を読み出し,表を埋めていく */ while (get_line(line, LINE_BYTE_MAX + 1)) { if (line[0] == '!') { print_table(tab); } else { add_result(tab, line); } } return 0; }
では実行しましょう.説明のため,実行環境などを以下の通りとします.
次のコマンドで,コンパイルをします.
gcc -o roundrobin roundrobin.c
実行は,こうですね.
./roundrobin
入力ですが,第2回で仕様を決めるときに書いた入力を,打ち込んでみましょう.
WNM
あっと,この時点で,中身が空の表が出てきます.いったんCtrl-Dを押してプログラムを終了させます.ソースファイルを見直してみると,main関数にある,「print_table(tab);」のひとつ*2が不要なことに気づきます.取り除いてコンパイル,実行して,入力をやり直しましょう.
gcc -o roundrobin roundrobin.c ./roundrobin WNM W>N W>M M>N
何も出ません.そうでした,「!」に与えないと,出力しないのでした.では…
! WNM W-oo Nx-x Mxo-
ちゃんと表が出ました.この入力はここまでなので,Ctrl-Dを押して,終了しましょう.
別の入力を試しましょう.総当たりプログラムを書くきっかけになった,とあるWebページの対戦結果を,打ち込んでみます.
./roundrobin ABCDEF A>B B>C D>C E>C F>C A>C D>A A>E E>F F>A B>F F>D E>D D>B B>E !
そうすると,出力は,
ABCDEF A-ooxox Bx-oxoo Cxx-xxx Dooo-xx Exxoo-o Foxoox-
となりました.期待通りの表です.ここでもCtrl-Dで終了させます.以下,「Ctrl-D」をどこで押すかについては省略します.
ではここから,もっといろいろな入力を投入しましょう.正常でない入力に対して,どんな出力になるのかを見ます.そうして仕様の抜けを見つけるとともに,必要に応じて,仕様とソースコードを修正することにします.
この作業はできれば,コードを書き,修正していく人と,様々な入力を試みる人とを別にしたいものです.すると,いろいろ困ったバグが目に見えてくるbe exposedものです.一人でするなら,とにかくいろんな角度,視点で「入力」を考えましょう.
まず,「3人なら3回の対戦で終わらないといけないのに,このプログラムでは回数制限がない」という点に注目して,すでに結果が記録されているのに,同一対戦の結果をまた送るとどうなるか,見てみましょう.
./roundrobin WNM W>N W>M M>N ! WNM W-oo Nx-x Mxo-
ここまでは前のと変わりません.続けて
M>N !
とすると…同じ表になりました.さらに,
N>M !
と,勝敗を反対にするとどうでしょう….
WNM W-oo Nx-o Mxx-
表が変化しました.これって…結果の改ざん?
このように,結果を後で変更できるようにしていいのか,ですが,実用上も,勝敗を記録したあとで,実は勝者と敗者が逆だったので訂正したいというのは,起こり得るのではないでしょうか.ということで,プログラムとしては,この入力を許容することにします.そして仕様に,『「α>β」という形の入力に対して,すでにα対βの対戦結果が登録されていれば,その結果を破棄して新たに記録する』というのを追加しましょう.
別の種類の「不正な入力」を与えます.
./roundrobin WNM W>W ! WNM Wx N - M -
これは,まずいですねえ.「α>α」の形の行,言い換えると,勝者と敗者が同一という入力があってはいけません.仕様を見直すと,
「α>β」(ただし,αとβは1行目にある文字とする)があれば…
とありましたが,これを
「α>β」(ただし,αとβは1行目にある,相異なる文字とする)があれば…
に訂正しましょう.
ソースファイルも修正します.関数add_resultを見直します.winnerは勝者の位置で,loserは敗者の位置ですから,
if (winner < 0 || loser < 0) {
を
if (winner < 0 || loser < 0 || winner == loser) {
./roundrobin WNM W>W W>W: ignored.
これで,入力は不正なものと認識され,無視されました.
「勝者と敗者が同じでいいの?」から,次の疑問も浮かび上がります…「1行目のプレイヤーに,『WNMW』のように重複があったら?」
試してみましょう.
./roundrobin WNMW ! WNMW W- N - M - W -
表は出てきます.でもおかしいですね.ひとつ,勝敗を入れてみますか.
W>N ! WNMW W-o Nx- M - W -
うーん,後者の「W」には結果が書けないようです.
そもそもこんなふうに,プレイヤー名に重複があれば,プログラムが見つけて「そんな入力,ダメですよ」とエラーメッセージを発し,終了すべきでしょう.プログラムを修正します.
どこを見るかというと,initialize関数です.引数playerについて,2つのif文で,仕様に合わない条件を排除していますが,さらに条件を付け加えます.
if (has_same_player(player)) { printf("A player appears twice.\n"); exit(1); }
いきなり,未定義の関数has_same_playerを呼び出しています.慣れないうちは,関数ではなく「文字列の中に,文字の重複がある」というのを判定する処理を書いてしまいがちですが,処理が簡単でなさそうに思えたら,関数名を決めて呼び出すコードを書く習慣をつけましょう.もちろんその後すぐ,その関数を定義します.そうすることで,見通しよくプログラムを書けるようになります.
さて,「文字列の中に,文字の重複がある」というのを判定する関数ですが,「i≠jでかつplayer[i]=player[j]」を見つればいいことに注意して,コードを示します*4.initialize関数のすぐ上に書きます.
/* 文字列playerの中に同一文字が2つ以上あるか判定する */ int has_same_player(char *player) { int i, j; for (i = 0; i < strlen(player); i++) { for (j = i + 1; j < strlen(player); j++) { if (player[i] == player[j]) { return 1; } } } return 0; }
これで問題解決のはずです.コンパイルして,入力を与えてみましょう.
./roundrobin WNMW A player appears twice.
ちゃんと,エラーで終了してくれています.確認のため,正しい入力も.
./roundrobin WNM ! WNM W- N - M -
これでよさそうです.安心する前に,仕様を書き換えておきましょう.『入力の1行目は,全プレイヤーの名前を並べる.文字数(プレイヤー数)は,2以上20以下とする.』の直後に,『各文字は相異なるものとする.』を追加します.
異常な入力としてはほかに,入力の字数がうんと少なかったり,うんと長かったりするのがありますが,いずれも不正な入力とみなしてくれます.
ということで,完了です.おつかれさまでした.
…なのですが,ここで終わったら,プログラミング能力は進歩しません.日ごろの心がけとして,あと2つ,やっておきたいことがあります.次回と次々回で取り上げましょう.