日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > windows >内容正文

windows

探秘扫雷游戏的C语言实现

發布時間:2023/12/24 windows 46 coder
生活随笔 收集整理的這篇文章主要介紹了 探秘扫雷游戏的C语言实现 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

1 引言

1.1 為什么寫這篇文章?

項目倉庫地址:基于 C 語言實現的掃雷游戲

我決定寫這篇文章的初衷是想分享我在使用C語言開發掃雷游戲的經驗和心得。通過這篇文章,我希望能夠向讀者展示我是如何利用C語言的基礎知識和編程技巧,實現了這個經典游戲的版本。

我相信這將對想要了解C應用的程序員或者C的初學者們應該會有所幫助。

1.2 什么是掃雷游戲?

掃雷游戲是一款經典的單人電腦游戲,玩家需要根據數字提示推斷雷的位置,最終目標是揭開所有非雷方塊而不揭開任何雷方塊。游戲通常會在一個方塊陣列中隱藏一些地雷,玩家需要根據周圍方塊中的數字提示來推斷哪些方塊中包含地雷。這是一款考驗玩家邏輯思維和推理能力的游戲,也是許多人童年時的回憶。

推廣一下我寫的油猴插件,別人學掃雷實現的時候,我在寫掃雷的外掛,聽課屬實有點無聊hhh,寫個掃雷游戲的作弊工具提提神

點擊跳轉->跟著我一起掃雷吧

在掃雷游戲中,玩家可以通過揭開方塊來逐步推斷哪些方塊是安全的,哪些方塊可能包含地雷。游戲中的數字提示會告訴玩家周圍8個方塊中有多少個地雷,玩家需要根據這些提示來推斷出地雷的位置。游戲的目標是盡可能地揭開所有非雷方塊,而不揭開任何地雷方塊。

1.3 實現怎樣的掃雷游戲?

  • 具備哪些功能?
    • 基本的掃雷游戲,能夠做到最基礎的游戲勝利與失敗的判斷
    • 可以選擇難度,不同的難度對應著不同的棋盤大小、地雷數目
    • 可以設置不同的用戶,每個用戶都能設置性別和進行計分
    • 構建分數排行榜,無論是同一用戶或者不同用戶,達到條件即上榜
  • 怎樣去操作?
    • 終端界面進行控制,理論上 Qt 界面也脫離不了其中核心,無非多了游戲循環處理以及信號和槽的使用等等
    • 傳統功能選項的菜單,比如開始游戲/繼續游戲/設置用戶/選擇難度/查看排行榜等等功能,都通過選項去調用
    • 游戲過程中,玩家輸入不同操作符和行列號操作游戲中的單元格,期間可以隨時退出,而且可以保存游戲

2 實現思路

開發工具: Microsoft Visual Studio

編譯器: MSVC ? ? C標準: C99

2.1 項目結構劃分

游戲入口: main.c 該文件負責游戲的入口以及預加載游戲的一些數據,通過玩家不同的輸入,對接不同的功能。

游戲模塊: game.hgame.c 其中頭文件聲明游戲中可能使用到的結構體、宏定義、全局變量、函數聲明等等,而源文件則是游戲中各功能模塊的相關代碼實現。

顯示模塊: display.hdisplay.c 該模塊負責游戲過程中的游戲板元素顯示、錯誤提示、打印玩家信息、結算成績、打印排行榜等功能的聲明與實現。

菜單模塊: menu.hmenu.c 這個模塊負責呈現各級菜單以及用戶對于菜單功能選擇的輸入反饋處理等。

存儲模塊: storage.hstorage.c 該存儲模塊負責將游戲數據、排行榜數據的本地存儲和加載。

2.2 預置數據類型

// game.h
#define _CRT_SECURE_NO_WARNINGS

#include <stdbool.h>  // 因為用到了bool類型

// 排行榜上存儲的玩家分數最大數量
#define MAX_PLAYERS 10

// 單元格
typedef struct {
	bool is_mine;  // 有沒有雷
	bool is_revealed;  // 有沒有探索
	bool is_flagged;  // 有沒有放小旗(掃雷游戲中的玩法: 當質疑是雷時,你可以防止旗幟標記)
	short int adjacent_mines;  // 附近的雷數量(0-8)
	short int value;  // 打印的時候顯示的值
} Cell;

// 游戲板的配置
typedef struct {
	int rows;  // 理論行數
	int cols;  // 理論列數
	int real_rows;  // 實際構建的行數
	int real_cols;  // 實際構建的列數
	int mine_count;  // 雷的數量
	int base_score;  // 當前配置的基礎分
} BoardConfig;

// 游戲狀態
typedef enum {
	GAME_INIT,  // 待初始化,該狀態象征著新的一局游戲還沒開始
	GAME_RUNNING,  // 已經初始化了,運行中,等待玩家下一步操作
	GAME_ENTER,  // 游戲接收了輸入,正在處理中,處理中可能進入更新界面狀態
	GAME_UPDATE,  // 更新界面狀態,用戶對某個單元格進行操作后的反饋
	GAME_WIN,  // 游戲勝利,在游戲更新界面至游戲失敗的中間,會進行成功的檢查
	GAME_LOSE  // 游戲失敗,該狀態在更新界面時會對玩家操作后的行為檢測
} GameState;

// 游戲難度
typedef enum {
	EASY, MEDIUM, HARD,  // 3個難度
	DIFFICULTY_COUNT  // 計數 3
} Diffuculty;

// 游戲配置
typedef struct {
	Cell** board;  // 游戲板 二維矩陣
	GameState state;  // 游戲狀態
	Diffuculty difficult;  // 游戲難度等級
	int time;  // 某局游戲的用時
} Game;

// 玩家
typedef struct {
	char name[64];  	// 玩家昵稱
	char gender[24];  	// 玩家性別
	int score;  		// 玩家當局分數
	int best_score;  // 玩家歷史最高分數
	short int right_flag;  // 玩家標記的正確旗幟數量
	short int error_flag;  // 玩家標記的錯誤旗幟數量
} Player;

// 排行榜
typedef struct {
	Player players[MAX_PLAYERS]; // 玩家列表
	int player_count;	// 當前上榜的玩家數量
} Leaderboard;

2.3 核心功能實現

2.3.1 棋盤設定

我們先以難度為容易的掃雷游戲為研究方向,容易難度下,我們給予棋盤這樣一些參數:9行 ? 9列,其中含有 10 個雷。

但是我們實際繪制的棋盤一定要在四周增加一行或一列才會更優,這是為什么呢?設想用戶自己明白什么是下標嗎?我們呈現給用戶看的時候,是否給游戲板注明清晰可見的行、列坐標是否會更好呢?我們作為程序員是否更加容易明了的去計算行、列坐標呢?

因此我在 BoardConfig 結構體中才命名了cols rows real_cols real_rows這一系列的成員變量.

2.3.2 數據預加載

友友可能會在這里問了,這里需要預加載什么?為什么要預加載?

首先,咱們在game.h 中是否聲明了很多的結構體呢?那我們還有什么東西沒有聲明呢?比如說一局游戲的配置列表,我們后續可以通過枚舉 Difficulty 進行切換,從而選擇當前游戲的配置(容易/微難/困難等等),又或者 Game 結構體對應的游戲對象,它存儲著掃雷的游戲板、游戲狀態等等。我們聲明了全局變量,但是并未初始化,就以配置列表來說

// game.h
// 游戲板的配置列表
BoardConfig* board_configs;

你會發現我們并沒有給它賦予值,那么我們就要在預加載中,提前預加載一些全局的數據,方便后續的功能模塊去共享、使用、操作這個數據。我們大致已經明白了預加載的作用,那么預加載肯定是程序打開時進行加載的東西,后續都不需要重復去加載咯。

BoardConfig temp_configs[DIFFICULTY_COUNT] = {
	{9, 9, 11, 11, 10, 1000}, 
	{16, 16, 18, 18, 40, 2000},
	{24, 24, 26, 26, 99, 3000}
};
board_configs = (BoardConfig*)malloc(DIFFICULTY_COUNT * sizeof(BoardConfig));
for (int i = 0; i < DIFFICULTY_COUNT; ++i) {
	board_configs[i] = temp_configs[i];
}

我們應該要明確哪些數據需要全局化并且預加載?

單元格顯示的值(未探索、周圍無雷、自身是雷、放了旗幟)

游戲板的配置列表(上述舉例)

當前選擇的游戲配置

游戲對象(存儲得有游戲狀態、游戲板等)

當前用戶操作的玩家對象

排行榜

其實從以上數據你會發現,有些數據我們暫時用不到,僅僅只是先聲明著,隨著后續的文章,你會逐漸了解到這些需要預加載的變量并且如何去使用。

現在 game.h 頭文件中對這些變量進行全局聲明。

// game.h
// cell顯示值 使用的是ASCII編碼
short int cell_unexplored;
short int cell_empty;
short int cell_mine;
short int cell_flagged;
// 游戲板的配置列表
BoardConfig* board_configs;
// 選擇的游戲配置
BoardConfig board_config;
// 游戲對象
Game* game;
// 玩家對象
Player* player;
// 排行榜
Leaderboard* leaderboard;

// 預加載
void Preload();

緊接著我們在 game.c 的預加載函數 Preload 中對已聲明的部分變量進行初始化賦值。

#include "game.h"  // 不要忘記包含頭文件

void Preload() {
	BoardConfig temp_configs[DIFFICULTY_COUNT] = {
		{9, 9, 11, 11, 10, 1000}, 
		{16, 16, 18, 18, 40, 2000},
		{24, 24, 26, 26, 99, 3000}
	};
    // 這里使用到了 malloc,因此你需要在 game.h 中導入 malloc.h 或者 stdlib.h
	board_configs = (BoardConfig*)malloc(DIFFICULTY_COUNT * sizeof(BoardConfig));
	for (int i = 0; i < DIFFICULTY_COUNT; ++i) {
		board_configs[i] = temp_configs[i];
	}
	cell_unexplored = 46;  // .
	cell_empty = 48;  // 0
	cell_mine = 42;  // *
	cell_flagged = 70;  // F
	// TODO 預加載創建游戲對象
}

2.3.3 創建游戲對象

這里我將通過工廠函數創建游戲對象,這個工廠函數的功能比較簡單,無非就是動態申請分配空間,初始化 game 對象的一些成員屬性即可,然后返回相應的結構體指針。

通常,寫一個功能函數要明白的是什么呢?目標功能是什么! 傳遞參數是什么! 可復用性考慮怎樣!

咱們創建一個函數名為 CreateGame,該函數返回一個 Game 結構體的指針,可以用來干嘛呢?創建后返回并賦值給我們的全局變量(游戲對象 game)

// game.h
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>  // 因為涉及到打印和使用到了NULL

// ...省略中間代碼

// 創建游戲對象
Game* CreateGame(GameState state, Diffuculty level);

傳遞的參數應該是游戲狀態以及難度等級,我認為這里的參數設計并不是唯一的哦!

我們來到 game.c 源文件中,編寫這個函數的內部代碼:

// game.c
Game* CreateGame(GameState state, Diffuculty level) {
	Game* game = (Game*)malloc(sizeof(Game));  // 動態創建
	if (game == NULL) return;
	
	game->time = 0; // 初始化時間為0
	game->state = state;  // 對接上外部賦予的狀態,一般是 GAME_INIT
	game->difficult = level;  // 傳遞難度等級,我們這里會傳入 EASY
	board_config = board_configs[game->difficult];  // 根據難度等級的枚舉值,獲取對應的配置

    // 接下來就利用配置去動態創建游戲板
	game->board = (Cell**)malloc(board_config.real_rows * sizeof(Cell*));
	if (game->board == NULL) {
		free(game);
		return;
	}

	for (int i = 0; i < board_config.real_rows; ++i) {
		game->board[i] = (Cell*)malloc(board_config.real_cols * sizeof(Cell));
		if (game->board[i] == NULL) {
			for (int j = 0; j < i; j++)
				free(game->board[j]);
			free(game->board);
			free(game);
			return;
		}
	}

	return game;  // 動態創建成功后返回game指針
}

接下來我們又回到預加載的函數中:

// game.c
void Preload() {
	// ...代碼省略
	// TODO 預加載創建游戲對象
    // 創建游戲對象
	game = CreateGame(GAME_INIT, EASY);
}

2.3.4 初始化游戲

上述我們完成了核心數據的一些預加載,緊接著就是如何合理的利用預加載的數據。這一步,我們將對游戲板的各個單元格賦予 Cell 類型的對象。簡而言之,這一步就是對游戲板部署行列號、未探索、地雷等區域。

這一步的難點,你需要分清楚 Cell 結構體中的所有成員屬性的意義,特別是對于 value 的理解。

一樣的,我們需要在 game.h 頭文件中先聲明函數。

// 預加載
void Preload();

// 新增:初始化游戲
void InitGame();

// 創建游戲對象
Game* CreateGame(GameState state, Diffuculty level);

來到 game.c 源文件中實現這個 InitGame 函數的相關功能.

這個函數是每次開始新的一局游戲都要調用的,因此我們要對 game->time 歸零操作,然后獲取配置的行號與列號信息,將棋盤的關鍵區域初始化成一系列沒有被探索的 Cell 單元格,可是在此之前,我們需要做那么一件事,還記得我們二維矩陣(游戲板)是什么樣的嗎?

是的,你千萬不要忘記,我們需要給左、上兩邊的區域填充一個特殊的 Cell,用于呈現我們行、列號信息。

void InitGame() {
    // 我們每個過程都是檢測游戲狀態的,這樣更加嚴格的對過程控制
	if (game->state != GAME_INIT) return;
	// 新的游戲時初始化參數
	game->time = 0;
	// 初始化行號、列號、單元格
	int rows = board_config.rows;
	int cols = board_config.cols;
	int real_rows = board_config.real_rows;
	int real_cols = board_config.real_cols;
	for (int i = 0; i < real_rows; ++i) {
		Cell edge_cell = { false, false, false, 0, i };
		game->board[i][0] = edge_cell;
	}
	for (int i = 0; i < real_cols; ++i) {
		Cell edge_cell = { false, false, false, 0, i };
		game->board[0][i] = edge_cell;
	}
	for (int row = 1; row < real_rows; ++row) {
		for (int col = 1; col < real_cols; ++col) {
			Cell init_cell = { false, false, false, 0, cell_unexplored };
			game->board[row][col] = init_cell;
		}
	}
    
    // TODO 埋雷/統計雷

}

接下來就是比較重要的事,隨機埋雷,我們需要利用srandrand 兩個函數獲取隨機值,這一步非常的簡單。

void InitGame() {
    // ...
    // 埋雷
	int mine_count = board_config.mine_count;  // 獲取當前配置指定的雷數量
   	int mine_row = 0, mine_col = 0;  // 初始化埋雷的行列坐標
    srand((unsigned int)time(NULL));
    while (mine_count > 0) {  // 當數量為0,就不再埋雷
        mine_row = rand() % rows + 1;  // 獲取 1 ~ rows 范圍的值(千萬不要忘記,我們的游戲板是什么樣的!!!)
        mine_col = rand() % cols + 1;  // 獲取 1 ~ cols 范圍的值
        if (game->board[mine_row][mine_col].is_mine) continue;  // 這個位置已經埋雷了,就跳過
        game->board[mine_row][mine_col].is_mine = true;  // 對沒有布置過的地方布置雷
        game->board[mine_row][mine_col].value = cell_unexplored;  // 顯示未探索,也就是隱藏嘛
        //game->board[mine_row][mine_col].value = cell_mine;  // 測試代碼,解開注釋后你能看到布置的雷
        mine_count--;  // 布置一顆地雷那就自減一
    }
}

Cell 結構體中的什么屬性在這里還沒有被處理過嗎?很簡單,答案是 Cell.adjacent_mines 還沒有處理,它僅僅只是被賦予得有一個無意義的初始值 0。

void InitGame() {
    // ...
    // 埋雷省略...
    // 統計雷
    for (int row = 1; row < real_rows; ++row) {
		for (int col = 1; col < real_cols; ++col) {
			game->board[row][col].adjacent_mines = GetMineNearCell(row, col);  // TODO 我們要實現GetMineNearCell函數
		}
	}
    
    game->state = GAME_RUNNING;  // 初始化好后,更換游戲狀態到運行中,等待輸入
}

GetMineNearCell函數的功能比較簡單,它主要負責統計某個單元格附近8格的含雷數量。該函數你也需要在 game.h 頭文件中提前聲明完畢,然后我們來看看具體實現:

int GetMineNearCell(int row, int col) {
	int mine_count = 0, new_row = 0, new_col = 0;
	int row_offsets[] = { -1, -1, -1, 0, 0, 1, 1, 1 };
	int col_offsets[] = { -1, 0, 1, -1, 1, -1, 0, 1 };
	for (int i = 0; i < 8; ++i) {
		new_row = row + row_offsets[i];
		new_col = col + col_offsets[i];
		if (new_row < 1 || new_row > board_config.rows || new_col < 1 || new_col > board_config.cols)
			continue;  // 周圍的8個坐標中出現不在范圍的,即不合法坐標會跳過
		if (game->board[new_row][new_col].is_mine) {
			mine_count++;  // 如果周圍出現雷,就讓變量+1進行統計
		}

	}
	return mine_count;
}

2.3.5 測試初始化

這里需要明白的是,我們僅僅只是從理論和人為的主觀考慮上,去實現對應功能的代碼,但是還未進行功能的測試,現在我們先來到 display.h 頭文件中,去聲明如下一些函數:

? void DisplayGameState(); // 這個函數用于顯示當前棋盤信息
? void DisplayErrorMsg(const char* message); // 當玩家輸入坐標不合法時提示有誤

聲明好后去到 display.c 文件中,一一實現對應函數。

#include "game.h"  # 需要導入 game.h 因為我們會使用到當前配置 board_config

void DisplayGameState() {
	for (int i = 0; i <= board_config.rows; ++i) {
		printf("%2d ", i);  // 打印第一行的行號
	}
	printf("\n");
	for (int i = 1; i <= board_config.rows; ++i) {
		for (int j = 0; j <= board_config.cols; ++j) {
			if (j == 0) {  // 打印第一列的列號
				printf("%2d ", game->board[i][j].value);
			}
			else {  // 打印理論板上的值(ASCII值轉字符)
				printf("%2c ", game->board[i][j].value);
			}
		}
		printf("\n");
	}
}

void DisplayErrorMsg(const char* message) {
	printf("錯誤:");
	printf("%s\n", message);
}

不要著急,我們順手把主菜單也實現, 在 menu.h 頭文件中,增加如下函數的聲明:

// menu.h 文件中
#define _CRT_SECURE_NO_WARNINGS
// 過濾掉 scanf 在 msvc 下不安全的問題

#include <stdbool.h>
#include <stdio.h>
#include <string.h>

// 顯示主菜單
void ShowMenu();

// 處理主菜單選項
int HandleMenuChoice();

一樣的,我們上方僅僅只是聲明功能函數,還沒有實現功能函數的具體內容。現在來實現,非常簡單!

// menu.c 文件中
#include "game.h"
#include "menu.h"

void ShowMenu() {
	printf("------------菜  單------------\n");
	printf("   1 開始游戲\t2 功能待定  \n");
	printf("    你可以輸入 0 退出程序  \n");
	printf("------------------------------\n");
}

int HandleMenuChoice() {
	int choice = 0;
	printf(">>> ");
	scanf("%d", &choice);
    // 這里是6的原因,僅是一個粗略估計
	if (choice <= 6 && choice >= 0) return choice;
	else return -1;
}

現在我們來到 main.c 文件中,構建好 main 函數。

#include "menu.h"

int main() {
	int choice = 0;
	Preload();
	do
	{
		ShowMenu();
		choice = HandleMenuChoice();
		if (choice == 0) break;
		if (choice == -1) continue;
		switch (choice)
		{
		case 1: {
			InitGame();
			DisplayGameState();
			break;
		}
		default:
			break;
		}
	} while (true);

    return 0;
}

如果每一步都按照我的步驟去做的話,那么正常情況應該顯示如下界面:

2.3.6 增加操作控制

當我們初始化游戲后,得到掃雷游戲的棋盤區域,但是我們還需要增加交互操作,我們要達到的效果如下:

輸入:f 1 1 這個將在有效的第一行第一列的區域進行插旗,更改的其實是 Cellis_flagged 成員屬性;

輸入:e 1 1 這個指令將探索對應的單元格,修改的其實是 Cellis_revealed 成員屬性,并且還要根據是否踩雷等情況去區分;

上述命令都要考慮好已探索、已標記的情況。

game.h 文件聲明如下的一系列函數:

// 處理游戲運行時的輸入
int HandleInput(char operate, int row, int col);

// 根據單元格情況更新游戲板
void UpdateGameState(int row, int col);

// 檢查判斷游戲結束
bool CheckGameOver();

然后我們在 game.c 文件中進行如下的實現:

int HandleInput(char operate, int row, int col) {
    // 該函數接收的參數分別是 操作符 行號 列號
	if (row < 1 || row > board_config.rows || col < 1 || col > board_config.cols) {
		DisplayErrorMsg("輸入的行號和列號并不在有效范圍內!");
		return -1;  // 校驗行號和列號是否有效 返回-1表示不得行,外部檢測到可以要求重新輸入
	}

	switch (operate)
	{
	case 'e':
		if (game->state == GAME_RUNNING) {
			game->state = GAME_ENTER;  // 修改游戲狀態
			UpdateGameState(row, col);  // 更新游戲界面信息
			if (CheckGameOver()) {  // 判斷游戲是否結束
				EndGame();  // 這局游戲結束時干什么,本小節不講解
			}
			else {
				game->state = GAME_RUNNING;  // 游戲沒有結束,恢復游戲狀態
			}
		}
		break;
	case 'f':
		if (game->state == GAME_RUNNING) {
			game->state = GAME_ENTER;
			if (!game->board[row][col].is_revealed) {  // 如果單元格沒有被探索過,因為探索了的標記起來沒有意義
				game->board[row][col].is_flagged = !game->board[row][col].is_flagged;  // 那么可以標記或者取消標記
				if (game->board[row][col].is_flagged) {  // 如果是標記行為
					game->board[row][col].value = cell_flagged;  // 修改value顯示值對應 F
				}
				else {
					game->board[row][col].value = cell_unexplored;  // 如果是取消標記行為,改為未探索的單元格
				}
			}
			game->state = GAME_RUNNING;
		}
		break;
	case 'q':  // 退出時依然需要給坐標(有點不合理,忍忍老鐵!)
		game->state = GAME_INIT;  // 退出游戲,意味著狀態恢復到待初始化
		break;
	default:
		break;
	}
	return game->state;  // 將游戲狀態拋出去,根據狀態干事
}

接下來我們看一下 UpdateGameState 函數的實現。

我們的游戲流程是以游戲狀態為主的,此時這個函數的開始應該修改游戲狀態為 GAME_UPDATE,結束后我們恢復到 GAME_RUNNING 或者 GAME_LOSE 甚至是 GAME_WIN 都有可能,讀者請自行琢磨函數調用的關系。

此處的難點在于遞歸探索,請看這張圖結合代碼慢慢理解,總而言之就是探索再探索!

void UpdateGameState(int row, int col) {
	game->state = GAME_UPDATE;
	Cell* cell = &(game->board[row][col]);
    // 探索過了 不能探索
	if (cell->is_revealed) {
		DisplayErrorMsg("你已經探索過這個區域咯!");
		return;
	}
    // 標記過了 不能探索
	if (cell->is_flagged) {
		DisplayErrorMsg("你已經標記了這個區域,不能探索哦!");
		return;
	}
    // 老鐵踩雷了。其實你應該發現,踩雷了好像沒做多少工作
    // 但是你往后繼續研究,我是通過游戲狀態去做工作的!
	if (cell->is_mine) {
		game->state = GAME_LOSE;  // 踩雷后修改游戲狀態
		cell->is_revealed = true;  // 修改為已探索
	}
	else
	{
        // 下方都是沒踩雷的情況
        // 當前格子附近有雷
		if (cell->adjacent_mines != 0) {
			cell->value = 48 + cell->adjacent_mines;
			cell->is_revealed = true;
		}
		else  // 當前格子附近沒有雷,那還用玩家動腦嗎,無腦點四周8個,我們這里程序代勞
		{
			cell->value = cell_empty;  // 當前格子附近沒雷,給 cell_empty
			cell->is_revealed = true;  // 當前格子改為已探索
			int new_row = 0, new_col = 0;  // 附近格子的行列號初始化
			int row_offsets[] = { -1, -1, -1, 0, 0, 1, 1, 1 };
			int col_offsets[] = { -1, 0, 1, -1, 1, -1, 0, 1 };
			for (int i = 0; i < 8; ++i) {
                 // 四周導出偏移1位,然后挨個該狀態 
				new_row = row + row_offsets[i];
				new_col = col + col_offsets[i];
				Cell new_cell = game->board[new_row][new_col];
				// 提前處理邊界以及已探索和已標記等情況
				if (new_row < 1 || new_row > board_config.rows || new_col < 1 || new_col > board_config.cols) continue;
				if (new_cell.is_revealed || new_cell.is_flagged) continue;
                  // 遞歸的更新周圍格子
				UpdateGameState(new_row, new_col);
			}
		}
	}
}

上面我們就成功實現了棋盤在用戶操作的影響下正確反饋信息,緊接著讀者請看我如何實現的判斷游戲是否結束,如何判斷游戲是勝利、失敗亦或者沒啥變化。

我們經過上述的更新游戲信息的函數操作時,請讀者設想,我們點擊的是雷,那么游戲狀態是什么呢?答案是 GAME_LOSE;如果沒有失敗呢?那么此時的游戲狀態就是 GAME_UPDATE

接下來的 CheckGameOver 函數,我們就是依據這兩個狀態去判斷和執行。首先判斷游戲失敗的情況,然后判斷狀態合不合法(如果是 GAME_UPDATE 就是合法),合法的話繼續判斷玩家是否勝利。

請知悉勝利條件:在掃雷游戲中,玩家勝利的條件通常是所有沒有地雷的單元格都被探索過。也就是說,如果所有的非地雷單元格都已經被探索過,那么玩家就贏了游戲。

代碼如下:

bool CheckGameOver() {
	// 判斷游戲失敗
	if (game->state == GAME_LOSE) {
		printf("踩雷咯,游戲失敗!!!\n");
		return true;
	}
	if (game->state != GAME_UPDATE) return;
	// 檢測是否勝利
	for (int row = 1; row <= board_config.rows; ++row) {
		for (int col = 1; col <= board_config.cols; ++col) {
			if (!game->board[row][col].is_mine && !game->board[row][col].is_revealed)
				return false;   // 只要有一個非雷元素沒有被探索,就屬于沒勝利情況
		}
	}
	// 勝利的情況
	game->state = GAME_WIN;
	printf("好厲害哦,人家好喜歡~\n");
	return true;
}

2.3.7 開始和結束

一定要在 game.h 中聲明如下函數:

// 將雷全部顯示
void ShowAllMines();

// 開始游戲
void StartGame();

// 游戲結束
void EndGame();

然后在 game.c 中實現相應代碼,這 3 個函數的功能代碼其實比較簡單,讀者需要明白何時調用它們、發生了什么即可。

void ShowAllMines() {
    // 調用:游戲結束時調用 EndGame函數中會調用它
	for (int row = 1; row <= board_config.rows; ++row) {
		for (int col = 1; col <= board_config.cols; ++col) {
			if (game->board[row][col].is_mine)
				game->board[row][col].value = cell_mine;
            	// 游戲結束時,將游戲板上是雷的value全部改為雷
		}
	}
}

void StartGame() {
    // 調用:在main.c中被調用 也就是開始游戲的時候
	char operate = 'e';  // 初始化操作符
	int row = 0, col = 0, game_state = 0;  // 初始化行列號、游戲狀態
	InitGame();  // 初始化游戲,得到布滿雷的游戲板
	while (true)
	{
         // 保證操作前 能看到棋盤
		DisplayGameState();
		printf("操作符:e 探索\tf 標記\tq 終止\t 格式[操作符 行號 列號]\n");
		printf("操作:");
         // 接收輸入并讓相應函數處理操作
		scanf(" %c %d %d", &operate, &row, &col);
		game_state = HandleInput(operate, row, col);  
		if (game_state == 0) break;  // 游戲狀態回到 GAME_INIT 就退出游戲咯
		if (game_state == -1) continue;  // 輸入的行列不合法跳過
	}
}

void EndGame() {
    // 調用:勝利或者失敗后調用 HandleInput函數中調用它
    // 顯示游戲板的完整信息
	ShowAllMines();
	DisplayGameState();  

	// TODO 計分并總結成績

	// TODO 計分后計入排行榜

	// 狀態恢復到待初始化
	game->state = GAME_INIT;

}

接下來回到 main.c 源文件中,我們修改入口函數中的代碼如下:

#include "menu.h"

int main() {
	int choice = 0;
	Preload();
	do
	{
		ShowMenu();
		choice = HandleMenuChoice();
		if (choice == 0) break;
		if (choice == -1) continue;
		switch (choice)
		{
		case 1: {
			StartGame();  // 僅僅修改此處代碼
			break;
		}
		default:
			break;
		}
	} while (true);

    return 0;
}

到此為止,這個掃雷游戲的基本核心我們就已經實現完畢,現在可以正常的玩這個很基礎的部分了!

3 拓展功能

上述我們完成掃雷游戲的核心部分,從本章節開始,我將介紹菜單中其它功能的實現,我們預計實現這么一些功能。

void ShowMenu() {
	printf("------------菜  單------------\n");
	printf("   1 開始游戲\t2 繼續游戲  \n");
	printf("   3 設置用戶\t4 選擇難度   \n");
	printf("   5 保存游戲\t6 預覽排行  \n");
	printf("    你可以輸入 0 退出程序  \n");
	printf("------------------------------\n");
}

相比之前的版本,看起來更加復雜了一點,在此我們增加了 保存游戲繼續游戲選擇難度設置用戶預覽排行 5大功能模塊。相對而言,這個掃雷項目并沒有涉及多么復雜的技術,考驗的仍然是C語言的基本功以及微末的算法知識。接下來的小章節我會按照各個功能的難易程度的遞增順序去書寫。

3.1 增加難度可選

首先來看難度可選這個模塊部分,如何實現,我們可以知道的是,在游戲未開始前,我們可以在主菜單的功能選項下輸入 4,然后進入到難度選擇菜單對難度進行選擇。因此需要在 menu.hmenu.c 文件中聲明和實現難度選擇菜單。

// menu.h
// ...之前的代碼已省略
// 顯示難度等級
void ShowLevelMenu();

// 設置難度等級
void HandleLevelChoice();


// menu.c
// ...之前的代碼已省略
void ShowLevelMenu() {
	printf("-----難度等級-----\n");
	printf("   1 非常輕松   \n");
	printf("   2 有點難度   \n");
	printf("   3 上點強度   \n");
	printf("-----------------\n");
}

void HandleLevelChoice() {
	int choice = 0;
	printf("[選擇難度]>>> ");
	scanf("%d", &choice);
	// TODO 利用好int類型的變量choice 去設置難度
}

我們在上述的 HandleLevelChoice 函數末尾增加一個函數調用,稍后我們來實現所調用的這個函數,這個函數將通過用戶所選擇的 choice 去設置游戲的難度。

// TODO 利用好int類型的變量choice 去設置難度
ModifyDifficulty(choice);

暫且沒有思路的讀者,可以回想,我們的游戲難度是怎么影響到游戲的,或者反向思維思考一下,游戲難度被什么影響呢?答案是:行數列數雷數量。那么這三個因素與什么相關呢?也就是我們預加載中的配置列表與當前配置!

// 臨時配置列表
BoardConfig temp_configs[DIFFICULTY_COUNT] = {
	// 理論行列數、實際行列數、雷數量、基礎分,暫且不理基礎分是什么!
    {9, 9, 11, 11, 10, 1000}, 
    {16, 16, 18, 18, 40, 2000},
    {24, 24, 26, 26, 99, 3000}
};
// 全局的配置列表
board_configs = (BoardConfig*)malloc(DIFFICULTY_COUNT * sizeof(BoardConfig));
for (int i = 0; i < DIFFICULTY_COUNT; ++i) {
    board_configs[i] = temp_configs[i];
}

我們通過從配置列表中獲取一個配置構建我們的游戲對象,當前配置即決定游戲難度,但是,還要再往前想想,當前配置是怎么知道的呢?其實就是我們的 Difficulty 枚舉對象去決定的,選擇 EASY 難度,那么游戲難度就是第一檔,非常容易,我們可以怎樣利用用戶選擇的 choice 去改變呢?本質上就是將 choice 進行類型轉換成對應的枚舉類型,然后通過我們封裝好的函數接口 CreateGame(<state>, <difficulty>) 去修改全局游戲對象。

因此來到 game.c 文件中,ModifyDifficulty(choice) 的實現如下(不要忘記在頭文件中聲明):

// game.h 
// ......
// 修改難度等級
void ModifyDifficulty(int choice);


// game.c
void ModifyDifficulty(int choice) {
	game = CreateGame(GAME_INIT, (Diffuculty)(choice - 1));
}

當玩家指定了難度后,當前配置的行、列、雷量等各元信息都會發生改變,從而下次游戲開始時,都會基于這些信息去構建我們的游戲板等等。

不要忘記了,還要在 main.c 源文件的 switch 分支中增加對應的選項和函數調用!

do
{
	ShowMenu();
	choice = HandleMenuChoice();
	if (choice == 0) break;
	if (choice == -1) continue;
	switch (choice)
	{
	case 1: {
		StartGame();
		break;
	}
    // 新增的功能4,修改游戲難度
	case 4: {
		ShowLevelMenu();
		HandleLevelChoice();
		break;
	}
	default:
		break;
	}
} while (true);

當程序被運行起來后,你應該在主菜單打印后選擇功能 4,然后去校驗不同難度下的掃雷游戲都能被正常的渲染出來。目前來看,我這邊暫時沒出現任何問題,程序可以正常運行。

3.2 增加用戶模塊

關于用戶對象,在預置數據類型中,我們已經設計好了如下結構體:

// 玩家
typedef struct {
	char name[64];  	// 玩家昵稱
	char gender[24];  	// 玩家性別
	int score;  		// 玩家當局分數
	int best_score;  // 玩家歷史最高分數
	short int right_flag;  // 玩家標記的正確旗幟數量
	short int error_flag;  // 玩家標記的錯誤旗幟數量
} Player;

// 全局對象,正在玩游戲的玩家對象
Player* player;

我們這個模塊需要實現玩家對象的初始化、玩家昵稱和性別的可修改、兩類分數的初始化。

在主菜單打印后,選擇功能3后可以進入到用戶設置的菜單,比如修改玩家的昵稱、性別,當選擇修改昵稱時,軟件應該要正確的從緩沖區中獲取到新的昵稱,并且要與舊的昵稱比較,當昵稱不同時,意味著是一個新的賬號,需要初始化相關的數據信息。當修改玩家的性別時,我們可以進入性別選擇子菜單中再度選擇,不同的選擇決定了 gender 的值是男、女或者不顯示。

首先在 menu.hmenu.c 中聲明和實現相關函數。

// menu.h
// ......
// 設置玩家的菜單
void ShowPlayerMenu();

// 設置玩家性別的菜單
void ShowGenderMenu();

// 處理設置玩家的選項
void HandlePlayerChoice();


// menu.c
// ......
void ShowPlayerMenu() {
	DisplayPlayerInfo();   // 稍后在 display.c 中實現
	printf("1 修改昵稱\t2 修改性別\t0 退出\n");
}

void ShowGenderMenu() {
	printf("-----性別選擇-----\n");
	printf("   1 成為男士   \n");
	printf("   2 成為女士   \n");
	printf("   3 我都不要   \n");
	printf("-----------------\n");
}

void HandlePlayerChoice() {
	int choice = 0;
	printf("[設置用戶]>>> ");
	scanf("%d", &choice);
	switch (choice)
	{
	case 1: {
		char name[50] = "";
		printf("請設置用戶昵稱:");
		scanf("%s", name);
		ModifyPlayerName(name);  // 稍后在 game.c 中實現
		break;
	}
	case 2: {
		ShowGenderMenu();
		int gender_choice = 0;
		printf("請選擇序號設置性別:");
		scanf(" %d", &gender_choice);
		ModifyPlayerGender(gender_choice);  // 稍后在 game.c 中實現
		break;
	}
	default: {
		break;
	}
	}
}

友友可能已經看到了上方的3個函數,DisplayPlayerInfo 函數負責打印玩家的信息。

// display.h
// ......
void DisplayPlayer();  // 打印 玩家性別  不換行,比如:圖圖女士、兔兔男士等等
void DisplayPlayerInfo();  // 打印 用戶的分數性別和當前是哪個用戶

// diplay.c
// ......

ModifyPlayerName(name) 函數的作用是修改全局變量 player 的昵稱,其中會有一些細節的小處理。而 ModifyPlayerGender(gender_choice) 修改的是玩家的性別,主要利用的還是 switch 語句。

// game.h
// ......
#include <string.h>  // 不要忘記了!!
// ......
// 修改玩家昵稱
void ModifyPlayerName(char name[50]);

// 修改玩家性別
void ModifyPlayerGender(int gender);

// game.c
// ......
void ModifyPlayerName(char name[50]) {
	if (strcmp(player->name, name) != 0) {
        // 如果昵稱和之前的不一樣,重新初始化用戶的相關信息
		strcpy(player->gender, "");
		player->score = 0;
		player->best_score = 0;
		player->right_flag = 0;
		player->error_flag = 0;
	}
	strcpy(player->name, name);

}

void ModifyPlayerGender(int gender) {
	switch (gender)
	{
	case 1: {
        // 修改結構體中的字符串,務必使用 strcpy 函數
		strcpy(player->gender, "男士");
		break;
	}
	case 2: {
		strcpy(player->gender, "女士");
		break;
	}
	default: {
		strcpy(player->gender, "");
		break;
	}
	}
}

友友是否認為這里就結束了呢?

答案是,沒有那么簡單,還記得我們僅僅只是聲明了全局變量 player 嗎?但是我們對它賦予一定的空間了嗎?貌似什么初始化的操作都還沒做。因此我們需要對它進行預加載,后續就能夠方便的使用分配好的內存空間。

// game.c 的 Preload 函數中
void Preload() {
    // ...
	// 預加載玩家缺省信息
	player = (Player*)malloc(sizeof(Player));
	if (player == NULL) return;
	strcpy(player->name, "無名大俠");
	strcpy(player->gender, "");
	player->score = 0;
	player->best_score = 0;
	player->error_flag = 0;
	player->right_flag = 0;
	// 創建游戲對象
	// ...
}

// game.c 的 InitGame 函數中
void InitGame() {
	// ...
	// 新的游戲時初始化參數
	game->time = 0;
    // 新增的,可以思考為什么要增加
    // 答案:除了最高分、昵稱、性別,其它信息都是當局游戲所有,而不是持續存在的,因此務必歸零!
	player->score = 0;
	player->error_flag = 0;
	player->right_flag = 0;
	// 初始化行號、列號、單元格
    // ...
}

還有一個函數我們寫了,但是還沒用,誰呢?當然是 DisplayPlayer() 咯。如下增加調用后,我們在游戲失敗或者勝利時都能夠加上稱謂,比如"無名大俠男士踩雷咯,游戲失敗!!!"

// 來到 game.c 的 CheckGameOver 函數中

bool CheckGameOver() {
	if (game->state == GAME_LOSE) {
		DisplayPlayer();
		printf("踩雷咯,游戲失敗!!!\n");
		return true;
	}
	// ....
	// 勝利的情況
	game->state = GAME_WIN;
    DisplayPlayer();
	printf("好厲害哦,人家好喜歡~\n");
	return true;
}

最最最重要的來了,要在 main.c 源文件的 switch 分支中增加對應的選項和函數調用!

    case 3: {
        ShowPlayerMenu();
        HandlePlayerChoice();
        break;
    }

實現后的效果:

3.3 游戲后如何計分

  1. 難度因素:不同的難度級別應該有不同的基礎分數。例如,簡單難度的基礎分數是1000,中等難度的基礎分數是2000,困難難度的基礎分數是3000。
  2. 時間因素:游戲的分數應該和玩家完成游戲所花費的時間成反比。例如,每過一秒,玩家的分數就減少1%。這意味著,如果玩家在100秒內完成游戲,那么他們的分數就會減少到原來的37%。
  3. 正確標記地雷的數量:每正確標記一個地雷,玩家的分數就增加一定的分數。例如,每正確標記一個地雷,玩家的分數就增加50分。
  4. 錯誤標記的數量:每錯誤標記一個地雷,玩家的分數就減少一定的分數。例如,每錯誤標記一個地雷,玩家的分數就減少100分。

以上的計分邏輯可以通過以下的公式來表示:

分數 = 基礎分數 * (0.99 ^ 時間) + 正確標記的地雷數量 * 50 - 錯誤標記的數量 * 100

來到 main.cEndGame 函數中:

void EndGame() {
	// ...
	// TODO 計分并總結成績
    CalFinalScore();  // 這個方法計算得分
	DisplayGameOver();  // 這個方法結算游戲結束成績
	// ...
}

根據我們的公式可知,基礎分數就是當前配置結構體中的成員——基礎分數,一局游戲的時間可以很輕松的得到,我們聲明兩個全局變量 start_timeend_time,用來統計游戲開始和結束的時間結點,差值賦予給游戲對象 game 的成員變量 time 中。

// game.h
// ...
#include <math.h>  // 要用到 pow 函數
#include <time.h>  // 要用到 time 函數
// ...
// 聲明全局的時間變量
time_t start_time;
time_t end_time;

// game.c 中
void EndGame() {
    end_time = time(NULL);
	game->time += (int)(end_time - start_time);
	// ...
}

接下來就是正確標記雷的數量以及錯誤標記雷的數量的獲取,這里有兩種方法,先來看第一種,第一種耦合度較低,并且你可以刪除掉player 對象中的right_flag和error_flag,直接在 CalFinalScore 函數中就可以完成統計,但是有一定的開銷。

short int right_flag = 0, error_flag = 0;
for (int row = 1; row <= board_config.rows; ++row) {
    for (int col = 1; col <= board_config.cols; ++col) {
        // 沒有標記就跳過
        if (!game->board[row][col].is_flagged) continue;
		// 標記了
        if (game->board[row][col].is_mine) 
            right_flag++; // 是雷
        else 
            error_flag++;  // 不是雷
    }
}

第二種,比較亂,高耦合,但是開銷比較低,思路很簡單,給玩家對象綁定上這兩個屬性right_flag和error_flag,我們之前已經做了,然后在玩家標記單元格時合理判斷即可:

int HandleInput(char operate, int row, int col) {
	// ...
	case 'f':
		if (game->state == GAME_RUNNING) {
			game->state = GAME_ENTER;
			if (!game->board[row][col].is_revealed) {
				game->board[row][col].is_flagged = !game->board[row][col].is_flagged;
                // 如果是標記
				if (game->board[row][col].is_flagged) {
					game->board[row][col].value = cell_flagged;
					if (game->board[row][col].is_mine)
						player->right_flag += 1;
					else
						player->error_flag += 1;

				}
				else {  // 如果是取消標記
					game->board[row][col].value = cell_unexplored;
					if (game->board[row][col].is_mine)
						player->right_flag -= 1;
					else
						player->error_flag -= 1;
				}
			}
			game->state = GAME_RUNNING;
		}
		break;
	// ...
	return game->state;
}

這里我選擇第二種。接下來,看一下正主 CalFinalScore 函數的實現。

// game.h
// ......
// 計算得分
int CalFinalScore();


// game.c
// ......
int CalFinalScore() {
	int game_time = game->time;
	int base_score = 0;
	if (game->state == GAME_WIN)  // 勝利了基礎分才有用
		base_score = board_config.base_score;
	short int right_flag = player->right_flag, error_flag = player->error_flag;
	int score = (int)(base_score * (pow(0.995, game_time)) + 50 * right_flag - 100 * error_flag);
    // 修正下限
	if (score < 0) score = 0;
    // 當前賬號的最高分判斷
	if (score > player->best_score) player->best_score = score;
	player->score = score;
	return score;
}

然后在實現一局游戲的成績結算打印函數 DisplayGameOver

// display.h
// ......
void DisplayGameOver();


// display.c
void DisplayGameOver() {
	printf("結算成績:\n");
	printf("本局得分——%d\n", player->score);
	printf("歷史最高——%d\n", player->best_score);
}

實現效果如下:

3.4 排行榜實現

我們之前就已經定義好了排行榜的數據結構和聲明了一個全局排行榜變量:

// game.h
// ...
// 排行榜上存儲的玩家分數最大數量
#define MAX_PLAYERS 10
// ...

// 排行榜
typedef struct {
	Player players[MAX_PLAYERS]; // 玩家列表
	int player_count;	// 當前上榜的玩家數量
} Leaderboard;

// 排行榜
Leaderboard* leaderboard;
// ...

我們這里使用的非常簡單,對鏈表亦或者順序表的選擇并無太大的要求,為什么?最大數據量僅為10,無論讀還是改的開銷,其實都很微弱,忽略不計。此處選擇順序表結構。

排行榜的實現無非克服兩個方向的問題,一個是榜上人數沒有滿時怎么添加,一個是榜上人數滿了怎么添加。

  • 當榜上的人數沒有滿時,我們可以將玩家插入到 players 數組中,然后進行倒序排序;

  • 當榜上的人數已滿,我們將排行榜倒序排序,然后比較最后一名與當前玩家的分數,后者小則證明當前玩家的分數無法上榜,反之我們從后往前遍歷的比較,直到出現第一個比當前玩家分數大的排名,這個排名的后一位就是該玩家所能擁有的排名!

對于排序方法,我這里僅僅只是當時想學習快速排序時而對應的寫下快排算法,你可以根據興趣來。

// game.h
// ......
// 添加用戶到排行榜
void AddPlayerToLeaderboard(Player* player);
// 指定下標插入玩家
void MovePlayerToEnd(Player* player, int index);
// 對排行榜進行排序
void SortLeaderboard();
// 交換兩個Player
void SwapPlayer(Player* a, Player* b);
// 快速排序的分區函數
int Partition(Player arr[], int low, int high);
// 快速排序函數
void QuickSort(Player arr[], int low, int high);


// game.c
// 預加載
void Preload() {
	// ......
	// 預加載排行榜
	leaderboard = (Leaderboard*)malloc(sizeof(Leaderboard));
	leaderboard->player_count = 0;
}

// ...
void AddPlayerToLeaderboard(Player* player) {
	if (leaderboard->player_count < MAX_PLAYERS) {
		leaderboard->players[leaderboard->player_count] = *player;
		leaderboard->player_count++;
		SortLeaderboard(); // 增加完后,進行倒序排序
	}
	else {
		// 先倒序排序,然后進行判斷和移動
		SortLeaderboard(); 
		int last_index = MAX_PLAYERS - 1;
		if (leaderboard->players[last_index].score >= player->score)
			return;  // 上榜資格的認定
		for (last_index; last_index >= 0; last_index--) {
			if (leaderboard->players[last_index].score < player->score) {
				continue;
			}
			else {
				int get_index = last_index + 1;
				MovePlayerToEnd(player, get_index);
				return; // 分數比前個玩家高
			}
		}
		// 當上述不滿足,即分數霸榜
		MovePlayerToEnd(player, 0);
	}
}

void SwapPlayer(Player* a, Player* b) {
	Player t = *a;
	*a = *b;
	*b = t;
}

int Partition(Player arr[], int low, int high) {
	int pivot = arr[low].score;
	int i = low, j = high;
	while (i < j) {
		while (i < j && arr[j].score < pivot)
		{
			j--;
		}
		if (i < j) {
			SwapPlayer(&arr[j], &arr[i]);
			i++;
		}
		while (i < j && arr[i].score > pivot)
		{
			i++;
		}
		if (i < j) {
			SwapPlayer(&arr[i], &arr[j]);
			j--;
		}
		if (i >= j){
			return j;
		}

	}
	return j;
}

void QuickSort(Player arr[], int low, int high) {
	if (low < high) {
		int pi = Partition(arr, low, high);

		QuickSort(arr, low, pi - 1);
		QuickSort(arr, pi + 1, high);
	}
}

void SortLeaderboard() {
	QuickSort(leaderboard->players, 0, leaderboard->player_count - 1);
}

void MovePlayerToEnd(Player* player, int index) {
	for (int cur = MAX_PLAYERS - 1; cur > index; cur--) {
		leaderboard->players[cur] = leaderboard->players[cur - 1];
	}
	leaderboard->players[index] = *player;
}

主要調用的是 AddPlayerToLeaderboard(Player* player) 函數,在何處調用呢?

void EndGame() {
	// ...

	// TODO 計分并總結成績
	CalFinalScore();  // 這個方法計算得分
	DisplayGameOver();  // 這個方法結算游戲結束成績

	// TODO 計分后計入排行榜
	AddPlayerToLeaderboard(player);

	// 狀態恢復到待初始化
	game->state = GAME_INIT;
}

主菜單的瀏覽排行榜功能非常簡單,在 diplay.h 頭文件中聲明函數 void DisplayLeaderboard();:

// display.c
void DisplayLeaderboard() {
	for (int i = 0; i < leaderboard->player_count; i++) {
		printf("%d. %s: %d\n", i + 1, leaderboard->players[i].name, leaderboard->players[i].score);
	}
}

然后更改 main.c 文件中的 main 函數,增加功能選項。

case 6: {
    DisplayLeaderboard();
    break;
}

效果預覽:

4 本地存儲

讀者可能在看上面的功能實現時,可能產生這樣的疑問,應該還有一個保存游戲、繼續游戲的模塊沒有實現吧?上面的功能再怎么增加,貌似都只能在該程序的生命周期中玩,而不能持久的玩,排行榜實現了,但是意義不大。其實我們還差得比較多,未實現的還有保存游戲加載游戲(繼續游戲也包含其中)保存排行榜加載排行榜。我們這一章的目的就是,如果不存在本地數據,那么我們直接按照之前的函數功能去創建游戲數據,如果已經存在了,那么就加載本地的游戲數據覆蓋。

現在 storage.h 文件中聲明以下函數和宏:

#define GAME_FILE "minesweeper.dat"
// 游戲數據路徑
#define BOARD_FILE "leapboard.dat"
// 排行榜數據路徑

// 以下含義?看名字吧~家人
bool LoadGame();
bool SaveGame();
bool SaveLeaderboard();
bool LoadLeaderboard();

4.1 存儲游戲數據

何時存儲游戲數據呢?那當然是玩家在主菜單功能選項選擇功能5時去人為保存。還有嗎?仔細想想,當用戶保存上一次的游戲數據,然后繼續游戲,玩了一會兒,玩家選擇中途退出,那么我們就要去保存繼續游戲的的數據,玩家就不用在菜單中再次去手動保存咯。因此這就是開始游戲和繼續游戲的區別,開始游戲完全就是新的一盤游戲,而繼續游戲將會讀取本地數據,未讀取到則以開始游戲的核心方法去開始,讀取到了,則加載游戲,玩家中途退出時,自動保存。

先來看看這兩個加載保存的函數的實現:

// storage.c
#include "game.h"
#include "storage.h"


bool LoadGame() {
    FILE* file = fopen(GAME_FILE, "rb");
    if (file == NULL) return false;
	// 讀取當前配置
    fread(&board_config, sizeof(BoardConfig), 1, file);
	// 讀取玩家
    player = (Player*)malloc(sizeof(Player));
    if (player == NULL) {
        printf("Failed to allocate memory for player.\n");
        return false;
    }
    fread(player, sizeof(Player), 1, file);
	// 讀取游戲對象包括游戲板等數據
    game = (Game*)malloc(sizeof(Game));
    if (game == NULL) {
        printf("Failed to allocate memory for game.\n");
        return false;
    }
    fread(game, sizeof(Game), 1, file);
    game->board = (Cell**)malloc(board_config.real_rows * sizeof(Cell*));
    if (game->board == NULL) {
        printf("Failed to allocate memory for game board.\n");
        return false;
    }
    for (int i = 0; i < board_config.real_rows; i++) {
        game->board[i] = (Cell*)malloc(board_config.real_cols * sizeof(Cell));
        if (game->board[i] == NULL) {
            printf("Failed to allocate memory for game board row.\n");
            return false;
        }
        fread(game->board[i], sizeof(Cell), board_config.real_cols, file);
    }

    fclose(file);
    return true;
}

bool SaveGame() {
	FILE* file = fopen(GAME_FILE, "wb");
	if (file == NULL) return false;
	// 向文件中寫入當前配置
    fwrite(&board_config, sizeof(BoardConfig), 1, file);
	// 向文件中寫入當前用戶
    fwrite(player, sizeof(Player), 1, file);
	// 向文件中寫入游戲對象以及游戲板
    fwrite(game, sizeof(Game), 1, file);
    for (int i = 0; i < board_config.real_rows; i++) {
        fwrite(game->board[i], sizeof(Cell), board_config.real_cols, file);
    }

	fclose(file);
    return true;
}

bool SaveLeaderboard(){

}

bool LoadLeaderboard(){

}

可以看到,讀取/保存成功與否都是返回 bool 值,這就很方便了。

// main.c
Preload();
if (!LoadGame()) InitGame();  // 如果加載失敗,則初始化,思考一下為什么預加載后要進行加載或者初始化
do
// ...

這里進行加載數據的原因,是我考慮到玩家在沒有任何數據的情況下,他第一次進入游戲,然后保存游戲,那么所保存的數據都將是未初始化的數據,這并不好,因此我們這里增加這樣一行代碼。

然后我們看一下主菜單處保存游戲的調用:

// main.c
	bool save_state = false;
// ...
    case 5: {
        save_state = SaveGame();
        if (save_state) printf("保存成功\n");
        else printf("保存失敗\n");
        break;
    }

緊接著來看一下主菜單繼續游戲的實現。

// game.h
// ...
// 繼續游戲
void ContinueGame();


// game.c
// 和開始游戲的函數非常相似,這里為什么不提取公共部分,我的考慮是因為這樣方便可定制部分功能。
void ContinueGame() {
	char operate = 'e';
	int row = 0, col = 0, game_state = 0;
	start_time = time(NULL);
	bool load_state = LoadGame();
	if (!load_state) {  // 加載成功的狀態
		InitGame();
	}
	else {
        // 如果加載成功更改狀態,因為改為 GAME_RUNNING,我們才能對其操作
		game->state = GAME_RUNNING;
	}
	while (true)
	{
		DisplayGameState();
		printf("操作符:e 探索\tf 標記\tq 終止\t 格式[操作符 行號 列號]\n");
		printf("操作:");
		scanf(" %c %d %d", &operate, &row, &col);
		game_state = HandleInput(operate, row, col);
		if (game_state == 0) {
			SaveGame();  // 保存游戲
			break;
		}
		if (game_state == -1) continue;
	}
}

void EndGame() {
	// ...

	// 上下初始狀態是為了防止繼續游戲后出現的還是游戲結束的畫面
	game->state = GAME_INIT;
	InitGame();  // 清空游戲板并恢復到初始
	game->state = GAME_INIT;
}


// main.c 中如何調用?
    switch (choice)
    {
    // ......
    case 2: {
        ContinueGame();
        break;
    }
    // ......

我們可以看一下效果,還是非常棒的:

4.2 存儲排行榜數據

該章節是本篇博文的最后一部分,這部分的代碼其實非常簡單,當你能夠對上述的存儲游戲數據有一定的了解之后,存儲排行榜和讀取排行榜真的再簡單不過了!直接上代碼:

// storage.c
// ......
bool SaveLeaderboard() {
    FILE* file = fopen(BOARD_FILE, "wb");
    if (file == NULL) return false;

    fwrite(leaderboard, sizeof(Leaderboard), 1, file);
    fwrite(leaderboard->players, sizeof(Player), leaderboard->player_count, file);

    fclose(file);
    return true;
}

bool LoadLeaderboard(){
    FILE* file = fopen(BOARD_FILE, "rb");
    if (file == NULL) return false;

    fread(leaderboard, sizeof(Leaderboard), 1, file);
    fread(leaderboard->players, sizeof(Player), leaderboard->player_count, file);

    fclose(file);
    return true;
}

什么時候保存呢?真相只有一個——在 game.c 中的 EndGame 中調用 SaveLeaderboard 函數,非常好理解,就是每次上榜后,我們就進行排行榜的保存。優化建議:我比較懶,這里就不處理未上榜的情況啦,因為未上榜就無需在重新寫一遍文件了嘛!

void EndGame() {
	// ...
	// 計入排行榜
	AddPlayerToLeaderboard(player);
	SaveLeaderboard();
	// ...
}

那什么時候加載排行榜呢?非常簡單!預加載之后立馬加載排行榜數據即可。

Preload();
LoadLeaderboard();  // 這里喲 親~
if (!LoadGame()) InitGame();

寫到這里,我也不知道讀者明白幾何,可曾注意到一些細節,比如繼續游戲,每次都是從本地讀取出來數據,當玩家一會兒繼續游戲、一會兒退出的,那么我們的計分方式還有問題嗎?這就是我為何使用的 += 而不是=的意義,只有 += ,才能夠統計每次玩了多長時間并加到原來的時間上。又或者我這里的快排分區的思想你自己又還能怎么修改呢心里可有答案?我已經在無資格上榜那里做了條件判斷,我所提到的優化僅僅只是幾行代碼的問題,這些都留給讀者慢慢的細嚼慢咽,學習掃雷游戲,我的收獲還是頗豐的,將來有機會,寫個最優決策的掃雷小掛玩玩。hh~生活愉快,友友們

總結

以上是生活随笔為你收集整理的探秘扫雷游戏的C语言实现的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

久久午夜国产 | 亚洲综合在线视频 | 色视频成人在线观看免 | 国产在线 一区二区三区 | 久久理论电影网 | 国产精华国产精品 | 久久成人视屏 | 日本女人的性生活视频 | 少妇bbbb搡bbbb桶 | 日韩av在线一区二区 | 国产精品一区免费观看 | 久久精品国产成人 | 国产精品久久久久婷婷 | 97视频人人澡人人爽 | 五月天堂网 | 国产精品99久久久久久宅男 | www黄com | 欧美激情第28页 | 色综合天天做天天爱 | 久久久免费看视频 | 国产精品第三页 | 日韩精品免费在线播放 | 麻豆成人小视频 | 久久精品观看 | 中文字幕永久在线 | 国产精品亚洲人在线观看 | 五月婷婷激情 | 中文字幕一区二 | 夜夜躁狠狠燥 | 亚洲综合在线播放 | 国产专区精品 | 欧美 激情 国产 91 在线 | 在线高清一区 | 97在线影视 | 中文字幕久久精品亚洲乱码 | 日日夜夜av | 国产视频精选在线 | 久久久久久免费 | 天天操欧美 | 亚洲免费在线观看视频 | 日本最大色倩网站www | 国产中文在线播放 | 国产无区一区二区三麻豆 | 日本公乱妇视频 | 国产96精品| 国产精品video爽爽爽爽 | 青青看片 | 片网站| 波多野结衣久久资源 | 亚洲国产精品一区二区久久hs | 国产精品久久久久久久久久99 | 久久久精品 一区二区三区 国产99视频在线观看 | 成人欧美一区二区三区黑人麻豆 | 国产精品视频999 | 久久免费精彩视频 | 久久久免费看片 | 黄色一级大片免费看 | 免费在线观看不卡av | 亚洲欧美日韩一区二区三区在线观看 | 欧美成人h版在线观看 | 免费视频一二三区 | 少妇bbb搡bbbb搡bbbb | 国产一级二级三级视频 | 日韩精品免费专区 | 精品成人a区在线观看 | 日韩精品视频在线免费观看 | 色综合婷婷 | 日韩黄色在线观看 | 国产99中文字幕 | 99久久精品免费视频 | 日韩av免费在线看 | 国产自偷自拍 | 国产免费区 | 日韩一区二区三区免费电影 | 久久免费国产电影 | 9草在线| 中文字幕免费在线 | 中文在线字幕观看电影 | 午夜精品久久久久久久99水蜜桃 | 日韩欧美在线观看一区二区三区 | 国产精品久久久久久久久久久久午夜 | 9999精品免费视频 | 99在线精品视频观看 | 在线视频你懂 | 色a在线观看 | 69久久久| 婷婷色狠狠 | 81精品国产乱码久久久久久 | 亚洲精品国产精品国自产观看浪潮 | 国产精品久久久久久久妇 | 伊人www22综合色 | 日韩在线观看一区二区三区 | 欧美日韩在线视频一区二区 | 色婷婷av一区 | 欧美精品视 | 日韩欧美精品在线 | 在线国产精品视频 | 最新真实国产在线视频 | 国产精品高清在线观看 | 久久精品国产亚洲精品 | 亚洲最大av网站 | 婷婷深爱网 | 国产剧情一区在线 | 国模视频一区二区 | 天天综合入口 | 日韩av女优视频 | 日韩手机在线观看 | 日韩在线免费看 | 国产精品第7页 | 在线观看视频免费大全 | 日日综合 | 欧美在线久久 | www.夜夜| 天天干天天操天天入 | 曰韩精品 | 免费裸体视频网 | 综合色爱| 国产精品一区一区三区 | 五月天六月色 | 亚洲91中文字幕无线码三区 | 天天综合网天天综合色 | 黄色日批网站 | 国产精品成人免费一区久久羞羞 | 狠狠色丁香九九婷婷综合五月 | 玖玖爱在线观看 | 在线精品观看国产 | 综合亚洲视频 | 国产九色视频在线观看 | 久久无码av一区二区三区电影网 | 国产乱码精品一区二区三区介绍 | 欧美性春潮| 日日综合网 | 亚洲专区在线 | 奇米网网址 | 91av资源在线 | 精品国产免费一区二区三区五区 | 99精品国产在热久久下载 | 国产成人精品国内自产拍免费看 | 永久免费观看视频 | 国产91丝袜在线播放动漫 | 美女精品久久 | 伊人中文在线 | 久久一视频 | 在线成人短视频 | 国产精品日韩欧美一区二区 | 99色视频| 久香蕉 | 人人草人人做 | 天天射网| 欧美国产日韩一区 | 午夜久操| 91在线porny国产在线看 | 天天弄天天操 | 色 免费观看 | 91正在播放 | 麻豆久久久 | 视频在线观看日韩 | 在线黄av| 在线免费观看麻豆视频 | 国产情侣一区 | 国内精品久久影院 | 欧美粗又大 | 国产在线视频导航 | 99精品国产一区二区三区麻豆 | 日韩在线观看小视频 | 亚洲黄色高清 | 久草免费资源 | 91精品婷婷国产综合久久蝌蚪 | 国产.精品.日韩.另类.中文.在线.播放 | 国产精品免费久久久久久久久久中文 | 人人澡人人爱 | 黄网站色欧美视频 | 国产精品婷婷 | 夜色资源网 | 亚洲在线网址 | 91成人免费电影 | 日韩久久电影 | 久久久久久久久久久黄色 | 婷婷亚洲综合五月天小说 | 91女子私密保健养生少妇 | 国内精品久久久久久久97牛牛 | 国产精品字幕 | 亚洲精品1234区 | 91亚色视频| 97超碰人人模人人人爽人人爱 | 天天碰天天操 | 在线观看视频国产一区 | 日韩特黄一级欧美毛片特黄 | 亚洲精品视频免费观看 | 亚洲视频免费在线 | 99色视频| 国产又粗又硬又爽视频 | 9999亚洲| 欧美污在线观看 | 国产精品免费不卡 | 日韩理论在线播放 | 欧洲亚洲国产视频 | 精产嫩模国品一二三区 | 国产精品一区久久久久 | 免费看亚洲毛片 | 17videosex性欧美| 日韩av专区 | 久久久国产影院 | 一区中文字幕 | 成人a视频片观看免费 | 黄色电影在线免费观看 | 91麻豆网站 | 亚洲黄色片在线 | 中文字幕在线观看第一区 | 免费在线观看视频一区 | 免费看v片 | 97在线视频网站 | 精品国产久 | 2019天天干夜夜操 | 日本护士撒尿xxxx18 | 国产成人精品久久久久蜜臀 | 久久99久| 在线国产欧美 | 99视频在线免费 | 日韩视频一区二区在线观看 | 最近最新中文字幕视频 | 免费人做人爱www的视 | 天天色天天色天天色 | 国产精品久久久久久久久久 | 狠狠躁夜夜躁人人爽超碰97香蕉 | 精品视频在线免费观看 | 91传媒视频在线观看 | 国产免费黄视频在线观看 | 日韩最新在线 | 麻豆视频国产 | 成人黄色大片 | 成人av中文字幕 | 国产午夜一区 | 中国一级特黄毛片大片久久 | 韩日三级在线 | 欧美日韩xxxxx| 日日夜夜精品免费 | 婷婷网站天天婷婷网站 | 欧美性视频网站 | 高清精品久久 | 日韩欧美在线国产 | 国产精品一区二区av麻豆 | 免费观看福利视频 | 日韩av电影中文字幕在线观看 | 一区二区伦理电影 | 91av社区| 在线免费观看羞羞视频 | 久久草网站 | 欧美成人一区二区 | 国产精品久久久久久久久久99 | 亚洲精品在线免费播放 | 日韩免费在线 | 免费观看国产成人 | 人人干人人干人人干 | 亚洲精品视频在线播放 | 香蕉视频免费看 | 网址你懂的在线观看 | 日韩精品不卡在线观看 | 中文字幕在线观看av | 午夜视频在线瓜伦 | 97超碰资源总站 | 日本中文字幕在线免费观看 | 91一区二区在线 | 久久av网| 狠狠网 | av三级av | 久久精选视频 | 国产一区二区在线免费播放 | 黄色大全在线观看 | 一区二区 不卡 | 精品自拍sae8—视频 | 国产成人99av超碰超爽 | 久久久国产视频 | 青青草国产在线 | 人人爽人人爽人人片av | 91av手机在线 | 91成人黄色 | 黄色在线观看网站 | 欧美性精品 | 亚洲精品88欧美一区二区 | 欧美精品久久久久a | 九九三级毛片 | 婷婷丁香在线 | 免费网址你懂的 | 中国一 片免费观看 | 精品国产一二三四区 | 国产91av视频在线观看 | 日本3级在线观看 | 欧美在线aa | 精品在线视频播放 | 亚洲欧美视频在线观看 | 在线v片| 亚洲精品成人在线 | 青春草免费视频 | 麻豆视频国产在线观看 | 国产精品久久久久一区二区国产 | 在线国产欧美 | 天天五月天色 | 国产主播99| 久久99国产精品二区护士 | 欧美国产一区二区 | 国产成人精品av在线观 | 天天色视频 | 91香蕉国产 | 国产精品99久久久精品 | 午夜性生活片 | 久久一区二区三区日韩 | 在线观看www视频 | 日本xxxx裸体xxxx17| 中文字幕av在线 | 美女黄频网站 | 成人av高清在线观看 | 麻豆视频一区 | 免费a v观看 | 99精品国产高清在线观看 | 天天综合视频在线观看 | 国产成人精品一区二区三区免费 | 伊人资源视频在线 | 免费av一级电影 | 高清视频一区 | 国产午夜av | 精品国产一区二区三区蜜臀 | 9热精品 | 免费看三级黄色片 | 五月婷婷丁香 | 色婷婷色 | 日韩午夜在线观看 | 97热久久免费频精品99 | 国产无吗一区二区三区在线欢 | 99久久久国产精品免费99 | 欧美激情精品久久 | 国产69精品久久久久久久久久 | 婷婷中文字幕在线观看 | 国产成人一区二区啪在线观看 | 激情五月伊人 | 免费进去里的视频 | 国产亚洲综合精品 | 日韩aⅴ视频 | 国产精品嫩草影院123 | 又黄又刺激视频 | 欧美精品三级在线观看 | 精品视频999 | 婷婷五综合 | 国产成人综合图片 | 国产精品中文字幕在线观看 | 黄色中文字幕在线 | 婷婷去俺也去六月色 | 91在线免费公开视频 | 一区二区三区动漫 | 色橹橹欧美在线观看视频高清 | 在线视频免费观看 | 91视频 - x99av | 蜜臀av性久久久久蜜臀aⅴ流畅 | 99视频国产在线 | 日韩欧美精品在线观看视频 | 日本九九视频 | 97色婷婷成人综合在线观看 | 天天摸天天舔 | 国产高清一区二区 | 五月天丁香视频 | 麻豆视频在线免费看 | 天天干天天在线 | 久久精选| 天天综合色 | 久久久噜噜噜久久久 | 日日躁天天躁 | 亚洲一区二区高潮无套美女 | 日韩在线在线 | 国产午夜精品一区二区三区嫩草 | 国产精品日韩在线播放 | 久久久久久久福利 | 成人黄色av免费在线观看 | 欧美一区二区三区在线视频观看 | 日本精品在线视频 | 国产精品久久久久久高潮 | 国产99久久精品 | 欧美精品一二 | www视频在线观看 | av成人免费网站 | 欧美日韩高清在线 | 国产福利91精品张津瑜 | 91在线视频在线 | 9在线观看免费高清完整 | 亚洲国产经典视频 | 国产视频欧美视频 | a天堂最新版中文在线地址 久久99久久精品国产 | 免费看黄色小说的网站 | www.久久免费视频 | 国产婷婷一区二区 | 3d黄动漫免费看 | a天堂中文在线 | 在线日韩av| 久久久久女人精品毛片九一 | 97超碰在线久草超碰在线观看 | 欧美日韩中文字幕在线视频 | 国产精品99久久久久久久久久久久 | 手机在线永久免费观看av片 | 国产精彩视频 | 国产精品18p | 中文字幕资源站 | 色视频网站在线观看一=区 a视频免费在线观看 | 中文字幕大全 | 日日干av | 日韩国产精品久久久久久亚洲 | 玖草在线观看 | 日韩网站中文字幕 | 国产精品久久久久久久久久久久午夜片 | 日韩影片在线观看 | 国产精品99久久免费黑人 | 国产免费久久久久 | 成人黄色片在线播放 | 精品久久毛片 | 欧美激情视频一区 | 精品久久国产精品 | 久久国产精品色婷婷 | av一区二区在线观看中文字幕 | 成人免费视频免费观看 | 麻豆精品视频在线观看免费 | 美女福利视频网 | 成人黄色小说在线观看 | 欧美日在线观看 | av片子在线观看 | 91视频 - x99av | 亚洲永久字幕 | 欧美地下肉体性派对 | 久久午夜网 | 日日夜夜天天综合 | 日韩av片无码一区二区不卡电影 | 在线观看激情av | 欧美一级视频在线观看 | 成人黄色大片在线观看 | 97色狠狠| 欧美美女一级片 | 91色亚洲 | 久久综合九色综合网站 | 在线不卡中文字幕播放 | 亚洲精品乱码久久久久久蜜桃欧美 | 国产精品白虎 | 国产精品乱码在线 | 欧美一区中文字幕 | 免费在线观看日韩 | 五月婷婷综合激情 | 亚洲婷婷综合色高清在线 | 美女免费视频一区二区 | 亚洲无吗天堂 | 一级黄色免费 | 免费男女网站 | 亚洲视频中文 | 黄色免费大全 | 久久国产精品一区二区三区四区 | 黄色软件在线观看 | 久久久久久久久久久久av | 99视频网站 | 久久99这里只有精品 | 6080yy精品一区二区三区 | 国产一区在线观看免费 | 欧美日韩亚洲在线 | 91九色性视频 | 日韩免费在线观看视频 | 欧美成人精品欧美一级乱黄 | 成+人+色综合 | 97视频资源 | 蜜臀av夜夜澡人人爽人人 | 一区二区国产精品 | 日韩av二区 | 天天射天天艹 | 日韩aa视频 | 欧美片一区二区三区 | 日本精品视频免费 | 国产日本在线播放 | 人人玩人人添人人 | 中文字幕久久精品 | 欧洲在线免费视频 | 欧美一区二区在线刺激视频 | 黄色在线免费观看网站 | 国产一区二区免费 | 国产1区在线 | 国产91精品久久久久 | 91亚·色 | 国产剧情一区二区在线观看 | 免费高清国产 | 黄色毛片网站在线观看 | 天天天色综合 | 日韩午夜视频在线观看 | 激情在线免费视频 | 国产91对白在线播 | 色婷婷电影网 | av在线看网站 | 99久热在线精品视频观看 | 亚洲狠狠婷婷综合久久久 | 操高跟美女 | 91精品视频在线播放 | 久久97视频 | 精品电影一区 | 久久草草热国产精品直播 | 99久高清在线观看视频99精品热在线观看视频 | 毛片.com| 黄色免费观看 | 色 免费观看 | 97超碰人人澡 | 日韩精品无码一区二区三区 | 天天天天天天天天操 | 九九精品视频在线观看 | 欧美精品在线免费 | 亚州av一区 | 久久久免费看 | 一本色道久久综合亚洲二区三区 | 成人毛片在线观看视频 | 欧美午夜精品久久久久久孕妇 | 麻豆精品视频在线 | 亚洲精品久久久久999中文字幕 | 精品国产一区二区三区久久久久久 | 黄色小视频在线观看免费 | 国产69精品久久久久9999apgf | 亚洲影院色 | 国产成人精品一区二区三区在线 | 国产精品美女视频 | 国产成人亚洲在线电影 | 91丨九色丨国产在线 | 亚洲一区二区视频在线 | 国产在线观看地址 | 久久国产精品免费一区二区三区 | 成人黄色小说视频 | 国产精品一区免费在线观看 | 欧美精品做受xxx性少妇 | 国产精品99久久久久人中文网介绍 | 久久国产精品精品国产色婷婷 | 国产一区欧美在线 | 久久国产精品免费看 | 国产又粗又猛又黄又爽的视频 | 国产精品中文字幕在线播放 | 日b视频国产 | 国产福利一区二区在线 | 欧美色婷 | 国产男男gay做爰 | 亚洲免费观看视频 | www.在线观看视频 | 中文字幕国语官网在线视频 | ,久久福利影视 | 色网址99 | 天天干天天搞天天射 | 欧美精品亚州精品 | 久久一二三四 | 国产成人av | 亚洲每日更新 | 91精品国产网站 | 久久图 | 黄网在线免费观看 | 久久天天躁狠狠躁亚洲综合公司 | 国产综合精品久久 | 国产成人黄色网址 | 黄色三级在线 | 中文字幕在线看视频 | 欧美另类性 | 婷婷国产视频 | 免费在线看成人av | 国产精品9999 | www免费在线观看 | 日韩 精品 一区 国产 麻豆 | 久久99亚洲网美利坚合众国 | 久久精品a| 69成人在线 | 国产精品一区二区三区免费视频 | 激情五月五月婷婷 | 我要色综合天天 | 亚洲在线精品 | 久久精品爱视频 | 天天操天天射天天插 | 天天搞天天干天天色 | 国产69久久久 | 亚洲有 在线 | 精品久久久久久久久久国产 | 国产亚洲91 | 在线日韩视频 | 国产91大片 | 国产黄色免费电影 | 国产中的精品av小宝探花 | 成人久久精品视频 | 97免费中文视频在线观看 | 丁香激情婷婷 | 欧美日韩免费在线视频 | 国产精品久久久av | 婷婷久月 | 久久综合久久综合这里只有精品 | 亚洲国产精彩中文乱码av | 欧美天堂视频在线 | 蜜臀一区二区三区精品免费视频 | av在线不卡观看 | 亚洲一区日韩在线 | 97在线资源| 国产精品理论片在线观看 | 狠狠干综合网 | 五月天综合在线 | 成人黄色电影在线观看 | 超碰在线资源 | 久久精品国产亚洲精品 | 91麻豆国产福利在线观看 | 中文字幕一二 | 夜夜骑日日操 | 国产成人一区二区三区电影 | 婷婷在线不卡 | 99在线观看精品 | 亚洲精选视频在线 | 探花视频在线观看 | 国产18精品乱码免费看 | 日韩精品一区不卡 | 91久久精品日日躁夜夜躁国产 | 精品欧美一区二区三区久久久 | 亚洲精品视频免费在线观看 | 91看片淫黄大片91 | 中文字幕在线影院 | 亚洲综合激情 | 精品国产视频一区 | 国产精品99在线播放 | www.com在线观看 | 午夜国产一区 | 九九色网 | 一区二区三区在线电影 | 欧美午夜理伦三级在线观看 | 国产一区二三区好的 | 一区二区视频在线看 | 国产精品二区在线观看 | 久久久久国产精品一区 | 亚洲欧美成人 | 国产第一福利 | 五月婷婷在线视频观看 | 欧美色图另类 | 色偷偷中文字幕 | 欧美一区二区精品在线 | 欧美夫妻性生活电影 | 日本成址在线观看 | 午夜精品久久久久久久99热影院 | 欧美日韩国产成人 | 国产精品自产拍在线观看桃花 | 日本xxxx.com | 狂野欧美激情性xxxx欧美 | 波多野结衣视频在线 | 欧美大片www | 亚洲日韩中文字幕在线播放 | 亚洲精品久久久蜜桃 | 特级免费毛片 | 欧美日韩免费观看一区=区三区 | 日韩视频免费在线观看 | 啪啪午夜免费 | 九九亚洲视频 | 999久久| 国产一二三区在线观看 | 91精品91 | 国产91亚洲精品 | 国产毛片久久 | 亚洲精品在线观看网站 | 久久av黄色| 亚洲天堂视频在线 | 91超级碰碰| www天天操 | 国产色道 | 激情视频久久 | 欧美日韩调教 | 午夜国产一区二区三区四区 | 91看片在线看片 | 在线 视频 一区二区 | 久久久久国产精品免费 | 热热热热热色 | 成 人 黄 色 视频播放1 | 日韩 精品 一区 国产 麻豆 | 色综合小说 | 亚洲免费av电影 | 一区二区中文字幕在线观看 | 免费能看的av | 中文字幕一区二区三区乱码在线 | 免费毛片一区二区三区久久久 | 91久久久久久久 | 国产尤物一区二区三区 | 中文字幕日本电影 | 综合激情| 国产一线二线三线性视频 | 日韩精品视频在线观看免费 | 日本在线观看视频一区 | 色综合久久99 | 成人午夜剧场在线观看 | 手机av网站| 深夜福利视频在线观看 | 亚洲视频999 | 91麻豆精品国产91 | 国产精品字幕 | 亚洲成人资源网 | 国产在线综合视频 | 天天操综 | 最近更新的中文字幕 | 黄色片网站av | 免费又黄又爽视频 | 亚洲精品美女久久久 | 日韩精品不卡 | 蜜桃麻豆www久久囤产精品 | 国产伦精品一区二区三区在线 | 精品国产免费久久 | 国产精品视频免费观看 | 日韩av五月天 | 99热在线观看免费 | 免费色视频网站 | 91久久爱热色涩涩 | 亚洲国内精品在线 | 天天操天天透 | 国产原创在线视频 | 日日夜夜免费精品 | 国产区精品在线 | 日本黄色一级电影 | 免费大片黄在线 | 四虎成人免费影院 | 伊甸园永久入口www 99热 精品在线 | 久久精品亚洲一区二区三区观看模式 | 人人澡超碰碰97碰碰碰软件 | 免费黄色网止 | 人人视频网站 | 亚洲国产精品视频在线观看 | 丰满少妇麻豆av | 丁香五月缴情综合网 | 久草国产在线观看 | 欧美极品少妇xbxb性爽爽视频 | 精品久久一 | 国产精品爽爽爽 | 天天综合网在线 | 欧美乱熟臀69xxxxxx | 欧美a在线免费观看 | 色a在线观看 | 日本不卡123 | 久久涩视频 | 中文亚洲欧美日韩 | 中文字幕中文字幕在线中文字幕三区 | 久久综合九色综合久99 | 亚洲综合爱 | 日韩成人精品一区二区三区 | 久久久久国产精品午夜一区 | 日日摸日日添夜夜爽97 | 国产精品不卡在线 | 精品国产精品一区二区夜夜嗨 | 一级成人网 | 亚洲国产中文字幕在线观看 | 国产成人精品午夜在线播放 | 日韩免费观看视频 | 91传媒91久久久 | 天天插综合 | 精品国产成人av在线免 | 久久乱码卡一卡2卡三卡四 五月婷婷久 | 日韩免费视频 | 欧美日韩久久不卡 | 色资源在线 | 99久久精品午夜一区二区小说 | 亚洲精品乱码 | 成人欧美一区二区三区在线观看 | 国产精品美女久久 | 成人午夜电影网站 | 狠狠色网 | 午夜黄色影院 | 欧美午夜a | 中文字幕黄网 | 五月婷婷中文字幕 | 天天干夜夜爱 | 久久激情视频 久久 | 国产精品视频地址 | 亚洲在线视频观看 | 六月丁香在线观看 | 在线观看91av | 久久精品视频在线播放 | 91精品一 | 精品久久一区二区三区 | 一级片视频免费观看 | 国产精品久久久久久久久久久久冷 | av电影不卡| 欧美日韩一区二区三区不卡 | 在线免费av电影 | 国产精品久久久久9999吃药 | 超碰97人人射妻 | 久久热首页 | 中文字幕国内精品 | 日韩一区二区三区高清免费看看 | 国产精品一区二区吃奶在线观看 | 粉嫩av一区二区三区四区在线观看 | 少妇高潮流白浆在线观看 | 男女激情片在线观看 | 亚洲精品国产精品国自产观看浪潮 | 国内外成人免费在线视频 | 人人看人人爱 | 人人插人人| 91麻豆免费视频 | 97在线精品国自产拍中文 | 91人人网| 亚洲一区 影院 | 欧美日韩视频 | 久久99视频精品 | 狠狠狠色丁香婷婷综合久久88 | 狠狠躁夜夜躁人人爽视频 | 国产精品一区二区久久精品爱涩 | 免费观看午夜视频 | 激情伊人五月天 | 国产色久| 操操综合 | 国产在线探花 | 国产成人精品一区二区 | 国产999精品久久久 免费a网站 | 久久精品国产久精国产 | 午夜精品福利影院 | 在线观看视频一区二区 | 最新高清无码专区 | 天天综合网久久综合网 | 久久精品老司机 | 蜜臀av麻豆 | 丁香婷婷深情五月亚洲 | 国产不卡在线看 | 在线视频 国产 日韩 | 免费观看丰满少妇做爰 | 91久久精品日日躁夜夜躁国产 | 国产精品久久久久久久久久久久午夜 | 午夜美女福利 | 中文字幕乱在线伦视频中文字幕乱码在线 | 国产一区二区免费在线观看 | 久青草视频在线观看 | 久久精品欧美一区二区三区麻豆 | 91麻豆精品国产自产在线游戏 | 精品中文字幕视频 | 国产精品成人一区二区三区 | 成人97人人超碰人人99 | 午夜精品一区二区三区在线 | 色天天天 | 韩国精品在线观看 | 国产精品久久久久影视 | 久草男人天堂 | 美女黄视频免费看 | 中文字幕免费观看全部电影 | 99色人| 亚洲黄色免费在线看 | 国产精品女人久久久 | av不卡中文字幕 | 天天在线免费视频 | 天天爱综合 | 九九久久精品 | 欧美精品三级在线观看 | 久久99精品久久只有精品 | 99精品欧美一区二区三区黑人哦 | 99视频在线精品国自产拍免费观看 | 丁香视频在线观看 | 中文字幕在线视频一区二区 | 国产人成一区二区三区影院 | 亚洲日本欧美在线 | 涩涩成人在线 | 99久久精品免费看国产 | 丰满少妇久久久 | 精品国产诱惑 | 超碰在线97免费 | 色伊人网 | 日b视频在线观看网址 | 欧美日性视频 | 成人黄色在线看 | 一级特黄av | 人人人爽 | 99这里只有精品视频 | 欧美日韩中文国产一区发布 | 成年人在线免费看 | 亚洲精品视频在线观看免费视频 | 中文区中文字幕免费看 | 五月色丁香 | 91九色蝌蚪视频网站 | 九九久久精品视频 | 丁香久久综合 | 麻花豆传媒mv在线观看网站 | 欧美日韩不卡在线视频 | 亚洲五月婷 | 亚洲欧洲精品一区 | 色香网 | 国产又粗又猛又爽又黄的视频免费 | 日韩精品欧美视频 | 91中文字幕 | 黄污视频网站 | 人人澡人人干 | 福利网址在线观看 | 三级av黄色| 国产一级免费视频 | 99久久精品一区二区成人 | 久久99国产综合精品 | 欧美成人黄色片 | 国产999精品久久久影片官网 | 国产精品一区二区久久精品 | 992tv在线成人免费观看 | 日本狠狠色| 精品国产乱码久久久久 | 亚洲精品视频在线观看免费视频 | 99久久精品国产网站 | 99资源网 | 伊人天天狠天天添日日拍 | 欧洲精品二区 | 免费久久99精品国产婷婷六月 | 人人添人人澡人人澡人人人爽 | 中文字幕在线观看视频免费 | 国产精品视频最多的网站 | 亚洲天天做 | 日本黄色免费在线 | 免费黄色a网站 | 天天色图 | 国产精品国产三级国产 | 久草视频一区 | 美腿丝袜av | 欧美日韩午夜 | 日韩一级成人av | 韩国av一区二区三区在线观看 | 韩国三级在线一区 | 欧美a视频在线观看 | 高清日韩一区二区 | 欧美有色 | 亚洲国产日韩一区 | 日韩av三区 | 亚洲久在线 | 在线а√天堂中文官网 | 久久婷婷一区二区三区 | 色偷偷人人澡久久超碰69 | 97精品超碰一区二区三区 | 日韩中出在线 | 国产中文字幕免费 | 日本黄色大片免费 | 国产精品网址在线观看 | 92国产精品久久久久首页 | 人人天天夜夜 | 久久久久久久av | 久久久久综合精品福利啪啪 | 美女网站视频一区 | 视频福利在线 | 伊人精品在线 | 日韩精品一区二区在线视频 | 亚洲精品国精品久久99热一 | 麻豆传媒视频在线免费观看 | 视频一区二区视频 | 狠狠狠狠狠狠天天爱 | 色婷婷综合成人av | 久久精品一二三区白丝高潮 | 免费亚洲视频在线观看 | 91在线一区二区 | 国偷自产视频一区二区久 | 超碰在线色| 欧美在线观看小视频 | 涩涩网站在线 | 综合久久一本 | 日韩在线观看你懂得 | 97天堂网| 亚洲视频资源在线 | 国内精品久久久久久久影视简单 | 国产精品久久久区三区天天噜 | 日韩欧美视频在线免费观看 | 国产三级视频在线 | 91x色| 国产原创在线视频 | 成年人在线视频观看 | 国产精品久久在线 | 九九热中文字幕 | 欧美一级性| 国产成a人亚洲精v品在线观看 | 最近日本字幕mv免费观看在线 | 日韩精品一区二区三区电影 | 夜夜操网站 | 亚洲精品91天天久久人人 | 婷婷综合伊人 | 国产成人区 | 五月天久久婷 | 日本中文字幕在线观看 | 中文字幕一区二区三区在线观看 | 国产成人综合在线观看 | 国产精品99蜜臀久久不卡二区 | 欧美一区二区精美视频 | 最近中文字幕免费观看 | 美女黄久久 | 亚洲成人蜜桃 | 国产精品久久毛片 | 国产高清在线观看 | 在线观看成人一级片 | 国产一级在线看 | 中文伊人 | 亚洲经典精品 | 狠狠综合网 | 日本在线观看一区 | 久草在线视频免费资源观看 | 久久久久看片 | 色综合中文字幕 | 国产免费久久精品 | 中文字幕888| 久久九九久久九九 | 91成人免费| www.神马久久 | www.天天干.com | 精品视频专区 | 日韩av免费在线看 | 一本色道久久综合亚洲二区三区 | 国产精品情侣视频 | 日韩欧美一区二区在线观看 | 成人免费xyz网站 | 日韩在线免费 | 天天综合成人 | 福利视频入口 | 久久曰视频 | 蜜臀av性久久久久av蜜臀三区 | 精品国精品自拍自在线 |