炸飞机游戏
去年新年联欢会的时候,我班同学好像沉迷于一个叫“炸飞机”的游戏无法自拔。也不知道这个游戏是谁发明的,谁引入的,大家都开始开开心心地玩了起来。只是可惜那个时候我还在去长沙的路上,自然也就感受不到班级里欢脱的氛围了。
这是一个对战游戏,两个同学互为对手,每个人都有一块 9 × 9 9 \times 9 9×9大小的格子棋盘,游戏开始前他们要在自己的棋盘上安置三架“飞机”,他们只知道自己放置飞机的位置而不知道对方放置飞机的位置。
上图就是一种合法的安排飞机的方式,大家应该能看出三个“飞机的形状”(红色的圆圈表示飞机头,黄色的菱形表示飞机的机身),飞机的放置不可以重叠,但可以把翅膀插在对方的空隙里,就像这样:
在游戏开始后,两个玩家轮流操作。每次操作可以询问对方玩家,对方 9 × 9 9 \times 9 9×9的棋盘上某一个格子的“状态”。对方需要根据自己棋盘的实际情况回答“飞机头”、“机身”或“空地”。如果一个玩家率先确定了另一个玩家的所有“飞机头”的位置,我们称这个玩家取得了胜利。
炸飞机游戏的所有合法的飞机放置方案
经过DFS,我们发现炸飞机游戏总共有8744种放置方案(p.s:如果棋盘的大小是 10 × 10 10 \times 10 10×10的, 那么总方案数为66816种),这个数字对于计算机来说应该不是很大,DFS程序几乎能在瞬间计算完成,为了营造一个“炫酷的效果”,我录制了一个DFS并且显示所有可行结果的视频。
我的QQ空间 不是我的好友的话大概看不了…
贴一下代码吧,但要注意,我写了一个小巧的头文件,要把这个小巧的头文件和源代码文件放在同一个文件夹下才能编译(哦,对了,我用的是Windows下的DEVC++,其他操作系统应该编译不了这个程序)。
winshow.cpp (我所说的那个头文件)
有同学可能会问,为什么这个头文件的扩展名是“.cpp”,我只想说,主要是我懒得把它改成“.hpp”了。这个头文件里面提供了一些很好的输出功能。
#ifndef __WINSHOW_CPP__
#define __WINSHOW_CPP__
/// 2019.6.6 控制台输出
/// 粘的 Tetris 原来的板子
#include <windows.h>
#include <cstdlib>
#include <cstdio>
namespace hwndset {
HWND hwnd=GetForegroundWindow(); //当前窗口句柄
bool checkupon() { /// 判断当前窗口是否在最顶层
HWND hwndn=GetForegroundWindow(); return hwndn == hwnd;
}
#define KEY_DOWN(VK_NONAME) ((GetAsyncKeyState(VK_NONAME) & 0x8000) ? 1:0) /// 检测字符是否按下
///222: #define VK_LEFT 0x25
///223: #define VK_UP 0x26
///224: #define VK_RIGHT 0x27
///225: #define VK_DOWN 0x28 -- from <windows.h>
#define SHAKE (5)
void shake(){ /// 屏幕晃动, 懒得写了, 粘的板子
RECT rect;
/// HWND hwnd=GetForegroundWindow();
GetWindowRect(hwnd,&rect);
MoveWindow(hwnd,rect.left+SHAKE,rect.top,rect.right-rect.left,rect.bottom-rect.top,TRUE);
Sleep(28);
MoveWindow(hwnd,rect.left+SHAKE,rect.top-SHAKE,rect.right-rect.left,rect.bottom-rect.top,TRUE);
Sleep(28);
MoveWindow(hwnd,rect.left,rect.top-SHAKE,rect.right-rect.left,rect.bottom-rect.top,TRUE);
Sleep(28);
MoveWindow(hwnd,rect.left,rect.top,rect.right-rect.left,rect.bottom-rect.top,TRUE);
}
#undef SHAKE
}
int RND(int L, int R) {
#define RND ((rand()<<15)+rand())
return RND%(R - L + 1) + L;
#undef RND
}
namespace clr { /// 关于颜色的一些常量
const int BLACK=0, BLUE=1, GREEN=2, CYAN=3, RED=4, PURPLE=5, YELLOW=6, WHITE=7, LIGHT=8;
const int DEFAULT = (clr::BLACK<<4)|clr::WHITE;
int makecol(int background, int letter) { /// 装载一种颜色
return (background << 4) | letter;
}
}
using clr::makecol; /// 开放一个对外函数 makecol
namespace wnd { /// 关于 Windows API 的使用
void gotoxy(int y, int x) { /// 更改屏幕输出位置
COORD pos; pos.X = x; pos.Y = y;
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),pos);
}
void color(int a) { /// 更改输出指针颜色
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),a);
}
}
namespace wshow {
struct dot { /// 屏幕上的基本单位
char ch[2], col; /// 两个字节内容,以及其颜色
dot() {
col = makecol(clr::BLACK, clr::WHITE); // 默认"黑底白字"
ch[0] = ch[1] = 0; /// 默认没有任何字符
}
dot(const char* s, int c = clr::DEFAULT) { /// 通过字符串构造 dot
ch[0] = s[0], ch[1] = s[1]; col = c; /// 初始化一个字符
if(ch[0]<' ' && ch[0]>=-2) ch[0] = 0;
if(ch[1]<' ' && ch[1]>=-2) ch[1] = 0; /// 避免 tab '\n' 之类的不必要的控制字符的出现
}
} Screen[24][39]; /// 用这个数组去描述屏幕中"受控制(被记录)"的位置
//int store[24][39]; /// 这是俄罗斯方块数组的"固化版"
bool operator == (dot A, dot B) { /// 判断两个 dot 是否相同
return A.ch[0]==B.ch[0] && A.ch[1]==B.ch[1] && A.col==B.col; /// 考虑颜色
}
//dot transcol(dot A,int ncol) {
// return dot(A.ch, ncol); /// 返回一个内容相同颜色不同的 dot
//}
void print(dot A) { /// 输出一个 dot
wnd::color(A.col); /// 先把输入指针变到对应的颜色
printf("%c%c", A.ch[0], A.ch[1]); /// 输出这个字符
wnd::color(clr::DEFAULT); /// 及时恢复到默认颜色
}
void output(dot A, int y, int x) { /// 唯一的一个新写的函数
if(!(Screen[y][x] == A)) { /// 简单粗暴地指定一个位置, 并输出字符
wnd::gotoxy(y, x*2);
print(A);
}
}
}
/*
int main() {
return 0;
}
*/
#else
/// 说明头文件已经被引用过了
#endif
dfs.cpp (核心程序)
视频中录制的内容就是这个程序的运行效果,由于这个程序是在另一个程序的基础上改编而来的,所以里面可能有一些与它所实现的功能无关的函数或变量。由于我们还没有介绍我是如何定义飞机的位置和方向的,所以说程序读不懂也属正常现象。
#include "winshow.cpp" /// 用于输出到屏幕
const int PlaneShape[5][5] = { /// 飞机的形态
{0, 0, 2, 0, 0},
{1, 1, 1, 1, 1},
{0, 0, 1, 0, 0},
{0, 1, 1, 1, 0},
{0, 0, 0, 0, 0}
};
/// 定义 Check 函数的返回值
#define OutBoard (-2) /// 查询了棋盘外的位置 或者玩家编号不合法
#define Undiscovered (-1)
#define EmptyLand ( 0)
#define PlaneBody (+1)
#define PlaneHead (+2)
#define Rin(A, B, C) (((A)<=(B))&&((B)<=(C))) /// 判断 A <= B <= C 是否成立
int ActualBoard[3][10][10]; /// 棋盘
bool Discovered [3][10][10]; /// 判断某个位置是否被查看过
/// !!! 玩家的程序不得调用 ActualBoard Discovered 两个数组 !!!
int Check(int player, int x, int y) { /// 检测棋盘上某个区域的状态
if(!Rin(1, player, 2)) { /// 玩家编号不合法
return OutBoard;
}
if(Rin(1, x, 9) && Rin(1, y, 9)) { /// 棋盘上的某个位置
if( 1 ) { /// 因为我只想显示全部信息
return ActualBoard[player][x][y];
}else {
return Undiscovered;
}
}else {
return OutBoard;
}
/// !!! 玩家的程序可以调用 Check 函数 !!!
}
/// !!! 在 -下方 -添加玩家的程序 !!!
//#include "stdplayer1.cpp"
//#include "stdplayer2.cpp"
/// !!! 在 -上方 -添加玩家的程序 !!!
#include <cstring>
wshow::dot MakeDot(int item) { /// 找到某一个格子应输出的符号
if(!Rin(-1, item, 2)) {
return wshow::dot("□", clr::makecol(clr::BLACK, clr::WHITE|clr::LIGHT));
}else {
switch(item) {
case -1: return wshow::dot("■", clr::makecol(clr::BLACK, clr::WHITE));
case 0: return wshow::dot(" ", clr::makecol(clr::BLACK, clr::WHITE));
case +1: return wshow::dot("◆", clr::makecol(clr::BLACK, clr::YELLOW));
case +2: return wshow::dot("●", clr::makecol(clr::BLACK, clr::RED|clr::LIGHT));
}
}
}
bool CheckFail(int player) { /// 判断某个玩家是否已经失败
int cnt = 0;
for(int i = 1; i <= 9; i ++) {
for(int j = 1; j <= 9; j ++) { /// 统计暴露的飞机头的数量
if(Discovered[player][i][j] && ActualBoard[player][i][j]==2) {
cnt ++;
}
}
}
return cnt >= 3;
}
void DisplayChessBoard(int player, int y, int x) { /// 向屏幕上显示一个棋盘
/// y,x 为左上角的坐标
for(int i = 0; i <= 10; i ++) {
for(int j = 0; j <= 10; j ++) { /// 输出一个 11 * 11 大小的棋盘
int item = Check(player, i, j);
wshow::output(MakeDot(item), y+i, x+j);
}
}
}
int Plane(int dir, int x, int y) { /// 得到旋转后的飞机模型
switch(dir) {
case 0: return PlaneShape[x][y];
case 1: return PlaneShape[y][x];
case 2: if(3-x >= 0) return PlaneShape[3-x][y]; else return 0;
case 3: if(3-y >= 0) return PlaneShape[3-y][x]; else return 0;
}
}
bool CheckSetPlane(int player, int x, int y, int dir) { /// 检测某个未知是否可以放置飞机
dir %= 4; /// dir = 0, 1, 2, 3 分别表示将标准飞机模型逆时针旋转 0du 90du 180du 270du
/// x, y 表示飞机所在的 4*5 区域中, 横纵坐标取值的最小值(此处为空地)
for(int i = 0; i <= 4; i ++) {
for(int j = 0; j <= 4; j ++) {
int xn = x+i, yn = y+j; /// 当前点的坐标
if(Plane(dir, i, j) > 0) {
if(!Rin(1, xn, 9) || !Rin(1, yn, 9))
return false; /// 在棋盘外
if(ActualBoard[player][xn][yn] != 0)
return false; /// 与其他飞机冲突
}
}
}
return true; /// 可以放置飞机
}
void SetPlane(int player, int x, int y, int dir) { /// 放置飞机
/// 放之前一定要记得检测是否可以放置
dir %= 4;
for(int i = 0; i <= 4; i ++) {
for(int j = 0; j <= 4; j ++) {
int xn = x+i, yn = y+j; /// 当前点的坐标
if(Plane(dir, i, j) > 0) {
ActualBoard[player][xn][yn] = Plane(dir, i, j);
}
}
}
}
void UnSetPlane(int player, int x, int y, int dir) { /// 放置飞机
/// 放之前一定要记得检测是否可以放置
dir %= 4;
for(int i = 0; i <= 4; i ++) {
for(int j = 0; j <= 4; j ++) {
int xn = x+i, yn = y+j; /// 当前点的坐标
if(Plane(dir, i, j) > 0) {
ActualBoard[player][xn][yn] = 0;
}
}
}
}
/*
int MsgCnt = 0;
void OutputMsg(const char* str, int col = clr::DEFAULT) {
wnd::gotoxy(MsgCnt%20+1, 47);
printf(" ");
wnd::gotoxy(MsgCnt%20, 47);
printf(" ");
wnd::gotoxy(MsgCnt%20, 47);
wnd::color(col);
printf("%3d: %s", MsgCnt+1, str);
wnd::color(clr::DEFAULT);
MsgCnt ++;
}*/
bool CheckExplore(int player, int x, int y) { /// 检测探测是否合法
/// player 表示被探测的玩家
if(Rin(1, x, 9) && Rin(1, y, 9)) {
if(Discovered[player][x][y]) {
//OutputMsg("探测到已探测区域");
return false; /// 探测到已经探测过的区域
}else return true; /// 合法
}else {
return false; /// 探测到游戏区域之外, 不合法
}
}
#include <ctime>
int Rest = 3; /// 剩余飞机数量
int X[4], Y[4], D[4], Cnt = 0; /// Cnt 表示合法的方案数
void DFS(int x, int y) {
if(Rest == 0) {
Cnt ++;
DisplayChessBoard(1, 0, 0);
printf("\n %6d", Cnt);
printf("\n");
//system("pause");
return;
}
if(x == 10) return;
for(int i = 0; i <= 3; i ++) { /// 当前位置放飞机
if(CheckSetPlane(1, x, y, i)) {
SetPlane(1, x, y, i);
Rest --;
if(y == 9) {
DFS(x+1, 1);
}else {
DFS(x, y+1);
}
Rest ++;
UnSetPlane(1, x, y, i);
}
}
/// 当前位置不放飞机
if(y == 9) {
DFS(x+1, 1);
}else {
DFS(x, y+1);
}
}
int Methods[8848][10][10]; /// 用于记录全部的合法方案
int main() {
system("pause");
DFS(1, 1);
//printf("\n共%d种\n", Cnt);
system("pause");
return 0;
}
让算法互掐的炸飞机游戏平台
人与人之间可以愉快地玩耍,那么电脑与电脑之间是否也可以愉快地玩耍呢?我们的答案是肯定的。我们实现电脑互掐的原理很简单,有点像竞赛界所说的“交互题”。
我们有三个程序:“chessboard.cpp”、“player1.cpp”、“player2.cpp”。然后我们让"chessboard.cpp"用调用头文件的方式调用"player1.cpp"和"player2.cpp",并利用其中的函数进行计算最后实现对战的目的。其中,我们将"player1.cpp"和“player2.cpp”统称为“玩家算法程序”。
玩家算法程序中需要包含哪些要素
玩家算法程序需要包含一个名为"player1"或"player2"的namespace(命名空间),除了声明头文件的部分外,玩家的其余代码必须全部在这个namespace内编写。
namespace中需要至少包含这三个函数,并且参数表必须与要求一致:
void Init() {
/// 用户初始化函数,在比赛开始前会被调用
}
void Begin(int playernum, int* X, int* Y, int* D) {
/// 每一局游戏开始之前这个函数都会被调用, playernum的值为1或2,表示被授予的玩家编号
/// X,Y,D 三个数组是用来放置飞机的
/// X[k], Y[k], D[k] (k=1,2,3)表示第k个飞机所在的位置及其头的朝向
}
void Play(int& X, int& Y) { /// 选择对方阵地中一个未被探索的位置进行探索
/// 并将你想要探索的那个位置的坐标存入变量X,Y中
}
在此我们需要统一一下描述飞机位置及朝向的方法:
无论飞机的朝向如何,我们用一个尽可能小的,各边与棋盘边线对应平行的矩形框将一个飞机|“圈住”,我们就用这个矩形框中位于最左上方的那个格子的坐标来描述这个飞机的位置。例如图中的绿色框线标明的位置记作 (1,1),红色框线标明的位置记作(1,5),蓝色框线标明的位置记作(6,4)。
飞机的朝向只有头向上、头向左、头向下、头向右四种。我们分别用D[k] = 0, 1, 2, 3
对应描述这四种情况。
chessboard.cpp 程序为玩家提供了什么
我们为玩家提供了这样两个函数:
int RND(int L, int R); /// 表示生成一个[L,R]闭区间内的随机数
int Check(int playernum, int x, int y); /// 返回某个玩家的棋盘上某一点的“状态”
关于棋盘上某一点的“状态”,我们是这么定义的:
/// 定义 Check 函数的返回值
#define OutBoard (-2) /// 查询了棋盘外的位置 或者玩家编号不合法
#define Undiscovered (-1) /// 这个位置还未被探索到
#define EmptyLand ( 0) /// 这个位置是空地
#define PlaneBody (+1) /// 这个位置试机身
#define PlaneHead (+2) /// 这个位置试飞机头
有了这些东西我们就可以开心的玩耍了,在此我们给出chessboard.cpp的代码。
#include "winshow.cpp" /// 用于输出到屏幕
const int PlaneShape[5][5] = { /// 飞机的形态
{0, 0, 2, 0, 0},
{1, 1, 1, 1, 1},
{0, 0, 1, 0, 0},
{0, 1, 1, 1, 0},
{0, 0, 0, 0, 0}
};
/// 定义 Check 函数的返回值
#define OutBoard (-2) /// 查询了棋盘外的位置 或者玩家编号不合法
#define Undiscovered (-1)
#define EmptyLand ( 0)
#define PlaneBody (+1)
#define PlaneHead (+2)
#define Rin(A, B, C) (((A)<=(B))&&((B)<=(C))) /// 判断 A <= B <= C 是否成立
int ActualBoard[3][10][10]; /// 棋盘
bool Discovered [3][10][10]; /// 判断某个位置是否被查看过
/// !!! 玩家的程序不得调用 ActualBoard Discovered 两个数组 !!!
int Check(int player, int x, int y) { /// 检测棋盘上某个区域的状态
if(!Rin(1, player, 2)) { /// 玩家编号不合法
return OutBoard;
}
if(Rin(1, x, 9) && Rin(1, y, 9)) { /// 棋盘上的某个位置
if(Discovered[player][x][y]) {
return ActualBoard[player][x][y];
}else {
return Undiscovered;
}
}else {
return OutBoard;
}
/// !!! 玩家的程序可以调用 Check 函数 !!!
}
/// !!! 在 -下方 -添加玩家的程序 !!!
#include "hjqplayer1.cpp"
#include "stdplayer2.cpp"
/// !!! 在 -上方 -添加玩家的程序 !!!
#include <cstring>
wshow::dot MakeDot(int item) { /// 找到某一个格子应输出的符号
if(!Rin(-1, item, 2)) {
return wshow::dot("□", clr::makecol(clr::BLACK, clr::WHITE|clr::LIGHT));
}else {
switch(item) {
case -1: return wshow::dot("■", clr::makecol(clr::BLACK, clr::WHITE));
case 0: return wshow::dot(" ", clr::makecol(clr::BLACK, clr::WHITE));
case +1: return wshow::dot("◆", clr::makecol(clr::BLACK, clr::YELLOW));
case +2: return wshow::dot("●", clr::makecol(clr::BLACK, clr::RED|clr::LIGHT));
}
}
}
bool CheckFail(int player) { /// 判断某个玩家是否已经失败
int cnt = 0;
for(int i = 1; i <= 9; i ++) {
for(int j = 1; j <= 9; j ++) { /// 统计暴露的飞机头的数量
if(Discovered[player][i][j] && ActualBoard[player][i][j]==2) {
cnt ++;
}
}
}
return cnt >= 3;
}
void DisplayChessBoard(int player, int y, int x) { /// 向屏幕上显示一个棋盘
/// y,x 为左上角的坐标
for(int i = 0; i <= 10; i ++) {
for(int j = 0; j <= 10; j ++) { /// 输出一个 11 * 11 大小的棋盘
int item = Check(player, i, j);
wshow::output(MakeDot(item), y+i, x+j);
}
}
}
int Plane(int dir, int x, int y) { /// 得到旋转后的飞机模型
switch(dir) {
case 0: return PlaneShape[x][y];
case 1: return PlaneShape[y][x];
case 2: if(3-x >= 0) return PlaneShape[3-x][y]; else return 0;
case 3: if(3-y >= 0) return PlaneShape[3-y][x]; else return 0;
}
}
bool CheckSetPlane(int player, int x, int y, int dir) { /// 检测某个未知是否可以放置飞机
dir %= 4; /// dir = 0, 1, 2, 3 分别表示将标准飞机模型逆时针旋转 0du 90du 180du 270du
/// x, y 表示飞机所在的 4*5 区域中, 横纵坐标取值的最小值(此处为空地)
for(int i = 0; i <= 4; i ++) {
for(int j = 0; j <= 4; j ++) {
int xn = x+i, yn = y+j; /// 当前点的坐标
if(Plane(dir, i, j) > 0) {
if(!Rin(1, xn, 9) || !Rin(1, yn, 9))
return false; /// 在棋盘外
if(ActualBoard[player][xn][yn] != 0)
return false; /// 与其他飞机冲突
}
}
}
return true; /// 可以放置飞机
}
void SetPlane(int player, int x, int y, int dir) { /// 放置飞机
/// 放之前一定要记得检测是否可以放置
dir %= 4;
for(int i = 0; i <= 4; i ++) {
for(int j = 0; j <= 4; j ++) {
int xn = x+i, yn = y+j; /// 当前点的坐标
if(Plane(dir, i, j) > 0) {
ActualBoard[player][xn][yn] = Plane(dir, i, j);
}
}
}
}
int MsgCnt = 0;
void OutputMsg(const char* str, int col = clr::DEFAULT) {
wnd::gotoxy(MsgCnt%20+1, 47);
printf(" ");
wnd::gotoxy(MsgCnt%20, 47);
printf(" ");
wnd::gotoxy(MsgCnt%20, 47);
wnd::color(col);
printf("%3d: %s", MsgCnt+1, str);
wnd::color(clr::DEFAULT);
MsgCnt ++;
}
void OutputState(const char* str, int col = clr::DEFAULT) {
wnd::gotoxy(14, 4);
printf(" ");
wnd::gotoxy(14, 4);
wnd::color(col);
printf("%s", str);
wnd::color(clr::DEFAULT);
}
bool CheckExplore(int player, int x, int y) { /// 检测探测是否合法
/// player 表示被探测的玩家
if(Rin(1, x, 9) && Rin(1, y, 9)) {
if(Discovered[player][x][y]) {
OutputMsg("探测到已探测区域");
return false; /// 探测到已经探测过的区域
}else return true; /// 合法
}else {
return false; /// 探测到游戏区域之外, 不合法
}
}
#include <ctime>
const int WARN_COLOR = makecol(clr::BLACK, clr::RED|clr::LIGHT);
const int SUCC_COLOR = makecol(clr::BLACK, clr::GREEN|clr::LIGHT);
const int TIPS_COLOR = makecol(clr::BLACK, clr::YELLOW|clr::LIGHT);
int Game() { /// 开始一轮游戏, 返回胜利玩家的编号
int X[4], Y[4], D[4]; /// 记录飞机信息
/// 游戏开局,初始化棋盘
memset( Discovered, 0x00, sizeof( Discovered));
memset(ActualBoard, 0x00, sizeof(ActualBoard));
OutputMsg("1号玩家放置飞机中 ...");
player1::Begin(1, X, Y, D); /// 一号玩家的开局
for(int i = 1; i <= 3; i ++) {
if(CheckSetPlane(1, X[i], Y[i], D[i])) {
SetPlane(1, X[i], Y[i], D[i]);
}else {
OutputMsg("1号玩家飞机放置无效!", WARN_COLOR);
return 2;
}
}
OutputMsg("2号玩家放置飞机中 ...");
player2::Begin(2, X, Y, D); /// 二号玩家的开局
for(int i = 1; i <= 3; i ++) {
if(CheckSetPlane(2, X[i], Y[i], D[i])) {
SetPlane(2, X[i], Y[i], D[i]);
}else {
OutputMsg("2号玩家飞机放置无效!", WARN_COLOR);
return 1;
}
}
OutputMsg("游戏开始!", TIPS_COLOR);
for(int Q = 1; ; Q ++) { /// Q 记录回合数
char tmp[32] = {};
sprintf(tmp, "--> 第%3d回合游戏 ...", Q);
OutputMsg(tmp, TIPS_COLOR);
int X, Y;
OutputMsg("1号玩家计算中 ...");
player1::Play(X, Y); /// 一号玩家开始探测
if(CheckExplore(2, X, Y)) {
Discovered[2][X][Y] = true;
DisplayChessBoard(2, 2, 12); /// 更新 2 号玩家的棋盘状态
}else {
OutputMsg("1号玩家探测非法!", WARN_COLOR);
return 2;
}
if(CheckFail(2)) { /// 判断二号玩家是否已经失败
OutputMsg("1号玩家胜利!", SUCC_COLOR);
return 1;
}
OutputMsg("2号玩家计算中 ...");
player2::Play(X, Y); /// 二号玩家开始探测
if(CheckExplore(1, X, Y)) {
Discovered[1][X][Y] = true;
DisplayChessBoard(1, 2, 0); /// 更新 1 号玩家的棋盘状态
}else {
OutputMsg("2号玩家探测非法!", WARN_COLOR);
return 1;
}
if(CheckFail(1)) { /// 判断一号玩家是否已经失败
OutputMsg("2号玩家胜利!", SUCC_COLOR);
return 2;
}
}
}
int main() {
OutputMsg("--- 信息公告栏 ---");
srand(time(NULL));
/*
SetPlane(1, 1, 1, 0);
SetPlane(1, 4, 4, 1);
SetPlane(1, 1, 5, 2);
memset(Discovered[1], 1, sizeof(Discovered[1]));
*/
wnd::gotoxy(0, 0);
printf("player1 player2");
DisplayChessBoard(1, 2, 0);
DisplayChessBoard(2, 2, 12);
/// 各自初始化
OutputMsg("1号玩家初始化中 ...");
player1::Init();
OutputMsg("2号玩家初始化中 ...");
player2::Init();
int wincnt[3] = {}; /// 统计胜利次数
for(int i = 1; i <= 100; i ++) {
char tmp[32] = {};
sprintf(tmp, "游戏已进行 %3d 轮 ...", i);
OutputState(tmp, SUCC_COLOR);
int winner = Game();
wincnt[winner] ++;
}
OutputMsg("游戏结束!", TIPS_COLOR);
char Score[30] = {};
sprintf(Score, "玩家1: %3d, 玩家2: %3d", wincnt[1], wincnt[2]);
OutputMsg(Score, SUCC_COLOR);
while(1);
return 0;
}
玩家算法程序中应用到的算法
我目前主要实现了这两种算法:“hjqの二分法”和“随机算法”。(注:hjq是我校信竞巨佬,这个算法虽然很简单,但是当我们最开始玩这个游戏的时候,他是最先提出这种方法的,为了纪念他,我们姑且这样称呼。)
随机算法的实现
严谨地说,我觉得这都不能称作一个算法,它的原理就是,每次从对方棋盘上所有没被探索过的位置中,随机挑一个进行探索,给出程序实现:
/// stdplayer2.cpp
/// 定义 Check 函数的返回值
#define OutBoard (-2) /// 查询了棋盘外的位置 或者玩家编号不合法
#define Undiscovered (-1)
#define EmptyLand ( 0)
#define PlaneBody (+1)
#define PlaneHead (+2)
int Check(int player, int x, int y);
#include <cmath>
#include <cstring>
#include <algorithm>
int RND(int L, int R); /// 生成随机数的函数(可以随意调用) 最大范围 [0, 2^30 - 1)
namespace player2 {
/// 如果此处的 namespace 的名字使用了 player1, player2 外的其他名字
/// 可以用 #define XXX player1 的方式实现替换
void Init() { /// 初始化, 里面可以什么都不写
}
int MyNumber;
void Begin(int playernum, int* X, int* Y, int* D) { /// 开局, 有一个参数 表示你自己的玩家编号
MyNumber = playernum; /// 记录一下自己的玩家编号是很有必要的
/// X[1], Y[1], D[1] 描述的是第一个飞机的参数, 其他飞机以此类推
/// 注意合法的坐标范围 (1~9, 1~9)
/// 放置三个飞机
X[1] = 1; Y[1] = 1; D[1] = 0;
X[2] = 4; Y[2] = 4; D[2] = 1;
X[3] = 1; Y[3] = 5; D[3] = 2;
/// 如果放置的飞机不合法会被视为游戏失败
}
void Play(int& X, int& Y) { /// 进行一轮游戏, 选择探索一个位置
/// 并将这个位置的坐标 存入X, Y
/// 这个位置必须是未知且合法的否则游戏失败
int x = RND(1, 9), y = RND(1, 9);
while(Check(3-MyNumber, x, y) != -1) { /// 随机选一个位置直到合法
x = RND(1, 9);
y = RND(1, 9);
}
X = x; Y = y;
}
}
hjqの二分法
hjq的算法思想很经典,就是通过二分的思想逐渐确定对方棋盘布局情况。我们记 A A A为当前对方棋盘有可能出现的所有排布方式的集合,随着已知信息的逐渐增多,集合 A A A中的元素个数会逐渐减少。那么我们该采取什么样的探索策略才能使集合 A A A元素个数减少的速度尽可能快(且稳定)呢?
我们采用二分思想,对于每一个位置(x,y),我们去统计这样一个数据:在 A A A中的所有元素中,(x,y)为空地的概率是多少,记为 P ( x , y ) P(x,y) P(x,y)。然后我们选择 P ( x , y ) P(x,y) P(x,y)最接近0.5的那个位置进行探索。这样,无论探索的结果为空地还是不为空地,我们都能将集合 A A A的元素个数减少大约一半,由于初始时 C a r d ( A ) = 8744 Card(A)=8744 Card(A)=8744,所以说我们推测,大概只需要十几次探索就能确定对方的飞机放置方式。确定了对方飞机放置方式之后,可能还存在未被探索的飞机头,逐一探索即可,给出程序实现:
/// hjqplayer1.cpp
//#include "winshow.cpp" /// 用于输出到屏幕
#include <cstring>
#include <cmath>
int RND(int L, int R);
int Check(int player, int x, int y);
namespace player1 { /// HJQ 大佬的二分算法
const int PlaneShape[5][5] = { /// 飞机的形态
{0, 0, 2, 0, 0},
{1, 1, 1, 1, 1},
{0, 0, 1, 0, 0},
{0, 1, 1, 1, 0},
{0, 0, 0, 0, 0}
};
/// 定义 Check 函数的返回值
#define OutBoard (-2) /// 查询了棋盘外的位置 或者玩家编号不合法
#define Undiscovered (-1)
#define EmptyLand ( 0)
#define PlaneBody (+1)
#define PlaneHead (+2)
#define Rin(A, B, C) (((A)<=(B))&&((B)<=(C))) /// 判断 A <= B <= C 是否成立
int ActualBoard[3][10][10]; /// 棋盘
bool Discovered [3][10][10]; /// 判断某个位置是否被查看过
int Plane(int dir, int x, int y) { /// 得到旋转后的飞机模型
switch(dir) {
case 0: return PlaneShape[x][y];
case 1: return PlaneShape[y][x];
case 2: if(3-x >= 0) return PlaneShape[3-x][y]; else return 0;
case 3: if(3-y >= 0) return PlaneShape[3-y][x]; else return 0;
}
}
bool CheckSetPlane(int player, int x, int y, int dir) { /// 检测某个未知是否可以放置飞机
dir %= 4; /// dir = 0, 1, 2, 3 分别表示将标准飞机模型逆时针旋转 0du 90du 180du 270du
/// x, y 表示飞机所在的 4*5 区域中, 横纵坐标取值的最小值(此处为空地)
for(int i = 0; i <= 4; i ++) {
for(int j = 0; j <= 4; j ++) {
int xn = x+i, yn = y+j; /// 当前点的坐标
if(Plane(dir, i, j) > 0) {
if(!Rin(1, xn, 9) || !Rin(1, yn, 9))
return false; /// 在棋盘外
if(ActualBoard[player][xn][yn] != 0)
return false; /// 与其他飞机冲突
}
}
}
return true; /// 可以放置飞机
}
void SetPlane(int player, int x, int y, int dir) { /// 放置飞机
/// 放之前一定要记得检测是否可以放置
dir %= 4;
for(int i = 0; i <= 4; i ++) {
for(int j = 0; j <= 4; j ++) {
int xn = x+i, yn = y+j; /// 当前点的坐标
if(Plane(dir, i, j) > 0) {
ActualBoard[player][xn][yn] = Plane(dir, i, j);
}
}
}
}
void UnSetPlane(int player, int x, int y, int dir) { /// 放置飞机
/// 放之前一定要记得检测是否可以放置
dir %= 4;
for(int i = 0; i <= 4; i ++) {
for(int j = 0; j <= 4; j ++) {
int xn = x+i, yn = y+j; /// 当前点的坐标
if(Plane(dir, i, j) > 0) {
ActualBoard[player][xn][yn] = 0;
}
}
}
}
/*
int MsgCnt = 0;
void OutputMsg(const char* str, int col = clr::DEFAULT) {
wnd::gotoxy(MsgCnt%20+1, 47);
printf(" ");
wnd::gotoxy(MsgCnt%20, 47);
printf(" ");
wnd::gotoxy(MsgCnt%20, 47);
wnd::color(col);
printf("%3d: %s", MsgCnt+1, str);
wnd::color(clr::DEFAULT);
MsgCnt ++;
}*/
bool CheckExplore(int player, int x, int y) { /// 检测探测是否合法
/// player 表示被探测的玩家
if(Rin(1, x, 9) && Rin(1, y, 9)) {
if(Discovered[player][x][y]) {
//OutputMsg("探测到已探测区域");
return false; /// 探测到已经探测过的区域
}else return true; /// 合法
}else {
return false; /// 探测到游戏区域之外, 不合法
}
}
//#include <ctime>
int Methods[8848][10][10]; /// 用于记录全部的合法方案
int VectorStore[8848][4][4];
/// 储存生成某种合法方案所需要的信息
/// VectorStore[i][j][1/2/3] 表示第i种方案中 第j架飞机 的 X,Y,D属性
int Rest = 3; /// 剩余飞机数量
int X[4], Y[4], D[4], Cnt = 0; /// Cnt 表示合法的方案数
/// X, Y, Z 记录 DFS 过程中, 飞机存放的位置
void DFS(int x, int y) {
if(Rest == 0) {
Cnt ++;
//DisplayChessBoard(1, 0, 0);
//printf("\n %6d", Cnt);
//printf("\n");
//system("pause");
memcpy(Methods[Cnt], ActualBoard[1], sizeof(ActualBoard[1]));
for(int i = 1; i <= 3; i ++) { /// 储存三架飞机的坐标
VectorStore[Cnt][i][1] = X[i];
VectorStore[Cnt][i][2] = Y[i];
VectorStore[Cnt][i][3] = D[i];
}
return;
}
if(x == 10) return;
for(int i = 0; i <= 3; i ++) { /// 当前位置放飞机
if(CheckSetPlane(1, x, y, i)) {
SetPlane(1, x, y, i);
/// 储存当前方案
X[Rest] = x;
Y[Rest] = y;
D[Rest] = i;
Rest --;
if(y == 9) {
DFS(x+1, 1);
}else {
DFS(x, y+1);
}
Rest ++;
UnSetPlane(1, x, y, i);
}
}
/// 当前位置不放飞机
if(y == 9) {
DFS(x+1, 1);
}else {
DFS(x, y+1);
}
}
bool Unavailable[8848]; /// 判断某种方案 当前是否还可能成为答案
void Init() {
DFS(1, 1); /// 统计所有方案数
}
int MyNumber=0;
int LastX = 0, LastY = 0; /// 记录上一次查询的位置
void Begin(int playernum, int* Xn, int* Yn, int* Dn) {
MyNumber = playernum; /// 记录我的选手编号
LastX = 0; LastY = 0; /// 清除上次最后一次探测的位置 这很重要
memset(Unavailable, 0x00, sizeof(Unavailable)); /// 清空记录的可行性信息
int tmp = RND(1, 8744); /// 随机选择一种排布阵列的方案
for(int i = 1; i <= 3; i ++) {
Xn[i] = VectorStore[tmp][i][1];
Yn[i] = VectorStore[tmp][i][2];
Dn[i] = VectorStore[tmp][i][3];
}
}
void Play(int& Xn, int& Yn) {
int NowAvaiCnt = 0; /// 计算当前可能成为答案的方案数
int LastMethod = 0; /// 记录当前可行方案中的最后一种
/// 用于处理当前可行方案只剩下一种的情况
if(LastX!=0 && LastY!=0) { /// 如果不是第一次查询
int tmp = Check(3-MyNumber, LastX, LastY); /// 得到上一次查询的位置的值
for(int i = 1; i<= 8744; i ++) {
if(Methods[i][LastX][LastY] != tmp) {
Unavailable[i] = true;
}
if(!Unavailable[i]) { /// 可行方案
NowAvaiCnt ++;
LastMethod = i;
}
}
}else NowAvaiCnt = 8744; /// 第一次查询
if(NowAvaiCnt != 1) { /// 方案未锁定
int AppearCnt[10][10] = {}; /// 记录每个位置不为空的概率
/// 我们寻找概率最接近 50% 点开
for(int i = 1; i <= 8744; i ++) {
if(!Unavailable[i]) {
for(int x = 1; x <= 9; x ++) {
for(int y = 1; y <= 9; y ++) {
AppearCnt[x][y] += (Methods[i][x][y] != 0);
}
}
}
}
double RateNow = 0;
int Xnow = 0, Ynow = 0; /// 当前寻找到的最优查询位置
for(int x = 1; x <= 9; x ++) {
for(int y = 1; y <= 9; y ++) {
if(Check(3-MyNumber, x, y) == -1) { /// 这个位置还未被查询过
double Rate = (double)AppearCnt[x][y]/NowAvaiCnt;
if(fabs(Rate-0.5) < fabs(RateNow-0.5)) {
RateNow = Rate;
Xnow = x;
Ynow = y; /// 寻找概率最接近 50% 的位置
}
}
}
}
Xn = LastX = Xnow;
Yn = LastY = Ynow;
}else { /// 方案已锁定
for(int x = 1; x <= 9; x ++) {
for(int y = 1; y <= 9; y ++) {
if(Check(3-MyNumber, x, y)==-1 && Methods[LastMethod][x][y]==2) {
Xn = LastX = x;
Yn = LastY = y;
goto outside;
}
}
}
outside:; /// 找到一个未被发现的飞机头就跳出循环
}
}
}
其它算法
如果有同学还想到了其他更好的算法,(卡常算法也可以,我指的不是时间上的卡常,而是探索次数上的卡常),Don’t hesitate to contact me.
后记
今天好像是高考…想到明年的这个时候我可能就在考场上了,心里说不出是什么滋味。衷心祝愿高三学长们都能取得令自己满意的成绩,也衷心祝愿HJQ大佬AKNOI2019。