前言:
不说废话!!!
设计思路(九宫格为例):
1、一共有81个格子,每个格子都会依照规律填入不同的随机数;
2、81个格子填完之后,挖空一部分;
3、玩家可以在被挖空的格子里面进行填空,当玩家填完最后一个数字的时候,要对81个格子里的每一个格子的数字继续遍历规则,成功则扣666,失败则给出提示。
分部代码讲解:
0、定义变量:
#define N 9
typedef struct
{
int base_col;// 随机起始列
int curr_col;// 当前存储列
// 1.当前行的列信息(用二进制表示当前行的数字在哪个位置)
// 例如第0行的数字是1,假设是在九宫格的第7列里面那么:
// sodoku.col_index[0].col_mask = 0x80;
// 2.在数字没有放进九宫格里面之前,这个变量用来展示当前列的有效位置
// 例如第0行的数字,那么他的有效位置就是:
// sodoku.col_index[0].col_mask = 0x1ff;
// 数字放入九宫格之前变量意义是第二条,反之放入之后是第一条
unsigned int col_mask;
} col_index_t;
typedef struct
{
int long_row; // 行
col_index_t col_index[81]; // 每行的数据(一共81个)
} sodoku_t;
// 二维数组储存数独 sodoku_ew[行][列]
int sodoku_ew[N][N]; // 题目
int sodoku_ew_answer[N][N]; // 回答
// col mask
// +-------+-------+-------+
// | 1 2 3 | 4 5 6 | 7 8 9 |
// | 1 2 3 | 4 5 6 | 7 8 9 |
// | 1 2 3 | 4 5 6 | 7 8 9 |
// +-------+-------+-------+
// | 1 2 3 | 4 5 6 | 7 8 9 |
// | 1 2 3 | 4 5 6 | 7 8 9 |
// | 1 2 3 | 4 5 6 | 7 8 9 |
// +-------+-------+-------+
// | 1 2 3 | 4 5 6 | 7 8 9 |
// | 1 2 3 | 4 5 6 | 7 8 9 |
// | 1 2 3 | 4 5 6 | 7 8 9 |
// +-------+-------+-------+
enum
{
COL_MASK_NONE = 0x000, // 0 0000 0000
COL_MASK_1 = 0x001, // 0 0000 0001
COL_MASK_2 = 0x002, // 0 0000 0010
COL_MASK_3 = 0x004, // 0 0000 0100
COL_MASK_4 = 0x008, // 0 0000 1000
COL_MASK_5 = 0x010, // 0 0001 0000
COL_MASK_6 = 0x020, // 0 0010 0000
COL_MASK_7 = 0x040, // 0 0100 0000
COL_MASK_8 = 0x080, // 0 1000 0000
COL_MASK_9 = 0x100, // 1 0000 0000
COL_MASK_123 = 0x007, // 0 0000 0111
COL_MASK_456 = 0x038, // 0 0011 1000
COL_MASK_789 = 0x1C0, // 1 1100 0000
COL_MASK_ALL = 0x1FF // 1 1111 1111
};
1、设计完整的大九宫格:
思路:一共81个格子,我们可以分为81行(第0行到第80行)和9列(第0列到第8列),我们只要在这81个格子里面填数字就好了,每一格填什么数字好一点,在什么位置填好一点。这两个是个难题,下面的代码的思路是在第0~8行填入1,第1~17行填入2,以此类推直到填完最后一个9。每一次填的列数序号都用随机数获取,并且每一次填入之前要判断这个位置是否能存放这一个数字,假设能,则填上数字,并且记录数字的位置信息;假设不能,则返回到上一个数字,更换他在那一行的列数序号。
函数介绍(里面有注释):
// 获得随机数
int calc_rand_col(void)
{
return rand() % 9;
}
// 获得不同于上一个随机数的数字
int calc_next_col(int col)
{
// col 0123456 => 1234560
return (col + 1) % 9;
}
// 获取当前行的有效列信息(通过mask的第一种含义推断出当前行有效列位置并且赋值)
void calc_col_mask_row(sodoku_t *sodoku_tmp, int long_row)
{
int row;
unsigned int mask = COL_MASK_NONE;
// 查看并且或上九宫格内第row行的信息(放了数字的用1表示,这里用二进制表示)
// 假设long_row等于18,则row = 0或者row = 9,
// col_index[0]和col_index[9]在九宫格里面是同一行,所以mask要或上他们的信息
for (row = long_row % 9; row < long_row; row += 9)
{
mask |= sodoku_tmp->col_index[row].col_mask;
}
// 查看并且或上九宫格内相同数字所在行的列信息
// 假设long_row等于6,则对于这个数独而言0~8都是数字1
// 那么此时的row的值就有0、1、2、3、4、5,所以此时的mask或上了第0、1、2、3、4、5的列信息
for (row = long_row - long_row % 9; row < long_row; row++)
{
mask |= sodoku_tmp->col_index[row].col_mask;
}
// 九宫格一共有九个格子,然后上面三个,中间三个,下面三个,
// 举个例子:0-8行的第0,3,6行是不会进来的,然后分为两种第1,4,7行和第2,5,8行
// 我们以第0行为例,那么第1行会进入这里然后或上123/456/789,他是根据第0行的col_mask(列信息)来进行if判断的
// 所以,第2行则会根据第1行和第0行的col_mask(列信息)来进行if判断,然后或上123/456/789;
// 最后再举一个列子:9-17行,又分为3种:1(第9,12,15行);2(第10,13,16行);3(第11,14,17行)
// 剩下的都是以此类推。
for (row = long_row - long_row % 3; row < long_row; row++)
{
if (sodoku_tmp->col_index[row].col_mask & COL_MASK_123)
{
mask |= COL_MASK_123;
}
else if (sodoku_tmp->col_index[row].col_mask & COL_MASK_456)
{
mask |= COL_MASK_456;
}
else
{
mask |= COL_MASK_789;
}
}
// 最后取反码,让有效位置置一
// 此时是第二种含义
sodoku_tmp->col_index[long_row].col_mask = ~mask;
}
// 初始化数独参数
void init_sodoku(sodoku_t *sodoku_tmp)
{
// 判断是否是空指针
if (sodoku_tmp == NULL)
{
return;
}
// 初始化数独的数据
memset(sodoku_tmp, 0, sizeof(sodoku_t));
sodoku_tmp->long_row = -1;
}
// 按照顺序获取第n行的列信息,n:0~80,每次获取一行
void goto_next_row(sodoku_t *sodoku_tmp)
{
// 判断是否是空指针
if (sodoku_tmp == NULL)
{
return;
}
// 更新正确的行数
sodoku_tmp->long_row++;
// 假设sodoku_tmp->long_row等于81,那么代表数独已经获取完成了,不用再继续了
if (sodoku_tmp->long_row > 80)
{
return;
}
// 获得<随即起始列>和初始化<当前存储列>
sodoku_tmp->col_index[sodoku_tmp->long_row].base_col = calc_rand_col();
sodoku_tmp->col_index[sodoku_tmp->long_row].curr_col = -1;
// 获取当前行的有效列信息
calc_col_mask_row(sodoku_tmp, sodoku_tmp->long_row);
}
// 判定有没有搜索完成
bool sodoku_not_finish(sodoku_t *sodoku_tmp)
{
bool not_finish = false;
// 判断是否是空指针
if (sodoku_tmp == NULL)
{
return false;
}
// 判断行数是否等于81,不等于的话就是搜索没完成
if (sodoku_tmp->long_row <= 80)
{
not_finish = true;
}
return not_finish;
}
// 根据有效位置和随机起始列判断是否能放进九宫格内,然后返回bool变量
bool goto_next_col(sodoku_t *sodoku_tmp)
{
bool success = false; // 放置是否成功变量
int base_col; // 用来代表随机起始列
int curr_col; // 用来代表当前存储列
int next_col; // 临时变量
bool found = false; // 称为<找到了>变量
do
{
// 判断是否是空指针
if (sodoku_tmp == NULL)
{
return false;
}
// 赋值
base_col = sodoku_tmp->col_index[sodoku_tmp->long_row].base_col;
curr_col = sodoku_tmp->col_index[sodoku_tmp->long_row].curr_col;
found = false;
// 假设当前存储列为-1,代表这个数是第一次存进去
if (curr_col == -1)
{
// 赋值
next_col = base_col;
// 假设随机起始列是有效的就进入if
if (sodoku_tmp->col_index[sodoku_tmp->long_row].col_mask & (1 << base_col))
{
found = true;
next_col = base_col;
}
// 反之,如果不是有效的就换下一个数字
else
{
next_col = calc_next_col(base_col);
}
}
else // 假设不是第一次进入
{
// 能进入到这里说明,上一行的数字位置出了问题,
// 那么就一定是上一行数字的实际存储列有问题,
// 所以这里要更换实际存储列才行
next_col = calc_next_col(curr_col);
}
// 假设没找到,则进去找到为止
if (!found)
{
while (next_col != base_col)
{
// 把能放的位置都跑一遍
if (sodoku_tmp->col_index[sodoku_tmp->long_row].col_mask & (1 << next_col))
{
// 找到了
found = true;
break;
}
// 不行就换一个数字
next_col = calc_next_col(next_col);
}
}
// 如果还是不行,就不要赋值了,一定是上一行有问题
if (!found)
{
break;
}
// 找到了,就记录这行的有效列信息和当前存储列
sodoku_tmp->col_index[sodoku_tmp->long_row].curr_col = next_col;
sodoku_tmp->col_index[sodoku_tmp->long_row].col_mask = (1 << next_col);
success = true;
} while (false);
return success;
}
// 返回上一行,并且重新获取上一行的有效列信息
void rollback_row(sodoku_t *sodoku_tmp)
{
// 判断是否是空指针
if (sodoku_tmp == NULL)
{
return;
}
sodoku_tmp->long_row--;
calc_col_mask_row(sodoku_tmp, sodoku_tmp->long_row);
return;
}
// 打印数独结构体
void print_sodoku(sodoku_t *sodoku_tmp)
{
int long_row;
int row;
int col;
int digit;
int cell[81] = {0};
do
{
if (sodoku_tmp == NULL)
break;
for (long_row = 0; long_row < sodoku_tmp->long_row; long_row++)
{
row = long_row % 9;
col = sodoku_tmp->col_index[long_row].curr_col;
digit = long_row / 9 + 1;
cell[row * 9 + col] = digit;
}
printf("sodoku_tmp %d\n", sodoku_tmp->long_row);
for (row = 0; row < 9; row++)
{
if (row % 3 == 0)
printf("+-----+-----+-----+\n");
printf("|");
for (col = 0; col < 9; col++)
{
printf("%d", cell[row * 9 + col]);
if (col % 3 == 2)
printf("|");
else
printf(" ");
}
printf("\n");
}
printf("+-----+-----+-----+\n");
} while (false);
}
main(部分):
// 定义数独
sodoku_t sodoku;
// 准备随机数
srand((unsigned int)time((time_t *)NULL));
// 初始化数独结构体
init_sodoku(&sodoku);
// 设置long_row进入第一行,设置一个随机开始列base_col和cur_col,并且获得第一行的有效列位置
goto_next_row(&sodoku);
// 判定有没有搜索完成
while (sodoku_not_finish(&sodoku) == true)
{
// 根据有效位置和随机起始列判断是否能放进九宫格内,然后返回bool变量
if (goto_next_col(&sodoku))
{
// 放置成功前往下一列,设置一个随机开始列base_col和cur_col,并且获得下一列的有效列位置
goto_next_row(&sodoku);
}
else
{
// 放置失败说明上一行的数字放错了位置,
// 将sodoku.long_row-1,并且从新获取减一之后的有效列信息
rollback_row(&sodoku);
}
}
// 打印
print_sodoku(&sodoku);
2、挖空
思路:准备两个二维数组,一个是题目,一个是玩家的答案;题目数组的作用可以在游玩的时候限制玩家填空的位置和难易程度,答案数组可以用来验证玩家的答案是否是正确的,因为挖空之后这个数独游戏至少是会有一个解法的。
函数介绍(里面有注释):
// 打印答案
void print_sodoku_answer(void)
{
int long_row;
int row;
int col;
int digit = 0;
int cell[81] = {0};
do
{
for (int i = 0; i < 9; i++)
{
for (int j = 0; j < 9; j++)
{
cell[digit] = sodoku_ew_answer[i][j];
digit++;
}
}
for (row = 0; row < 9; row++)
{
if (row % 3 == 0)
printf("+-----+-----+-----+\n");
printf("|");
for (col = 0; col < 9; col++)
{
printf("%d", cell[row * 9 + col]);
if (col % 3 == 2)
printf("|");
else
printf(" ");
}
printf("\n");
}
printf("+-----+-----+-----+\n");
} while (false);
}
// 打印数独二维数组
void print_sodoku_arr(void)
{
for (int i = 0; i < 9; i++)
{
for (int j = 0; j < 9; j++)
{
printf("%d ", sodoku_ew[i][j]);
}
printf("\n");
}
}
// 先准备好数独的二维数组
void prepare__sudoku_array(sodoku_t *sodoku_tmp)
{
int long_row;
int row;
int col;
int digit;
if (sodoku_tmp == NULL)
{
return;
}
for (long_row = 0; long_row < sodoku_tmp->long_row; long_row++)
{
row = long_row % 9; // 行
col = sodoku_tmp->col_index[long_row].curr_col; // 列
digit = long_row / 9 + 1; // 数值
// 赋值
sodoku_ew[row][col] = digit;
sodoku_ew_answer[row][col] = digit;
}
}
// 随机挖空数独的某一些格子
void random_hollow_sodoku(sodoku_t *sodoku_tmp)
{
// 需要的变量:
// 1.挖空的个数40-70
// 2.挖空的随机行 0-8
// 3.挖空的随机列 0-8
int random_number_of_hollow; // 随机挖空的个数
int random_number_of_row; // 随机挖空的行
int random_number_of_col; // 随机挖空的列
// 1.先确定挖空的个数
random_number_of_hollow = 40 + rand() % (70 - 40 + 1);
for (int i = 0; i < random_number_of_hollow; i++)
{
// 在里面确定随机挖空的行和随机挖空的列
random_number_of_row = calc_rand_col();
random_number_of_col = calc_rand_col();
// 假设这里的数不为0则让他等于0代表挖空
if (sodoku_ew[random_number_of_row][random_number_of_col] != 0)
{
sodoku_ew[random_number_of_row][random_number_of_col] = 0;
sodoku_ew_answer[random_number_of_row][random_number_of_col] = 0;
}
else
{
i--;
}
}
}
main(部分):
// 先准备好数独的二维数组
prepare__sudoku_array(&sodoku);
// 准备开始挖空
random_hollow_sodoku(&sodoku);
// 打印数独二维数组
print_sodoku_answer();
3、游玩判断:
思路:用while1来获取玩家每次填下的数字。假设当玩家填下最后一个数字,且整个答案数组是符合游戏规律的,则获胜,假设不符合,则给出提示。
函数介绍(里面有注释):
// 判断是否正确
bool sodoku_finish_flag(void)
{
bool success = true;
for (int i = 0; i < 9; i++)
{
for (int j = 0; j < 9; j++)
{
// 只要有一个空没有填满就退出
if (sodoku_ew_answer[i][j] == 0)
{
return success;
}
}
}
// 假设都填满了
int answer_tmp[9];
bool flag[10];
memset(flag, false, sizeof(bool) * 10);
// 检查同一行有没有相同数字出现
for (int i = 0; i < 9; i++)
{
answer_tmp[0] = sodoku_ew_answer[i][0];
answer_tmp[1] = sodoku_ew_answer[i][1];
answer_tmp[2] = sodoku_ew_answer[i][2];
answer_tmp[3] = sodoku_ew_answer[i][3];
answer_tmp[4] = sodoku_ew_answer[i][4];
answer_tmp[5] = sodoku_ew_answer[i][5];
answer_tmp[6] = sodoku_ew_answer[i][6];
answer_tmp[7] = sodoku_ew_answer[i][7];
answer_tmp[8] = sodoku_ew_answer[i][8];
for (int j = 0; j < 9; j++)
{
if (flag[answer_tmp[j]] == true)
{
printf("同一行出现问题\n");
return success;
}
else
{
flag[answer_tmp[j]] = true;
}
}
memset(flag, false, sizeof(bool) * 10);
}
// 检查同一列有没有相同数字出现
for (int i = 0; i < 6; i++)
{
answer_tmp[0] = sodoku_ew_answer[0][i];
answer_tmp[1] = sodoku_ew_answer[1][i];
answer_tmp[2] = sodoku_ew_answer[2][i];
answer_tmp[3] = sodoku_ew_answer[3][i];
answer_tmp[4] = sodoku_ew_answer[4][i];
answer_tmp[5] = sodoku_ew_answer[5][i];
answer_tmp[6] = sodoku_ew_answer[6][i];
answer_tmp[7] = sodoku_ew_answer[7][i];
answer_tmp[8] = sodoku_ew_answer[8][i];
for (int j = 0; j < 9; j++)
{
if (flag[answer_tmp[j]] == true)
{
printf("同一列出现问题\n");
return success;
}
else
{
flag[answer_tmp[j]] = true;
}
}
memset(flag, false, sizeof(bool) * 10);
}
// 检查同一格内有没有相同数字出现
for (int i = 0; i < 9; i += 3)
{
for (int k = 0; k < 9; k += 3)
{
int i_big_1 = i + 1;
int k_big_1 = k + 1;
int i_big_2 = i + 2;
int k_big_2 = k + 2;
answer_tmp[0] = sodoku_ew_answer[i][k];
answer_tmp[1] = sodoku_ew_answer[i][k_big_1];
answer_tmp[2] = sodoku_ew_answer[i][k_big_2];
answer_tmp[3] = sodoku_ew_answer[i_big_1][k];
answer_tmp[4] = sodoku_ew_answer[i_big_1][k_big_1];
answer_tmp[5] = sodoku_ew_answer[i_big_1][k_big_2];
answer_tmp[6] = sodoku_ew_answer[i_big_2][k];
answer_tmp[7] = sodoku_ew_answer[i_big_2][k_big_1];
answer_tmp[8] = sodoku_ew_answer[i_big_2][k_big_2];
for (int j = 0; j < 9; j++)
{
if (flag[answer_tmp[j]] == true)
{
printf("同一格子出现问题\n");
return success;
}
else
{
flag[answer_tmp[j]] = true;
}
}
memset(flag, false, sizeof(bool) * 10);
}
}
// 假设上面都没有触发,则代表成功解出来了
printf("成功\n");
success = false;
return success;
}
main(部分):
// 输入的行,列,数值
int input_row;
int input_col;
int input_num;
// 开始填表
while (sodoku_finish_flag() == true)
{
input_row = 0;
input_col = 0;
input_num = 0;
printf("请依次输入行,列,和要填的数字,分别用空格隔开:\n");
scanf("%d %d %d", &input_row, &input_col, &input_num);
input_row--;
input_col--;
// 判断是否打破规则
if (((input_col > 9) && (input_col < 1)) || ((input_row > 9) && (input_row < 1)) || ((input_num > 9) && (input_num < 0)))
{
printf("不符合规则\n");
continue;
}
// 遍历数组
if (sodoku_ew[input_row][input_col] == 0)
{
sodoku_ew_answer[input_row][input_col] = input_num;
}
else
{
printf("不符合规则\n");
continue;
}
print_sodoku_answer();
}
结尾:
看懂了的话,六宫格和四宫格等等这些都可以尝试一下,只用改几个参数和函数就可以了。
关于游戏的难易度更改可以在挖空那里进行修改,但是并不是挖的空越多,游戏就越难(个人感觉)。目前这套代码我试过弄到STM32F103那边,用了一个OLED和矩阵搭配来,我感觉还可以,有条件的可以试试。
注意:一开始的设计完整的大九宫格并不是我自己敲的,而是观摩大佬的代码才懂的。我在下面放上代码,想要的可以自己去看看。
设计完整的大九宫格的完整代码: GitHub - baojian256/sodoku