プログラミングC
第10回 演習問題

A問題

演習問題1(文字列の扱い復習)

C言語では、'0', '1', .... '9' の数字であっても、文字として扱う場合はアスキーコードで管理される (例えば scanf() で書式に "%c" や "%s" を使って読み込んだ時など)。
では "123" のように文字列として数字の並びが与えられたとき、これを int型の整数値 (123等)に変換する関数 int myatoi(char *) を作成してみよう。 関数の仕様は以下の通り。
以下に様々な文字列を引数にしてこの関数を呼び出すプログラムを示すので、 main関数は変更せず、末尾に myatoi 関数を追加して 仕様通り動作するようにすること。また、string.h をインクルードしてはならない。ライブラリ関数 atoiも使用しないこと。
(提出ファイル名: prog01.c)

#include <stdio.h>

int myatoi(char *);

int main()
{
  char str2[][20]={ "  2022",
                    " -60.50",
                    "+32m",
                    " 999,654",
                    "+-24",
                    "x86",
                    "2022/11/30",
                    ""};
  int ival, i;

  for ( i=0; str2[i][0]!='\0'; i++ ){
    ival = myatoi( str2[i] );
    printf("string:%s\n", str2[i]);
    printf("value:%d\n", ival);
  }
  return 0;
}
/* この後に関数 myatoi を作成する */
実行例:
% ./a.out
String:  2022
Value:2022
String: -60.50
Value:-60
String:+32m
Value:32
String: 999,654
Value:999
String:+-24
Value:0
String:x86
Value:0
String:2022/11/30
Value:2022
%

B問題

演習問題2(問題1の応用)

日本では西暦年のほかに、昭和xx年や平成xx年という和暦が並行して使われている。 令和元年(1年)は西暦では2019年、平成元年は1989年、昭和元年は1926年、大正元年は1912年、明治元年は1868年 にあたる。
それでは、標準入力から和暦で H30(平成30年の場合)などと入力すると、それが西暦何年かを 計算して表示するプログラムを作成しなさい。
なお、入力は1文字目がアルファベットで R, H, S, T, M がそれぞれ令和、平成、昭和、大正、明治 を表しており、数字との間にスペースなどの区切りは無く、ひと続きの文字列として読み込む。
文字列から数値への変換には問題1で作成した関数 myatoiそのまま コピーして使用し、main関数のみを新たに作成すること(ヒント:ハンドアウトLec05-9, 17)。 入力が実際の年号の範囲に収まっているか(昭和なら64以下か等)はチェックしなくてよい。そのため、H32が西暦2020年と表示されるのは正しいことになる。 なお、入力の1文字目のアルファベットが R, H, S, T, M のいずれでもない場合、また2文字目以降の内容が数字として読み取れない場合はそれぞれその旨のメッセージを表示した上で、前者の場合は exit(1) で、後者の場合はexit(2) で強制終了せよ(最初に stdlib.h をインクルードしておくこと)。
(提出ファイル名: prog02.c)

実行例:

% ./a.out
和暦(H30, R4等): R4
西暦 (A.D.): 2022
% ./a.out
和暦(H30, R4等): H31
西暦 (A.D.): 2019
% ./a.out
和暦(H30, R4等): H32
西暦 (A.D.): 2020
% ./a.out
和暦(H30, R4等): H29
西暦 (A.D.): 2017
% ./a.out
和暦(H30, R4等): S53
西暦 (A.D.): 1978
% echo $?
0
% ./a.out
和暦(H30, R4等): K1
元号の文字が変です
% echo $?
1
% ./a.out
和暦(H30, R4等): TSHR
数字を読み取れません
% echo $?
2
%
[exit()関数とプログラムの終了コード]

exit()関数はハンドアウトのp56、Lec11-24(プログラミング入門パート)やp90、Lec03-8と演習第3回などで紹介・利用されている。exit()関数が呼び出されると、その時点でプログラムは強制終了し、プログラムの呼び出し元であるシェルには終了コードとしてexit()関数の引数が戻る。main()関数でreturn文に到達してプログラムが終了した際も、return文のパラメータが終了コードとなる。

bashでは直前のプログラム・コマンドの終了コードはシェル変数$?に格納されているので、echo $?とすれば表示される。exit()関数の戻り値や、main()関数中のreturn文のパラメータを適切に設定しておけば、終了コードを調べることで、プログラム実行の成否などを確認することができる。多くのコマンドでは、正常終了の場合の終了コードは0、何らかのエラーがあった場合はそれ以外の整数値となっている。

演習問題3(リスト内の検索)

前回(第9回演習)の演習問題2を元に、クラスの一致したもの あるいはID番号の前方一致したものをリストから検索し表示したい。 そのために、前回のディレクトリから今回のディレクトリにprog02.cprog03.cとして、また stulist02.hstulist05.hとしてコピーし、検索表示するための 関数void listprint_sel(char *)を追加する。 またデータとしてStudent.datも前回のディレクトリからコピーしておくこと。 listprint_selは文字列のポインタを引数とし、 この引数の先頭文字が数字('0'から'9')なら、リスト内の IDをsprintfを使って一旦文字列に変換した後strncmpを 使って比較する(前方一致検索)。数字でなければ、引数には クラス名が入っているものとして、strcmpを使うと良い。 strncmp, sprintfの使い方についてはオンラインマニュアル (man)を参照せよ。
(提出ファイル名: prog03.c, stulist05.h)
実行例:
% ./a.out
Head - 
   C1 1301001 AIZU         Taro        
   C2 1301022 BANDAI       Hanako      
   C4 1301033 TSURUGA      Shirou      
   C3 1301004 BANGE        Jiro        
   C5 1301015 WAKAMATSU    Saburo      
   C6 1301036 KORIYAMA     Sakura      
   C1 1301007 FUKUSHIMA    Ichiro      
   C3 1301081 TADAMI       Testuko     
   C2 1301098 HIGASHIYAMA  Tadashi     
   C5 1301099 IWAKI        Youko       
   C3 1301123 SOUMA        Hikaru      
   C5 1301150 KITAKATA     Testuo      
   C5 1301164 SUKAGAWA     Yoshiko     
   C6 1301200 ASHINOMAKI   Masaru      
14 nodes exist in the list.

Input match data (Class or ID) -> 130103
   C4 1301033 TSURUGA      Shirou      
   C6 1301036 KORIYAMA     Sakura      

Input match data (Class or ID) -> 13011
   C3 1301123 SOUMA        Hikaru      
   C5 1301150 KITAKATA     Testuo      
   C5 1301164 SUKAGAWA     Yoshiko     

Input match data (Class or ID) -> C3
   C3 1301004 BANGE        Jiro        
   C3 1301081 TADAMI       Testuko     
   C3 1301123 SOUMA        Hikaru      

Input match data (Class or ID) -> Ctrl+d
%

Extra問題

演習問題4(座標や属性を表す構造体)

チェス盤上に、1 から n までの番号が割り振られた n 匹のアリがいるとする。 チェス盤には横に w 個、縦に h 個のマスがあり、左上隅を白として、白黒のマス目が交互に 並んでいる。以下、左上隅のマスを (x, y) = (1, 1) とし、右に向かって x が増加、 下に向かって y が増加すると考える。

初期状態では、どのアリも盤上のいずれかのマスの中にいて、右または下を向いており、 一マスの中に複数のアリは居なかったものとする。 (図の例だと、チェス盤のサイズが横 5、縦 4 であり、3 匹のアリが (4,1), (1,4), (5,3) にいる。)
時間が1ステップ進むごとに、すべてのアリはその向いている方向に1マス移動する。 移動先がチェス盤の外に出たら、アリは姿を消すとする。
盤上で2匹のアリが同じマスに入った時、アリは以下のような行動をとる。

チェス盤のサイズと初期状態のアリの位置・方向の情報がファイル(ファイル名は コマンドライン引数で指定)から与えられた時、時間とともにアリがどう動くかをシミュレーション して、何ステップ目にどのアリが盤の外に出るか表示するプログラムを作成しなさい。 なお、入力ファイルには、1 行目に横のマス目の数 w、縦のマス目の数 h、アリの数 n が書かれており、2 行目から n+1 行目に各アリの横方向の位置 x、縦方向の位置 y、 アリの向き(右なら 'R'、下なら 'D')が与えられている。

なおプログラムでは、下記のリスト1のように、アリの情報を保持しておくための構造体として AntPos型を定義して使用する。 アリの数の上限は決まっていないので、ファイルに書かれたアリの数に合わせて動的にメモリを 確保して情報を格納すること。使用する変数はリストに示した物で十分なはずだが、 追加しても構わない。


テスト用入力ファイル: Antdata1.txt, Antdata2.txt, Antdata3.txt(図の配置の場合)

(提出ファイル名: prog04.c)

リスト1:
#include <stdio.h>
#include <stdlib.h>

typedef struct {
  int x;        /* アリの位置(横) */
  int y;        /* アリの位置(縦) */
  char dr;      /* 'R':右向き  'D':下向き */
  int in;       /* 1:アリは盤上にいる 0:既に盤外に出ている */
}AntPos;

int main(int argc, char *argv[])
{
  int w, h, n;  /* 幅、高さ、初期状態でのアリの数 */
  int t=0;      /* 時間ステップのカウンタ */
  int i, j, num;
  AntPos *ant;
  FILE *ifile;

  if (argc < 2) {
    printf( "error: input file name is required!\n");
    exit(1);
  }
  /* ファイルオープン */

  /* ファイルの1行目から盤の幅w、高さh、アリの数nを読み込む */

  /* アリの情報を収めておく構造体配列を動的に確保 */

  /* ファイルから各アリの位置と向きを読み込む。なお、文字型を読む時は
     " %c"のように%c手前にスペースを入れると、R/D手前のスペースを読み飛ばせる。
     読み込めたら、確認のため読んだ情報を一度出力しておこう */

  /* 1ステップごとのループ。盤上にいるアリの数が0になったら終了 */
    /* 時間の更新、アリの座標の更新、アリが盤の外に出たかどうかのチェックと表示、
       2匹のアリが同じマスにいるかどうかのチェックと(必要なら)向きの更新 */
    .....

  return 0;
}
実行例:
% ./a.out Antdata1.txt
2 1 D
1 2 R
2 2 R
Step 2: Ant No.3 has gone out to (4,2).     ←2ステップでアリNo.3が盤の外へ
Step 3: Ant No.1 has gone out to (4,2).     ←3ステップでアリNo.1が盤の外へ
Step 3: Ant No.2 has gone out to (2,4).     ←アリNo.2も同時に盤の外へ
% ./a.out Antdata2.txt
3 1 D
2 2 R
1 3 R
Step 4: Ant No.2 has gone out to (6,2).
Step 4: Ant No.3 has gone out to (3,5).
Step 5: Ant No.1 has gone out to (6,3).
% ./a.out Antdata3.txt
4 1 D
1 4 R
5 3 D
Step 2: Ant No.3 has gone out to (5,5).
Step 4: Ant No.2 has gone out to (4,5).
Step 5: Ant No.1 has gone out to (6,4).
%
ヒント:マスの色が白か黒かは、(1,1) が白で、x 座標、y 座標とも 1 増えるごとに 黒、白、黒、白、と変化することを利用すると、容易に計算で判定できます。

演習問題5(ソースコード・リーディング)

今まで、皆さんは演習問題では1つの課題につき、長くても100行を越える程度のプログラムを書いてきました。 こうした100行くらいのプログラムでは、大体作る関数も多くて3~4個であり、何か一つの機能のみを扱うというようなものでした。

実際のソフトウエア製作現場では、100行程度でプログラムが終わることはありません。システムを1つ組み上げるのには、1万行、3万行、10万行とかかるものがあります。みなさんが普段使っているUnixというOSも一つのプログラムですが、そのOSはドライバー等を含めない純粋なシステム部分で10万行、ドライバーを含めると100万行を超えると言われています。

今回の演習では、今までの100行程度のサイズからワンサイズ大きい、500行を超えるプログラムを見てみましょう。

プログラム /home/course/prog1/public_html/2022/ex/ex10/consoletetris.htm は、コンソール上で動くテトリスのプログラムです。 テトリスは皆さんご存知かと思いますが、4つのブロックがくっついた7パターンのブロックの塊が、20x12のサイズのフィールドにランダムに1個ずつ落ちてきて、1行ぴったり埋め尽くすと行のブロックが消える『落ちモノパズル』の代表格ともいえるゲームです。

このプログラムをまずはコンパイルして実行してみてください。

注意事項

・ゲームスタート時のスプライトの表示では外部からファイルを読みますので、ファイル(CentOSでは sprite.txt 、Macでは sprite.txt)をダウンロードして、コンパイルした a.out のバイナリがあるディレクトリと同じところに入れてください。

・ゲーム中のブロックを表現するのに, CentOSとMacでフォントの違いの問題が発生するため(具体的には■が小さく表示される), Macを使用している教室では文字を変更する必要があります。 '圖' 等を使用すると良いと思います。

・また、実際に動かす機種によって、ブロックの落ちるスピードが変わります。これは、その動かすシステム自体の処理速度の影響です。ブロックの落ちるスピードが遅い場合は、
#define FPS 2000
の値を変更して調整するようにしてください。LV1で待ちが3秒から4秒くらいが適正だと思います。

なお、このプログラムには、ハンドアウトには無いものを利用して作っている int kbhit() と言う関数が含まれています。これについては、ブラックボックスとして扱ってもらってよいです。 kbhit() 関数は、キーボードが押されたか押されていないかの判定を行う関数で、押された場合は 1 が、押されていない場合は 0 が常に返るものです。

各関数の仕様は次の通り。

int main()
メイン関数。ゲーム全体の遷移を行う。
void Initialize()
ゲーム起動直後の初期設定を行う関数。画面と壁のデータを初期化

int CreateBlock(); /* 新しいブロックを生成して次のブロックに発生させる */
void ShowGameField(); /* field[][]の中身に応じて、画面を描画する */
void ControlBlock(); /* キー入力に応じてブロックに移動や回転等の処理を行わせる */
int CheckOverlap(int, int); /* 落下中のブロックが壁や固定済みブロックに接触していないか判別 */
void MoveBlock(int, int); /* 落下中ブロックを一旦消して、任意の座標に移動させる */
int TurnRightBlock(); /* ブロックの右回転を処理する */
int TurnLeftBlock(); /* ブロックの左回転を処理する */
void DropBlock(); /* ブロックを落下させる。下に移動できない場合ブロックをその位置に固定 */
void LockBlock(); /* 着地したブロックを固定済みブロックに加える関数 */
void CheckLines(); /* ブロックが横一列にそろえばそこを消去後、上のブロックをそこに下ろす */
int kbhit(); /* キーボードを1回ヒットしたかどうかの判定 */
int GetTimeOfLevel(int); /* レベル毎の時間設定 */
void GameStart(); /* ゲームスタート時のスプライト表示 */

課題

このプログラムを理解して、主要な部分各行にコメントを記入してください。 その際、特に次のことに注意してソースコードにコメントを書くようにしてください。

  1. 各関数の所に、『何を引数として渡すか』、『引数の意味は何か』、『戻り値は何を意味するか』を必ず書く
  2. 関数内の個々の処理について、特に forループは何をするために回しているのかを明確に書いてください。
  3. 外部変数に代入する等の時には、その代入しているものの意味を明確に書いてください。
(提出ファイル名: prog05.c)

演習問題6(新しいゲームの製作)

演習問題5のテトリスを骨組みとして、自分のアイディアを付与した新しいゲームを製作してください。
この時、インクルード宣言、定義、グローバル変数および関数のプロトタイプ宣言は、ヘッダファイルとして分離してください。

また prog05.h の上部に、どこに新しい要素を入れたのかを、コメントとして明確に記してください。
(提出ファイル名: prog06.c, prog06.h)