本文目录
- 1、前言
- 2、程序规格
- 3、核心算法与象棋规则、术语
-
-
- 3.1 行棋规则与行棋判定函数设计
-
- 3.1.0 象棋棋盘
- 3.1.1 兵/卒
- 3.1.2 士
- 3.1.3 象
- 3.1.4 马
- 3.1.5 车
- 3.1.6 炮
- 3.1.7 将/帅
- 3.2 其他规则与术语
-
- 3.2.1 吃子
- 3.2.2 将军/应将/送将
- 3.2.3 将帅照面/王不见王
- 3.2.4 绝杀/困毙
- 3.2.5 长打/禁止长打
- 3.3 程序中的其它函数说明
-
- 3.3.1 abs()取绝对值函数
- 3.3.2 sgn()取符函数
- 3.3.3 sq()计算平方函数
-
- 4、源代码
- 5、运行结果
-
- 5.1 界面与操作展示
- 5.2 测试结果
- 6、心得
- 7、开心一刻:象棋笑话一则
1、前言
2017年应该是我迄今为止的人生当中最快乐的整年了(没有之一),虽然它也是我发誓这一年从头到尾都坚决不谈恋爱、坚决不找对象的一年。但,也许正因为如此,才让我的心灵没有了相思的羁绊,没有了虚妄的念头,从而发现了世界上除了儿女情长以外,还有更多更有意义、更有趣味、更有价值的美好事物吧!而这其中,就包括了3月份的时候,我在学校机房奋斗了三天三夜用C语言捣鼓出来的象棋小程序。
三天,九页A4纸。现在回过头来看,就对那句话颇感认同——真正令你快乐的,不是结果,而是过程。最后阶段看着BUG一个一个被改掉,测试表上面的项目一个一个通过,心里固然很高兴。但比这更让我的心中充满奇妙喜悦滋味的,是设计成功了一个又一个判定函数的算法的瞬间。一个你原本以为高不可攀的山峰,经过你的努力,也终于被你驯服。这一瞬间的自恋值真是爆表了,以致于都产生了幻觉:认为自己简直就是个天才,是这个世界上最聪明的人(just开个玩笑)——“仰天大笑出门去,我辈岂是蓬蒿人!”
而当时设计算法的草稿纸,我珍藏了将近4年,一直到租房期满搬家之前,都没舍得扔。但很可惜的是,最后搬家时,因为东西实在是太多了,才不得不选择性地带回了最最重要的几样东西,而把其他的全都忍痛割爱扔了——不管它刻录下了我怎样的美好时光。后来,由于一些原因,原本在CSDN只是一届潜水看客的我,也开始在这里的写博之旅了。于是灵光闪现——我能不能让自己以前写过的五子棋、汉诺塔、象棋等小程序的代码,在CSDN这片天地里得以永生呢?
但愿我今天写的所有程序,千百年后都能成为证明我曾经来到过这个世界上的化石吧!
2、程序规格
【外部规格】
界面表现形式:CUI
操作方法:通过WSAD键控制光标上下左右移动,按空格键选择或移动棋子。
对战形式:仅可进行双人单机对战,无法联网或人机对战。
胜负判定:程序无法自动判断绝杀或困毙,须棋手手动吃掉对方的将或帅,才判定胜负。将帅照面时,将或帅可以直接飞到对方的九宫里吃掉对方棋子。
犯规检测:本程序无用以检测是否存在长打犯规行为的功能。
回合制:否。当前版本暂时为一回合游戏。胜负分晓即结束游戏。
BUG情况:最新版本中暂未发现肉眼可见的BUG。
【内部规格】
开发所用语言:C语言
代码总行数:478行
函数总数:16个(包括main函数)
初版时间:2017年3月5日
此版时间:2021年4月23日
3、核心算法与象棋规则、术语
3.1 行棋规则与行棋判定函数设计
在这里,只考虑了红方棋子的情况,目的是为了让大家理解棋盘上的一些相对位置和线如何用数学语言去表达。至于黑方棋子的情况,由于这个博主很懒 为了给大家一个自己思考的机会,也为了文章更加简洁美观,在这里不作探讨。请有兴趣的同志自己思考补充完成。
3.1.0 象棋棋盘
象棋棋盘长这样。应该不会有人没见过吧?不会吧不会吧?
摆上棋子之后是这样滴。这是象棋布局的初始状态,这个记不住倒很正常。
河界:全称“楚河汉界”,是划分双方阵地的界限。过了河界就是对方阵地。
九宫:画斜线的地方的九个点叫九宫。将/帅和士只能在九宫内行棋,永远不可出宫(将帅照面的情况除外,此时将或帅可直接飞到对方九宫里吃掉对方的王)。
3.1.1 兵/卒
兵/卒的行棋规则
①兵卒全程禁止后退;
②兵卒每步只能走一格
③过河兵卒可横向行棋,没过河的只能前进
兵/卒合法行棋条件的数学表达
{ ① 向 前 行 一 格 ( y 起 始 点 > 4 ) ① 向 前 行 一 格 ∨ ② 横 向 行 一 格 ( y 起 始 点 ≤ 4 ) \begin{cases}①向前行一格&\ (y_{起始点}>4)\\ ①向前行一格\lor②横向行一格&\ (y_{起始点}≤4) \end{cases} {①向前行一格①向前行一格∨②横向行一格 (y起始点>4) (y起始点≤4)
①向前行一格
y 起 始 点 − y 目 标 点 = 1 ∧ x 起 始 点 = x 目 标 点 y_{起始点}-y_{目标点}=1 \ \land \ x_{起始点}=x_{目标点} y起始点−y目标点=1 ∧ x起始点=x目标点
②横向行一格
y 起 始 点 = y 目 标 点 ∧ ∣ x 目 标 点 − x 起 始 点 ∣ = 1 y_{起始点}=y_{目标点} \ \land \ |x_{目标点}-x_{起始点}|=1 y起始点=y目标点 ∧ ∣x目标点−x起始点∣=1
3.1.2 士
士的行棋规则
①士只可在九宫内行棋
②士只能斜向行棋,每次斜向一格
士合法行棋条件的数学表达
① 斜 向 行 一 格 ∧ ② 九 宫 内 行 棋 ①斜向行一格\ \land\ ②九宫内行棋 ①斜向行一格 ∧ ②九宫内行棋
①斜向行一格
∣ y 目 标 点 − y 起 始 点 ∣ = 1 ∧ ∣ x 目 标 点 − x 起 始 点 ∣ = 1 |y_{目标点}-y_{起始点}|=1\ \land\ |x_{目标点}-x_{起始点}|=1 ∣y目标点−y起始点∣=1 ∧ ∣x目标点−x起始点∣=1
②九宫内行棋
6 < y 目 标 点 < 10 ∧ 2 < x 目 标 点 < 6 6<y_{目标点}<10\ \land\ 2<x_{目标点}<6 6<y目标点<10 ∧ 2<x目标点<6
3.1.3 象
象的行棋规则
①象沿“田”字形对角线走(俗话说:马走日,象走田)
②象不能过河界
③当“田”字形中心点(象眼)受阻时,象不能移动。
象合法行棋条件的数学表达
① 沿 “ 田 ” 字 对 角 线 走 ∧ ② 象 眼 无 阻 ∧ ③ 不 能 过 河 ①沿“田”字对角线走\ \land\ ②象眼无阻\ \land\ ③不能过河 ①沿“田”字对角线走 ∧ ②象眼无阻 ∧ ③不能过河
①沿“田”字对角线走
∣ y 目 标 点 − y 起 始 点 ∣ = 2 ∧ ∣ x 目 标 点 − x 起 始 点 ∣ = 2 |y_{目标点}-y_{起始点}|=2\ \land\ |x_{目标点}-x_{起始点}|=2 ∣y目标点−y起始点∣=2 ∧ ∣x目标点−x起始点∣=2
②象眼无阻
点 ( y 目 标 点 + y 起 始 点 2 , x 目 标 点 + x 起 始 点 2 ) 为 空 点(\frac {y_{目标点}+y_{起始点}} 2, \frac {x_{目标点}+x_{起始点}} 2)为空 点(2y目标点+y起始点,2x目标点+x起始点)为空
③不能过河
y 目 标 点 ≥ 5 y_{目标点}≥5 y目标点≥5
3.1.4 马
马的行棋规则
①马沿“日”字形对角线走(还是:马走日,象走田)
②蹩脚马:马在欲行棋的方向上,如果紧挨着的一点有棋子阻挡,则称作“蹩脚马”,不能在被阻挡的方向行棋。
马合法行棋条件的数学表达
① 沿 “ 日 ” 字 形 对 角 线 走 ∧ ¬ ② 蹩 脚 马 ①沿“日”字形对角线走\ \land\ \neg②蹩脚马 ①沿“日”字形对角线走 ∧ ¬②蹩脚马
①沿“日”字形对角线走
∣ y 目 标 点 − y 起 始 点 ∣ 2 + ∣ x 目 标 点 − x 起 始 点 ∣ 2 = 5 |y_{目标点}-y_{起始点}|^2+|x_{目标点}-x_{起始点}|^2=5 ∣y目标点−y起始点∣2+∣x目标点−x起始点∣2=5
②蹩脚马
∣ x 目 标 点 − x 起 始 点 ∣ = 2 ∧ ¬ 点 ( y 起 始 点 , x 目 标 点 + x 起 始 点 2 ) 为 空 ∨ ∣ x 目 标 点 − x 起 始 点 ∣ = 1 ∧ ¬ 点 ( y 目 标 点 + y 起 始 点 2 , x 起 始 点 ) 为 空 |x_{目标点}-x_{起始点}|=2\ \land \ \neg点(y_{起始点}, \frac {x_{目标点}+x_{起始点}} 2)为空 \ \lor\ |x_{目标点}-x_{起始点}|=1\ \land\ \neg点(\frac {y_{目标点}+y_{起始点}} 2, x_{起始点})为空 ∣x目标点−x起始点∣=2 ∧ ¬点(y起始点,2x目标点+x起始点)为空 ∨ ∣x目标点−x起始点∣=1 ∧ ¬点(2y目标点+y起始点,x起始点)为空
3.1.5 车
车(jū)的行棋规则
①可在横竖方向行棋任意格数,而不可斜向行棋。
②不可跨越棋子。可以吃掉在自己行棋方向上遇到的第一个对方棋子。
3.1.6 炮
炮的行棋规则
①炮的走法与车大体相同,可横竖行棋任意格数,不可斜向行棋。
②除非吃子,否则炮也不可跨越棋子移动。
③炮要吃子,与目标棋子中间必须间隔且只间隔一个棋子(术语“炮架”)。炮架可以是己方棋子,也可以是对方棋子。
3.1.7 将/帅
将/帅的行棋规则
①一般情况下,将或帅只能在九宫内行棋,每次横向或纵向移动一格。
②将帅照面时,将或帅可直接飞到对方九宫,吃掉对方的王。
将/帅合法行棋条件的数学表达
① 横 竖 行 棋 一 格 ∧ ② 九 宫 内 行 棋 ①横竖行棋一格\ \land \ ②九宫内行棋 ①横竖行棋一格 ∧ ②九宫内行棋
①横竖行棋一格
∣ y 目 标 点 − y 起 始 点 ∣ = 1 ∧ x 目 标 点 = x 起 始 点 ∨ ∣ x 目 标 点 − x 起 始 点 ∣ = 1 ∧ y 目 标 点 = y 起 始 点 |y_{目标点}-y_{起始点}|=1\ \land\ x_{目标点}=x_{起始点} \lor |x_{目标点}-x_{起始点}|=1\ \land\ y_{目标点}=y_{起始点} ∣y目标点−y起始点∣=1 ∧ x目标点=x起始点∨∣x目标点−x起始点∣=1 ∧ y目标点=y起始点
②九宫内行棋
6 < y 目 标 点 < 10 ∧ 2 < x 目 标 点 < 6 6<y_{目标点}<10\ \land\ 2<x_{目标点}<6 6<y目标点<10 ∧ 2<x目标点<6
3.2 其他规则与术语
3.2.1 吃子
玩过暗棋(翻棋)的同志可能下象棋时会有一种错觉,认为不同的棋子之间像暗棋一样存在大小关系,就是谁不能吃谁啦,之类的。
不是的。
象棋的棋子之间是不存在大小关系的。任何棋子都可以吃掉任何棋子,也可能被任何棋子吃掉——只要符合行棋规则。
3.2.2 将军/应将/送将
一招棋走下去,如果对方不采取相应的措施,下一步他的将帅就会被吃掉。称这样的一招棋为“将军”,称对方采取的相应的措施为“应将”。而,行棋的一方主动把己方将帅走到对方棋子可攻击的范围内的行为,称作“送将”。
3.2.3 将帅照面/王不见王
将帅照面:将和帅两个棋子直接在一条竖直线上相对,二者之间没有任何棋子作阻挡,这种局面叫将帅照面。
王不见王:禁止将帅照面局面出现的规则。不同版本的说明,在表述上会有所不同。有的版本认为,造成将帅照面局面的一方直接犯规判负;而有的版本则认为,将帅照面局面出现时,先行的一方将帅可以直接飞到对方九宫内吃掉对方的王(我学棋的时候学的就是这个版本,程序编的也是这个版本)。但这两个版本殊途同归,意思都是一样的。
3.2.4 绝杀/困毙
绝杀:一招棋将军下去,对方无论如何应将,下一步都会被吃掉将帅,称作绝杀。
困毙:一招棋走下去,虽然不构成将军,但下一步对方无论怎么走,都会送将,称作困毙。
3.2.5 长打/禁止长打
同样的将法来来回回将好几次,称作“长打”。就跟围棋限制“劫”、将棋禁止“千日手”一样,象棋也禁止长打。同样的将法循环使用,到一定次数(好像是同样的局面连续出现五次?),长打的一方就会被判负。
但是,在我的这个程序里是没有长打判定的,请注意。如果你和你的小伙伴玩,他长打犯规了,请口头宣布判他负!
3.3 程序中的其它函数说明
3.3.1 abs()取绝对值函数
a b s ( x ) = { x (x≥0) − x (x<0) abs(x)=\begin{cases} x &\text{(x≥0)}\\ -x &\text{(x<0)} \end{cases} abs(x)={x−x(x≥0)(x<0)
取绝对值函数主要用于兵卒、象、士、将帅、马这五个在不同方向上有相同标量规定的棋子。因为不能提前预知棋手会往哪个方向行棋,而如果把所有情况都考虑进if条件里面统统用&&和||隔开,这样做也过于小白了(最主要是代码冗长,码形不美观,可读性也差)。所以加了这个绝对值函数,代码简洁不少。
int abs(int num){
if(num>=0) return num;
else return -num;
}
3.3.2 sgn()取符函数
s g n ( x ) = { 1 (x>0) 0 (x=0) − 1 (x<0) sgn(x)=\begin{cases} 1 &\text{(x>0)} \\ 0 &\text{(x=0)} \\ -1 &\text{(x<0)} \end{cases} sgn(x)=⎩⎪⎨⎪⎧10−1(x>0)(x=0)(x<0)
与取绝对值函数一样,取符函数也是考虑到棋手行棋方向未知而导入的函数。但是它更主要用于车、炮两个棋子。具体的应用是,设for循环里i的初始值为棋子原坐标加上目标点减原坐标的差的取符值,从这一点开始一直到目标点的前一个点为止,在这段范围内检查中间的隔子情况。循环迭代也是通过i+=sgn(目标点-原坐标)来实现的。
int sgn(int num){
if(num > 0) return 1;
else if(num == 0) return 0;
else if(num < 0) return -1;
}
3.3.3 sq()计算平方函数
s q ( x ) = x 2 sq(x)=x^2 sq(x)=x2
在这个程序里,平方函数就是专门用来计算跳马操作规范性的。通过验证以目标点与原点的连线为弦的直角三角形的勾股平方和是否为5,来验证马走的是否为“日”字形。
int sq(int num){
return num * num;
}
4、源代码
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#define SPACE 32
#define RED_BING 1
#define BLACK_ZU 2
#define RED_SHI 3
#define BLACK_SHI 4
#define RED_XIANG 5
#define BLACK_XIANG 6
#define RED_MA 7
#define BLACK_MA 8
#define RED_PAO 9
#define BLACK_PAO 10
#define RED_JU 11
#define BLACK_JU 12
#define RED_SHUAI 13
#define BLACK_JIANG 14
#define RED 20
#define BLACK 21
#define POINT 0
#define SPECIAL_POINT 30
#define CORRECT 100
#define WRONG 101
#define UNKNOWN 1000
int isEmpty(int num){
if(num==POINT || num==SPECIAL_POINT)
return 1;
else{
return 0;
}
}
int abs(int num){
if(num>=0)
return num;
else
return -num;
}
int sgn(int num){
if(num>0)
return 1;
else if(num<0)
return -1;
else if(num==0)
return 0;
}
int sq(int num){
return num * num;
}
void toSymbol(int num){
switch(num){
case POINT:
printf(" · ");
break;
case SPECIAL_POINT:
printf(" + ");
break;
case RED_BING:
printf("【兵】");
break;
case RED_SHI:
printf("【仕】");
break;
case RED_XIANG:
printf("【相】");
break;
case RED_MA:
printf("【傌】");
break;
case RED_PAO:
printf("【炮】");
break;
case RED_JU:
printf("【俥】");
break;
case RED_SHUAI:
printf("【帥】");
break;
case BLACK_ZU:
printf("(卒)");
break;
case BLACK_SHI:
printf("(士)");
break;
case BLACK_XIANG:
printf("(象)");
break;
case BLACK_MA:
printf("(馬)");
break;
case BLACK_PAO:
printf("(砲)");
break;
case BLACK_JU:
printf("(車)");
break;
case BLACK_JIANG:
printf("(將)");
break;
}
}
void toSymbolWithCur(int num){
printf("[");
toSymbol(num);
printf("]");
}
void toSymbolWithMark(int num){
printf("*");
toSymbol(num);
printf("*");
}
void tab(int num){
for(int i = 0; i<num; i++){
printf("\t");
}
}
void space(int num){
for(int i = 0; i<num; i++){
printf(" ");
}
}
void line(int num){
for(int i = 0; i<num; i++){
printf("\n");
}
}
void cls(){
system("cls");
}
void hr(int num){
for(int i = 0; i<num; i++){
printf("—");
}
}
int color(int piece){
if(piece>=1 && piece <=14){
if(piece % 2 != 0)
return RED;
else
return BLACK;
}else{
return -1;
}
}
void pause(){
system("pause");
}
int isLegal(int panel[10][9], int y_begin, int x_begin, int y_target, int x_target){
int counter = 0;
switch(panel[y_begin][x_begin]){
case RED_BING:
if(y_begin > 4){ //兵未过河:只能向前前进1格
if(!(y_begin-y_target==1 && x_begin==x_target)){
return WRONG;
}
}
else if(y_begin <= 4){ //过河兵:可以任意在前、左、右三个方向移动一格
if(!(y_begin-y_target==1 && x_begin==x_target || y_begin == y_target && abs(x_target-x_begin)==1)){
return WRONG;
}
}
return CORRECT;
break;
case BLACK_ZU:
if(y_begin <= 4){
if(!(y_target-y_begin==1 && x_begin==x_target)){
return WRONG;
}
}
else if(y_begin > 4){
if(!(y_target-y_begin==1 && x_begin==x_target || y_begin == y_target && abs(x_target-x_begin)==1)){
return WRONG;
}
}
return CORRECT;
break;
case RED_SHI: //士:只能在九宫内斜向移动一格
if(!(abs(y_target-y_begin)==1 && abs(x_target-x_begin)==1 && y_target > 6 && y_target < 10 && x_target > 2 && x_target < 6)){
return WRONG;
}
return CORRECT;
break;
case BLACK_SHI:
if(!(abs(y_target-y_begin)==1 && abs(x_target-x_begin)==1 && y_target > -1 && y_target < 3 && x_target > 2 && x_target < 6)){
return WRONG;
}
return CORRECT;
break;
case RED_XIANG: //象:只能沿“田”字对角线行走,并且象眼不能受阻
if(!(abs(y_target-y_begin)==2 && abs(x_target-x_begin)==2 && isEmpty(panel[(y_target+y_begin)/2][(x_target+x_begin)/2]))){
return WRONG;
}else if(y_target < 5){ //象不能过河
return WRONG;
}
return CORRECT;
break;
case BLACK_XIANG:
if(!(abs(y_target-y_begin)==2 && abs(x_target-x_begin)==2 && isEmpty(panel[(y_target+y_begin)/2][(x_target+x_begin)/2]))){
return WRONG;
}else if(y_target > 4){
return WRONG;
}
return CORRECT;
break;
case RED_MA: case BLACK_MA:
if(!(sq(abs(y_target-y_begin))+sq(abs(x_target-x_begin))==5)){ //检查是否沿“日”字对角线行棋
return WRONG;
}else if(abs(x_target-x_begin)==2 && !isEmpty(panel[y_begin][(x_target+x_begin)/2]) //检查是否蹩脚马
||abs(x_target-x_begin)==1 && !isEmpty(panel[(y_target+y_begin)/2][x_begin])){
return WRONG;
}
else{
return CORRECT;
}
break;
case RED_PAO: case BLACK_PAO:
//计数炮到指定坐标之间有多少棋子
if(x_begin==x_target){
for(int i=y_begin+sgn(y_target-y_begin); i!=y_target; i+=sgn(y_target-y_begin)){
if(!isEmpty(panel[i][x_begin])){
counter++;
}
}
}else if(y_begin==y_target){
for(int i=x_begin+sgn(x_target-x_begin); i!=x_target; i+=sgn(x_target-x_begin)){
if(!isEmpty(panel[y_begin][i])){
counter++;
}
}
}else if(x_begin!=x_target && y_begin!=y_target){ //斜向行棋不通过
return WRONG;
}
if(!isEmpty(panel[y_target][x_target])){ //如果目标点是某个对方的棋子,要求隔1子,否则不能吃
if(counter!=1){
return WRONG;
}
}else if(isEmpty(panel[y_target][x_target])){ //如果目标点是空点,要求中间无隔子,否则不能移动
if(counter!=0){
return WRONG;
}
}
return CORRECT;
break;
case RED_JU: case BLACK_JU:
//车到目标点范围内有隔子则不可行棋
//纵向
if(x_begin==x_target){
for(int i = y_begin+sgn(y_target-y_begin); i!=y_target; i+=sgn(y_target-y_begin)){
if(!isEmpty(panel[i][x_begin])){
return WRONG;
}
}
}
//横向
else if(y_begin==y_target){
for(int i = x_begin+sgn(x_target-x_begin); i!=x_target; i+=sgn(x_target-x_begin)){
if(!isEmpty(panel[y_begin][i])){
return WRONG;
}
}
}else if(x_begin!=x_target && y_begin!=y_target){
return WRONG;
}
return CORRECT;
break;
case RED_SHUAI:
//将帅照面的情况:帅可飞到对方九宫直接吃将
if(panel[y_target][x_target]==BLACK_JIANG && x_begin==x_target){
for(int i = y_begin-1; i>y_target; i--){
if(!isEmpty(panel[i][x_begin])){
return WRONG;
}
}
return CORRECT;
}
//一般情况:在九宫范围内横或竖移动一格
if(!((abs(y_target-y_begin)==1 && x_target==x_begin || abs(x_target-x_begin)==1 && y_target==y_begin)&&y_target>6 && y_target<10 &&x_target>2 && x_target<6)){
return WRONG;
}
return CORRECT;
break;
case BLACK_JIANG:
if(panel[y_target][x_target]==RED_SHUAI && x_begin==x_target){
for(int i = y_begin+1; i<y_target; i++){
if(!isEmpty(panel[i][x_begin])){
return WRONG;
}
}
return CORRECT;
}
if(!((abs(y_target-y_begin)==1 && x_target==x_begin || abs(x_target-x_begin)==1 && y_target==y_begin)&&y_target>-1 && y_target<3 &&x_target>2 && x_target<6)){
return WRONG;
}
return CORRECT;
break;
default:
return CORRECT;
break;
}//end switch
}
int main(void){
int panel[10][9]={{12,8,6,4,14,4,6,8,12},
{0,0,0,0,0,0,0,0,0},
{0,10,0,0,0,0,0,10,0},
{2,0,2,0,2,0,2,0,2},
{0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0},
{1,0,1,0,1,0,1,0,1},
{0,9,0,0,0,0,0,9,0},
{0,0,0,0,0,0,0,0,0},
{11,7,5,3,13,3,5,7,11}};
int cur[2] = {7,1}; //光标默认在红方炮的位置
int isSelected[2] = {-1,-1};
int turn = RED;
int winner = UNKNOWN;
int key;
while(1){
cls();
line(1);
for(int i = 0; i < 10; i++){
for(int j = 0; j < 9; j++){
if(cur[0]==i && cur[1]==j){
toSymbolWithCur(panel[i][j]);
}else if(isSelected[0]==i && isSelected[1]==j){
toSymbolWithMark(panel[i][j]);
}
else{
space(1);
toSymbol(panel[i][j]);
space(1);
}
}
if(i==4){
line(1);hr(35);line(1);
tab(2); space(6);printf("楚 河");tab(1);printf("漢 界");
line(1);hr(35);line(1);
continue;
}
line(3);
}
if(winner!=UNKNOWN){
if(winner==RED)
printf("系统:【帥】红方获胜!\n");
else
printf("系统:(將)黑方获胜!\n");
return 0;
}
switch(turn){
case RED:
printf("系统:请红方棋手【帥】行棋……\n");
break;
case BLACK:
printf("系统:请黑方棋手(將)行棋……\n");
break;
}
key = getch();
switch(key){
case 'w': case 'W':
if(cur[0]==0) cur[0]=9;
else cur[0]--;
break;
case 'a': case 'A':
if(cur[1]==0) cur[1]=8;
else cur[1]--;
break;
case 's': case 'S':
if(cur[0]==9) cur[0]=0;
else cur[0]++;
break;
case 'd': case 'D':
if(cur[1]==8) cur[1]=0;
else cur[1]++;
break;
case 'l': case 'L':
printf("系统:结束程序……\n");
system("pause");
cls();
return 0;
case SPACE:
if(isSelected[0]==-1 && isSelected[1]==-1){ //未选中任何棋子的状态下按下了空格
if(!isEmpty(panel[cur[0]][cur[1]]) && color(panel[cur[0]][cur[1]])==turn){ //将这个坐标激活为选中状态
isSelected[0] = cur[0];
isSelected[1] = cur[1];
}
}else{ //如果是在选中了某棋子的状况下按下了空格
//如果坐标没有移动,清除当前激活状态
if(isSelected[0]==cur[0] && isSelected[1] == cur[1]){
isSelected[0]=-1;
isSelected[1]=-1;
}
//如果选到了其他自己的子,则将激活点转移到新坐标上。
else if(color(panel[cur[0]][cur[1]])==turn){
isSelected[0] = cur[0];
isSelected[1] = cur[1];
}else{
//如果所选的点不是自己的子,则判断移动(吃子)方式是否违规
if(isLegal(panel,isSelected[0],isSelected[1],cur[0],cur[1])==CORRECT){
//如果行棋符合规则:
//如果被吃掉的是对方的将帅,直接判定玩家获胜。
if(panel[cur[0]][cur[1]] == BLACK_JIANG || panel[cur[0]][cur[1]] == RED_SHUAI){
winner = turn;
}
//否则就将棋子移动到目标点,
panel[cur[0]][cur[1]] = panel[isSelected[0]][isSelected[1]];
//清空原来坐标上面的棋子,
if((isSelected[0] == 3 || isSelected[0] == 6) && isSelected[1]%2==0 ||
(isSelected[0] == 2 || isSelected[0] == 7)&&(isSelected[1]==1 || isSelected[1]==7)){
panel[isSelected[0]][isSelected[1]] = SPECIAL_POINT;
}else{
panel[isSelected[0]][isSelected[1]] = POINT;
}
//状态调回未选中,
isSelected[0]=-1;
isSelected[1]=-1;
//并反转turn的状态值。
if(turn==RED)turn=BLACK;
else turn = RED;
}
//如果行棋不符合规则
else{
//清空已经激活了的点
isSelected[0]=-1;
isSelected[1]=-1;
}
}
}
}
}
}
5、运行结果
5.1 界面与操作展示
这是程序运行的效果。
棋谱选用的是2020年九城杯全国象棋个人锦标赛上,时凤兰(红方)vs王子涵(黑方)的对局的前七回合。
5.2 测试结果
编号 | 测试项目 | 测试方式 | 测试结果 | 备注 |
---|---|---|---|---|
1 | 炮隔子移动 | 炮二平九 | 选中标记消失 | 通过 |
2 | 自己打自己棋子 | 车一平二 | 选中标记转移 | 通过 |
3 | 自己的回合选择对方棋子 | 象3进5 | 无反应 | 通过 |
4 | 炮不隔子吃子 | 炮二进五 | 选中标记消失 | 通过 |
5 | 炮正常移动 | 炮二平三 | 成功 | 通过 |
6 | 炮隔两子吃子 | 砲8平7→炮三进五 | 选中标记消失 | 通过 |
7 | 炮正常吃子 | 炮三进四 | 成功 | 通过 |
8 | 马不走“日”字形 | 马从(1,8)到(3,8) | 选中标记消失 | 通过 |
9 | 蹩脚马 | 马8进6 | 选中标记消失 | 通过 |
10 | 马正常移动 | 马8进9 | 成功 | 通过 |
11 | 车隔子移动 | 车一进九 | 选中标记消失 | 通过 |
12 | 车正常移动 | 车一进一 | 成功 | 通过 |
13 | 原阵地兵后退或横向移动 | 卒5退1→卒5平4→卒5平6 | 选中标记消失 | 通过 |
14 | 过河兵后退 | 卒5进1→兵五进一→卒5进1→车一平四→卒5退1 | 选中标记消失 | 通过 |
15 | 士直走 | 士从(1,4)到(2,4) | 选中标记消失 | 通过 |
16 | 士出九宫 | 象3进5→马二进三→士4平3 | 选中标记消失 | 通过 |
17 | 士走两格 | 士从(1,4)到(3,6) | 选中标记消失 | 通过 |
18 | 士正常移动 | 士4进5 | 成功 | 通过 |
19 | 象不走“田”字形 | 相从(10,7)到(8,8) | 选中标记消失 | 通过 |
20 | 象眼受阻方向上走象 | 相三进五 | 选中标记消失 | 通过 |
21 | 象正常移动 | 相三进一 | 成功 | 通过 |
22 | 帅斜走 | 将从(1,5)到(2,4) | 选中标记消失 | 通过 |
23 | 非将帅照面时,帅的出宫 | 将5平4→帅五进一→将4平3 | 选中标记消失 | 通过 |
24 | 将帅照面飞吃王 | 将4进1→帅五平六→将4进7 | 成功 | 通过 |
25 | 相过河 | 相三进五→卒1进1→相五进三→卒1进1→相三进五 | 选中标记消失 | 通过 |
6、心得
遇到难题不要退缩,不要逃避,勇敢地拿起草稿纸分析、思考就完事了。当然了,这对数学基础有一定要求。否则怎么能在要检测跳马操作规范性时想到验勾股平方和的这种算法呢?所以啊,好好学数学,真的不是只有买菜会用上。
也许你买菜时当然不会用上它,但,谁能保证你赖以挣买菜钱的生计——你的工作当中也没有哪天会用上它呢?
就像你下围棋,布局时落一颗子,乍一看,于局部可能并没有什么作用。但等你收官的时候,你才会发现,这颗子的存在竟然挽救了你的整盘棋!
书到用时方恨少,“无用”的东西,积累着积累着,量变引起质变,也就有用了。而且是大有用途,是很基础、决定你和没积累它的人本质不同的作用。
7、开心一刻:象棋笑话一则
有两个老大爷在户外摆着小板凳摇着扇子下象棋,一个小伙子伸长脖子看他们下棋。突然,小伙子说了一声,“大爷,你车(chē)没了。”
大爷甲:“什么车(chē)没了?这念jū!”
大爷乙:“念jū啊!小伙子。不念chē。”
年轻人:“大爷,我是说你停在路边的自行jū被人骑跑了!”