写在前面:

写于18年,搬运一些文章到这里,顺便测试动图的显示。

仓库在这里:https://github.com/sleepingcat42/A-puzzle , 当时还不了解 Git 的指令,还是拖动上传的。

游戏概览

游戏画面

整个游戏的画面由 MATLAB 的 figure 窗口显示,没有用到 MATLAB 的 GUI 功能,直接点击图窗内的元素就能够进行交互。这个方案实际上存在一些问题:每一次刷新画面,都会重新生成生成一个 figure 窗口,所以用户移动了窗口的位置或者改变了窗口的大小,在下一个窗口生成后又会跳回原来的位置或者恢复原来的大小。忽略这个小问题,还是比较完善的一个程序。

规则展示

复原画面

游戏介绍

游戏玩法来源

玩法应该是扩展了灯的状态的关灯游戏的。最早看到是多年前(10年前后)百度魔方吧有人制作了可以在网页上玩的版本,同时还制作了4×4版本。网页连接很早之前就失效,作者也不可考。感谢作者制作了这样一个有意思的小游戏。

设计参考

游戏的总体框架和设计都受到这篇博客的影响,没有用 MATLAB GUI 在 figure 内点击并用回调函数和这篇博客的是一致的。

基于MATLAB的拼图游戏设计(图文详解,附完整代码)https://blog.csdn.net/qq_32892383/article/details/79219110

两个程序有很多相似之处,这篇文章条理很清晰,对于整个编写过程带来了很大帮助。

规则和功能

游戏的基本规则:如图所示,点击某一个方块自身及相邻的方块数值加一,到达最大值(设定的是9)循环回最小值(1),最小值的索引不是0而是1对应了Matlab的索引下标是1起始。关灯游戏中,只存在 0 和 1 两种状态。

游戏目标:通过点击方块使所有的方块的值相同。
当玩家完成后显示步数和胜利语句。

一些定义

游戏画面有长宽两个维度,同时还有数字循环的次数。
仿照魔方将维度定义为m×n,可以在后续看到这个游戏的长宽不一定需要相等。
将一个循环中有的数字个数称之为深度。

设计逻辑

流程图如下:

流程图

前期准备

在这里插入图片描述
首先是绘制这几张图片(用画图软件画的)。
然后用imread()读入Matlab,直接拼接成一长条。

1
2
3
4
5
6
img=[];
for k=1:9
img=[img,imread(strcat(num2str(k),'.png'))];
end
imshow(img)%显示
imwrite(img,'puzzle1.png');%输出到文件目录

读入图片
本来打算用小方块来实现,后来发现拼合成长条直接用也很方便。使用长条形的素材,构造一个GetImg函数以获取第k个数字的图片。

1
2
3
4
function X=GetImg(img,k)
%取出第k张图
X=img(:,100*(k-1)+1:100*k,:);
end

绘制游戏画面

游戏画面的9个数字可以用一个3×3的矩阵来标记。绘制游戏画面的函数drawmap就依靠得到的状态,也就是3×3的标记矩阵来显示,代码如下

1
2
3
4
5
6
7
8
9
10
11
function drawmap(A)
im=imread('puzzle1.png');
img=uint8(zeros(300,300,3));%预分配,且类型需要为unint8
% 对要显示的图片进行赋值
for row=1:3
for col=1:3
img(1+(row-1)*100:100*row,1+(col-1)*100:100*col,:)=GetImg(im,A(row,col)); %将A矩阵中的数字和图片对应
end
end
imshow(img)%显示画面
end

用以下语句测试,矩阵A和显示的每个数字都对应

1
2
A=[1,2,3;4,5,6;7,8,9]
drawmap(A);

在这里插入图片描述

打乱函数

虽然没有严格的证明,但是经验得出(没有遇到随机的打乱无法复原的情况)任意状态应该都是可以复原的,所以打乱就可以很简单用随机生成的9个数字来实现,并不需要模拟人为打乱。任意状态是否都能复原还有待证明。

1
2
3
4
function A=Disrupt()
A=unidrnd(9,3,3);随机生成值在19之间的3×3矩阵
drawmap(A);
end

主函数和规则

Matlab提供了两种获取鼠标坐标的途径,一个是ginput()函数,会有一个十字光标进行定位;另一个利用figure的WindowButtonDownFcn属性,在figure界面点击鼠标可以调用回调函数,实现一些功能。游戏的主函数如下

1
2
3
4
5
6
7
8
9
function puzzle()
%% 主函数
loading()%开场动画
global Tag;%Tag是标记矩阵,定义成全局变量,方便传递参数
Tag=Disrupt();%将标记矩阵的排列顺序打乱Tag_A;
global count;%计算步数,也设为全局变量
count=0;
set(gcf,'windowButtonDownFcn',@ButttonDownFcn);%点击鼠标时调用ButttonDownFcn函数

鼠标点击时调用ButttonDownFcn函数,ButttonDownFcn函数中实现游戏的规则和画面的刷新,比较偷懒没有分模块来写,所有都写在一块了。图像的大小时300×300,所以坐标值/100向上取整(ceil函数)转化为矩阵对应的坐标。用四个if语句来实现了规则。最后检测是否胜利,可以用标记矩阵和k*ones(3,3)比较,也可以逐项比较每个元素是否相等。达成胜利条件,输出包含还原步数的消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function ButttonDownFcn(src,event)
pt = get(gca,'CurrentPoint');
y = uint8(ceil(pt(1,1)/100));
x = uint8(ceil(pt(1,2)/100));%获取点击的位置转化为矩阵中的坐标
global Tag
global count%声明全局变量

if x>=1&&x<=3&&y>=1&&y<=3%点击位置满足才执行
count=count+1;%计算步数
Tag(x,y)=Tag(x,y)+1;
if x<3 %四个if实现规则
Tag(x+1,y)=Tag(x+1,y)+1;
end
if x>1
Tag(x-1,y)=Tag(x-1,y)+1;
end
if y<3
Tag(x,y+1)=Tag(x,y+1)+1;
end
if y>1
Tag(x,y-1)=Tag(x,y-1)+1;
end
Tag(Tag==10)=1 ;
end
drawmap(Tag);

%胜利条件
for k=1:9
if Tag==uint8(k*ones(3,3))
msgbox(strcat(num2str(count),' !You win!')); %提示完成信息
pause(5);%延迟
close all %游戏结束,关闭所有图像窗口

end
end
end

至此所有的工作都完成了,运行puzzle()函数就能可以玩了。

谜题解法

我的解法利用了它的对称性,优化一下步数大概在40-60之间。欢迎交流解法。
叙述可能有些繁杂,有时间再详写。

扩展

以上整个3×3的拼图就算是完成了,但是功能并不灵活。经过一些小修改可以实现十分丰富的玩法。

更多阶数和深度

在这个代码的基础上制作更高阶的版本也非常容易实现。在puzzle()函数中引入3个全局变量dim_x,dim_y和depth,编写setdim()分别来设置维数。

puzzle.m修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
function puzzle()
%% 主函数
%loading()%开场动画
global Tag;%Tag是标记矩阵,定义成全局变量,方便传递参数
global count;%计算步数,也设为全局变量
count=0;
global dim_x;
global dim_y;
global depth;%维度
[dim_x,dim_y,depth]=setdim(4,4,6);
Tag=Disrupt();%将标记矩阵的排列顺序打乱Tag_A;
set(gcf,'windowButtonDownFcn',@ButtonDownFcn);%点击鼠标时调用ButttonDownFcn函数

setdim()函数如下:

1
2
3
4
5
function [dim_x,dim_y,depth]=setdim(x,y,d)
dim_x=x;
dim_y=y;
depth=d;
end

同时在其他的函数中也需要进行一些修改
绘制画面根据dim_x,dim_y绘制画面,

1
2
3
4
5
6
7
8
9
10
11
12
13
function drawmap(A)
im=imread('puzzle1.png');
img=uint8(zeros(100,100,3));%预分配,且类型需要为unint8
global dim_x
global dim_y
% 对要显示的拼图进行赋值
for row=1:dim_x
for col=1:dim_y
img(1+(row-1)*100:100*row,1+(col-1)*100:100*col,:)=GetImg(im,A(row,col));
end
end
imshow(img)%显示
end

标记矩阵的数值变为1到depth,维度为dim_x,dim_y

1
2
3
4
5
6
7
function A=Disrupt()
global dim_x;
global dim_y;
global depth
A=unidrnd(depth,dim_x,dim_y);
drawmap(A);
end

回调函数,基本是将3×3的3换成dim_x,dim_y,当数字超过depth变为1,胜利条件变为与1到depth的dim_x×dim_y的矩阵比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function ButttonDownFcn(src,event)
pt = get(gca,'CurrentPoint');
y = uint8(ceil(pt(1,1)/100));
x = uint8(ceil(pt(1,2)/100));
global Tag
global count
global dim_x
global dim_y
global depth
if x>=1&&x<=dim_x&&y>=1&&y<=dim_y%点击位置满足才执行
count=count+1;%计算步数
Tag(x,y)=Tag(x,y)+1;
if x<dim_x %四个if实现规则
Tag(x+1,y)=Tag(x+1,y)+1;
end
if x>1
Tag(x-1,y)=Tag(x-1,y)+1;
end
if y<dim_y
Tag(x,y+1)=Tag(x,y+1)+1;
end
if y>1
Tag(x,y-1)=Tag(x,y-1)+1;
end
Tag(Tag==depth+1)=1 ;
end
drawmap(Tag);

%胜利条件
for k=1:depth
if Tag==uint8(k*ones(dim_x,dim_y))
msgbox(strcat(num2str(count),' !You win!')); %提示完成信息
pause(5);%延迟
close all %游戏结束,关闭所有图像窗口

end
end
end

只需修改这几个小地方,通过setdim()即可调整设置不同维度

2×2×7

在这里插入图片描述

3×3×2

这个换成0和1可能更好一点
在这里插入图片描述

更高阶

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

不等阶

在这里插入图片描述

修改规则

经过修改回调函数可以实现别的规则,新规则同样适用于不同的阶数

1.注释掉回调函数的第10行

1
%Tag(x,y)=Tag(x,y)+1;

规则就改为被点击的方块数值不发生变化,相邻方块数值加1。

在这里插入图片描述
2.更改为被点击的方块所在的行列+1
代码修改为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if x>=1&&x<=dim_x&&y>=1&&y<=dim_y%点击位置满足才执行
count=count+1;%计算步数
Tag(x,y)=Tag(x,y)+1;
if x<dim_x %四个if实现规则
Tag(x+1:dim_x,y)=Tag(x+1:dim_x,y)+1;
end
if x>1
Tag(1:x-1,y)=Tag(1:x-1,y)+1;
end
if y<dim_y
Tag(x,y+1:dim_y)=Tag(x,y+1:dim_y)+1;
end
if y>1
Tag(x,1:y-1)=Tag(x,1:y-1)+1;
end
Tag(Tag==depth+1)=1 ;
end

效果如下:
在这里插入图片描述
3.其他
当然也可以设计成以上两种规则的混合,还可以设计成对角相加,或者其他一些更奇怪的规则。胜利条件也可以被设置得更苛刻,比如只有都为1才算时胜利。未来会加入更多的修改。

问题和改进

  • 对于新规则,打乱可能存在问题,没有严格的证明其他任意阶数的任意状态和任意规则都可以复原
  • 缺少一个GUI界面以便选择更多模式,记录最好成绩等
  • 可以考虑增加一个计时模式
  • 对于某一规则可以写一个通用解法
  • 欢迎提出建议或意见

后记

游戏解法方面,除了最原始的规则和阶数,目前对于更高阶数和新设计的解法也没什么好的解法,运气好可能能解出来。

欢迎交流解法和游戏相关规则设计的问题。