先来一张图先把。自己和自己下棋无聊的最高 境界
恩恩,还是我比较厉害一点的。
--------
我们首先看下界面,这是一个wpf 的程序,因为在界面写上更加的方便一些,当然里面的核心代码,可以跟换成语言。
下面我们将一点点的编写 一个下棋程序()
1、先部个局画个棋盘先呗。
这个没有什么好说的,直接上代码吧
<Window x:Class="wuziqi3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:wuziqi3"
Title="五子棋V1.1" Height="950" Width="1050" Loaded="MainWindow_Loaded" >
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<Grid x:Name="gridLeft" Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Vertical" Grid.Row="0" Width="100">
<Button Click="Button_Click" Height="50" Content="从新开始"></Button>
<ComboBox x:Name="xianxiacbb" SelectionChanged="xianxiacbb_Selected">
<ComboBoxItem>我要先下</ComboBoxItem>
<ComboBoxItem IsSelected="True">电脑先下</ComboBoxItem>
<ComboBoxItem >交替着来</ComboBoxItem>
</ComboBox>
<CheckBox x:Name="noConputer" Content="不要电脑哦" IsChecked="True"/>
<Button x:Name="btnhuiqi" Click="Button_Click_2" Height="50" Content="悔棋"></Button>
<!--<Button Click="Button_Click_3" Height="50" Content="导出"></Button>-->
</StackPanel>
<Grid Grid.Row="1">
<TextBox x:Name="txtHistory" />
<Label x:Name="lblgailu" Content="" VerticalAlignment="Bottom"/>
</Grid>
</Grid>
<Border x:Name="bor" Grid.Column="1" Height="900" Width="900" BorderBrush="Black" BorderThickness="2" Background="#C57747" MouseLeftButtonDown="Border_MouseLeftButtonDown">
<Grid>
<Canvas x:Name="canqipan"></Canvas>
<Canvas x:Name="can">
</Canvas>
<Grid>
<ItemsControl x:Name="ItemsCtrlx" VerticalAlignment="Top">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"></StackPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Height="60" Width="60">
<Label Content="{Binding}" FontSize="15"></Label>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl x:Name="ItemsCtrly" HorizontalAlignment="Left">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Height="60" Width="60">
<Label Content="{Binding}" FontSize="15"></Label>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
<Canvas x:Name="cancurpoint">
<Path x:Name="pathCurPoint" SnapsToDevicePixels="False" StrokeThickness="2" Data="M20,30 L40,30 M30,20 L30,40" Stroke="Black" />
</Canvas>
</Grid>
</Border>
</Grid>
</Window>
简单的对xaml 说明下,
1)gridLeft 主要存放对 棋盘控制的一些按钮。里面的一些按钮的点击事件我们在后面进行添加。
2)bor 是五子棋的整个棋盘,其中 canqipan 只是五子棋棋盘的条条框框,can 存放的是棋子,下面的2个itemscontrol 是2个记录或者调试用的编号,最下面的 pathcurpoint 其实是 记录当前落子的位置的,至于 canqipan 和can 为什么分开来,因为棋子在从新开始下的时候回对所有下面的子元素进行清空,但是棋盘不需要。
在xaml 只是简单的写了下界面的布局。我们需要在后台界面中编写,棋局的条条框框。还有参数的一些初始化。
int qiziwidth = 30;
int qiziheight = 30;
int hei = 15;
int wid = 15;
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
qiziheight = (int)this.bor.Height / hei;
qiziwidth = (int)this.bor.Width / wid;
this.bor.Height = hei * qiziheight;
this.bor.Width = wid * qiziwidth;
initqipan(wid, hei);
rd = new Random((int)DateTime.Now.Ticks);
shuixianxia();
}
void initqipan(int wid,int hei)
{
List<int> lix = new List<int>();
for (int i = 0; i < wid; i++)
{
lix.Add(i);
Line l = new Line() { X1 = qiziwidth / 2, X2 = qiziwidth / 2, Y1 = qiziwidth / 2, Y2 = bor.Width - qiziwidth / 2, Stroke = Brushes.Black };
Canvas.SetLeft(l, (i * qiziwidth));
canqipan.Children.Add(l);
}
for (int i = 0; i < hei; i++)
{
Line l = new Line() { Y1 = qiziheight / 2, Y2 = qiziheight / 2, X1 = qiziheight / 2, X2 = bor.Height - qiziheight / 2, Stroke = Brushes.Black };
Canvas.SetTop(l, (i * qiziheight));
canqipan.Children.Add(l);
}
Ellipse el = new Ellipse()
{
Height = 10,
Width = 10,
Fill = Brushes.Black
};
Canvas.SetLeft(el, (3.5 * qiziwidth) - el.Width / 2);
Canvas.SetTop(el, (3.5 * qiziheight) - el.Height / 2);
canqipan.Children.Add(el);
Ellipse el2 = new Ellipse()
{
Height = 10,
Width = 10,
Fill = Brushes.Black
};
Canvas.SetLeft(el2, (3.5 * qiziwidth) - el.Width / 2);
Canvas.SetTop(el2, (11.5 * qiziheight) - el.Height / 2);
canqipan.Children.Add(el2);
Ellipse el3 = new Ellipse()
{
Height = 10,
Width = 10,
Fill = Brushes.Black
};
Canvas.SetTop(el3, (3.5 * qiziheight) - el.Height / 2);
Canvas.SetLeft(el3, (11.5 * qiziwidth) - el.Width / 2);
canqipan.Children.Add(el3);
Ellipse el4 = new Ellipse()
{
Height = 10,
Width = 10,
Fill = Brushes.Black
};
Canvas.SetTop(el4, (11.5 * qiziheight)-el.Height/2);
Canvas.SetLeft(el4, (11.5 * qiziwidth)-el.Width/2);
canqipan.Children.Add(el4);
Ellipse el5 = new Ellipse()
{
Height = 15,
Width = 15,
Fill = Brushes.Black
};
Canvas.SetLeft(el5, (7.5 * qiziwidth) - el5.Width / 2);
Canvas.SetTop(el5, (7.5 * qiziheight) - el5.Height / 2);
canqipan.Children.Add(el5);
ItemsCtrlx.ItemsSource = lix;
ItemsCtrly.ItemsSource = lix;
initqizi(wid, hei);
inithuiqi();
}
void shuixianxia()
{
if ((xianxiacbb.SelectedIndex == 1) ||(xianxiacbb.SelectedIndex == 2 && curiscouputerfirst))
{
ConputerLoad(false);
}
curiscouputerfirst = !curiscouputerfirst;
}
bool curiscouputerfirst = true;
void initqizi(int wid,int hei)
{
lli = new List<bool?[]>();
for (int i = 0; i < wid; i++)
{
lli.Add(new bool?[hei]);
}
}
static public Random rd;
List<bool?[]> lli ;
做一些参数的说明
首先是全局变量,lli 当前棋盘的状态,他是一个二维数组吧,他的第一维表示棋子的x,第二维表示棋子的y 里面存放一个bool? 让棋子有3个属性 true false null ,我们人为的给他定义下, true 表示黑色棋子,false 表示白色棋子, null 表示这个位置没有落子
hei 和wid 表示棋子可以落子的范围,五子棋的棋盘是15*15 为了方便我将它设置为全局变量
函数的说明
initqipan 主要是对 canqipan 的条条框框还有上面的5个小圆点进行添加。
initqizi 主要是对所有lli 进行初始化, 并没有做删除can 里面内容的操作。
shuixianxia 就是设置 落子的循序。
代码写到这里的时候我们的准备工作做完了。后面我们需要对棋盘落子进行操作了。
2、落子
bool isblack = true;
private void Border_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Point p = Mouse.GetPosition(this.bor);
int x = (int)p.X / qiziwidth;
int y = (int)p.Y / qiziheight;
if(lli[x][y] != null)
{
return;
}
qiziPoint curpoint = new qiziPoint(x, y,isblack);
if (!loadqizi(curpoint) && noConputer.IsChecked.Value)
{
ConputerLoad(!curpoint.Color);
}
}
这是一个 xaml 里面绑定的方法, 我们可以暂时先不去管ConputerLoad (这是电脑落子的一个函数)
我们先来看看
loadqizi 这个函数,这个函数主要提供了,对 棋盘上棋子的绘制,绘制结束后,对是否连成5同样颜色的五子进行判断。
哦哦 ,我在这边定义了2个类,qiziPoint ZeroPoint 2个类一个是带棋子颜色的一个是不带棋子颜色的。会做说明
class ZeroPoint
{
public ZeroPoint()
{
}
public ZeroPoint(int x, int y)
{
X = x;
Y = y;
}
public ZeroPoint(ZeroPoint zp)
{
X = zp.X;
Y = zp.Y;
}
public int X { get; set; }
public int Y { get; set; }
public static ZeroPoint operator +(ZeroPoint zp1, ZeroPoint zp2)
{
return new ZeroPoint(zp1.X + zp2.X, zp1.Y + zp2.Y);
}
}
class qiziPoint
{
public qiziPoint(int x,int y,bool color)
{
this.X = x;
this.Y = y;
this.Color = color;
}
public int X { get; set; }
public int Y { get; set; }
public bool Color { get; set; }
public static qiziPoint operator +(qiziPoint zp1, ZeroPoint zp2)
{
return new qiziPoint(zp1.X + zp2.X, zp1.Y + zp2.Y,zp1.Color);
}
public static bool operator ==(qiziPoint zp1, qiziPoint zp2)
{
return ((zp1.X == zp2.X)&& (zp1.Y == zp2.Y));
}
public static bool operator !=(qiziPoint zp1, qiziPoint zp2)
{
return ((zp1.X != zp2.X) || (zp1.Y != zp2.Y));
}
}
我们看看
bool loadqizi(qiziPoint zp)
{
int x = zp.X;
int y = zp.Y;
lli[x][y] = zp.Color;
historyload.Add(zp);
Ellipse ell = new Ellipse() { Margin = new Thickness(qiziwidth * 1 / 20),Width = qiziwidth * 9 / 10, Height = qiziheight * 9 / 10 };
if (zp.Color)
{
ell.Fill = Brushes.Black;
pathCurPoint.Stroke = Brushes.White;
}
else
{
ell.Fill = Brushes.White;
pathCurPoint.Stroke = Brushes.Black;
}
Canvas.SetLeft(pathCurPoint, x * qiziwidth);
Canvas.SetTop(pathCurPoint, y * qiziheight);
isblack = !isblack;
Canvas.SetLeft(ell, (x * qiziwidth));
Canvas.SetTop(ell, (y * qiziheight));
RegisterName("el" + historyload.Count, ell);
can.Children.Add(ell);
string zhongwen = (zp.Color ? "黑" : "白");
this.txtHistory.AppendText(zhongwen + ":" + x + "," + y + "\r\n");
if (finishLoad(zp))
{
this.txtHistory.AppendText(zhongwen + "赢了");
MessageBox.Show( zhongwen + "赢了");
return true;
}
return false;
}
bool finishLoad(qiziPoint zp)
{
bool tempcol = zp.Color;
qiziPoint zpTemp = zp;
int halfpoint = lzp.Count() / 2;
for (int curpoint = 0; curpoint < halfpoint; curpoint++)
{
int curindex = 0;
qiziPoint tzp;
for (int i = 0; i < 4; i++)
{
tzp = zp + lzp[curpoint];
if (!pointishefa(tzp))
break;
if ((lli[tzp.X][tzp.Y] != tempcol))
{
break;
}
curindex++;
zp = tzp;
}
zp = zpTemp;
for (int i = 0; i < 4; i++)
{
tzp = zp + lzp[curpoint + halfpoint];
if (!pointishefa(tzp))
break;
if ((lli[tzp.X][tzp.Y] != tempcol))
{
break;
}
curindex++;
zp = tzp;
}
zp = zpTemp;
if (curindex >= 4)
{
return true;
}
}
return false;
}
loadqizi很简单,对鼠标点击的xy 进行再对应位置添加一个 颜色的园,当然可以用图片啦。并且修改lli 里面值的属性。并且移动pathCurPoint 这个来显示 当前下子的位置。并且调换了下当前落子的颜色,并且在结束的时候调用 finishLoad
finishLoad 这个函数主要用来,当前落子的是否连成了5个子。 这里有一个lzp 主要是对一个棋子的八个方向进行迭代循环。
List<ZeroPoint> lzp = new List<ZeroPoint>() {
new ZeroPoint(-1,0),
new ZeroPoint(-1,-1),
new ZeroPoint(0,-1),
new ZeroPoint(1,-1),
new ZeroPoint(1,0),
new ZeroPoint(1,1),
new ZeroPoint(0,1),
new ZeroPoint(-1,1)
};
lzp 有着一定规律 lzp[0] 和lzp[4] 是刚好相反的,这很容易明白,一个子左边 同色的2个右边同色的2个,加上自己就是5个了,没有必要同一边有4个的情况才算5个吧。所以我们在只用循环lzp 的一半,另一半在在循环 里面 + 4 再次循环即可,如果里面2个小循环得到同样颜色的子大于等于4 就表示成功,(加上自己刚好是5) zpTemp 是用于迭代位置保存传进来棋子的位置。(我感觉代码写的比我说的可能清楚点吧)。
写到这里的时候,已经可以打开,2个人下棋的目的了。
但是本篇的目的才刚刚开始。。。我们需要写一个可以下棋的电脑ai程序来。。。
我们回到上面的
ConputerLoad 这是电脑落子的一个函数,
我们思考一个问题,
我们下棋 为什么下这里的优先级是什么呢?
1】为了达到5子,然后是为了达到4子,然后是3子,然后又2子,
2】那么问题来了,假如我们是电脑(毕竟我们要写一个可以下五子棋的电脑嘛。。)玩家当前是3个子了,电脑下完当前的才是3个子了, 那么我们优先的循序应该是选择防守,
3】还有一点,是这样的,我们下棋将要凑成了4个子了,但是这一边又一边已经被被人下了,我们下下去并不能胜利,但是如果下下去 2遍都没有被下,那么我们很可能胜利。
总结上面的3点,可以得到一个优先级,
五子》活四》死四》活3》死3》活2》死2》活1》死1
//下下去后的状态,也就是,
enum qijutype:int
{
wu = 9,
huosi = 8, //4*2
sishi = 7, //4*2-1
huosan = 6,//3*2
sisan = 5, //3*2
huoer = 4, //2*2
sier = 3, //2*2 -1
huoyi = 2, //1*2
siyi = 1, //1*2-1
}
活 死就 是否有一边被堵死,如果有,就是死,如果没有就是活。(你会不会问
那么2边都被堵死了呢? 他的优先级是最低的。无论当前已经是几个了。五子除外)
并且,如果当前我达到了五子的优先级比你达到了五子的优先级搞。,毕竟进攻才是最好的防守嘛。
说了这么多我们来写代码吧。
List<qiziPoint> historyload = new List<qiziPoint>();
private void ConputerLoad(bool color)
{
var mhl = historyload.Where((c) => c.Color == color).ToList();
var thl = historyload.Where((c) => c.Color == !color).ToList();
List<qiziPoint> mmosttype = new List<qiziPoint>();
List<qiziPoint> tmosttype = new List<qiziPoint>();
qijutype mqjt = mabytest(mhl, ref mmosttype);
qijutype tqjt = mabytest(thl, ref tmosttype);
this.lblgailu.Content = (int)mqjt + ":" + mmosttype.Count + "/" + (int)tqjt + ":" + tmosttype.Count;
qiziPoint tzp;
if (mmosttype.Count == 0)
{
//要一个虚拟的。然后计算,效果。
#region temp 随机给个位置被
if (tmosttype.Count == 0)
{
tzp = new qiziPoint(7, 7, isblack);
}
else
{
var ttts = tmosttype[rd.Next(lzp.Count())];
tzp = new qiziPoint(ttts.X, ttts.Y, color);
}
}
else
{
if (tqjt > mqjt)
{
//var tt = Conputerdanglu(tmosttype[0]);
tzp = new qiziPoint(tmosttype[0].X, tmosttype[0].Y, mhl[0].Color);
}
else
{
int rdnet = rd.Next(mmosttype.Count());
tzp = mmosttype[rdnet];
}
}
loadqizi(tzp);
#endregion
}
这里我们定义一个,historyload 来记录历史下子的位置,可能在loadqizi 中就出现过。
开始我们或许所有的我们下过棋子的位置,和别人下过棋子的位置
然后获取我们下的位置最高的级别是什么下什么地方(mabytest),如果我们的级别比对方高,就下我们的,如果我们的级别比对方低,就下对方的位置,让他没有地方可以走(这就是防守啦)
这里有一个小插入,如果我们没下怎么办,我的处理很简单,看下对方有没有下,如果有,就他的8个方向随便放一个就好了,如果对方也没有,那就固定下7,7(也就是中间拉)
最后的loadqizi 也就是落子就不多说了。
我们详细看看mabytest 这个函数吧。
2个参数, 历史下过的位置,还有个返回值参数,表示高的类型可以下的位置。
private qijutype mabytest(List<qiziPoint> mhl, ref List<qiziPoint> mosttype)
{
List<qiziPoint> comvirload1 = new List<qiziPoint>();
foreach (var h in mhl)
{
foreach (var fzp in lzp)
{
qiziPoint tqzp = h + fzp;
if (!pointishefa(tqzp))
continue;
if (lli[tqzp.X][tqzp.Y] == null && (comvirload1.Where(cv => cv == tqzp).ToList().Count == 0))
{
comvirload1.Add(tqzp);
}
}
}
//可以先不要。下一步的。深度什么的再说吧。
qijutype myqjt = qijutype.siyi;
foreach (var qzp in comvirload1)
{
qijutype qjtt = visLoadqiziMax(qzp);
if (myqjt == qjtt)
{
mosttype.Add(qzp);
}
else if (myqjt < qjtt)
{
myqjt = qjtt;
mosttype.Clear();
mosttype.Add(qzp);
}
}
return myqjt;
}
先获取了下,根据历史下过的位置,判断出,后面可以下的位置(有效的),并排除了重复的位置,突然来个最边边肯定不科学也不可能
然后调用visLoadqiziMax 进行对下这个位置最高类型的获取。继续看visLoadqiziMax 吧
private qijutype visLoadqiziMax(qiziPoint zp)
{
List<qijutype> vislist = visLoadqizi(zp);
qijutype tqjt = qijutype.siyi;
foreach(var vis in vislist)
{
if(vis > tqjt)
{
tqjt = vis;
}
}
return tqjt;
}
private List<qijutype> visLoadqizi(qiziPoint zp)
{
List<qijutype> vislist = new List<qijutype>();
qiziPoint zpTemp = zp;
int halfpoint = lzp.Count() / 2;
for (int curpoint = 0; curpoint < halfpoint; curpoint++)
{
bool issi = false;
int curindex = 0;
qiziPoint tzp;
//bool iskuazi = false;
for (int i = 0; i < 4; i++)
{
tzp = zp + lzp[curpoint];
if (!pointishefa(tzp))
break;
if ((lli[tzp.X][tzp.Y] == !zp.Color))
{
issi = true;
break;
}
else if (lli[tzp.X][tzp.Y] == null)
{
break;
}
curindex++;
zp = tzp;
}
zp = zpTemp;
for (int i = 0; i < 4; i++)
{
tzp = zp + lzp[curpoint + halfpoint];
if (!pointishefa(tzp))
break;
if ((lli[tzp.X][tzp.Y] == !zp.Color))
{
if (issi)
{
curindex = 0;
break;
}
else
{
issi = true;
}
break;
}
else if (lli[tzp.X][tzp.Y] == null)
{
break;
}
curindex++;
zp = tzp;
}
zp = zpTemp;
vislist.Add((qijutype)((curindex + 1) * 2 - (issi? 1:0)));
}
return vislist;
}
visLoadqiziMax 这个并不是重点,
visLoadqiziMax 这个函数里面只是处理了,visLoadqizi 返回过来的最高类型并返回。
visLoadqizi 这个函数,乍一看怎么和,finishLoad 这么像,道理也很简单,当然是根据怎么赢怎么判断啦。
只不过visLoadqizi 这个函数的返回值是一个最高的类型,而不是true false,
里面加了一个issi的 表示,表示,是否一遍被堵死了,如果是的话,那么他就是一个死的4,如果没有那么他就是活的4,如果2遍都被堵死了,那么他就是0了最低的登记。
其实代码还有一些的的比如边上的按钮。
第一次写讲解的代码,不是很习惯。就这样吧。好东西拿出来分享分享嘛。道理说起来很简单,写起来调试起来的话,会比较的麻烦。最重要的是有一个清晰的思路。
后面吧,源码加上吧。其实已经放了所有的程序源码了。