Escape SequnceとC言語を使ったテトリス
概要
ターミナル上で動作するCUIアプリケーションとしてテトリスを作成したので,使用した技術,経験などをまとめてみようと思い書いています.間違っている情報,使い方などが多いと思うのでご指摘または注意して閲覧してください.
また,この記事は私と同じようにC言語とEscape SequenceでCUIアプリケーション作成できるんだと初めて知ってくれる人がいたら嬉しいなという趣旨の記事です.ちなみにncursesライブラリだと矢印キーが使えるのでおすすめです.
経緯
前にCUIアプリケーションを作成した際はncursesライブラリを用いて画面描画を行ったが,YoutubeでASMR Programming: Snake Game, C - No Talkingを視聴し,Escape Sequnceでも制御できることを知ったので試しにテトリスを作成してみることにした.
Escape Sequenceによる制御まとめ
使用したEscape Sequenceによる画面制御をまとめる.使い方は通常のprintf関数のように引数へ渡す.位置指定などでは%iや%dで値を受け取ることもできる.
// Clear terminal
printf("\033[2J\033[H");
// print "NEXT:" texts
printf("\e[%i;%iHNEXT:",5+SCOREROWS,COLS+5);
// print "❤"
printf("\e[%i;%iH❤",starty,2);
| Escape Sequnce | 処理 |
|---|---|
| \033[2J | 画面クリア |
| \033[H | カーソルを左上に持っていく |
| \e[?25l | カーソルを隠す |
| \e[?25h | カーソルを表示する |
| \e[%iB | 現在のカーソル位置から%i行下へ移動 |
| \e[%iC | 現在のカーソル位置から%i列右へ移動 |
| \e[%i;%iH | 二つの%iを用いて,絶対位置の行,列を指定する |
カーソルの絶対位置は環境によって違う可能性があります.
今回は左上が(1,1)でしたが,(0,0)の場合もあるそうです.
盤面配列管理
本テトリスでは,画面描画用の変数を盤面用配列で処理するために値を調節した.下のコードに示すtetYとtetXが描画用に使用している変数で,カーソル内のテトリス盤面に必要な二次元配列をtetYとtetXに加減算を加えることでindexを調整している.
何を基準としている変数なのかがわかりずらく時間を食ったので,結構もっとわかりやすい名前にしておけばよかったなと後悔している.
特に地面か他のブロックに当たったか判定であるisStopTetris()ではx,yを-2 -3としているが一目でわからないマジックナンバーと化しているので良くない.
void printBlock(const int block[][BLOCKSIZE], int tetY, int tetX) {
for(int j = 0; j < BLOCKSIZE; j++) {
int y = tetY - 2 + j;
for(int i = 0; i < BLOCKSIZE; i++) {
int x = tetX - 2 + i;
if( block[j][i] == 1 && starty <= y) {
printf("\e[%i;%iH#",y,x);
}
}
}
}
void clearBlock(int block[][BLOCKSIZE], int tetY, int tetX, int newRotaion, int oldRotation) {
if(newRotaion != oldRotation) {
for(int j = 0; j < BLOCKSIZE; j++) {
int y = tetY - 2 + j;
for(int i = 0; i < BLOCKSIZE; i++) {
// int x = tetX - 2 + i;
if( block[j][i] == 1 && starty < y) {
printf("\e[%i;%iH·",tetY - 2 + BLOCKSIZE - 1 - i, tetX - 2 + j);
}
}
}
}else {
for(int j = 0; j < BLOCKSIZE; j++) {
int y = tetY - 2 + j;
for(int i = 0; i < BLOCKSIZE; i++) {
int x = tetX - 2 + i;
if( block[j][i] == 1 && starty <= y) {
printf("\e[%i;%iH·",y,x);
}
}
}
}
}
int isStopTetris(int board[][COLS],const int block[][BLOCKSIZE],int tetY,int tetX) {
// touch floar or block
for(int j = 0; j < BLOCKSIZE; j ++) {
int y = tetY - 2 -3 + j;
for(int i = 0; i < BLOCKSIZE; i ++) {
int x = tetX - 2 -2 + i;
if(block[j][i] == 1) {
if(y >= 0) {
if(y > ROWS - 2 || board[y+1][x] == 1) { // || board[y+1][x] == 1
return 1;
}
}
}
}
}
return 0;
}
終わりに
経験したこと
- 前回の状態と今回の状態をnew_...,old_...といった変数で持っておくと便利である
- 二次元配列を関数の引数にポインタで渡したい時は,
int(*matrixName)[固定長]で渡せる - うまくいかない時はコードをいじるより設計をよくみた方が早く済む(indexの数値の計算とか)
- 新しいテトリスを降らすような次の処理に入る前とき,初期化すべき変数を決めておくこと
- Escape Sequenceだけでも画面描画できるけど矢印キー使いたいならncursesの方が楽
まとめ
実際には4日間ほどで作成できたが,より情報をわかりやすい形で置いておくこととコードでうまくいかなかったら設計を見直すのが大事だと実感した.とりあえず,Escape Sequenceでテトリスを作れてよかった.