C++入門系列博客七 俄羅斯方塊小游戲制作


作者:AceTan,轉(zhuǎn)載請(qǐng)標(biāo)明出處形入!


俄羅斯方塊游戲可謂童年經(jīng)典浓若,遙想當(dāng)年拿著那種掌機(jī)挪钓,玩一下午的俄羅斯方塊耳舅,是多么愜意和悠閑的事情啊碌上,滿滿地都是回憶啊(等等浦徊,是不是無意之間暴露了什么……)馏予。今天帶大家來實(shí)現(xiàn)這款小游戲,也是對(duì)前面博客所講內(nèi)容的一個(gè)綜合實(shí)踐盔性。 效果圖如下:

俄羅斯方塊小游戲

0x00 游戲開發(fā)##

先扯一些沒用的,一款游戲一般由游戲策劃冕香、游戲程序員和美術(shù)人員來共同完成蛹尝。游戲開發(fā)的主流語言還是C/C++。你平時(shí)可以寫一些小游戲來提高你的C/C++的水平∠の玻現(xiàn)代大型游戲多在游戲引擎基礎(chǔ)之上開發(fā)突那,目前主流的游戲引擎有U3D、UE4和CE3等等构眯。各大游戲引擎的優(yōu)缺點(diǎn)我們也不做討論愕难,唯一的共同點(diǎn)就是他們都非常復(fù)雜。游戲引擎開發(fā)是一項(xiàng)極具挑戰(zhàn)的工作惫霸,牛叉的游戲引擎一般由團(tuán)隊(duì)共同合作完成(國產(chǎn)電視劇《微微一笑很傾城》 男主角貌似自己開發(fā)了一款游戲引擎猫缭,呵呵)。關(guān)于游戲引擎的更多知識(shí)可以讀一下這本書—《游戲引擎構(gòu)架》它褪。嗯饵骨,你沒猜錯(cuò),筆者就是國內(nèi)某游戲公司的程序猿茫打。

回歸正題居触,這款簡(jiǎn)單的俄羅斯方塊游戲肯定不基于任何游戲引擎啦,甚至它不使用任何渲染庫老赤。我們來寫一個(gè)控制臺(tái)版的小游戲轮洋。


0x01 如何下手##

前面提到過,一款游戲的制作一般由游戲策劃抬旺、游戲程序員和美術(shù)來共同完成弊予。其中,游戲策劃一般負(fù)責(zé)游戲的玩法开财、規(guī)則汉柒、界面误褪、數(shù)值等設(shè)計(jì),美術(shù)人員負(fù)責(zé)模型碾褂、動(dòng)畫兽间、原畫,插圖和游戲整體風(fēng)格的把握等正塌。游戲程序員負(fù)責(zé)實(shí)現(xiàn)游戲策劃所提的需求嘀略。那么,這款小游戲我們也可以從這幾個(gè)方面入手:

  • 游戲的規(guī)則是什么乓诽?

  • 游戲的界面應(yīng)該是什么樣子的帜羊,計(jì)分面板、說明面板放在哪里鸠天?

  • 如何控制游戲(按鍵控制)讼育?

  • 游戲的整體風(fēng)格應(yīng)該是什么樣子?

上面的這幾個(gè)問題解決了粮宛,就可以交給程序員去搞了窥淆。


0x02 程序設(shè)計(jì)##

接到策劃的需求后卖宠,程序如何設(shè)計(jì)呢巍杈?通過需求分析,仔細(xì)查看策劃人員給的設(shè)計(jì)圖(例如上面的效果圖)扛伍,你可以很容易得出以下結(jié)論:這是一個(gè)在Windows平臺(tái)下跑的一個(gè)控制臺(tái)游戲筷畦。顯然,你可能需要設(shè)計(jì)一個(gè)Console類和Window類(其中Console類是Window類的成員)刺洒。這兩個(gè)類應(yīng)該具有如下的能力:

  • 控制窗口的標(biāo)題鳖宾,窗口大小,緩沖區(qū)大小逆航,光標(biāo)等

  • 完全的控制輸出的能力鼎文,包括但不限于文字的位置,顏色因俐,前景色和背景色拇惋。

有了這些信息,你就可以Google和百度一下相關(guān)的API了抹剩,看哪些是已經(jīng)有的撑帖,哪些需要自己設(shè)計(jì)的。比如澳眷,你就可以查到以下的一些函數(shù):

  • GetStdHandle() // 獲得句柄

  • SetConsoleCursorInfo() // 設(shè)置光標(biāo)信息

  • SetConsoleWindowInfo() // 設(shè)置窗口信息

  • SetConsoleScreenBufferSize() //設(shè)置窗口緩沖區(qū)大小

  • SetConsoleTitle() //設(shè)置標(biāo)題

  • WriteConsoleOutputCharacter() 和 WriteConsoleOutputAttribute() //控制輸出的函數(shù)

其中胡嘿,WriteConsoleOutputCharacter()和WriteConsoleOutputAttribute()函數(shù)你也許并不熟悉,這就需要你查看相關(guān)文檔钳踊,弄懂這兩個(gè)函數(shù)了衷敌,因?yàn)檫@兩個(gè)函數(shù)至關(guān)重要勿侯,承擔(dān)了游戲的打印(渲染)任務(wù)。

簡(jiǎn)單的查一下缴罗,很快就能得到該函數(shù)的原型和相關(guān)參數(shù)說明:

// 函數(shù)原型:
BOOL WriteConsoleOutputCharacter( // 在指定位置處插入指定數(shù)量的字符
HANDLE hConsoleOutput, // 句柄
LPCTSTR lpCharacter, // 字符串
DWORD nLength, // 字符個(gè)數(shù)
COORD dwWriteCoord, // 起始位置
LPDWORD lpNumberOfCharsWritten // 已寫個(gè)數(shù)
);

/* 參數(shù)簡(jiǎn)介:
hConsoleOutput:控制臺(tái)輸出句柄罐监,通過調(diào)用GetStdHandle函數(shù)獲得
HANDLE hnd;
hnd=GetStdHandle(STD_INPUT_HANDLE);
lpCharacter:要輸出的字符串
nLength:輸出長度
dwWriteCoord:起始位置
pNumberOfCharsWritten:已寫個(gè)數(shù),通常置為NULL
其中瞒爬,COORD是個(gè)結(jié)構(gòu)體變量類型*/
typedef struct _COORD 
{
    SHORT X;
    SHORT Y;
} COORD;

上面這個(gè)是來自百度百科弓柱。其實(shí)更權(quán)威的說明應(yīng)該查詢MSDN,例如WriteConsoleOutputAttribute的傳送門侧但。 MSDN上對(duì)這個(gè)函數(shù)講解的非常詳細(xì)矢空,也非常權(quán)威,前提是你有閱讀英文文獻(xiàn)的能力禀横。還有就是在VS里打出這個(gè)函數(shù)名屁药,然后按F12直接查看這個(gè)函數(shù)的原型,根據(jù)參數(shù)的命名柏锄,大概了解一下這個(gè)函數(shù)酿箭。

設(shè)計(jì)這個(gè)小游戲剩下的就是游戲的邏輯了。我們?cè)O(shè)計(jì)Tetris類來進(jìn)行游戲的邏輯控制趾娃。我們還需要設(shè)計(jì)一個(gè)數(shù)據(jù)結(jié)構(gòu)來表示方塊缭嫡。單個(gè)方塊如何表示呢?通過我們隊(duì)游戲規(guī)則的了解和對(duì)圖形的觀察抬闷,我們可以使用4*4的矩陣來表示一個(gè)方塊妇蛀。例如:


單個(gè)方塊的表示

我們使用一個(gè)四維數(shù)組表示所有的方塊。 diamonds[x][y][4][4],其中x表示有幾種方塊笤成,y表示這種方塊有幾種變形,[4][4]表示這個(gè)方塊评架。


0x03 工程結(jié)構(gòu)##

這個(gè)小游戲很簡(jiǎn)單,沒有那么多模塊】挥荆現(xiàn)在列一下這個(gè)工程的結(jié)構(gòu)纵诞,并做簡(jiǎn)要說明。其中.h頭文件為聲明培遵,定義在對(duì)應(yīng)的.cpp文件中浙芙。

  • Console 控制臺(tái)類

  • GameDefine 定義游戲的一些常量。

  • StringUtil 字符串工具類

  • Tetris 俄羅斯方塊類

  • Window 窗體類

其中的字符串工具類荤懂,最后并沒有用到茁裙。


0x04 code##

懶得放github上了,直接上代碼了节仿。代碼注釋還是比較詳盡的晤锥,應(yīng)該能看得懂。

Talk is Cheap, show you the code.

Console.h文件:

//--------------------------------------------------------------------
// 文件名:        Console.h
// 內(nèi)  容:        控制臺(tái)類
// 說  明:        控制臺(tái)類的一些聲明
// 創(chuàng)建日期:        2016年9月6日
//--------------------------------------------------------------------

#pragma once            // 保證該文件只被包含一次

#include <wchar.h>
#include <windows.h>    // 使用windows系統(tǒng)下的東西需要引入的頭文件

class Console
{
    friend class Window;

public:

    /// \brief 初始化控制臺(tái)
    /// \param caption 控制臺(tái)標(biāo)題
    /// \param coordinate 控制臺(tái)的高和寬
    void Init(const wchar_t* caption, COORD coordinate);

public:
    HANDLE m_hStdInput;            // 標(biāo)準(zhǔn)輸入句柄

private:
    HANDLE m_hStdOutput;        // 標(biāo)準(zhǔn)輸出句柄
    COORD  m_coord;                // 位置信息(x,y)

};

Console.cpp文件

#include "Console.h"

#ifndef INVALID_RETURN_VOID
#define INVALID_RETURN_VOID(condition) if((condition)) {return;}
#endif

// 一些常量的定義
const DWORD CURSOR_SIZE = 25;
const SHORT SMALL_RECT_TOP = 0;
const SHORT SMALL_RECT_LEFT = 0;


/// \brief 打開控制臺(tái)
/// \param caption 控制臺(tái)標(biāo)題
/// \param coordinate 控制臺(tái)的高和寬
void Console::Init(const wchar_t* caption, COORD coordinate)
{
    // 如果所給坐標(biāo)不合法,則直接退出
    INVALID_RETURN_VOID(coordinate.X <= 0 || coordinate.Y <= 0);

    // 獲得輸出句柄
    m_hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    m_hStdInput = GetStdHandle(STD_INPUT_HANDLE);
    // 判斷得到的句柄是否合法
    INVALID_RETURN_VOID(INVALID_HANDLE_VALUE == m_hStdOutput);
    INVALID_RETURN_VOID(INVALID_HANDLE_VALUE == m_hStdInput);
    
    // 去除光標(biāo)
    CONSOLE_CURSOR_INFO cci = { CURSOR_SIZE, false };
    SetConsoleCursorInfo(m_hStdOutput, &cci);
    
    // 設(shè)置窗體大小
    SMALL_RECT sr = { SMALL_RECT_TOP, SMALL_RECT_LEFT, coordinate.X - 1, coordinate.Y - 1 };
    SetConsoleWindowInfo(m_hStdOutput, true, &sr);
    
    // 設(shè)置緩沖區(qū)大小
    m_coord = coordinate;
    SetConsoleScreenBufferSize(m_hStdOutput, m_coord);

    // 設(shè)置窗口標(biāo)題
    SetConsoleTitle(caption);
}

Window.h文件

//--------------------------------------------------------------------
// 文件名:        Window.h
// 內(nèi)  容:        窗體類
// 說  明:        它是控制臺(tái)的一個(gè)子部分
// 創(chuàng)建日期:        2016年9月6日
//--------------------------------------------------------------------

#pragma once
#include "Console.h"

class Window
{
public:
    /// \brief 初始化窗口
    /// \param console 控制臺(tái)引用
    /// \param rect 位置信息
    void Init(Console& console, SMALL_RECT rect);

    /// \brief 輸出信息
    /// \param str 要輸出的字符串
    /// \param coordinate 位置信息 x, y
    /// \param color 顏色
    /// \param len 字符串長度
    void Output(const char* str, COORD coordinate, WORD color, size_t len = INT_MAX);

private:
    Console* m_pConsole;
    SMALL_RECT m_rect;
};

Window.cpp文件

#include <Windows.h>
#include "Window.h"
#include "StringUtil.h"

#ifndef INVALID_RETURN_VOID
#define INVALID_RETURN_VOID(condition) if((condition)) {return;}
#endif

// 一些常量的定義
const DWORD CURSOR_SIZE = 25;
const SHORT SMALL_RECT_TOP = 0;
const SHORT SMALL_RECT_LEFT = 0;

/// \brief 初始化窗口
/// \param console 控制臺(tái)引用
/// \param rect 位置信息
void Window::Init(Console& console, SMALL_RECT rect)
{
    // 檢測(cè)位置信息是否合法
    INVALID_RETURN_VOID(rect.Left >= rect.Right 
        && rect.Top >= rect.Bottom
        && rect.Left < 0
        && rect.Right > console.m_coord.X
        && rect.Top > console.m_coord.Y);
        
    m_pConsole = &console;
    m_rect = rect;
}

/// \brief 輸出信息
/// \param str 要輸出的字符串
/// \param coordinate 位置信息 x, y
/// \param color 顏色
/// \param len 字符串長度
void Window::Output(const char* str, COORD coordinate, WORD color, size_t len)
{
    // 先檢測(cè)位置信息是否合法
    INVALID_RETURN_VOID(coordinate.X < 0
        || coordinate.Y < 0
        || coordinate.X > (m_rect.Right - m_rect.Left)
        || coordinate.Y > (m_rect.Bottom - m_rect.Top));

    COORD coord = {m_rect.Left + coordinate.X, m_rect.Top + coordinate.Y};
    DWORD num = 0;
    WORD colorArray[2] = { color, color };

    // 字符串轉(zhuǎn)換
    for (const char* p = str; len != 0 && *p != 0; --len, ++p, ++coord.X)
    {
        // 需要換行
        if (coord.X >= m_rect.Right)    
        {
            coord.X = m_rect.Left + coordinate.X;
            ++coord.Y;
            INVALID_RETURN_VOID(coord.Y >= m_rect.Bottom);
        }

        // 單字節(jié)字符
        if (*p > 0)
        {
            WriteConsoleOutputCharacterA(m_pConsole->m_hStdOutput, p, 1, coord, &num);
            INVALID_RETURN_VOID(num != 1);
            WriteConsoleOutputAttribute(m_pConsole->m_hStdOutput, colorArray, 1, coord, &num);
            INVALID_RETURN_VOID(num != 1);
        }
        // 雙字節(jié)字符
        else
        {
            INVALID_RETURN_VOID( len < 2 || *(p + 1) == 0 || (coord.X + 1) >= m_rect.Right);
            WriteConsoleOutputCharacterA(m_pConsole->m_hStdOutput, p, 2, coord, &num);
            INVALID_RETURN_VOID(num != 2);
            WriteConsoleOutputAttribute(m_pConsole->m_hStdOutput, colorArray, 2, coord, &num);
            INVALID_RETURN_VOID(num != 2);

            --len;
            ++p; 
            ++coord.X;
        }
    }
}

Tetris.h文件

//--------------------------------------------------------------------
// 文件名:        Tetris.h
// 內(nèi)  容:        俄羅斯方塊類
// 說  明:        
// 創(chuàng)建日期:        2016年9月6日
//--------------------------------------------------------------------

#pragma once
#include "Console.h"
#include "Window.h"
#include "GameDefine.h"

class Tetris
{
public:
    
    /// \brief 構(gòu)造函數(shù)
    /// \param console 控制臺(tái)
    /// \param coordinate 控制臺(tái)的高和寬
    Tetris(Console& console, COORD coordinate);

    /// \brief 初始化游戲
    /// \param keys 按鍵
    /// \param keyDesc 按鍵描述
    /// \param frequency 聲效頻率
    /// \param duration 延續(xù)時(shí)間
    void Init(int keys[KeyNum], char keyDesc[KeyNum][5], DWORD frequency, DWORD duration);

    /// \brief 是否正在運(yùn)行游戲
    bool IsRun();

    /// \brief 獲取當(dāng)前等級(jí)
    int GetLevel() const;

    /// \brief 方塊下落
    bool Fall();

    /// \brief 消息處理
    /// \param key 按鍵
    /// \return 游戲結(jié)束返回false
    bool MessageProc(const Cmd cmd);

private:
    /// \brief 聲效
    void VoiceBeep();

    /// \brief 繪制得分
    void DrawScoreLevel();

    /// \brief 繪制下一個(gè)將要出現(xiàn)的圖形
    void DrawNext();

    /// \brief 繪制游戲結(jié)束界面
    void DrawGameOver();

    /// \brief 繪制顏色
    void Draw(WORD color);

    /// \brief 給定的是否可行
    bool IsFit(int x, int y, int c, int z);

    /// \brief 消除行
    void RemoveRow();

    /// \brief 旋轉(zhuǎn)(逆時(shí)針)
    void MoveTrans();

    /// \brief 向左移動(dòng)
    void MoveLeft();

    /// \brief 向右移動(dòng)
    void MoveRight();

    /// \brief 向下移動(dòng)
    /// \return 0: 游戲結(jié)束矾瘾; -1:觸底女轿; 1:沒有觸底
    int MoveDown();

    /// \brief 下落到底
    bool FallToBottom();

private:
    char bg[GAME_HIGHT * GAME_WIDTH + 1];
    char bk[DIAMONDS_TYPES][DIAMONDS_TRANS][DIAMONDS_IFNO_ROW][DIAMONDS_IFNO_COL];

private:
    // 聲效頻率
    DWORD m_voiceFrequency;
    
    // 延續(xù)時(shí)間
    DWORD m_voiceDuration;

    // 控制按鍵
    int m_keys[KeyNum];

    // 控制按鍵的描述
    char m_keyDesc[KeyNum][5];

    // 游戲是否結(jié)束
    bool m_gameover;

    // 游戲暫停
    bool m_pause;

    // 游戲聲效開關(guān)
    bool m_voice;

    // 游戲得分
    int m_score;

    // 游戲速度
    int m_speed;

    // 游戲數(shù)據(jù)(實(shí)際方塊的存放數(shù)據(jù))
    char m_data[ROWS][COLS];

    // 下一個(gè)方塊
    int m_next;

    // 位置(x, y)
    int m_x, m_y;

    // 當(dāng)前方塊
    int m_currentDiamonds;

    // 當(dāng)前方向
    int m_currentDir;

    // 窗口
    Window win;

};

Tetris.cpp文件

#include "Tetris.h"
#include <time.h>
#include <stdio.h>

/// \brief 構(gòu)造函數(shù)
/// \param console 控制臺(tái)
/// \param coordinate 控制臺(tái)的高和寬
Tetris::Tetris(Console & console, COORD coordinate)
{
    // 創(chuàng)建一個(gè)矩形
    SMALL_RECT rect = { coordinate.X, coordinate.Y, coordinate.X + GAME_WIDTH, coordinate.Y + GAME_HIGHT };

    // 初始化這個(gè)窗口
    win.Init(console, rect);
    
}

/// \brief 初始化游戲
/// \param keys 按鍵
/// \param keyDesc 按鍵描述
/// \param frequency 聲效頻率
/// \param duration 延續(xù)時(shí)間
void Tetris::Init(int keys[KeyNum], char keyDesc[KeyNum][5], DWORD frequency, DWORD duration)
{
    // 初始化游戲的數(shù)據(jù)
    memcpy(m_keys, keys, sizeof(m_keys));
    memcpy(m_keyDesc, keyDesc, sizeof(m_keyDesc));
    memcpy(bk, Diamonds, sizeof(bk));
    memcpy(bg, Background, sizeof(bg));

    m_voiceFrequency = frequency;
    m_voiceDuration = duration;
    m_gameover = false;
    m_pause = true;
    m_voice = true;
    m_score = 0;
    m_speed = 0;

    // 方塊數(shù)據(jù)部分置0
    memset(m_data, 0, sizeof(m_data));

    // 設(shè)置隨機(jī)種子
    srand((unsigned)time(NULL));

    // 下一個(gè)方塊
    m_next = rand() % DIAMONDS_TYPES;

    m_x = 4;
    m_y = 2;
    m_currentDiamonds = -1;
    m_currentDir = 0;

    COORD coord = { 0, 0 };

    win.Output(bg + 0, coord, COLOR_STILL, GAME_WIDTH);

    for (int i = 1; i < ROWS - 1; ++i)
    {
        coord = { 0, (SHORT)i };
        win.Output(bg + GAME_WIDTH * i + 0, coord, COLOR_STILL, 2);
        coord = { 2, (SHORT)i };
        win.Output(bg + GAME_WIDTH * i + 2, coord, COLOR_BLANK, 22);
        coord = { 24, (SHORT)i };
        win.Output(bg + GAME_WIDTH * i + 24, coord, COLOR_STILL, 14);
    }

    coord = { 0, 20 };
    win.Output(bg + GAME_WIDTH * 20, coord, COLOR_STILL, GAME_WIDTH);

    for (int j = 0; j < KeyNum; ++j)
    {
        coord = { 33, (SHORT)j + 7 };
        win.Output(m_keyDesc[j], coord, COLOR_STILL, 4);
    }

    // 繪制下一個(gè)將要出現(xiàn)的方塊
    DrawNext();
}

/// \brief 是否正在運(yùn)行游戲
bool Tetris::IsRun()
{
    return !m_gameover && !m_pause;
}

/// \brief 獲取當(dāng)前等級(jí)
int Tetris::GetLevel() const
{
    return m_speed;
}

/// \brief 方塊下落
bool Tetris::Fall()
{
    return MessageProc(CMD_DOWN);
}

/// \brief 消息處理
/// \param key 按鍵
/// \return 游戲結(jié)束返回false
bool Tetris::MessageProc(const Cmd cmd)
{
    int const key = m_keys[cmd];
    // 游戲結(jié)束
    if (m_gameover)
    {
        // 游戲重新開始
        if (m_keys[GameBegin] == key)
        {
            Init(m_keys, m_keyDesc, m_voiceFrequency, m_voiceDuration);
            return true;
        }

        return false;
    }

    // 游戲暫停
    if (m_pause)
    {
        // 游戲重新開始
        if (m_keys[GameBegin] == key)
        {
            m_pause = false;
            if (m_currentDiamonds == -1)
            {
                m_currentDiamonds = m_next;
                m_next = rand() % DIAMONDS_TYPES;
                DrawNext();
            }
        }
        else if (m_keys[GameVoice] == key)
        {
            m_voice = !m_voice;
        }
        else
        {
            return true;
        }

        VoiceBeep();

        return true;
    }

    if (m_keys[GamePause] == key)        // 按下暫停鍵
    {
        m_pause = true;
    }
    else if (m_keys[GameVoice] == key)    // 按下聲效鍵
    {
        m_voice = !m_voice;
    }
    else if (m_keys[Up] == key)            // 按下變形鍵
    {
        MoveTrans();
    }
    else if (m_keys[Left] == key)        // 按下方向左鍵
    {
        MoveLeft();
    }
    else if (m_keys[Right] == key)        // 按下方向右鍵
    {
        MoveRight();
    }
    else if (m_keys[Down] == key)        // 按下方向下鍵
    {
        if (0 == MoveDown())
        {
            return false;            
        }
    }
    else if (m_keys[FallDown] == key)        // 按下方塊直接落地鍵
    {
        if (!FallToBottom())
        {
            return false;
        }
    }
    
    return true;
}

/// \brief 聲效
void Tetris::VoiceBeep()
{
    if (m_voice)
    {
        Beep(m_voiceFrequency, m_voiceDuration);
    }
}

/// \brief 繪制得分
void Tetris::DrawScoreLevel()
{
    char tmp[6];
    COORD coord = { 0, 0 };
    sprintf_s(tmp, "%05d", m_score);
    coord = {31, 19};
    win.Output(tmp, coord, COLOR_STILL, 5);
    sprintf_s(tmp, "%1d", m_speed);
    coord = { 28, 19 };
    win.Output(tmp, coord, COLOR_STILL, 1);
}

/// \brief 繪制下一個(gè)將要出現(xiàn)的圖形
void Tetris::DrawNext()
{
    for (int i = 0; i < 2; ++i)
    {
        for (int j = 0; j < 4; ++j)
        {
            COORD coord = {28 + (SHORT)j * 2, 1 + (SHORT)i};
            char* tmp = bk[m_next][0][i][j] == 0 ? " " : "■";
            win.Output(tmp, coord, COLOR_STILL, 2);
        }
    }
}

/// \brief 繪制游戲結(jié)束界面
void Tetris::DrawGameOver()
{
    COORD coord = { 28, 1 };
    win.Output("游戲結(jié)束", coord, COLOR_STILL);
    coord = { 28, 2 };
    win.Output(" ", coord, COLOR_STILL);
}

/// \brief 繪制顏色
void Tetris::Draw(WORD color)
{
    COORD coord = { 0, 0 };

    for (int i = 0; i < 4; ++i)
    {
        if (m_y + i < 0 || m_y + i >= ROWS - 2)
        {
            continue;
        }
        
        for (int j = 0; j < 4; ++j)
        {
            if (bk[m_currentDiamonds][m_currentDir][i][j] == 1)
            {
                coord = { SHORT(2 + m_x * 2 + j * 2), SHORT(1 + m_y + i) };
                win.Output("■", coord, color, 2);
            }
        }
    }
}

/// \brief 給定的是否可行
bool Tetris::IsFit(int x, int y, int c, int z)
{
    for (int i = 0; i < 4; ++i)
    {
        for (int j = 0; j < 4; ++j)
        {
            if (bk[c][z][i][j] == 1)
            {
                if (y + i < 0)
                {
                    continue;
                }
                if (y + i >= (ROWS - 2) || x + j < 0 || x + j >= (COLS - 2) || m_data[y + i][x + j] == 1)
                {
                    return false;
                }
            }
        }
    }

    return true;
}

/// \brief 消除行
void Tetris::RemoveRow()
{
    int lineCount = 0;
    COORD coord = { 0, 0 };
    for (int i = 0; i < (ROWS - 2); ++i)
    {
        if (0 == memcmp(m_data[i], FULL_LINE, (COLS - 2)))
        {
            ++lineCount;
            for (int m = 0; m < (COLS - 2); ++m)
            {
                for (int n = i; n > 1; --n)
                {
                    m_data[n][m] = m_data[n - 1][m];
                    coord = {SHORT(2 + m * 2), SHORT(1 + n)};
                    WORD color = m_data[n][m] == 1 ? COLOR_STILL : COLOR_BLANK;
                    win.Output("■", coord, color, 2);
                }

                m_data[0][m] = 0;
                coord = { SHORT(2 + m * 2) , 1};
                win.Output("■", coord, COLOR_BLANK, 2);
            }
        }
    }

    char data[ROWS - 2][COLS - 2] = { 0 };
    if (lineCount == 0)
    {
        return;
    }

    int score = 0;
    switch (lineCount)
    {
    case 1:
        score = ONE_ROW_SCORE;
        break;
    case 2:
        score = TWO_ROWS_SCORE;
        break;
    case 3:
        score = THREE_ROWS_SCORE;
        break;
    case 4:
        score = FOUR_ROWS_SCORE;
        break;
    }

    m_score += score;

    if (score > MAX_SCORE)
    {
        score = MAX_SCORE;
    }

    m_speed = score / SPEED_ADD_SCORE;

    DrawScoreLevel();
}

/// \brief 旋轉(zhuǎn)(逆時(shí)針)
void Tetris::MoveTrans()
{
    if (IsFit(m_x, m_y, m_currentDiamonds, (m_currentDir + 1) % 4))
    {
        VoiceBeep();
        Draw(COLOR_BLANK);

        m_currentDir = (m_currentDir + 1) % 4;
        Draw(COLOR_MOVE);
    }
}

/// \brief 向左移動(dòng)
void Tetris::MoveLeft()
{
    if (IsFit(m_x - 1, m_y, m_currentDiamonds, m_currentDir))
    {
        VoiceBeep();
        Draw(COLOR_BLANK);

        --m_x;
        Draw(COLOR_MOVE);
    }
}

/// \brief 向右移動(dòng)
void Tetris::MoveRight()
{
    if (IsFit(m_x + 1, m_y, m_currentDiamonds, m_currentDir))
    {
        VoiceBeep();
        Draw(COLOR_BLANK);

        ++m_x;
        Draw(COLOR_MOVE);
    }
}

/// \brief 向下移動(dòng)
/// \return 0: 游戲結(jié)束; -1:觸底壕翩; 1:沒有觸底
int Tetris::MoveDown()
{
    if (IsFit(m_x, m_y + 1, m_currentDiamonds, m_currentDir))
    {
        VoiceBeep();
        Draw(COLOR_BLANK);

        ++m_y;
        Draw(COLOR_MOVE);

        return 1;
    }

    // 觸底了
    if (m_y != -2)
    {
        Draw(COLOR_STILL);
        for (int i = 0; i < 4; ++i)
        {
            if (m_y + i < 0)
            {
                continue;
            }

            for (int j = 0; j < 4; ++j)
            {
                if (bk[m_currentDiamonds][m_currentDir][i][j] == 1)
                {
                    m_data[m_y + i][m_x + j] = 1;
                }
            }

        }
        RemoveRow();

        m_x = 4;
        m_y = -2;
        m_currentDir = 0;
        m_currentDiamonds = m_next;

        m_next = rand() % DIAMONDS_TYPES;
        DrawNext();

        return -1;
    }

    // 游戲結(jié)束

    m_gameover = true;
    DrawGameOver();

    return 0;
}

/// \brief 下落到底
bool Tetris::FallToBottom()
{
    int r = MoveDown();
    while (r == 1)
    {
        r = MoveDown();
    }

    return r == -1;
}

StringUtil.h文件

//--------------------------------------------------------------------
// 文件名:        StringUtil.h
// 內(nèi)  容:        字符串工具類
// 說  明:        提供字符串操作的一些便捷工具類
// 創(chuàng)建日期:        2016年9月6日
// 創(chuàng)建人:        AceTan
// 版權(quán)所有:        AceTan
//--------------------------------------------------------------------

#pragma once

#include <string>
#include <wchar.h>
#include <Windows.h>

// 字符串處理
class StringUtil
{
public:
    // 字符串轉(zhuǎn)換成寬字符串
    static const wchar_t* StringToWideStr(const char* info, wchar_t* buf,
        size_t size, long codepage = CP_UTF8);

    // 寬字符串轉(zhuǎn)換成字符串
    static const char* WideStrToString(const wchar_t* info, char* buf,
        size_t size, long codepage = CP_UTF8);
};

StringUtil.cpp文件

#include "StringUtil.h"
#include <windows.h>

// 字符串轉(zhuǎn)換到寬字符串
const wchar_t* StringUtil::StringToWideStr(const char* info, wchar_t* buf,
    size_t size, long codepage)
{
    if (NULL == info || NULL == buf || size < sizeof(wchar_t))
    {
        return L"";
    }

    const size_t len = size / sizeof(wchar_t);

    int res = MultiByteToWideChar(codepage, 0, info, -1, buf, int(len));

    if (res == 0)
    {
        if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
        {
            buf[len - 1] = 0;
        }
        else
        {
            buf[0] = 0;
        }
    }

    return buf;
}

// 寬字符串轉(zhuǎn)換成字符串
const char* StringUtil::WideStrToString(const wchar_t* info, char* buf,
    size_t size, long codepage)
{
    if (NULL == info || NULL == buf || size < sizeof(char))
    {
        return "";
    }

    int res = WideCharToMultiByte(codepage, 0, info, -1, buf, int(size),
        NULL, NULL);

    if (0 == res)
    {
        if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
        {
            buf[size - 1] = 0;
        }
        else
        {
            buf[0] = 0;
        }
    }

    return buf;
}

GameDefine.h文件

//--------------------------------------------------------------------
// 文件名:        GameDefine.h
// 內(nèi)  容:        游戲定義文件
// 說  明:        定義游戲的一些常量蛉迹,比如窗口大小等
// 創(chuàng)建日期:        2016年9月6日
//--------------------------------------------------------------------

#pragma once
#include <windows.h>

// 高度
const SHORT GAME_HIGHT = 21;

// 寬度
const SHORT GAME_WIDTH = 38;

// 方塊的行數(shù)
const SHORT ROWS = 21;

// 方塊的列數(shù)
const SHORT COLS = 13;

// 運(yùn)動(dòng)中的顏色
const WORD COLOR_MOVE = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY;

// 固定不動(dòng)的顏色
const WORD COLOR_STILL = FOREGROUND_GREEN;

// 空白處的顏色
const WORD COLOR_BLANK = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE;

// 方塊種類
const unsigned int DIAMONDS_TYPES = 7;

// 每個(gè)方塊有幾種變形
const unsigned int DIAMONDS_TRANS = 4;

// 表示單個(gè)方塊的行數(shù)
const unsigned int DIAMONDS_IFNO_ROW = 4;

// 表示單個(gè)方塊的列數(shù)
const unsigned int DIAMONDS_IFNO_COL = 4;

// 消除1行的得分
const int ONE_ROW_SCORE = 100;

// 消除2行的得分
const int TWO_ROWS_SCORE = 300;

// 消除3行的得分
const int THREE_ROWS_SCORE = 700;

// 消除4行的得分
const int FOUR_ROWS_SCORE = 1500;

// 最大分值
const int MAX_SCORE = 99999;

// 得分滿,加一個(gè)速度
const int SPEED_ADD_SCORE = 10000;

// 默認(rèn)聲效頻率
const DWORD DEFAULT_FREQUENCY = 1760;

// 默認(rèn)聲效延續(xù)時(shí)間
const DWORD DEFAULT_DURATION = 20;

// 超時(shí)下落
const DWORD TIME_OUT = 1000;

// 休眠間隔時(shí)間(毫秒)
const int SLEEP_TIME = 200;

// 游戲按鍵對(duì)應(yīng)的索引
enum KeyIndex
{
    GameBegin = 0,    // 游戲開始
    GamePause,        // 游戲暫停
    GameVoice,        // 游戲聲效
    Up,                // 方向鍵-上
    Left,            // 方向鍵-左
    Right,            // 方向鍵-右
    Down,            // 方向鍵-下
    FallDown,        // 方塊直接落地

    KeyNum,            // 按鍵總數(shù)
};


// 對(duì)應(yīng)的鍵值(這個(gè)需要查表或者自己實(shí)驗(yàn)所得)
enum KeyMap
{
    KEY_ENTER = 13,
    KEY_F1 = 59,
    KEY_F2 = 60,
    KEY_UP = 72,
    KEY_LEFT = 75,
    KEY_RIGHT = 77,
    KEY_DOWN = 80,
    KEY_SPACE = 32,
    KEY_ESC = 27,
};

// 游戲操作定義
enum Cmd
{
    CMD_BEGIN,        // 游戲開始
    CMD_PAUSE,        // 游戲暫停
    CMD_VOICE,        // 游戲聲效
    CMD_ROTATE,        // 方塊變形
    CMD_LEFT,        // 方塊左移
    CMD_RIGHT,        // 方塊右移
    CMD_DOWN,        // 方塊下移
    CMD_SINK,        // 方塊沉底
    CMD_QUIT,        // 游戲退出
};


// 某一行滿了
const char FULL_LINE[] = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 };

// 方塊用一個(gè)4維數(shù)組表示:共7種不同方塊放妈,4種變形北救。每個(gè)方塊用 4*4 表示。
const char Diamonds[DIAMONDS_TYPES][DIAMONDS_TRANS][DIAMONDS_IFNO_ROW][DIAMONDS_IFNO_COL] =
{
{
{ { 0,1,1,0 },{ 1,1,0,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 1,0,0,0 },{ 1,1,0,0 },{ 0,1,0,0 },{ 0,0,0,0 } },
{ { 0,1,1,0 },{ 1,1,0,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 1,0,0,0 },{ 1,1,0,0 },{ 0,1,0,0 },{ 0,0,0,0 } }
}
,
{
{ { 1,1,0,0 },{ 0,1,1,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 0,1,0,0 },{ 1,1,0,0 },{ 1,0,0,0 },{ 0,0,0,0 } },
{ { 1,1,0,0 },{ 0,1,1,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 0,1,0,0 },{ 1,1,0,0 },{ 1,0,0,0 },{ 0,0,0,0 } }
}
,
{
{ { 1,1,1,0 },{ 1,0,0,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 1,0,0,0 },{ 1,0,0,0 },{ 1,1,0,0 },{ 0,0,0,0 } },
{ { 0,0,1,0 },{ 1,1,1,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 1,1,0,0 },{ 0,1,0,0 },{ 0,1,0,0 },{ 0,0,0,0 } }
}
,
{
{ { 1,1,1,0 },{ 0,0,1,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 1,1,0,0 },{ 1,0,0,0 },{ 1,0,0,0 },{ 0,0,0,0 } },
{ { 1,0,0,0 },{ 1,1,1,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 0,1,0,0 },{ 0,1,0,0 },{ 1,1,0,0 },{ 0,0,0,0 } }
}
,
{
{ { 1,1,0,0 },{ 1,1,0,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 1,1,0,0 },{ 1,1,0,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 1,1,0,0 },{ 1,1,0,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 1,1,0,0 },{ 1,1,0,0 },{ 0,0,0,0 },{ 0,0,0,0 } }
}
,
{
{ { 0,1,0,0 },{ 1,1,1,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 0,1,0,0 },{ 1,1,0,0 },{ 0,1,0,0 },{ 0,0,0,0 } },
{ { 1,1,1,0 },{ 0,1,0,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 1,0,0,0 },{ 1,1,0,0 },{ 1,0,0,0 },{ 0,0,0,0 } }
}
,
{
{ { 1,1,1,1 },{ 0,0,0,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 1,0,0,0 },{ 1,0,0,0 },{ 1,0,0,0 },{ 1,0,0,0 } },
{ { 1,1,1,1 },{ 0,0,0,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 1,0,0,0 },{ 1,0,0,0 },{ 1,0,0,0 },{ 1,0,0,0 } }
}
};

// 游戲背景
const char Background[GAME_HIGHT * GAME_WIDTH + 1] =
    "┏━━━━━━━━━━━┓┏━━━━┓"
    "┃■■■■■■■■■■■┃┃┃"
    "┃■■■■■■■■■■■┃┃┃"
    "┃■■■■■■■■■■■┃┗━━━━┛"
    "┃■■■■■■■■■■■┃"
    "┃■■■■■■■■■■■┃ 退出= ESC  "
    "┃■■■■■■■■■■■┃"
    "┃■■■■■■■■■■■┃ 開始=  "
    "┃■■■■■■■■■■■┃ 暫停=  "
    "┃■■■■■■■■■■■┃ 聲效=  "
    "┃■■■■■■■■■■■┃ 變形=  "
    "┃■■■■■■■■■■■┃ 左移=  "
    "┃■■■■■■■■■■■┃ 右移=  "
    "┃■■■■■■■■■■■┃ 下移=  "
    "┃■■■■■■■■■■■┃ 落地=  "
    "┃■■■■■■■■■■■┃"
    "┃■■■■■■■■■■■┃"
    "┃■■■■■■■■■■■┃ 速度  得分 "
    "┃■■■■■■■■■■■┃┏━━━━┓"
    "┃■■■■■■■■■■■┃┃0  00000┃"
    "┗━━━━━━━━━━━┛┗━━━━┛";


// 游戲開始時(shí)的X坐標(biāo)
const unsigned int GameStartX = 38;

// 游戲開始時(shí)的Y坐標(biāo)
const unsigned int GameStartY = 21;

main.cpp文件

#include "Console.h"
#include "Window.h"
#include "GameDefine.h"
#include "Tetris.h"
#include <WinUser.h>
#include <conio.h>

DWORD oldTime = 0;

// 得到按鍵命令
Cmd GetCmd(Tetris& tetris, Console& console)
{
    while (true)
    {
        // 延時(shí)芜抒,減少CPU占用率
        Sleep(SLEEP_TIME);

        DWORD newTime = GetTickCount();

        // 超時(shí)下落
        if (newTime - oldTime > TIME_OUT)
        {
            oldTime = newTime;
            return CMD_DOWN;
        }

        // 有按鍵
        if (_kbhit())
        {
            switch (_getch())
            {
            case KEY_ENTER:
                return CMD_BEGIN;
            case KEY_SPACE:
                return CMD_SINK;
            case KEY_ESC:
                return CMD_QUIT;

            case 0:
            case 0xE0:        
                switch (_getch())
                {
                case KEY_F1:
                    return CMD_PAUSE;
                case KEY_F2:
                    return CMD_VOICE;
                case KEY_UP:
                    return CMD_ROTATE;
                case KEY_LEFT:
                    return CMD_LEFT;
                case KEY_RIGHT:
                    return CMD_RIGHT;
                case KEY_DOWN:
                    return CMD_DOWN;
                }

            }
        }

        if (tetris.IsRun() && tetris.GetLevel() <= 10)
        {
            return CMD_DOWN;
        }
    }
}

// 分發(fā)按鍵命令處理
void DispatchCmd(Tetris& tetris, Console& console, Cmd cmd)
{
    switch (cmd)
    {
    case CMD_QUIT:
        exit(0);
        break;
    default:
        tetris.MessageProc(cmd);
        break;

    }
}

int main()
{
    // 創(chuàng)建一個(gè)控制臺(tái)
    Console console;

    // 創(chuàng)建一個(gè)坐標(biāo)
    COORD coordinate = {GameStartX, GameStartY};

    const wchar_t* strGameName = L"俄羅斯方塊 ---- By AceTan ";
    console.Init(strGameName, coordinate);

    int keys[KeyNum] = {KEY_ENTER, KEY_F1, KEY_F2, KEY_UP, KEY_LEFT, KEY_RIGHT, KEY_DOWN, KEY_SPACE };
    char decs[KeyNum][5] = { "回車", "F1", "F2", "↑", "←", "→", "↓", "空格"};

    COORD coord = { 0, 0 };
    Tetris tetris(console, coord);

    tetris.Init(keys, decs, DEFAULT_FREQUENCY, DEFAULT_DURATION);

    Cmd cmd;
    while (true)
    {
        cmd = GetCmd(tetris, console);
        DispatchCmd(tetris, console, cmd);
    }

    return 0;
}

0x05 結(jié)束語##

以上文件在VS2015下編譯通過珍策,并且可以運(yùn)行,其他版本沒有試過宅倒。另外攘宙,這個(gè)小游戲參考了我很久之前寫的代碼,現(xiàn)在進(jìn)行了代碼重構(gòu)和調(diào)整拐迁。記得之前寫的時(shí)候是參考了網(wǎng)上的設(shè)計(jì)蹭劈,無奈找不到源出處了,侵刪线召。

這里面用到的知識(shí)都是我在之前的博客里講到的铺韧,如果讀者感覺有疑惑或者困難,請(qǐng)移步去看一下前面的博客內(nèi)容灶搜。如果你完全看不懂這寫的啥祟蚀,那么我建議你多敲代碼多看書工窍。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末割卖,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子患雏,更是在濱河造成了極大的恐慌鹏溯,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件淹仑,死亡現(xiàn)場(chǎng)離奇詭異丙挽,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)匀借,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門颜阐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來唁影,“玉大人拓售,你說我怎么就攤上這事〈氛希” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵肤舞,是天一觀的道長紫新。 經(jīng)常有香客問我,道長李剖,這世上最難降的妖魔是什么芒率? 我笑而不...
    開封第一講書人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮篙顺,結(jié)果婚禮上偶芍,老公的妹妹穿的比我還像新娘。我一直安慰自己德玫,他們只是感情好腋寨,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著化焕,像睡著了一般萄窜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上撒桨,一...
    開封第一講書人閱讀 51,737評(píng)論 1 305
  • 那天查刻,我揣著相機(jī)與錄音,去河邊找鬼凤类。 笑死穗泵,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的谜疤。 我是一名探鬼主播佃延,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼夷磕!你這毒婦竟也來了履肃?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤坐桩,失蹤者是張志新(化名)和其女友劉穎尺棋,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體绵跷,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡膘螟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了碾局。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片荆残。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖净当,靈堂內(nèi)的尸體忽然破棺而出内斯,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布嘿期,位于F島的核電站品擎,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏备徐。R本人自食惡果不足惜萄传,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蜜猾。 院中可真熱鬧秀菱,春花似錦、人聲如沸蹭睡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽肩豁。三九已至脊串,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間清钥,已是汗流浹背琼锋。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留祟昭,地道東北人缕坎。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像篡悟,于是被迫代替她去往敵國和親谜叹。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容