Lenia の定義

はじめに

前回はセルオートマトンの数学的な定義について述べました.
今回はセルオートマトンを利用したライフゲーム(Game of Life; GoL)を一般化した Lenia-0 の数学的な定義について述べていきます.

Lenia-0 の定義

セルオートマトン  \mathcal{A} は次のような5つ組で定義されました:


\mathcal{A} \triangleq (\mathcal{L}, \mathcal{T}, \mathcal{S}, \mathcal{N}, \phi)

Lenia-0  \mathcal{A}_\text{L0} は次のような5つ組で定義されます:


\mathcal{A}_\text{L0} \triangleq (\Delta \boldsymbol{x}\mathbb{Z}^2, \ \Delta t \mathbb{Z}, \ \Delta p \{0, \dots, P\}, \ \mathbb{B}_1[0], \ \mathbf{A}^{t + \Delta t} \longmapsto [\mathbf{A}^t + \Delta t \ \mathbf{G}^t(\mathbf{K}_\boldsymbol{\beta} * \mathbf{A}^t)]^1_0)

ここで, \mathcal{L} = \Delta \boldsymbol{x} \mathbb{Z}^2 は格子点間の距離を  \Delta \boldsymbol{x} = \dfrac{1}{R} ( R \in \mathbb{N} 空間分解能)とした2次元格子, \mathcal{T} = \Delta t \mathbb{Z} は時間ステップの間隔を  \Delta t = \dfrac{1}{T}( T \in \mathbb{N} 時間分解能)とした時間集合, \mathcal{S} = \Delta p \{0, \dots, P\} は状態精度  \Delta p = \dfrac{1}{P}( P \in \mathbb{N} 状態分解能)の状態集合, \mathcal{N} = \mathbb{B}_1[0] = \{\boldsymbol{x} \in \mathcal{L} \mid \|\boldsymbol{x}\|_2 \leq 1\} は Euclid  L^2-ノルム上の半径1の単位円近傍, \mathbf{A}^{t + \Delta t} \longmapsto [\mathbf{A}^t + \Delta t \ G^t(\mathbf{K}_\boldsymbol{\beta} * \mathbf{A}^t)]^1_0 は局所規則で,


\begin{array}{lcll}
\phi: & \mathcal{S}^\mathcal{N} & \longrightarrow & \mathcal{S}\\
 & \mathbf{A}^t(\mathcal{N}_\boldsymbol{x}) & \longmapsto & \phi(\mathbf{A}^t(\mathcal{N}_\boldsymbol{x})) \triangleq [\mathbf{A}^t(\boldsymbol{x}) + \Delta t \ \mathbf{G}^t(\boldsymbol{x})]^1_0
\end{array}

を表します.

局所規則の詳細

ここでは局所規則についてもう少し詳しく説明します.


\begin{array}{lcll}
\phi: & \mathcal{S}^\mathcal{N} & \longrightarrow & \mathcal{S}\\
 & \mathbf{A}^t(\mathcal{N}_\boldsymbol{x}) & \longmapsto & \phi(\mathbf{A}^t(\mathcal{N}_\boldsymbol{x})) \triangleq [\mathbf{A}^t(\boldsymbol{x}) + \Delta t \ \mathbf{G}^t(\boldsymbol{x})]^1_0
\end{array}

前回説明した内容ではありますが, \mathbf{A}^t(\boldsymbol{x}) は時刻  t \in \mathcal{T} における点  \boldsymbol{x} \in \mathcal{L} の状態を表していて,点  \boldsymbol{x} における近傍を  \mathcal{N}_\boldsymbol{x} \triangleq \{\boldsymbol{x} + \boldsymbol{n} \mid \boldsymbol{n} \in \mathcal{N}\} とすれば,


\mathbf{A}^t(\mathcal{N}_\boldsymbol{x}) \triangleq \{\mathbf{A}^t(\boldsymbol{n}) \mid \boldsymbol{n} \in \mathcal{N}_\boldsymbol{x}\}

は近傍  \mathcal{N}_\boldsymbol{x} 上の状態集合を表します.
また, \mathbf{G}^t(\boldsymbol{x}) は成長分布というものです.
成長分布について説明するために,いくつか準備します.

カーネル

カーネル  \mathbf{K}_\boldsymbol{\beta}: \ \mathcal{N} \longrightarrow \mathcal{S} は,その詳細な「質感」を決定するカーネルコア関数  K_\text{c} \: [0,1] \longrightarrow [0,1] と,全体の「骨格」を決定するカーネルシェル関数  K_\text{s} \: [0,1] \longrightarrow [0,1] によって構成されています.
カーネルコア関数  K_\text{c} は  K_\text{c}(0) = K_\text{c}(1) = 0 を満たす単峰性関数(山が1つであるような関数)となっていて,通常は  K_\text{c}\left(\dfrac{1}{2}\right) = 1 を満たすように構成されます.
カーネルコア関数の引数に中心からの距離  r \in [0, 1] を取ることによって,点の周囲に均一な輪を作ることができます:


K_\text{c}(r) = \left\{
\begin{array}{ll}
e^{\alpha - \frac{\alpha}{4r(1-r)}} & \text{指数関数,} \alpha = 4\\
(4r(1-r))^\alpha & \text{多項式関数,} \alpha = 4\\
\mathbb{I}_{\left[\frac{1}{4}, \frac{3}{4}\right]}(r) & \text{矩形}\\
\cdots & \text{or others}
\end{array}
\right.

ここで, \mathbb{I} はステップ関数です:


\mathbb{I}_A(r) = \left\{
\begin{array}{ll}
1 & r \in A\\
0 & \text{otherwise}
\end{array}
\right.

次に,カーネルシェル関数  K_\text{s} はサイズ  B \in \mathbb{N} のベクトル  \boldsymbol{\beta} = (\beta_1, \dots, \beta_B) \in [0, 1]^B を引数に取って,カーネルコア関数  K_\text{c} をピークの高さ  \beta_i の等距離同心円にコピーするものになっています:


K_\text{s}(r; \boldsymbol{\beta}) = \beta_{\lfloor Br \rfloor} K_\text{c}(Br \mod 1)

最後に,カーネル  \mathbf{K}_\boldsymbol{\beta} として次のように正規化します:


\mathbf{K}_\boldsymbol{\beta}(\boldsymbol{n}) \triangleq \dfrac{K_\text{s}(\|\boldsymbol{n}\|_2; \boldsymbol{\beta})}{|K_\text{s}|}, \quad \boldsymbol{n} \in \mathcal{N}

ここで, |K_\text{s}| は  |K_\text{s}| = \displaystyle\sum_{\boldsymbol{n} \in \mathcal{N}} K_\text{s}(\|\boldsymbol{n}\|_2; \boldsymbol{\beta}) \Delta \boldsymbol{x}^2 を表しています.

まとめると,カーネル  \mathbf{K}_\boldsymbol{\beta} は次のように定義されます:


\begin{array}{llll}
\mathbf{K}_\boldsymbol{\beta}: & \mathcal{N} & \longrightarrow & \mathcal{S}\\
 & \boldsymbol{n} & \longmapsto & \mathbf{K}_\boldsymbol{\beta}(\boldsymbol{n}) \triangleq \dfrac{K_\text{s}(\|\boldsymbol{n}\|_2; \boldsymbol{\beta})}{|K_\text{s}|}
\end{array}

ポテンシャル分布

カーネル  \mathbf{K}_\boldsymbol{\beta}:\ \mathcal{N} \longrightarrow \mathcal{S} と,時刻  t \in \mathcal{T} における点  \boldsymbol{x} \in \mathcal{L} の状態  \mathbf{A}^t(\boldsymbol{x}) とで畳み込み  \mathbf{K}_\boldsymbol{\beta} * \mathbf{A}^t(\boldsymbol{x}) を行うことで,ポテンシャル分布   \mathbf{U}^t が得られます:


\begin{align}
\mathbf{U}^t(\boldsymbol{x}) &= \mathbf{K}_\boldsymbol{\beta} * \mathbf{A}^t(\boldsymbol{x})\\
 &\triangleq \sum_{\boldsymbol{n} \in \mathcal{N}} \mathbf{K}_\boldsymbol{\beta}(\boldsymbol{n}) \mathbf{A}^t (\boldsymbol{x} + \boldsymbol{n}) \Delta \boldsymbol{x}^2
\end{align}

成長分布

成長分布を考える前に,成長関数  G: \ [0,1] \longrightarrow [-1, 1] について考えます.
成長関数  G は  G(\mu)=1 を満たす,パラメータ  \mu,\ \sigma \in \mathbb{R}(成長中心,成長幅)を持つ単峰性非単調関数がよく用いられています:


G(u; \mu, \sigma) = \left\{
\begin{array}{ll}
2e^{-\frac{(u-\mu)}{2\sigma^2}}-1 & \text{指数関数}\\
2 \mathbb{I}_{[\mu \pm 3\sigma]}(u) \left(1 - \dfrac{(u-\mu)^2}{9\sigma^2}\right)^\alpha - 1 & \text{多項式関数,} \alpha=4\\
2 \mathbb{I}_{[\mu \pm \sigma]}(u) - 1 & \text{矩形}\\
\dots & \text{or others}
\end{array}
\right.

先程定義したポテンシャル分布  \mathbf{U}^t を成長関数  G:\ [0,1] \longrightarrow [-1,1] に与えることで,成長分布  \mathbf{G}^t が得られます:


\mathbf{G}^t(\boldsymbol{x}) = G(\mathbf{U}^t(\boldsymbol{x}))

状態更新

定義した局所規則から状態更新の式を考えると,


\begin{align}
\mathbf{A}^{t + \Delta t}(\boldsymbol{x}) &= \Phi(\mathbf{A}^t)(\boldsymbol{x})\\
 &= \phi(\mathbf{A}^t(\mathcal{N}_\boldsymbol{x}))\\
 &= [\mathbf{A}^t(\boldsymbol{x}) + \Delta t \ \mathbf{G}^t(\boldsymbol{x})]^1_0
\end{align}

となるため,


\mathbf{A}^{t + \Delta t}(\boldsymbol{x}) = [\mathbf{A}^t(\boldsymbol{x}) + \Delta t \ \mathbf{G}^t(\boldsymbol{x})]^1_0

と表すことができます.
ここで, [\cdot]^b_a は clip 関数で,


[\cdot]^b_a \triangleq \min\{\max\{\cdot, a\}, b\}

と定義されます.

おわりに

Lenia-0 の数学的な定義を述べました.
次の記事の内容は未定です.

セルオートマトンの定義

はじめに

ここではセルオートマトン(Cellular Automata; CA)という数理モデルを数学的に定義していきます.
セルオートマトンがどのようなものか知りたい人はここから確認できます.

セルオートマトンの定義

セルオートマトン  \mathcal{A} は次の5つ組で定義されます:

 
\mathcal{A} \triangleq (\mathcal{L}, \mathcal{T}, \mathcal{S}, \mathcal{N}, \phi)

ここで, \mathcal{L} は  d 次元格子, \mathcal{S} は状態集合, \mathcal{T} は時間集合, \mathcal{N} は原点近傍, \phi:\ \mathcal{S}^\mathcal{N} \longrightarrow \mathcal{S} は局所規則を表します.
これらがどのように働いていくのかを説明していきます.

まず,次のような写像  \mathbf{A}^t を考えます:

 
\begin{array}{cccc}
\mathbf{A}^t: & \mathcal{L} & \longrightarrow & \mathcal{S}\\
 & \boldsymbol{x} & \longmapsto & \mathbf{A}^t(\boldsymbol{x})
\end{array}.

ここで, \mathbf{A}^t(\boldsymbol{x}) は時刻  t \in \mathcal{T} における点  \boldsymbol{x} \in \mathcal{L} の状態を表します.
また,点  \boldsymbol{x} における近傍を  \mathcal{N}_\boldsymbol{x} \triangleq \{\boldsymbol{x} + \boldsymbol{n} \mid \boldsymbol{n} \in \mathcal{N}\} とすれば,


\mathbf{A}^t(\mathcal{N}_\boldsymbol{x}) \triangleq \{\mathbf{A}^t(\boldsymbol{n}) \mid \boldsymbol{n} \in \mathcal{N}_\boldsymbol{x}\}

は近傍  \mathcal{N}_\boldsymbol{x} 上の状態集合を表します.

状態更新

局所規則  \phi を基に大域規則  \Phi を次のように定義します:


\Phi:\ \mathcal{S}^\mathcal{L} \longrightarrow \mathcal{S}^\mathcal{L} \quad \text{s.t.} \quad \Phi(\mathbf{A})(\boldsymbol{x}) \triangleq \phi(\mathbf{A}(\mathcal{N}_\boldsymbol{x})).

この大域規則に従って,初期状態  \mathbf{A}^0 から始まり,時間ステップ  \Delta t ごとに状態を更新していきます:


\Phi(\mathbf{A}^0) = \mathbf{A}^{\Delta t},\quad \Phi(\mathbf{A}^{\Delta t}) = \mathbf{A}^{\Delta t + \Delta t},\quad \cdots \quad, \Phi(\mathbf{A}^t) = \mathbf{A}^{t + \Delta t}.

この式から, N 回更新の式が次のように表されます:


\Phi^N(\mathbf{A}^t) = \mathbf{A}^{t + N\Delta t}.

まとめると,このセルオートマトン  \mathcal{A} は次の式に従って状態を更新していくことになります:


\mathbf{A}^{t + \Delta t}(\boldsymbol{x}) = \Phi(\mathbf{A}^t) = \phi(\mathbf{A}(\mathcal{N}_\boldsymbol{x})).

おわりに

セルオートマトンの数学的な定義を述べました.
次の記事では,セルオートマトンを利用したライフゲーム(Game of Life; GoL)を一般化した Lenia の数学的な定義について述べていきます.

お久しぶりです

はじめに

ものすごく久しぶりにこちらの Hatena Blog にログインしました.
今年大学院へ進学したので,今回は現在私がどのような研究をしているのかについてお話させていただけたらいいなと思っています.

研究分野

私の研究分野は複雑系科学の一分野のうち,人工生命(Artificial Life; ALife)というものです.
人工知能(Artificial Intelligence; AI)という言葉には聞き馴染みがあると思いますが,人工知能とはまた違ったものです.

近年で注目を集めている人工知能といえば,たとえば高度な会話機能を持つ Google の大規模言語モデル LaMDA や,入力した文字情報から画像を出力する OpenAI の DALL・E2 などがあります.
これらの人工知能は非常に高い性能を誇っており,今後さらに社会に強い影響を与えていくものと思われます.

しかし,どれだけ人工知能の性能が高くとも,現状の人工知能ができることは「大量の情報に基づいて最適な結果を探し出すこと」です.
つまり,人工知能は最適化問題の解決に関しては強みを得ていますが,一方で,新しいものを開発したりアイディアを出したりする,いわゆる創発的な問題に弱いということです.

そこで,この人工生命という分野では生命の働き方(例えば,自己複製,自己組織化,オープンエンドな進化など)から,「創造性」が必要なすべての分野に対して,その「創造性」を活用するというテーマの研究になります.
主な研究対象は創発現象(emergence)(多数の構成要素の動的な相互作用に基づく機能・構造・振舞いの自律的な出現)となっていて,これが複雑系の本質になっています.

人工生命 Lenia

人工生命ではさまざまな研究対象があるのですが,私はその中でも生物の振舞いに関する研究を対象としています.
そのモデルの一つに Lenia というものがあります.
どんなものなのか,説明するより先に動画を見てみてください.

youtu.be
youtu.be

まるで本物の生き物のように振舞う様子が確認できるかと思います.
このモデルを用いてさまざまなことを実験していきたいと考えています.

vol.2_6 ぷよの回転

はじめに

今回はぷよを回転させていきたいと思います。
ぷよが回転できるようになればぐっとPaizaCloudでぷよぷよっぽくなりますね(連鎖は発生しません。)
細かい回転規則にも注意して実装していきましょう。

回転規則

組みぷよの回転規則は次のようになります:
① 回転の軸は、組ぷよが落下体勢に入った際の下側のぷよが担う。
② 回転先のぷよがフィールドの壁・床・他のぷよに入ってしまう場合は、組ぷよを平行移動させて整合性を保つ。
③ ②が解決されない場合は操作は実現させずに、180度回転となる際に回転が実現する。

①は補足する必要はないと思います。
②、③は補足していきます。

②
例えば、組ぷよがフィールドの右下に来たとき、
(画像)
ここで右回転しようとすると、②の処理なしだとこのようになります:
(画像)
これでは壁にめり込んでしまっているので、左に1マス平行移動することによって整合性を保ちます:
(画像)

ちなみに、
(画像)
このような状態で右回転をしようとすると、
(画像)
上に1マス平行移動することによって整合性を保ちます。

このようにして、狭い箇所での回転を実現します。

③
例えば、図のような状況としましょう。
(画像)
ここで右回転しようとしても組ぷよは横にはなれないので、回転は実現しません。
でも、更に右回転しようとすると、
(画像)
180度回転します。

横回転はできなくても180度回転ができる場合があるので、このような実装が必要となります。

回転を定義(①)

では、①を実装していきましょう!
回転を行うために、少々数学的な思考を使います。

今、図は●が回転するぷよの座標(rx, ry)が指す点、✖が軸となるぷよの座標(ax, ay)が指す点を表しているとします。
(画像)
これを右回転するとして、(rx, ry)が回転した後の座標を(X, Y)とします。
(画像)
ここで注意したいことは、ターミナルは半角を1マスとして定義しているので、マスが縦長になっていることです。
目に見える軌跡は円を描いて欲しいので、マス目を正方形に整形してグラフに描くとこのような軌跡になります。
(画像)
これは、中心(ax, ay)、長径4マス、短径2マスの楕円を表しています。
この楕円を中心が原点に来るように平行移動させると、
(画像)
このようになります。
このグラフは、y^2/4 + x^2 = 1 という式で表すことができます。

今からは、点(rx - ax, ry - ay)から原点を中心にθだけ楕円上を移動した点(x', y')を求めます。
(画像)
媒介変数表示して、
rx - ax = 2kcosψ
ry - ay = ksinψ
(k:正の実数, ψ:実数)
加法定理を用いて、
x' = 2kcos(ψ + θ)
= 2k(cosψcosθ - 2ksinψsinθ)
= 2kcosψcosθ - 2ksinψsinθ
= (rx -ax)cosθ - 2(ry - ay)sinθ (∵ rx - ax = 2kcosψ, ry - ay = ksinψ).
∴ x' = (rx -ax)cosθ - 2(ry - ay)sinθ.

y' = ksin(ψ + θ)
= k(sinψcosθ + cosψsinθ)
= ksinψcosθ + kcosψsinθ
= ksinψcosθ + (1/2)*2cosψsinθ (∵ 1 = (1/2)*2)
= (ry - ay)cosθ + (1/2)(rx - ax)sinθ (∵ rx - ax = 2kcosψ, ry - ay = ksinψ).
∴ y' = (ry - ay)cosθ + (1/2)(rx - ax)sinθ.

今、楕円を原点に平行移動させてきていたのでもとの場所に戻すと、
X = x' + ax
= (rx -ax)cosθ - 2(ry - ay)sinθ + ax.
∴ X = (rx -ax)cosθ - 2(ry - ay)sinθ + ax.

Y = y' + ay
= (ry - ay)cosθ + (1/2)(rx - ax)sinθ + ay.
∴ Y = (ry - ay)cosθ + (1/2)(rx - ax)sinθ + ay.

このようにして回転移動した先の座標を得ることができました。
これを用いてぷよを回転させるrollPuyof関数を作成していきます。
数学関数を用いるのでヘッダにmath.hをインクルードします。

※math.hをインクルードした場合、コンパイルする際に語尾に -lm を付け加えてください。
 これがないとコンパイルできません!

___game.h____

#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <sys/time.h>
#include <math.h>

#define ...

typedef struct { ... } twinPuyoes_t;

int kbhit(void);
void putColor(int, int, int);
int isPassedTime(unsigned long int, struct timeval *);
twinPuyoes_t initPuyo(void);
void rollPuyof(twinPuyoes_t *, int);

____rollPuyof.c____

#include "game.h"

void rollPuyof(twinPuyoes_t *tsumo, int degree)
{
    //回転前の状態を取得
    twinPuyoes_t before = *tsumo;
    
    //回転座標を代入
    tsumo -> rx = (before.rx - before.ax) * cos(degree * M_PI / 180) - 2 * (before.ry - before.ay) * sin(degree * M_PI / 180) + before.ax;
    tsumo -> ry = (before.ry - before.ay) * cos(degree * M_PI / 180) + (1.0 / 2) * (before.rx - before.ax) * sin(degree * M_PI / 180) + before.ay;
    
    //回転状態も更新
    //右回転
    if(degree == 90){
        tsumo -> angle += ANGLE_90;
        tsumo -> angle %= ANGLE_MAX; //回転状態は360度 = 0度
    }
    //左回転
    else if(degree == -90){
        tsumo -> angle += ANGLE_270;
        tsumo -> angle %= ANGLE_MAX;
    }
}

____puyo.c____

#include "game.h"

int main(void)
{
    //乱数の初期化
    srand((unsigned int)time(NULL));
    
    system("clear"); //画面のクリア

    //フィールドの描画
    for(int y = 1; y <= FIELD_HEIGHT; y++){
        for(int x = 1; x <= FIELD_WIDTH; x++){
            if((y != 1) && (x == 1 || x == FIELD_WIDTH || y == FIELD_HEIGHT))
                putColor(y, x, CELL_TYPE_WALL); //壁・床の描画
        }
        printf("\n");
    }
    
    //ぷよの状態をここに格納、壁・床は含めない。
    //簡略のため画面の座標通りに情報を格納していく。
    int mainField[FIELD_HEIGHT - 1][FIELD_WIDTH - 1];
    //初期化
    for(int y = 1; y < FIELD_HEIGHT; y++){
        for(int x = 1 + 2; x < FIELD_WIDTH - 1; x += 2){
            //空白で埋める
            mainField[y][x] = CELL_TYPE_EMPTY;
        }
    }
    
    //ゲーム部分
    while(1){ ......
    
        while(1){
            //自由落下
            if(isPassedTime(FREE_FALL_TIME_PER_SQUARE, &startTime)){ ...... }
        
            if(kbhit()){
                //キーが入力されたかどうかのフラグ
                int keyFlag = FALSE_BOOL;
                //位置を変更するとき、もとの位置のぷよはクリアしたい
                //もとのぷよの情報をここに保持しておく
                twinPuyoes_t old;
            
                switch(getchar()){
                    case 'a':{ ...... }
                    
                    case 'd':{ ...... }
                
                    case 's':{ ...... }
                    
                    //右回転
                    case 'k':
                        old = tsumo;
                        old.rollPuyoColor = CELL_TYPE_EMPTY;
                        old.axisPuyoColor = CELL_TYPE_EMPTY;
                        rollPuyof(&tsumo, 90);
                        keyFlag = TRUE_BOOL;
                        break;
                    
                    //左回転
                    case 'j':
                        old = tsumo;
                        old.rollPuyoColor = CELL_TYPE_EMPTY;
                        old.axisPuyoColor = CELL_TYPE_EMPTY;
                        rollPuyof(&tsumo, -90);
                        keyFlag = TRUE_BOOL;
                        break;
                        
                    default: break;
                }
            
                if(keyFlag == TRUE_BOOL){
                    //oldの消去
                    putColor(old.ry, old.rx, old.rollPuyoColor);
                    putColor(old.ay, old.ax, old.axisPuyoColor);
                    //ぷよの描画
                    putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor);
                    putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor);
                }
            }
        }
    }
    
    return 0;
}

実行結果:
(コマンド)
~$ gcc -o run kbhit.o initPuyo.o puyo.o rollPuyof.o isPassedTime.o putColor.o -lm
~$ ./run

実行結果は最後にまとめます。

回転の整合性(②)

②を実装していきます。
先ほど作成したrollPuyof関数を更新していきます。
考え方としては、数値上でとりあえず回転させてみて②の状態を満たしていないなら元に戻して、②の状態なら整合性を取るように平行移動させる、といった感じです。

ここの判断自体は簡単なのですが、問題がひとつだけあります。
フィールドの状態を確認するためには配列mainFieldを用いる必要がありますが、これは2次元配列なので関数の引数に渡す際には色々なトラブルが発生してきます。
そこで、なんとかして関数の引数に渡すべく、新たに構造体info_tを宣言して、その中でmainFieldを扱うこととします。
構造体として宣言しておけば関数の引数に渡した時にトラブルが防げます。
(具体的にどんなトラブルが発生するかは、各自でやってみるとわかります。ポインタ難しい。)
また、mainFieldはフィールドの状態のみを保持していましたが、壁・床という判定も盛り込もうと思います。

____game.h____

#include ......

#define ......

typedef struct { ...... } twinPuyoes_t;

typedef struct {
    int mainField[FIELD_HEIGHT + 1][FIELD_WIDTH + 1];
} info_t;

int kbhit(void);
void putColor(int, int, int);
int isPassedTime(unsigned long int, struct timeval *);
twinPuyoes_t initPuyo(void);
void rollPuyof(twinPuyoes_t *, int, info_t);

あとはpuyo.c内で使っていたmainField[y][x]みたいなのをinfo.mainField[y][x]のような形に書き直して、rollPuyof関数の引数を正しくしてあげられればOKです。

____puyo.c____

#include "game.h"

int main(void)
{
    //乱数の初期化
    srand((unsigned int)time(NULL));
    
    system("clear"); //画面のクリア

    //フィールドの描画
    for(int y = 1; y <= FIELD_HEIGHT; y++){
        for(int x = 1; x <= FIELD_WIDTH; x++){
            if((y != 1) && (x == 1 || x == FIELD_WIDTH || y == FIELD_HEIGHT))
                putColor(y, x, CELL_TYPE_WALL); //壁・床の描画
        }
        printf("\n");
    }
    
    //ぷよの状態をここに格納、壁・床は含めない。
    //簡略のため画面の座標通りに情報を格納していく。
    info_t info;
    //初期化
    for(int y = 1; y <= FIELD_HEIGHT; y++){
        for(int x = 1; x <= FIELD_WIDTH; x += 2){
            //壁・床
            if(x == 1 || x == FIELD_WIDTH || y == 1 || y == FIELD_HEIGHT)
                info.mainField[y][x] = CELL_TYPE_WALL;
                
            //空白で埋める
            else{
                info.mainField[y][x] = CELL_TYPE_EMPTY;
            }
        }
    }
    
    //ゲーム部分
    while(1){
        //組みぷよの生成
        twinPuyoes_t tsumo = initPuyo();
    
        //初期位置に設置
        putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor);
        putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor);
        
        struct timeval startTime; //組みぷよ生成時の時間
        gettimeofday(&startTime, NULL); //時間を取得
    
        while(1){
            //自由落下
            if(isPassedTime(FREE_FALL_TIME_PER_SQUARE, &startTime)){
                
                //移動先にぷよがあるか判定
                int puyoFlag = FALSE_BOOL; 
                switch(tsumo.angle){
                    case ANGLE_0:
                        if(info.mainField[tsumo.ay + 1][tsumo.ax] != CELL_TYPE_EMPTY)
                            puyoFlag = TRUE_BOOL;
                        break;
                        
                    case ANGLE_90:
                    case ANGLE_270:
                        if(info.mainField[tsumo.ry + 1][tsumo.rx] != CELL_TYPE_EMPTY || info.mainField[tsumo.ay + 1][tsumo.ax] != CELL_TYPE_EMPTY)
                            puyoFlag = TRUE_BOOL;
                        break;
                    
                    case ANGLE_180:
                        if(info.mainField[tsumo.ry + 1][tsumo.rx] != CELL_TYPE_EMPTY)
                            puyoFlag = TRUE_BOOL;
                        break;
                    
                    default: break;
                }
                
                //移動先が適切でなければ終了
                if(tsumo.ry + 1 == FIELD_HEIGHT || tsumo.ay + 1 == FIELD_HEIGHT || puyoFlag == TRUE_BOOL){
                    //ぷよが設置したら、状態を取得
                    info.mainField[tsumo.ry][tsumo.rx] = tsumo.rollPuyoColor;
                    info.mainField[tsumo.ay][tsumo.ax] = tsumo.axisPuyoColor;
                    break;
                }
            
                twinPuyoes_t old; //再描画用
                old = tsumo;
                old.rollPuyoColor = CELL_TYPE_EMPTY;
                old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                tsumo.ry++;
                tsumo.ay++;
            
                //再描画
                //oldの削除
                putColor(old.ry, old.rx, old.rollPuyoColor);
                putColor(old.ay, old.ax, old.axisPuyoColor);
                //ぷよの描画
                putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor);
                putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor);
            
                //時間をリセット
                gettimeofday(&startTime, NULL);
            }
        
            if(kbhit()){
                //キーが入力されたかどうかのフラグ
                int keyFlag = FALSE_BOOL;
                //位置を変更するとき、もとの位置のぷよはクリアしたい
                //もとのぷよの情報をここに保持しておく
                twinPuyoes_t old;
            
                switch(getchar()){
                    case 'a':{
                        
                        //移動先にぷよがあるか判定
                        int puyoFlag = FALSE_BOOL; 
                        switch(tsumo.angle){
                            case ANGLE_0:
                            case ANGLE_180:
                                if(info.mainField[tsumo.ry][tsumo.rx - 2] != CELL_TYPE_EMPTY || info.mainField[tsumo.ay][tsumo.ax - 2] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;
                            
                            case ANGLE_90:
                                if(info.mainField[tsumo.ay][tsumo.ax - 2] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;
                            
                            case ANGLE_270:
                                if(info.mainField[tsumo.ry][tsumo.rx - 2] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;

                            default: break;
                        }
                    
                        //移動先が適切でなければ終了
                        if(tsumo.rx - 2 == FIELD_START_X || tsumo.ax - 2 == FIELD_START_X || puyoFlag == TRUE_BOOL)
                            break;
                        
                        old = tsumo;
                        old.rollPuyoColor = CELL_TYPE_EMPTY;
                        old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                        tsumo.rx -= 2;
                        tsumo.ax -= 2;
                        keyFlag = TRUE_BOOL;
                        break;
                    }
                    
                    case 'd':{
                    //移動先にぷよがあるか判定
                        int puyoFlag = FALSE_BOOL; 
                        switch(tsumo.angle){
                            case ANGLE_0:
                            case ANGLE_180:
                                if(info.mainField[tsumo.ry][tsumo.rx + 2] != CELL_TYPE_EMPTY || info.mainField[tsumo.ay][tsumo.ax + 2] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;
                            
                            case ANGLE_90:
                                if(info.mainField[tsumo.ay][tsumo.ax + 2] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;
                            
                            case ANGLE_270:
                                if(info.mainField[tsumo.ry][tsumo.rx + 2] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;

                            default: break;
                        }
                        //移動先が適切でなければ終了
                        if(tsumo.rx + 2 == FIELD_WIDTH || tsumo.ax + 2 == FIELD_WIDTH || puyoFlag == TRUE_BOOL)
                            break;
                        
                        old = tsumo;
                        old.rollPuyoColor = CELL_TYPE_EMPTY;
                        old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                        tsumo.rx += 2;
                        tsumo.ax += 2;
                        keyFlag = TRUE_BOOL;
                        break;
                    }
                
                    case 's':{
                        //移動先にぷよがあるか判定
                        int puyoFlag = FALSE_BOOL; 
                        switch(tsumo.angle){
                            case ANGLE_0:
                                if(info.mainField[tsumo.ay + 1][tsumo.ax] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;
                        
                            case ANGLE_90:
                            case ANGLE_270:
                                if(info.mainField[tsumo.ry + 1][tsumo.rx] != CELL_TYPE_EMPTY || info.mainField[tsumo.ay + 1][tsumo.ax] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;
                    
                            case ANGLE_180:
                                if(info.mainField[tsumo.ry + 1][tsumo.rx] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;
                    
                            default: break;
                        }
                        
                        //移動先が適切でなければ終了
                        if(tsumo.ry + 1 == FIELD_HEIGHT || tsumo.ay + 1 == FIELD_HEIGHT || puyoFlag == TRUE_BOOL)
                            break;
                        
                        old = tsumo;
                        old.rollPuyoColor = CELL_TYPE_EMPTY;
                        old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                        tsumo.ry++;
                        tsumo.ay++;
                        keyFlag = TRUE_BOOL;
                        break;
                    }
                    
                    //右回転
                    case 'k':
                        old = tsumo;
                        old.rollPuyoColor = CELL_TYPE_EMPTY;
                        old.axisPuyoColor = CELL_TYPE_EMPTY;
                        rollPuyof(&tsumo, 90, info);
                        keyFlag = TRUE_BOOL;
                        break;
                    
                    //左回転
                    case 'j':
                        old = tsumo;
                        old.rollPuyoColor = CELL_TYPE_EMPTY;
                        old.axisPuyoColor = CELL_TYPE_EMPTY;
                        rollPuyof(&tsumo, -90, info);
                        keyFlag = TRUE_BOOL;
                        break;
                        
                    default: break;
                }
            
                if(keyFlag == TRUE_BOOL){
                    //oldの消去
                    putColor(old.ry, old.rx, old.rollPuyoColor);
                    putColor(old.ay, old.ax, old.axisPuyoColor);
                    //ぷよの描画
                    putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor);
                    putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor);
                }
            }
        }
    }
    
    return 0;
}

では、本題のrollPuyof関数を更新していきましょう。
____rollPuyof.c____

#include "game.h"

void rollPuyof(twinPuyoes_t *tsumo, int degree, info_t info)
{
    //回転前の状態を取得
    twinPuyoes_t before = *tsumo;
    
    //回転座標を代入
    tsumo -> rx = (before.rx - before.ax) * cos(degree * M_PI / 180) - 2 * (before.ry - before.ay) * sin(degree * M_PI / 180) + before.ax;
    tsumo -> ry = (before.ry - before.ay) * cos(degree * M_PI / 180) + (1.0 / 2) * (before.rx - before.ax) * sin(degree * M_PI / 180) + before.ay;
    
    //回転の整合性
    if(info.mainField[tsumo -> ry][tsumo -> rx] != CELL_TYPE_EMPTY){

        //上に平行移動
        //角に引っかかって持ち上がるケース
        if(tsumo -> angle == ANGLE_0 && info.mainField[tsumo -> ry - 1][tsumo -> rx] == CELL_TYPE_EMPTY){
            tsumo -> ry -= 1;
            tsumo -> ay -= 1;
        }
        //ツモが縦向きになるケース
        else if(info.mainField[tsumo -> ry - 1][tsumo -> rx] == info.mainField[tsumo -> ay][tsumo -> ax]){
            tsumo -> ry -= 1;
            tsumo -> ay -= 1;
        }
        
        //左右に平行移動
        else{
            int move = tsumo -> rx - tsumo -> ax;
            tsumo -> rx -= move;
            tsumo -> ax -= move;
        }
    }
    
    //回転状態も更新
    //右回転
    if(degree == 90){
        tsumo -> angle += ANGLE_90;
        tsumo -> angle %= ANGLE_MAX; //回転状態は360度 = 0度
    }
    //左回転
    else if(degree == -90){
        tsumo -> angle += ANGLE_270;
        tsumo -> angle %= ANGLE_MAX;
    }
}

実行結果:
最後にまとめます。

回転の整合性(③)

では最後に180度回転を実装して今回は終了となります。
②を経た結果、軸となるぷよが他の領域に入ってしまった場合にツモの位置を元に戻します。
ただし、すべての情報を元に戻してしまうと180度回転が実装できなくなってしまうので、構造体twinpuyoes_tに180度回転判定用のフラグを追加します。
____game.h____

#include
#define ......

typedef struct {
    int rx, ry; //軸でないぷよ(生成時は上側のぷよ)の座標
    int ax, ay; //軸となるぷよ(生成時は下側のぷよ)の座標
    int rollPuyoColor; //軸でないぷよの色情報
    int axisPuyoColor; //軸となるぷよの色情報
    int angle; //回転状態,
    int turn180Flag; //180度回転待ち判定
} twinPuyoes_t;

......
}

では、本日最後のプログラミングとなります。
____rollPuyof.c____

#include "game.h"

void rollPuyof(twinPuyoes_t *tsumo, int degree, info_t info)
{
    //回転前の状態を取得
    twinPuyoes_t before = *tsumo;
    
    //回転座標を代入
    tsumo -> rx = (before.rx - before.ax) * cos(degree * M_PI / 180) - 2 * (before.ry - before.ay) * sin(degree * M_PI / 180) + before.ax;
    tsumo -> ry = (before.ry - before.ay) * cos(degree * M_PI / 180) + (1.0 / 2) * (before.rx - before.ax) * sin(degree * M_PI / 180) + before.ay;
    
    //回転の整合性
    if(info.mainField[tsumo -> ry][tsumo -> rx] != CELL_TYPE_EMPTY){
        
        //上に平行移動
        //角に引っかかって持ち上がるケース
        if(tsumo -> angle == ANGLE_0 && info.mainField[tsumo -> ry - 1][tsumo -> rx] == CELL_TYPE_EMPTY){
            tsumo -> ry -= 1;
            tsumo -> ay -= 1;
        }
        //ツモが縦向きになるケース
        else if(info.mainField[tsumo -> ry - 1][tsumo -> rx] == info.mainField[tsumo -> ay][tsumo -> ax]){
            tsumo -> ry -= 1;
            tsumo -> ay -= 1;
        }
        
        //左右に平行移動
        else{
            int move = tsumo -> rx - tsumo -> ax;
            tsumo -> rx -= move;
            tsumo -> ax -= move;
        }
    }
    
    //180度回転
    //軸となるぷよが領域に入ってしまったら
    if(info.mainField[tsumo -> ay][tsumo -> ax] != CELL_TYPE_EMPTY){
        //ツモの座標を基に戻す
        *tsumo = before;
        tsumo -> turn180Flag++;
        
        //180度回転になるなら
        if(tsumo -> turn180Flag == ANGLE_180){
            tsumo -> rx = (before.rx - before.ax) * cos(M_PI) - 2 * (before.ry - before.ay) * sin(M_PI) + before.ax;
            tsumo -> ry = (before.ry - before.ay) * cos(M_PI) + (1.0 / 2) * (before.rx - before.ax) * sin(M_PI) + before.ay;
            
            //フラグのリセット
            tsumo -> turn180Flag = ANGLE_0;
        }
    }
    
    //回転状態も更新
    //右回転
    if(degree == 90){
        tsumo -> angle += ANGLE_90;
        tsumo -> angle %= ANGLE_MAX; //回転状態は360度 = 0度
    }
    //左回転
    else if(degree == -90){
        tsumo -> angle += ANGLE_270;
        tsumo -> angle %= ANGLE_MAX;
    }
}

実行結果:
(あとで貼る)

おわりに

ものすごく煩雑としてしまいましたが、なんとか回転も実装することができました。
途中でメインソースで配列mainFieldの仕様変更をしたのでやたら長く感じましたが、ひとまず完成してよかったです。
次回はぷよの設置を完全なものにします。
現状では中途半端に浮くぷよが発生してしまうので、設置が完了したら中途半端なぷよは下まで落下させるようにします。
また、今はカーソルがプレイ中に邪魔になったりしているので、その改善もしていきたいなと思います。

ではでは、お疲れ様でした。

p.s. パソコン壊れました。今は大学のPCから編集等しています。
なので、今は実行結果を貼ることができない環境にあるので、ご容赦くださいな。
また復旧したら実行結果をgif画像でお見せします。

vol.2_5 ぷよが積もるようにする

はじめに

今回は連続して組みぷよを生成し、実際にぷよが積もるようにします。
組みぷよの再生成に伴い、色もランダム化します。

組みぷよの連続生成

単純にwhile文を追加するだけです。
組みぷよの再生成の位置には注意しましょう。

____puyo.c____

#include "game.h"

int main(void)
{
    system("clear"); //画面のクリア

    //フィールドの描画
    for(int y = 1; y <= FIELD_HEIGHT; y++){
        for(int x = 1; x <= FIELD_WIDTH; x++){
            if((y != 1) && (x == 1 || x == FIELD_WIDTH || y == FIELD_HEIGHT))
                putColor(y, x, CELL_TYPE_WALL); //壁・床の描画
        }
        printf("\n");
    }
    
    //ゲーム部分
    
    while(1){
        //組みぷよの生成
        twinPuyoes_t tsumo = {
            INIT_ROLL_PUYO_X, INIT_ROLL_PUYO_Y,
            INIT_AXIS_PUYO_X, INIT_AXIS_PUYO_Y,
            CELL_TYPE_GREEN,
            CELL_TYPE_RED
        };
    
        //初期位置に設置
        putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor);
        putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor);
        
        struct timeval startTime; //組みぷよ生成時の時間
        gettimeofday(&startTime, NULL); //時間を取得
    
        while(1){
            //自由落下
            if(isPassedTime(FREE_FALL_TIME_PER_SQUARE, &startTime)){
                if(tsumo.ry + 1 == FIELD_HEIGHT || tsumo.ay + 1 == FIELD_HEIGHT)
                            break;
            
                twinPuyoes_t old; //再描画用
                old = tsumo;
                old.rollPuyoColor = CELL_TYPE_EMPTY;
                old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                tsumo.ry++;
                tsumo.ay++;
            
                //再描画
                //oldの削除
                putColor(old.ry, old.rx, old.rollPuyoColor);
                putColor(old.ay, old.ax, old.axisPuyoColor);
                //ぷよの描画
                putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor);
                putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor);
            
                //時間をリセット
                gettimeofday(&startTime, NULL);
            }
        
            if(kbhit()){
                //キーが入力されたかどうかのフラグ
                int keyFlag = FALSE_BOOL;
                //位置を変更するとき、もとの位置のぷよはクリアしたい
                //もとのぷよの情報をここに保持しておく
                twinPuyoes_t old;
            
                switch(getchar()){
                    case 'a':
                        //移動先が適切でなければ終了
                        if(tsumo.rx - 2 == FIELD_START_X || tsumo.ax - 2 == FIELD_START_X)
                            break;
                        
                        old = tsumo;
                        old.rollPuyoColor = CELL_TYPE_EMPTY;
                        old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                        tsumo.rx -= 2;
                        tsumo.ax -= 2;
                        keyFlag = TRUE_BOOL;
                        break;
                
                    case 'd':
                        //移動先が適切でなければ終了
                        if(tsumo.rx + 2 == FIELD_WIDTH || tsumo.ax + 2 == FIELD_WIDTH)
                            break;
                        
                        old = tsumo;
                        old.rollPuyoColor = CELL_TYPE_EMPTY;
                        old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                        tsumo.rx += 2;
                        tsumo.ax += 2;
                        keyFlag = TRUE_BOOL;
                        break;
                
                    case 's':
                        //移動先が適切でなければ終了
                        if(tsumo.ry + 1 == FIELD_HEIGHT || tsumo.ay + 1 == FIELD_HEIGHT)
                            break;
                        
                        old = tsumo;
                        old.rollPuyoColor = CELL_TYPE_EMPTY;
                        old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                        tsumo.ry++;
                        tsumo.ay++;
                        keyFlag = TRUE_BOOL;
                        break;
                    
                    default: break;
                }
            
                if(keyFlag == TRUE_BOOL){
                    //oldの消去
                    putColor(old.ry, old.rx, old.rollPuyoColor);
                    putColor(old.ay, old.ax, old.axisPuyoColor);
                    //ぷよの描画
                    putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor);
                    putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor);
                }
            }
        }
    }
    
    return 0;
}

実行結果:
f:id:sion2000114:20191216235901g:plain

ぷよの当たり判定

今現在当たり判定があるのは壁と床のみです。これにぷよを追加します。
ところが壁・床と違い、ぷよはプレイヤーが操作によって任意の場所に置きます。
一定の場所に当たり判定があるわけではないので、設置後の固定されたぷよの座標を取得する必要があります。
まずはその座標の取得から始めましょう。

フィールドの状態を取得

ゲーム開始前に予めフィールドの情報を取得しておくための配列mainFieldを宣言・初期化しておきます。
____puyo.c____(一部)

#include "game.h"

int main(void)
{
    system("clear"); //画面のクリア

    //フィールドの描画
    for(int y = 1; y <= FIELD_HEIGHT; y++){
        for(int x = 1; x <= FIELD_WIDTH; x++){
            if((y != 1) && (x == 1 || x == FIELD_WIDTH || y == FIELD_HEIGHT))
                putColor(y, x, CELL_TYPE_WALL); //壁・床の描画
        }
        printf("\n");
    }
    
    //ぷよの状態をここに格納、壁・床は含めない。
    //簡略のため画面の座標通りに情報を格納していく。
    int mainField[FIELD_HEIGHT - 1][FIELD_WIDTH - 1];
    //初期化
    for(int y = 1; i < FIELD_HEIGHT; y++){
        for(int x = 1 + 2; x < FIELD_WIDTH - 1; x += 2){
            //空白で埋める
            mainField[y][x] = CELL_TYPE_EMPTY;
        }
    }

    //ゲーム部分
    
    while(1){
    ・
    ・
    ・
    (省略)
    ・
    ・
    ・
}

実際に状態を取得するのは設置するときなので、設置のタイミング(2つ目のwhile文)で配列mainFieldを更新していきます。
____puyo.c____(一部)

#include "game.h"

int main(void)
{
    system("clear"); //画面のクリア

    //フィールドの描画
    for(int y = 1; y <= FIELD_HEIGHT; y++){
        for(int x = 1; x <= FIELD_WIDTH; x++){
            if((y != 1) && (x == 1 || x == FIELD_WIDTH || y == FIELD_HEIGHT))
                putColor(y, x, CELL_TYPE_WALL); //壁・床の描画
        }
        printf("\n");
    }
    
    //ぷよの状態をここに格納、壁・床は含めない。
    //簡略のため画面の座標通りに情報を格納していく。
    int mainField[FIELD_HEIGHT - 1][FIELD_WIDTH - 1];
    //初期化
    for(int y = 1; y < FIELD_HEIGHT; y++){
        for(int x = 1 + 2; x < FIELD_WIDTH - 1; x += 2){
            //空白で埋める
            mainField[y][x] = CELL_TYPE_EMPTY;
        }
    }
    
    //ゲーム部分
    while(1){
        //組みぷよの生成
        twinPuyoes_t tsumo = {
            INIT_ROLL_PUYO_X, INIT_ROLL_PUYO_Y,
            INIT_AXIS_PUYO_X, INIT_AXIS_PUYO_Y,
            CELL_TYPE_GREEN,
            CELL_TYPE_RED
        };
    
        //初期位置に設置
        putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor);
        putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor);
        
        struct timeval startTime; //組みぷよ生成時の時間
        gettimeofday(&startTime, NULL); //時間を取得
    
        while(1){
            //自由落下
            if(isPassedTime(FREE_FALL_TIME_PER_SQUARE, &startTime)){
                
                //ここが現在設置を担当している
                if(tsumo.ry + 1 == FIELD_HEIGHT || tsumo.ay + 1 == FIELD_HEIGHT){
                    //ぷよが設置したら、状態を取得
                    mainField[tsumo.ry][tsumo.rx] = tsumo.rollPuyoColor;
                    mainField[tsumo.ay][tsumo.ax] = tsumo.axisPuyoColor;
                    break;
                }
            
                twinPuyoes_t old; //再描画用
                old = tsumo;
                old.rollPuyoColor = CELL_TYPE_EMPTY;
                old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                tsumo.ry++;
                tsumo.ay++;
            
                //再描画
                //oldの削除
                putColor(old.ry, old.rx, old.rollPuyoColor);
                putColor(old.ay, old.ax, old.axisPuyoColor);
                //ぷよの描画
                putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor);
                putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor);
            
                //時間をリセット
                gettimeofday(&startTime, NULL);
            }
        
            if(kbhit()){
   ・
   ・
   ・
   (省略)
   ・
   ・
   ・
}

ぷよの当たり判定を追加

壁・床の当たり判定にぷよの当たり判定を追加します。
配列mainFieldを用いて移動先にぷよがあるかどうかを判定します。
CELL_TYPE_EMPTYであれば移動可能ですし、そうでないなら何かしらぷよがあるということになります。

ところがこれには少々問題があります。
落下しているのは「組みぷよ」なので、例えば上側にあるぷよが下へ移動する際の移動先は下側のぷよになってしまうので正常に働きません。
では、どのようにすればいいのか。

今回の問題を解決する手段としては「移動先側にあるぷよを検知」することです。
先程の例では下側のぷよが「移動先側にあるぷよ」という立ち位置になります。
下へ移動しようとするとき、このぷよを判断材料として扱えば正常に動作します。

一足早いのですが、今後のことも視野に入れ組みぷよの回転状態を定義します。
※まだ回転はさせません。あくまでも「移動先側にあるぷよを検知」するために必要なものです。

回転状態は
・無回転(ANGLE_0) = 0
・右に90度回転(ANGLE_90) = 1
・半回転(ANGLE_180) = 2
・左に90度回転(ANGLE_270) = 3
で定義します。
あと一つ、
・処理用アングル(ANGLE_MAX) = 4
を定義しますが、これは後日解説します。
この回転状態は構造体twinPuyoes_tで保持することとします。

____game.h____

・
・
・
(省略)
・
・
・
#define ANGLE_0 0
#define ANGLE_90 1
#define ANGLE_180 2
#define ANGLE_270 3
#define ANGLE_MAX 4 //処理用

typedef struct {
    int rx, ry; //軸でないぷよ(生成時は上側のぷよ)の座標
    int ax, ay; //軸となるぷよ(生成時は下側のぷよ)の座標
    int rollPuyoColor; //軸でないぷよの色情報
    int axisPuyoColor; //軸となるぷよの色情報
    int angle; //回転状態
} twinPuyoes_t;

int kbhit(void);
void putColor(int, int, int);
int isPassedTime(unsigned long int, struct timeval *);

回転状態を定義したので、組みぷよ生成時に回転状態もANGLE_0で初期化します。

組みぷよがANGLE_0の状態なら
・下に移動⇛下側のぷよ
・左右に移動⇛両方のぷよ
を判断材料とすればよくなります。
以下に自由落下時に移動先にぷよがあるかどうかの判定をしたものを書きます。

                //移動先にぷよがあるか判定
                int puyoFlag = FALSE_BOOL; 
                switch(tsumo.angle){
                    case ANGLE_0:
                        if(mainField[tsumo.ry + 1][tsumo.rx] != CELL_TYPE_EMPTY)
                            puyoFlag = TRUE_BOOL;
                        break;
                        
                    case ANGLE_90:
                    case ANGLE_270:
                        if(mainField[tsumo.ry + 1][tsumo.rx] != CELL_TYPE_EMPTY || mainField[tsumo.ay + 1][tsumo.ax] != CELL_TYPE_EMPTY)
                            puyoFlag = TRUE_BOOL;
                        break;
                    
                    case ANGLE_180:
                        if(mainField[tsumo.ay + 1][tsumo.ax] != CELL_TYPE_EMPTY)
                            puyoFlag = TRUE_BOOL;
                        break;
                    
                    default: break;
                }

                //移動先が適切でなければ終了
                if(tsumo.ry + 1 == FIELD_HEIGHT || tsumo.ay + 1 == FIELD_HEIGHT || puyoFlag == TRUE_BOOL){
                    //ぷよが設置したら、状態を取得
                    mainField[tsumo.ry][tsumo.rx] = tsumo.rollPuyoColor;
                    mainField[tsumo.ay][tsumo.ax] = tsumo.axisPuyoColor;
                    break;
                }

これを基に判定を書き換えて行きましょう!

____puyo.c____

#include "game.h"

int main(void)
{
    system("clear"); //画面のクリア

    //フィールドの描画
    for(int y = 1; y <= FIELD_HEIGHT; y++){
        for(int x = 1; x <= FIELD_WIDTH; x++){
            if((y != 1) && (x == 1 || x == FIELD_WIDTH || y == FIELD_HEIGHT))
                putColor(y, x, CELL_TYPE_WALL); //壁・床の描画
        }
        printf("\n");
    }
    
    //ぷよの状態をここに格納、壁・床は含めない。
    //簡略のため画面の座標通りに情報を格納していく。
    int mainField[FIELD_HEIGHT - 1][FIELD_WIDTH - 1];
    //初期化
    for(int y = 1; y < FIELD_HEIGHT; y++){
        for(int x = 1 + 2; x < FIELD_WIDTH - 1; x += 2){
            //空白で埋める
            mainField[y][x] = CELL_TYPE_EMPTY;
        }
    }
    
    //ゲーム部分
    while(1){
        //組みぷよの生成
        twinPuyoes_t tsumo = {
            INIT_ROLL_PUYO_X, INIT_ROLL_PUYO_Y,
            INIT_AXIS_PUYO_X, INIT_AXIS_PUYO_Y,
            CELL_TYPE_GREEN,
            CELL_TYPE_RED,
            ANGLE_0
        };
    
        //初期位置に設置
        putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor);
        putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor);
        
        struct timeval startTime; //組みぷよ生成時の時間
        gettimeofday(&startTime, NULL); //時間を取得
    
        while(1){
            //自由落下
            if(isPassedTime(FREE_FALL_TIME_PER_SQUARE, &startTime)){
                
                //移動先にぷよがあるか判定
                int puyoFlag = FALSE_BOOL; 
                switch(tsumo.angle){
                    case ANGLE_0:
                        if(mainField[tsumo.ay + 1][tsumo.ax] != CELL_TYPE_EMPTY)
                            puyoFlag = TRUE_BOOL;
                        break;
                        
                    case ANGLE_90:
                    case ANGLE_270:
                        if(mainField[tsumo.ry + 1][tsumo.rx] != CELL_TYPE_EMPTY || mainField[tsumo.ay + 1][tsumo.ax] != CELL_TYPE_EMPTY)
                            puyoFlag = TRUE_BOOL;
                        break;
                    
                    case ANGLE_180:
                        if(mainField[tsumo.ry + 1][tsumo.rx] != CELL_TYPE_EMPTY)
                            puyoFlag = TRUE_BOOL;
                        break;
                    
                    default: break;
                }
                
                //移動先が適切でなければ終了
                if(tsumo.ry + 1 == FIELD_HEIGHT || tsumo.ay + 1 == FIELD_HEIGHT || puyoFlag == TRUE_BOOL){
                    //ぷよが設置したら、状態を取得
                    mainField[tsumo.ry][tsumo.rx] = tsumo.rollPuyoColor;
                    mainField[tsumo.ay][tsumo.ax] = tsumo.axisPuyoColor;
                    break;
                }
            
                twinPuyoes_t old; //再描画用
                old = tsumo;
                old.rollPuyoColor = CELL_TYPE_EMPTY;
                old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                tsumo.ry++;
                tsumo.ay++;
            
                //再描画
                //oldの削除
                putColor(old.ry, old.rx, old.rollPuyoColor);
                putColor(old.ay, old.ax, old.axisPuyoColor);
                //ぷよの描画
                putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor);
                putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor);
            
                //時間をリセット
                gettimeofday(&startTime, NULL);
            }
        
            if(kbhit()){
                //キーが入力されたかどうかのフラグ
                int keyFlag = FALSE_BOOL;
                //位置を変更するとき、もとの位置のぷよはクリアしたい
                //もとのぷよの情報をここに保持しておく
                twinPuyoes_t old;
            
                switch(getchar()){
                    case 'a':{
                        
                        //移動先にぷよがあるか判定
                        int puyoFlag = FALSE_BOOL; 
                        switch(tsumo.angle){
                            case ANGLE_0:
                            case ANGLE_180:
                                if(mainField[tsumo.ry][tsumo.rx - 2] != CELL_TYPE_EMPTY || mainField[tsumo.ay][tsumo.ax - 2] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;
                            
                            case ANGLE_90:
                                if(mainField[tsumo.ay][tsumo.ax - 2] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;
                            
                            case ANGLE_270:
                                if(mainField[tsumo.ry][tsumo.rx - 2] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;

                            default: break;
                        }
                    
                        //移動先が適切でなければ終了
                        if(tsumo.rx - 2 == FIELD_START_X || tsumo.ax - 2 == FIELD_START_X || puyoFlag == TRUE_BOOL)
                            break;
                        
                        old = tsumo;
                        old.rollPuyoColor = CELL_TYPE_EMPTY;
                        old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                        tsumo.rx -= 2;
                        tsumo.ax -= 2;
                        keyFlag = TRUE_BOOL;
                        break;
                    }
                    
                    case 'd':{
                    //移動先にぷよがあるか判定
                        int puyoFlag = FALSE_BOOL; 
                        switch(tsumo.angle){
                            case ANGLE_0:
                            case ANGLE_180:
                                if(mainField[tsumo.ry][tsumo.rx + 2] != CELL_TYPE_EMPTY || mainField[tsumo.ay][tsumo.ax + 2] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;
                            
                            case ANGLE_90:
                                if(mainField[tsumo.ay][tsumo.ax + 2] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;
                            
                            case ANGLE_270:
                                if(mainField[tsumo.ry][tsumo.rx + 2] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;

                            default: break;
                        }
                        //移動先が適切でなければ終了
                        if(tsumo.rx + 2 == FIELD_WIDTH || tsumo.ax + 2 == FIELD_WIDTH || puyoFlag == TRUE_BOOL)
                            break;
                        
                        old = tsumo;
                        old.rollPuyoColor = CELL_TYPE_EMPTY;
                        old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                        tsumo.rx += 2;
                        tsumo.ax += 2;
                        keyFlag = TRUE_BOOL;
                        break;
                    }
                
                    case 's':{
                        //移動先にぷよがあるか判定
                        int puyoFlag = FALSE_BOOL; 
                        switch(tsumo.angle){
                            case ANGLE_0:
                                if(mainField[tsumo.ay + 1][tsumo.ax] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;
                        
                            case ANGLE_90:
                            case ANGLE_270:
                                if(mainField[tsumo.ry + 1][tsumo.rx] != CELL_TYPE_EMPTY || mainField[tsumo.ay + 1][tsumo.ax] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;
                    
                            case ANGLE_180:
                                if(mainField[tsumo.ry + 1][tsumo.rx] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;
                    
                            default: break;
                        }
                        
                        //移動先が適切でなければ終了
                        if(tsumo.ry + 1 == FIELD_HEIGHT || tsumo.ay + 1 == FIELD_HEIGHT || puyoFlag == TRUE_BOOL)
                            break;
                        
                        old = tsumo;
                        old.rollPuyoColor = CELL_TYPE_EMPTY;
                        old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                        tsumo.ry++;
                        tsumo.ay++;
                        keyFlag = TRUE_BOOL;
                        break;
                    }
                    
                    default: break;
                }
            
                if(keyFlag == TRUE_BOOL){
                    //oldの消去
                    putColor(old.ry, old.rx, old.rollPuyoColor);
                    putColor(old.ay, old.ax, old.axisPuyoColor);
                    //ぷよの描画
                    putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor);
                    putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor);
                }
            }
        }
    }
    
    return 0;
}

やたらとmain関数が長くなってしまいましたが、ひとまずは実装完了です。
長くなってしまった部分は、気が向けば関数化させてすっきりさせますネ。
実行結果はこの後の色のランダム化を行ってから貼ります。

組みぷよの色のランダム化

ここまでの流れから相対的に見ると最早おまけのようなものです。
組みぷよをランダムに生成するために、initPuyo関数を作成します。

____initPuyo.c____

#include "game.h"

twinPuyoes_t initPuyo(void)
{
    twinPuyoes_t buf;
    
    buf.rx = INIT_ROLL_PUYO_X;
    buf.ry = INIT_ROLL_PUYO_Y;
    buf.ax = INIT_AXIS_PUYO_X;
    buf.ay = INIT_AXIS_PUYO_Y;
    buf.angle = ANGLE_0;
    
    buf.rollPuyoColor = rand() % 4 + 2;
    buf.axisPuyoColor = rand() % 4 + 2;
    
    return buf;
}

ヘッダにプロトタイプ宣言するのを忘れないようにしましょう。

____game.h____

#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <sys/time.h>

#define TRUE_BOOL 1
#define FALSE_BOOL 0

#define FIELD_HEIGHT 14
#define FIELD_WIDTH 15
#define FIELD_START_X 1

#define INIT_ROLL_PUYO_X 7
#define INIT_ROLL_PUYO_Y 2
#define INIT_AXIS_PUYO_X 7
#define INIT_AXIS_PUYO_Y 3

#define CELL_TYPE_EMPTY 0
#define CELL_TYPE_WALL 1
#define CELL_TYPE_RED 2
#define CELL_TYPE_BLUE 3
#define CELL_TYPE_GREEN 4
#define CELL_TYPE_YELLOW 5
#define CELL_TYPE_PURPLE 6
#define CELL_TYPE_OJAMA 7

#define FREE_FALL_TIME_PER_SQUARE 416000 //自由落下:1マスあたり0.416秒

#define ANGLE_0 0
#define ANGLE_90 1
#define ANGLE_180 2
#define ANGLE_270 3
#define ANGLE_MAX 4 //処理用

typedef struct {
    int rx, ry; //軸でないぷよ(生成時は上側のぷよ)の座標
    int ax, ay; //軸となるぷよ(生成時は下側のぷよ)の座標
    int rollPuyoColor; //軸でないぷよの色情報
    int axisPuyoColor; //軸となるぷよの色情報
    int angle; //回転状態
} twinPuyoes_t;

int kbhit(void);
void putColor(int, int, int);
int isPassedTime(unsigned long int, struct timeval *);
twinPuyoes_t initPuyo(void);

____puyo.c____

#include "game.h"

int main(void)
{
    //乱数の初期化
    srand((unsigned int)time(NULL));
    
    system("clear"); //画面のクリア

    //フィールドの描画
    for(int y = 1; y <= FIELD_HEIGHT; y++){
        for(int x = 1; x <= FIELD_WIDTH; x++){
            if((y != 1) && (x == 1 || x == FIELD_WIDTH || y == FIELD_HEIGHT))
                putColor(y, x, CELL_TYPE_WALL); //壁・床の描画
        }
        printf("\n");
    }
    
    //ぷよの状態をここに格納、壁・床は含めない。
    //簡略のため画面の座標通りに情報を格納していく。
    int mainField[FIELD_HEIGHT - 1][FIELD_WIDTH - 1];
    //初期化
    for(int y = 1; y < FIELD_HEIGHT; y++){
        for(int x = 1 + 2; x < FIELD_WIDTH - 1; x += 2){
            //空白で埋める
            mainField[y][x] = CELL_TYPE_EMPTY;
        }
    }
    
    //ゲーム部分
    while(1){
        //組みぷよの生成
        twinPuyoes_t tsumo = initPuyo();
    
        //初期位置に設置
        putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor);
        putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor);
        
        struct timeval startTime; //組みぷよ生成時の時間
        gettimeofday(&startTime, NULL); //時間を取得
    
        while(1){
            //自由落下
            if(isPassedTime(FREE_FALL_TIME_PER_SQUARE, &startTime)){
                
                //移動先にぷよがあるか判定
                int puyoFlag = FALSE_BOOL; 
                switch(tsumo.angle){
                    case ANGLE_0:
                        if(mainField[tsumo.ay + 1][tsumo.ax] != CELL_TYPE_EMPTY)
                            puyoFlag = TRUE_BOOL;
                        break;
                        
                    case ANGLE_90:
                    case ANGLE_270:
                        if(mainField[tsumo.ry + 1][tsumo.rx] != CELL_TYPE_EMPTY || mainField[tsumo.ay + 1][tsumo.ax] != CELL_TYPE_EMPTY)
                            puyoFlag = TRUE_BOOL;
                        break;
                    
                    case ANGLE_180:
                        if(mainField[tsumo.ry + 1][tsumo.rx] != CELL_TYPE_EMPTY)
                            puyoFlag = TRUE_BOOL;
                        break;
                    
                    default: break;
                }
                
                //移動先が適切でなければ終了
                if(tsumo.ry + 1 == FIELD_HEIGHT || tsumo.ay + 1 == FIELD_HEIGHT || puyoFlag == TRUE_BOOL){
                    //ぷよが設置したら、状態を取得
                    mainField[tsumo.ry][tsumo.rx] = tsumo.rollPuyoColor;
                    mainField[tsumo.ay][tsumo.ax] = tsumo.axisPuyoColor;
                    break;
                }
            
                twinPuyoes_t old; //再描画用
                old = tsumo;
                old.rollPuyoColor = CELL_TYPE_EMPTY;
                old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                tsumo.ry++;
                tsumo.ay++;
            
                //再描画
                //oldの削除
                putColor(old.ry, old.rx, old.rollPuyoColor);
                putColor(old.ay, old.ax, old.axisPuyoColor);
                //ぷよの描画
                putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor);
                putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor);
            
                //時間をリセット
                gettimeofday(&startTime, NULL);
            }
        
            if(kbhit()){
                //キーが入力されたかどうかのフラグ
                int keyFlag = FALSE_BOOL;
                //位置を変更するとき、もとの位置のぷよはクリアしたい
                //もとのぷよの情報をここに保持しておく
                twinPuyoes_t old;
            
                switch(getchar()){
                    case 'a':{
                        
                        //移動先にぷよがあるか判定
                        int puyoFlag = FALSE_BOOL; 
                        switch(tsumo.angle){
                            case ANGLE_0:
                            case ANGLE_180:
                                if(mainField[tsumo.ry][tsumo.rx - 2] != CELL_TYPE_EMPTY || mainField[tsumo.ay][tsumo.ax - 2] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;
                            
                            case ANGLE_90:
                                if(mainField[tsumo.ay][tsumo.ax - 2] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;
                            
                            case ANGLE_270:
                                if(mainField[tsumo.ry][tsumo.rx - 2] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;

                            default: break;
                        }
                    
                        //移動先が適切でなければ終了
                        if(tsumo.rx - 2 == FIELD_START_X || tsumo.ax - 2 == FIELD_START_X || puyoFlag == TRUE_BOOL)
                            break;
                        
                        old = tsumo;
                        old.rollPuyoColor = CELL_TYPE_EMPTY;
                        old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                        tsumo.rx -= 2;
                        tsumo.ax -= 2;
                        keyFlag = TRUE_BOOL;
                        break;
                    }
                    
                    case 'd':{
                    //移動先にぷよがあるか判定
                        int puyoFlag = FALSE_BOOL; 
                        switch(tsumo.angle){
                            case ANGLE_0:
                            case ANGLE_180:
                                if(mainField[tsumo.ry][tsumo.rx + 2] != CELL_TYPE_EMPTY || mainField[tsumo.ay][tsumo.ax + 2] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;
                            
                            case ANGLE_90:
                                if(mainField[tsumo.ay][tsumo.ax + 2] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;
                            
                            case ANGLE_270:
                                if(mainField[tsumo.ry][tsumo.rx + 2] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;

                            default: break;
                        }
                        //移動先が適切でなければ終了
                        if(tsumo.rx + 2 == FIELD_WIDTH || tsumo.ax + 2 == FIELD_WIDTH || puyoFlag == TRUE_BOOL)
                            break;
                        
                        old = tsumo;
                        old.rollPuyoColor = CELL_TYPE_EMPTY;
                        old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                        tsumo.rx += 2;
                        tsumo.ax += 2;
                        keyFlag = TRUE_BOOL;
                        break;
                    }
                
                    case 's':{
                        //移動先にぷよがあるか判定
                        int puyoFlag = FALSE_BOOL; 
                        switch(tsumo.angle){
                            case ANGLE_0:
                                if(mainField[tsumo.ay + 1][tsumo.ax] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;
                        
                            case ANGLE_90:
                            case ANGLE_270:
                                if(mainField[tsumo.ry + 1][tsumo.rx] != CELL_TYPE_EMPTY || mainField[tsumo.ay + 1][tsumo.ax] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;
                    
                            case ANGLE_180:
                                if(mainField[tsumo.ry + 1][tsumo.rx] != CELL_TYPE_EMPTY)
                                    puyoFlag = TRUE_BOOL;
                                break;
                    
                            default: break;
                        }
                        
                        //移動先が適切でなければ終了
                        if(tsumo.ry + 1 == FIELD_HEIGHT || tsumo.ay + 1 == FIELD_HEIGHT || puyoFlag == TRUE_BOOL)
                            break;
                        
                        old = tsumo;
                        old.rollPuyoColor = CELL_TYPE_EMPTY;
                        old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                        tsumo.ry++;
                        tsumo.ay++;
                        keyFlag = TRUE_BOOL;
                        break;
                    }
                    
                    default: break;
                }
            
                if(keyFlag == TRUE_BOOL){
                    //oldの消去
                    putColor(old.ry, old.rx, old.rollPuyoColor);
                    putColor(old.ay, old.ax, old.axisPuyoColor);
                    //ぷよの描画
                    putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor);
                    putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor);
                }
            }
        }
    }
    
    return 0;
}

実行結果:
f:id:sion2000114:20191217000409g:plain

できました!!

おわりに

今回はやることの単純さに反して中々に大変なパートでした。
本来はもっと効率良く動かすようなプログラムを考えるべきなのですが、今はとにかく思いついたことをプログラムに起こしていく事にします。
次回は組みぷよを回転させます。
回転処理はもっと大変そうです。。。
では、お疲れ様でした。

vol.2_4 ぷよの自由落下

はじめに

今回はぷよを自由落下させます。もちろん、自由落下中も操作できるように仕組みます。
細かい設置までの猶予時間等は今回は実装しませんが、実際にぷよを組めるようにします。
まだ回転はできませんし、ぷよの消去等もしません。w

時間経過

現在の状況では、キー入力がなければぷよはその場にとどまることになります。これを単位時間ごとに更新して自由落下するようにします。
これには時間の取得が必要となってきます。
sys/time.hとtime.hをインクルードします。
sys/time.hはtime.hの上位互換的なやつで、time.hでは時間を秒単位で取得しますが、sys/time.hには 構造体timevalが定義されていて、次のようになっています:

struct timeval {
        time_t      tv_sec;     /* 秒 */
        suseconds_t tv_usec;    /* マイクロ秒 */
};

これを利用してぷよの自由落下を実装しましょう!

ぷよは2.4マス/sの速度で自由落下をしていくので、1マス移動するのに1/2.4秒(=0.41666...秒)
だいたい0.416秒を計測するようなものを作成します。

作成の前に今回使う関数とデータ型についての説明から入れます。

gettimeofday関数

・プロトタイプ宣言

int gettimeofday(struct timeval *tv, struct timezone *tz);

・詳細
sys/time.hのインクルードが必要。
現在時刻を秒数と、それに伴うマイクロ秒で取得し第一引数に格納する。
(1970年1月1日0時0分0秒を紀元としてカウント)
※第二引数は現在廃止予定なので、NULLを必ず指定すること!
戻り値:成功時0 失敗時−1

time_t型、suseconds_t型

データ型の一種で、正体はunsigned long int 型です。
printf関数などで出力するときは変換指定子を%ldにしてください。

時間経過を実装

では、指定時間が経過しているかどうかを判断するisPassedTime関数を作成します!

____game.h____

#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <sys/time.h>

#define TRUE_BOOL 1
#define FALSE_BOOL 0
#define FIELD_HEIGHT 14
#define FIELD_WIDTH 15
#define FIELD_START_X 1

#define INIT_ROLL_PUYO_X 7
#define INIT_ROLL_PUYO_Y 2
#define INIT_AXIS_PUYO_X 7
#define INIT_AXIS_PUYO_Y 3

#define CELL_TYPE_EMPTY 0
#define CELL_TYPE_WALL 1
#define CELL_TYPE_RED 2
#define CELL_TYPE_BLUE 3
#define CELL_TYPE_GREEN 4
#define CELL_TYPE_YELLOW 5
#define CELL_TYPE_PURPLE 6
#define CELL_TYPE_OJAMA 7

#define FREE_FALL_TIME_PER_SQUARE 416000 //自由落下:1マスあたり0.416秒

typedef struct {
    int rx, ry; //軸でないぷよ(生成時は上側のぷよ)の座標
    int ax, ay; //軸となるぷよ(生成時は下側のぷよ)の座標
    int rollPuyoColor; //軸でないぷよの色情報
    int axisPuyoColor; //軸となるぷよの色情報
} twinPuyoes_t;

int kbhit(void);
void putColor(int, int, int);
int isPassedTime(unsigend long int, struct timeval *);

____isPassedTime.c____

#include "game.h"

int isPassedTime(unsigned long int usec, struct timeval *basis)
{
    suseconds_t passedTime;
    struct timeval present;
    
    gettimeofday(&present, NULL);
    passedTime = 1000000 * (present.tv_sec - basis->tv_sec) + (present.tv_usec - basis->tv_usec);
    
    if(passedTime < usec) return FALSE_BOOL;
    
    return TRUE_BOOL;    
}

____puyo.c____(isPassedTime関数の実行の確認のため)

#include "game.h"

int main(void)
{
    system("clear"); //画面のクリア
    
    //isPassedTime関数のサンプル実行
    //ここから
    struct timeval basis;
    gettimeofday(&basis, NULL);
    getchar(); //待ち
    
    printf("%d\n", isPassedTime(FREE_FALL_TIME_PER_SQUARE, &basis));
    getchar(); //待ち
    //ここまで
    
    
    //フィールドの描画
    for(int y = 1; y <= FIELD_HEIGHT; y++){
        for(int x = 1; x <= FIELD_WIDTH; x++){
            if((y != 1) && (x == 1 || x == FIELD_WIDTH || y == FIELD_HEIGHT))
                putColor(y, x, CELL_TYPE_WALL); //壁・床の描画
        }
        printf("\n");
    }
    
    //組みぷよの生成
    twinPuyoes_t tsumo = {
        INIT_ROLL_PUYO_X, INIT_ROLL_PUYO_Y,
        INIT_AXIS_PUYO_X, INIT_AXIS_PUYO_Y,
        CELL_TYPE_GREEN,
        CELL_TYPE_RED
    };
    
    //初期位置に設置
    putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor);
    putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor);
    
    //ゲーム部分
    while(1){
        if(kbhit()){
            //キーが入力されたかどうかのフラグ
            int keyFlag = FALSE_BOOL;
            //位置を変更するとき、もとの位置のぷよはクリアしたい
            //もとのぷよの情報をここに保持しておく
            twinPuyoes_t old;
            
            switch(getchar()){
                case 'a':
                    //移動先が適切でなければ終了
                    if(tsumo.rx - 2 == FIELD_START_X || tsumo.ax - 2 == FIELD_START_X)
                        break;
                        
                    old = tsumo;
                    old.rollPuyoColor = CELL_TYPE_EMPTY;
                    old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                    tsumo.rx -= 2;
                    tsumo.ax -= 2;
                    keyFlag = TRUE_BOOL;
                    break;
                
                case 'd':
                    //移動先が適切でなければ終了
                    if(tsumo.rx + 2 == FIELD_WIDTH || tsumo.ax + 2 == FIELD_WIDTH)
                        break;
                        
                    old = tsumo;
                    old.rollPuyoColor = CELL_TYPE_EMPTY;
                    old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                    tsumo.rx += 2;
                    tsumo.ax += 2;
                    keyFlag = TRUE_BOOL;
                    break;
                
                case 's':
                    //移動先が適切でなければ終了
                    if(tsumo.ry + 1 == FIELD_HEIGHT || tsumo.ay + 1 == FIELD_HEIGHT)
                        break;
                        
                    old = tsumo;
                    old.rollPuyoColor = CELL_TYPE_EMPTY;
                    old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                    tsumo.ry++;
                    tsumo.ay++;
                    keyFlag = TRUE_BOOL;
                    break;
                    
                default: break;
            }
            
            if(keyFlag == TRUE_BOOL){
                //oldの消去
                putColor(old.ry, old.rx, old.rollPuyoColor);
                putColor(old.ay, old.ax, old.axisPuyoColor);
                //ぷよの描画
                putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor);
                putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor);
            }
        }
    }
    
    return 0;
}

実行結果:

~$ gcc -c kbhit.c
~$ gcc -c putColor.c
~$ gcc -c isPassedTime.c
~$ gcc -c puyo.c
~$ gcc -o run kbhit.o putColor.o isPassedTime.o puyo.o
~$ ./run
(エンターを0.416秒以内に再び押せば)
0

(エンターを押したのが0.416秒以降であれば)
1

ちゃんと時間経過を取得できていますね。
これを使って次にぷよの自動落下を実装しましょう!

ぷよの自動落下

時間経過が取得できれば自動落下自体は簡単に実装できます。
0.416秒経過したらぷよのy座標を1つ増やせばいいわけですから、構造自体は単純です。
前回同様壁・床の判定はしておきたいのでそこは注意点となります。
再描画等の処理はキー入力の時に作成したものをほぼコピペします。
ゲーム部分を編集していきます。

____puyo.c____

#include "game.h"

int main(void)
{
    system("clear"); //画面のクリア

    //フィールドの描画
    for(int y = 1; y <= FIELD_HEIGHT; y++){
        for(int x = 1; x <= FIELD_WIDTH; x++){
            if((y != 1) && (x == 1 || x == FIELD_WIDTH || y == FIELD_HEIGHT))
                putColor(y, x, CELL_TYPE_WALL); //壁・床の描画
        }
        printf("\n");
    }
    
    //組みぷよの生成
    twinPuyoes_t tsumo = {
        INIT_ROLL_PUYO_X, INIT_ROLL_PUYO_Y,
        INIT_AXIS_PUYO_X, INIT_AXIS_PUYO_Y,
        CELL_TYPE_GREEN,
        CELL_TYPE_RED
    };
    
    //初期位置に設置
    putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor);
    putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor);
    
    //ゲーム部分
    struct timeval startTime; //組みぷよ生成時の時間
    gettimeofday(&startTime, NULL); //時間を取得
    
    while(1){
        //自由落下
        if(isPassedTime(FREE_FALL_TIME_PER_SQUARE, &startTime)){
            if(tsumo.ry + 1 == FIELD_HEIGHT || tsumo.ay + 1 == FIELD_HEIGHT)
                        break;
            
            twinPuyoes_t old; //再描画用
            old = tsumo;
            old.rollPuyoColor = CELL_TYPE_EMPTY;
            old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
            tsumo.ry++;
            tsumo.ay++;
            
            //再描画
            //oldの削除
            putColor(old.ry, old.rx, old.rollPuyoColor);
            putColor(old.ay, old.ax, old.axisPuyoColor);
            //ぷよの描画
            putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor);
            putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor);
            
            //時間をリセット
            gettimeofday(&startTime, NULL);
        }
        
        if(kbhit()){
            //キーが入力されたかどうかのフラグ
            int keyFlag = FALSE_BOOL;
            //位置を変更するとき、もとの位置のぷよはクリアしたい
            //もとのぷよの情報をここに保持しておく
            twinPuyoes_t old;
            
            switch(getchar()){
                case 'a':
                    //移動先が適切でなければ終了
                    if(tsumo.rx - 2 == FIELD_START_X || tsumo.ax - 2 == FIELD_START_X)
                        break;
                        
                    old = tsumo;
                    old.rollPuyoColor = CELL_TYPE_EMPTY;
                    old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                    tsumo.rx -= 2;
                    tsumo.ax -= 2;
                    keyFlag = TRUE_BOOL;
                    break;
                
                case 'd':
                    //移動先が適切でなければ終了
                    if(tsumo.rx + 2 == FIELD_WIDTH || tsumo.ax + 2 == FIELD_WIDTH)
                        break;
                        
                    old = tsumo;
                    old.rollPuyoColor = CELL_TYPE_EMPTY;
                    old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                    tsumo.rx += 2;
                    tsumo.ax += 2;
                    keyFlag = TRUE_BOOL;
                    break;
                
                case 's':
                    //移動先が適切でなければ終了
                    if(tsumo.ry + 1 == FIELD_HEIGHT || tsumo.ay + 1 == FIELD_HEIGHT)
                        break;
                        
                    old = tsumo;
                    old.rollPuyoColor = CELL_TYPE_EMPTY;
                    old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                    tsumo.ry++;
                    tsumo.ay++;
                    keyFlag = TRUE_BOOL;
                    break;
                    
                default: break;
            }
            
            if(keyFlag == TRUE_BOOL){
                //oldの消去
                putColor(old.ry, old.rx, old.rollPuyoColor);
                putColor(old.ay, old.ax, old.axisPuyoColor);
                //ぷよの描画
                putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor);
                putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor);
            }
        }
    }
    
    return 0;
}

実行結果:
f:id:sion2000114:20191216235357g:plain

おわりに

自動で落下するようになっただけでもなんだか進歩が感じられますね。
現状のプログラムではぷよが床にあたってしまうとwhile文をbreakするのでプログラム自体が終了するようになっています。
次回は連続して組みぷよを生成していくとともに、ぷよが実際に積もるようにします。
また、連続して組みぷよを生成するということで、色もランダムにしましょうか。
では、お疲れ様でした。

vol.2_3 ぷよが動くようにする

はじめに

今回はぷよが動けるようにします。前回作ったkbhit関数が役に立ちます。
まずは自由落下や高速落下ではなく、ぷよの座標に応じて適切にぷよが移動するようにします。

ぷよの座標の操作

試作としてa,d,sキーを押すことでぷよが左右・下に動くように実装します。
ぷよが移動する際にもとあった場所の色は黒(CELL_TYPE_EMPTY)で塗るようにします。
そうすれば毎回フィールド全体を更新する必要はありません。
___puyo.c____

#include "game.h"

int main(void)
{
    system("clear"); //画面のクリア
    
    //フィールドの描画
    for(int y = 1; y <= FIELD_HEIGHT; y++){
        for(int x = 1; x <= FIELD_WIDTH; x++){
            if((y != 1) && (x == 1 || x == FIELD_WIDTH || y == FIELD_HEIGHT))
                putColor(y, x, CELL_TYPE_WALL); //壁・床の描画
        }
        printf("\n");
    }
    
    //組みぷよの生成
    twinPuyoes_t tsumo = {
        INIT_ROLL_PUYO_X, INIT_ROLL_PUYO_Y,
        INIT_AXIS_PUYO_X, INIT_AXIS_PUYO_Y,
        CELL_TYPE_GREEN,
        CELL_TYPE_RED
    };
    
    //初期位置に設置
    putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor);
    putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor);
    
    //ゲーム部分
    while(1){
        if(kbhit()){
            //キーが入力されたかどうかのフラグ
            int keyFlag = FALSE_BOOL;
            //位置を変更するとき、もとの位置のぷよはクリアしたい
            //もとのぷよの情報をここに保持しておく
            twinPuyoes_t old;
            
            switch(getchar()){
                case 'a':
                    old = tsumo;
                    old.rollPuyoColor = CELL_TYPE_EMPTY;
                    old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                    tsumo.rx -= 2;
                    tsumo.ax -= 2;
                    keyFlag = TRUE_BOOL;
                    break;
                
                case 'd':
                    old = tsumo;
                    old.rollPuyoColor = CELL_TYPE_EMPTY;
                    old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                    tsumo.rx += 2;
                    tsumo.ax += 2;
                    keyFlag = TRUE_BOOL;
                    break;
                
                case 's':
                    old = tsumo;
                    old.rollPuyoColor = CELL_TYPE_EMPTY;
                    old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                    tsumo.ry++;
                    tsumo.ay++;
                    keyFlag = TRUE_BOOL;
                    break;
                    
                default: break;
            }
            
            if(keyFlag == TRUE_BOOL){
                //oldの消去
                putColor(old.ry, old.rx, old.rollPuyoColor);
                putColor(old.ay, old.ax, old.axisPuyoColor);
                //ぷよの描画
                putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor);
                putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor);
            }
        }
    }
    
    return 0;
}

※実行結果は最後にまとめて表示します

壁・床判定

今のままだとぷよはどこまでも突き進みます。これでは困ってしまうので当たり判定をつけます。
判定自体は簡単で、組みぷよのうちどちらかのぷよの座標が移動先で壁・床にめり込んでしまうなら、座標の変更はしないようにすればOKです。
左側の壁の判定をするために、

#define FIELD_START_X 1

としておきます。
____game.h____

#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>

#define TRUE_BOOL 1
#define FALSE_BOOL 0
#define FIELD_HEIGHT 14
#define FIELD_WIDTH 15
#define FIELD_START_X 1

#define INIT_ROLL_PUYO_X 7
#define INIT_ROLL_PUYO_Y 2
#define INIT_AXIS_PUYO_X 7
#define INIT_AXIS_PUYO_Y 3

#define CELL_TYPE_EMPTY 0
#define CELL_TYPE_WALL 1
#define CELL_TYPE_RED 2
#define CELL_TYPE_BLUE 3
#define CELL_TYPE_GREEN 4
#define CELL_TYPE_YELLOW 5
#define CELL_TYPE_PURPLE 6
#define CELL_TYPE_OJAMA 7

typedef struct {
    int rx, ry; //軸でないぷよ(生成時は上側のぷよ)の座標
    int ax, ay; //軸となるぷよ(生成時は下側のぷよ)の座標
    int rollPuyoColor; //軸でないぷよの色情報
    int axisPuyoColor; //軸となるぷよの色情報
} twinPuyoes_t;

int kbhit(void);
void putColor(int, int, int);

____puyo.c____

#include "game.h"

int main(void)
{
    system("clear"); //画面のクリア
    
    //フィールドの描画
    for(int y = 1; y <= FIELD_HEIGHT; y++){
        for(int x = 1; x <= FIELD_WIDTH; x++){
            if((y != 1) && (x == 1 || x == FIELD_WIDTH || y == FIELD_HEIGHT))
                putColor(y, x, CELL_TYPE_WALL); //壁・床の描画
        }
        printf("\n");
    }
    
    //組みぷよの生成
    twinPuyoes_t tsumo = {
        INIT_ROLL_PUYO_X, INIT_ROLL_PUYO_Y,
        INIT_AXIS_PUYO_X, INIT_AXIS_PUYO_Y,
        CELL_TYPE_GREEN,
        CELL_TYPE_RED
    };
    
    //初期位置に設置
    putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor);
    putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor);
    
    //ゲーム部分
    while(1){
        if(kbhit()){
            //キーが入力されたかどうかのフラグ
            int keyFlag = FALSE_BOOL;
            //位置を変更するとき、もとの位置のぷよはクリアしたい
            //もとのぷよの情報をここに保持しておく
            twinPuyoes_t old;
            
            switch(getchar()){
                case 'a':
                    //移動先が適切でなければ終了
                    if(tsumo.rx - 2 == FIELD_START_X || tsumo.ax - 2 == FIELD_START_X)
                        break;
                        
                    old = tsumo;
                    old.rollPuyoColor = CELL_TYPE_EMPTY;
                    old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                    tsumo.rx -= 2;
                    tsumo.ax -= 2;
                    keyFlag = TRUE_BOOL;
                    break;
                
                case 'd':
                    //移動先が適切でなければ終了
                    if(tsumo.rx + 2 == FIELD_WIDTH || tsumo.ax + 2 == FIELD_WIDTH)
                        break;
                        
                    old = tsumo;
                    old.rollPuyoColor = CELL_TYPE_EMPTY;
                    old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                    tsumo.rx += 2;
                    tsumo.ax += 2;
                    keyFlag = TRUE_BOOL;
                    break;
                
                case 's':
                    //移動先が適切でなければ終了
                    if(tsumo.ry + 1 == FIELD_HEIGHT || tsumo.ay + 1 == FIELD_HEIGHT)
                        break;
                        
                    old = tsumo;
                    old.rollPuyoColor = CELL_TYPE_EMPTY;
                    old.axisPuyoColor = CELL_TYPE_EMPTY;
                    
                    tsumo.ry++;
                    tsumo.ay++;
                    keyFlag = TRUE_BOOL;
                    break;
                    
                default: break;
            }
            
            if(keyFlag == TRUE_BOOL){
                //oldの消去
                putColor(old.ry, old.rx, old.rollPuyoColor);
                putColor(old.ay, old.ax, old.axisPuyoColor);
                //ぷよの描画
                putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor);
                putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor);
            }
        }
    }
    
    return 0;
}

実行結果:
f:id:sion2000114:20191216234815g:plain

おわりに

今回でいよいよぷよが動き始めました。
次回はぷよの自由落下→組みぷよの再生成を実装していきます。
では、お疲れ様でした。.