数独游戏设计(C语言)

前言:

 不说废话!!!

设计思路(九宫格为例):

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

  • 11
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值