Tetris3
【施工中】复制自Tetris2,用于制定一版更好的规则文档。
如有需求,裁判程序源码可以参考Tetris2Judge。
目录
作者
裁判程序 zys 播放器 zhouhy
新版规则 farter
人类玩家操作说明
两种操作方式任选其一即可。
- 鼠标
- 点击对方场地上的图标可以给对手指定方块
- 拖动块可以移动位置,也可以穿墙(仅限可以直接从顶部落到的地方)
- 左键点击块可以使之坠落到底
- 右键点击屏幕可以提交操作(此时方块必须落地)
- 键盘
- 数字1~7可以给对手指定方块
- 上下左右键可以移动位置
- 英文/键可以将方块坠落到底
- 回车键可以提交操作(此时方块必须落地)
- 英文.键可以枚举方块的下一个可能的初始状态(用于在场地顶部转不过来之类的情况)
游戏规则
本游戏为双人回合制游戏,每个玩家在独立的矩阵场地上进行游戏。
每回合,双方玩家同时决策,既需要控制自己的方块落地,又需要为对手指定下一回合的方块。
玩家消去行后,方块会(有条件地)转移到对方场地底部(即发动攻击),从而给对方增加难度。
地图
每个玩家的场地是高20格、宽10格的矩阵。在程序中,坐标是(x, y)的形式,其中x是横坐标,y是纵坐标。最左下方的格子的坐标是(1, 1)。
场地的正上方视作左右两道墙向上无限延伸的空间。不过只要有方块落地锁定瞬间,有方格所在高度超过20格(即场外),就判定游戏结束。游戏结束判定优先于消行处理。
初始场地有可能有预置地形,可能双方一致可能不一致,公平起见应当一致,但为了欢乐也可能不一致。
方块
游戏中,方块共有七种类型,每种类型的方块有四种姿态(即方向)。
方块的旋转系统(也就是七种方块四个姿态各是什么偏移量,出现时的高度、位置、姿态),不仅不统一,还可以很复杂。在一些现代俄罗斯方块中,为了能够充分发挥软降(向下移动)后移动旋转的作用,还会添加“踢墙”等特性使得放块能够通过旋转钻入一些常规操作进不了的洞,导致规则更加复杂。
为了达到上述灵活的目的,实现方便且判定方便,但又不涉及一个具体的旋转系统、踢墙系统(不失一般性(?)),本题拟定方块的放置规则如下:
- 对于某个块的某个旋转姿态,以其包围盒的左下角块坐标为这个状态的坐标。(即在一个空场地中,所有方块姿态能达到的最左下位置,其坐标都是(1, 1))
- 方块的初始位置在一个足够高的地方,可以自由旋转移动而不会碰到任何已固定的方块。
- 如果可以构造一条路径(本块的某两个状态(点)是一条无向“边”的条件是:这两个状态的四格位置同占至少一格)连接目标位置和一个与块池上方(第21行及以上)的空间有交集的状态,则目标位置可达。
- 是否允许180°转?我觉得可以有,毕竟已经可以同状态瞬移两格【x
- 包含向上,现有的踢墙系统确实是可以向上踢的,甚至可以形成一些上下转圈的回路。不过本规则只管可以连通性。
- 实际上SRS的 T/S/Z/J/L-spin Triple 已经无了
- 更松版本:只要按单格空气连通就可达
- 更紧版本:路径分为旋转和移动,旋转前后两状态可以装入一个该块包围盒长边正方形的包围盒(O-spin无了,部分SRS的常用spin也无了)
- 再紧版本:软降后只能平移不能旋转
- 当然,一个可达位置要可以放置四连块,还需要这个四连块着地(向下不可移动)。
给对手指定方块
第一回合,双方的方块类型由玄学系统指定,而且是相同的类型。
除了第一回合之外,每个回合的方块类型都是由对方指定的。因此,每回合玩家都要给出对方下一个方块的类型。
指定方块时,必须保证对方的各类方块的数目的极差(即最大值减最小值)不超过2,否则将会被视为非法行为。
描述自己的方块落地点
每回合,玩家可以得知自己本回合控制的方块类型。
【常见问题】
我需要给出什么?
- 你需要给出本块放置的坐标(包围盒左下角)与旋转姿态,系统校验合法性(是否空且着地,是否存在合法路径)。
最终状态有什么要求?
- 满足上文方块放置规则,且方块落地(不能下移)
这是不是意味着我的方块只能垂直落到场地里?
- 这个规则下,能达到的位置比这多多了~
方块的消除与转移
每回合方块落地后,如果有行已满,那么这些行将会被“消除”,其它的行下落补上空行。
被消除的行并不会消失,而是会去除最后一个落地的方块(即本回合刚刚造成这几行消除的方块),然后按照同样的顺序堆叠起来,放置在对方的场地底部。对方场地原有的方块就会被“顶起来”。
可能的变体规则:为了让对局更有趣,在第N(如100)块及以前,一次消除≥2行才会把这几行都送给对方,N块后消除一行也送。
胜负
一切非法行为会被立即判负,包括程序崩溃、超时、坐标越界、格式错误、找不到到落点的路径等。双方同时犯规判负会被认定为平局。
在互相给出方块后,如果场地中无法容纳该方块,则会被判负。(改为还是等对手做决策?)
在发生方块转移后,如果场地的最高方块高度超过边界(即大于20),则会被判负。
如果双方同时放置一个过顶的块,或者同时因为转移而有方块过顶:
此时总计转移行数多者胜。
如果双方转移出的行数相同:
此时消行数更少的一方胜。
如果消行数也相同,那么游戏平局。
游戏交互方式
与Botzone上其他游戏一样,本游戏每步(每次程序运行)限时1秒。
如果希望在回合间传递数据,请参阅Bot#交互。
本游戏仅提供简单交互的样例程序和介绍。
提示
如果你不能理解以下交互方式,可以直接看#样例程序,按照说明填写代码,并修改其中
// 做出决策(你只需修改以下部分)
到
// 决策结束,输出结果(你只需修改以上部分)
之间的部分即可!
本游戏与Botzone上其他游戏一样,使用相同的交互方式:Bot#交互
调试时可以通过拖动进度条来快速调试。
简单交互
- request
- 第一回合的request是一行两个数字t和c,空格分隔,t表示自己拿到的方块类型,n表示自己的颜色(0红1蓝)
- 以后的request是一行四个数字t、x、y、o,空格分隔,t表示自己拿到的方块类型,x和y表示对方方块落在的位置坐标,o表示对方方块的姿态
- response
- 每个回合的response都是一行四个数字t、x、y、o,空格分隔,t表示给对方下回合的方块类型,x和y表示自己方块最终点的位置坐标,o表示最终点方块的姿态
定义了request和response后,玩家的输入总体格式可以参看Bot#简化交互。
对于样例对局,第四回合红色玩家的样例输入和样例输出如下:
输入
4 1 0 3 2 1 2 1 2 1 2 5 5 1 2 2 1 4 1 2 8 1 2 2 4 2 1
输出
2 3 2 2
样例程序
请注意展开按钮在右侧!-----=====≡≡≡≡≡>
简单交互样例程序
20171031更新:将elimBonus[count]改为elimBonus[count - hasBonus]。 20171027更新:将trans数组的第二维长度从4加大到6,感谢kczno1用户的指正。 【Tetris 3】待更新
/**
* Tetris2 样例程序
* 20171031更新:将elimBonus[count]改为elimBonus[count - hasBonus]。
* 20171027更新:将trans数组的第二维长度从4加大到6,感谢kczno1用户的指正。
* https://wiki.botzone.org/index.php?title=Tetris2
*/
// 注意:x的范围是1~MAPWIDTH,y的范围是1~MAPHEIGHT
// 数组是先行(y)后列(c)
// 坐标系:原点在左下角
#include <iostream>
#include <string>
#include <cmath>
#include <algorithm>
#include <cstdlib>
#include <ctime>
using namespace std;
#define MAPWIDTH 10
#define MAPHEIGHT 20
// 我所在队伍的颜色(0为红,1为蓝,仅表示队伍,不分先后)
int currBotColor;
int enemyColor;
// 先y后x,记录地图状态,0为空,1为以前放置,2为刚刚放置,负数为越界
// (2用于在清行后将最后一步撤销再送给对方)
int gridInfo[2][MAPHEIGHT + 2][MAPWIDTH + 2] = { 0 };
// 代表分别向对方转移的行
int trans[2][6][MAPWIDTH + 2] = { 0 };
// 转移行数
int transCount[2] = { 0 };
// 运行eliminate后的当前高度
int maxHeight[2] = { 0 };
// 总消去行数的分数之和
int elimTotal[2] = { 0 };
// 连续几回合发生过消去了
int elimCombo[2] = { 0 };
// 一次性消去行数对应分数
const int elimBonus[] = { 0, 1, 3, 5, 7 };
// 给对应玩家的各类块的数目总计
int typeCountForColor[2][7] = { 0 };
const int blockShape[7][4][8] = {
{ { 0,0,1,0,-1,0,-1,-1 },{ 0,0,0,1,0,-1,1,-1 },{ 0,0,-1,0,1,0,1,1 },{ 0,0,0,-1,0,1,-1,1 } },
{ { 0,0,-1,0,1,0,1,-1 },{ 0,0,0,-1,0,1,1,1 },{ 0,0,1,0,-1,0,-1,1 },{ 0,0,0,1,0,-1,-1,-1 } },
{ { 0,0,1,0,0,-1,-1,-1 },{ 0,0,0,1,1,0,1,-1 },{ 0,0,-1,0,0,1,1,1 },{ 0,0,0,-1,-1,0,-1,1 } },
{ { 0,0,-1,0,0,-1,1,-1 },{ 0,0,0,-1,1,0,1,1 },{ 0,0,1,0,0,1,-1,1 },{ 0,0,0,1,-1,0,-1,-1 } },
{ { 0,0,-1,0,0,1,1,0 },{ 0,0,0,-1,-1,0,0,1 },{ 0,0,1,0,0,-1,-1,0 },{ 0,0,0,1,1,0,0,-1 } },
{ { 0,0,0,-1,0,1,0,2 },{ 0,0,1,0,-1,0,-2,0 },{ 0,0,0,1,0,-1,0,-2 },{ 0,0,-1,0,1,0,2,0 } },
{ { 0,0,0,1,-1,0,-1,1 },{ 0,0,-1,0,0,-1,-1,-1 },{ 0,0,0,-1,1,-0,1,-1 },{ 0,0,1,0,0,1,1,1 } }
}; // 7种形状(长L| 短L| 反z| 正z| T| 直一| 田格),4种朝向(上左下右),8:每相邻的两个分别为x,y
const int rotateBlank[7][4][10] = {
{ { 1,1,0,0 },{ -1,1,0,0 },{ -1,-1,0,0 },{ 1,-1,0,0 } },
{ { -1,-1,0,0 },{ 1,-1,0,0 },{ 1,1,0,0 },{ -1,1,0,0 } },
{ { 1,1,0,0 },{ -1,1,0,0 },{ -1,-1,0,0 },{ 1,-1,0,0 } },
{ { -1,-1,0,0 },{ 1,-1,0,0 },{ 1,1,0,0 },{ -1,1,0,0 } },
{ { -1,-1,-1,1,1,1,0,0 },{ -1,-1,-1,1,1,-1,0,0 },{ -1,-1,1,1,1,-1,0,0 },{ -1,1,1,1,1,-1,0,0 } },
{ { 1,-1,-1,1,-2,1,-1,2,-2,2 } ,{ 1,1,-1,-1,-2,-1,-1,-2,-2,-2 } ,{ -1,1,1,-1,2,-1,1,-2,2,-2 } ,{ -1,-1,1,1,2,1,1,2,2,2 } },
{ { 0,0 },{ 0,0 } ,{ 0,0 } ,{ 0,0 } }
}; // 旋转的时候需要为空的块相对于旋转中心的坐标
class Tetris
{
public:
const int blockType; // 标记方块类型的序号 0~6
int blockX; // 旋转中心的x轴坐标
int blockY; // 旋转中心的y轴坐标
int orientation; // 标记方块的朝向 0~3
const int(*shape)[8]; // 当前类型方块的形状定义
int color;
Tetris(int t, int color) : blockType(t), shape(blockShape[t]), color(color)
{ }
inline Tetris &set(int x = -1, int y = -1, int o = -1)
{
blockX = x == -1 ? blockX : x;
blockY = y == -1 ? blockY : y;
orientation = o == -1 ? orientation : o;
return *this;
}
// 判断当前位置是否合法
inline bool isValid(int x = -1, int y = -1, int o = -1)
{
x = x == -1 ? blockX : x;
y = y == -1 ? blockY : y;
o = o == -1 ? orientation : o;
if (o < 0 || o > 3)
return false;
int i, tmpX, tmpY;
for (i = 0; i < 4; i++)
{
tmpX = x + shape[o][2 * i];
tmpY = y + shape[o][2 * i + 1];
if (tmpX < 1 || tmpX > MAPWIDTH ||
tmpY < 1 || tmpY > MAPHEIGHT ||
gridInfo[color][tmpY][tmpX] != 0)
return false;
}
return true;
}
// 判断是否落地
inline bool onGround()
{
if (isValid() && !isValid(-1, blockY - 1))
return true;
return false;
}
// 将方块放置在场地上
inline bool place()
{
if (!onGround())
return false;
int i, tmpX, tmpY;
for (i = 0; i < 4; i++)
{
tmpX = blockX + shape[orientation][2 * i];
tmpY = blockY + shape[orientation][2 * i + 1];
gridInfo[color][tmpY][tmpX] = 2;
}
return true;
}
// 检查能否逆时针旋转自己到o
inline bool rotation(int o)
{
if (o < 0 || o > 3)
return false;
if (orientation == o)
return true;
int fromO = orientation;
int i, blankX, blankY;
while (true)
{
if (!isValid(-1, -1, fromO))
return false;
if (fromO == o)
break;
// 检查旋转碰撞
for (i = 0; i < 5; i++) {
blankX = blockX + rotateBlank[blockType][fromO][2 * i];
blankY = blockY + rotateBlank[blockType][fromO][2 * i + 1];
if (blankX == blockX && blankY == blockY)
break;
if (gridInfo[color][blankY][blankX] != 0)
return false;
}
fromO = (fromO + 1) % 4;
}
return true;
}
};
// 围一圈护城河
void init()
{
int i;
for (i = 0; i < MAPHEIGHT + 2; i++)
{
gridInfo[1][i][0] = gridInfo[1][i][MAPWIDTH + 1] = -2;
gridInfo[0][i][0] = gridInfo[0][i][MAPWIDTH + 1] = -2;
}
for (i = 0; i < MAPWIDTH + 2; i++)
{
gridInfo[1][0][i] = gridInfo[1][MAPHEIGHT + 1][i] = -2;
gridInfo[0][0][i] = gridInfo[0][MAPHEIGHT + 1][i] = -2;
}
}
namespace Util
{
// 检查能否从场地顶端直接落到当前位置
inline bool checkDirectDropTo(int color, int blockType, int x, int y, int o)
{
auto &def = blockShape[blockType][o];
for (; y <= MAPHEIGHT; y++)
for (int i = 0; i < 4; i++)
{
int _x = def[i * 2] + x, _y = def[i * 2 + 1] + y;
if (_y > MAPHEIGHT)
continue;
if (_y < 1 || _x < 1 || _x > MAPWIDTH || gridInfo[color][_y][_x])
return false;
}
return true;
}
// 消去行
void eliminate(int color)
{
int &count = transCount[color] = 0;
int i, j, emptyFlag, fullFlag, firstFull = 1, hasBonus = 0;
maxHeight[color] = MAPHEIGHT;
for (i = 1; i <= MAPHEIGHT; i++)
{
emptyFlag = 1;
fullFlag = 1;
for (j = 1; j <= MAPWIDTH; j++)
{
if (gridInfo[color][i][j] == 0)
fullFlag = 0;
else
emptyFlag = 0;
}
if (fullFlag)
{
if (firstFull && ++elimCombo[color] >= 3)
{
// 奖励行
for (j = 1; j <= MAPWIDTH; j++)
trans[color][count][j] = gridInfo[color][i][j] == 1 ? 1 : 0;
count++;
hasBonus = 1;
}
firstFull = 0;
for (j = 1; j <= MAPWIDTH; j++)
{
// 注意这里只转移以前的块,不包括最后一次落下的块(“撤销最后一步”)
trans[color][count][j] = gridInfo[color][i][j] == 1 ? 1 : 0;
gridInfo[color][i][j] = 0;
}
count++;
}
else if (emptyFlag)
{
maxHeight[color] = i - 1;
break;
}
else
for (j = 1; j <= MAPWIDTH; j++)
{
gridInfo[color][i - count + hasBonus][j] =
gridInfo[color][i][j] > 0 ? 1 : gridInfo[color][i][j];
if (count)
gridInfo[color][i][j] = 0;
}
}
if (count == 0)
elimCombo[color] = 0;
maxHeight[color] -= count - hasBonus;
elimTotal[color] += elimBonus[count - hasBonus];
}
// 转移双方消去的行,返回-1表示继续,否则返回输者
int transfer()
{
int color1 = 0, color2 = 1;
if (transCount[color1] == 0 && transCount[color2] == 0)
return -1;
if (transCount[color1] == 0 || transCount[color2] == 0)
{
if (transCount[color1] == 0 && transCount[color2] > 0)
swap(color1, color2);
int h2;
maxHeight[color2] = h2 = maxHeight[color2] + transCount[color1];
if (h2 > MAPHEIGHT)
return color2;
int i, j;
for (i = h2; i > transCount[color1]; i--)
for (j = 1; j <= MAPWIDTH; j++)
gridInfo[color2][i][j] = gridInfo[color2][i - transCount[color1]][j];
for (i = transCount[color1]; i > 0; i--)
for (j = 1; j <= MAPWIDTH; j++)
gridInfo[color2][i][j] = trans[color1][i - 1][j];
return -1;
}
else
{
int h1, h2;
maxHeight[color1] = h1 = maxHeight[color1] + transCount[color2];//从color1处移动count1去color2
maxHeight[color2] = h2 = maxHeight[color2] + transCount[color1];
if (h1 > MAPHEIGHT) return color1;
if (h2 > MAPHEIGHT) return color2;
int i, j;
for (i = h2; i > transCount[color1]; i--)
for (j = 1; j <= MAPWIDTH; j++)
gridInfo[color2][i][j] = gridInfo[color2][i - transCount[color1]][j];
for (i = transCount[color1]; i > 0; i--)
for (j = 1; j <= MAPWIDTH; j++)
gridInfo[color2][i][j] = trans[color1][i - 1][j];
for (i = h1; i > transCount[color2]; i--)
for (j = 1; j <= MAPWIDTH; j++)
gridInfo[color1][i][j] = gridInfo[color1][i - transCount[color2]][j];
for (i = transCount[color2]; i > 0; i--)
for (j = 1; j <= MAPWIDTH; j++)
gridInfo[color1][i][j] = trans[color2][i - 1][j];
return -1;
}
}
// 颜色方还能否继续游戏
inline bool canPut(int color, int blockType)
{
Tetris t(blockType, color);
for (int y = MAPHEIGHT; y >= 1; y--)
for (int x = 1; x <= MAPWIDTH; x++)
for (int o = 0; o < 4; o++)
{
t.set(x, y, o);
if (t.isValid() && checkDirectDropTo(color, blockType, x, y, o))
return true;
}
return false;
}
// 打印场地用于调试
inline void printField()
{
#ifndef _BOTZONE_ONLINE
static const char *i2s[] = {
"~~",
"~~",
" ",
"[]",
"##"
};
cout << "~~:墙,[]:块,##:新块" << endl;
for (int y = MAPHEIGHT + 1; y >= 0; y--)
{
for (int x = 0; x <= MAPWIDTH + 1; x++)
cout << i2s[gridInfo[0][y][x] + 2];
for (int x = 0; x <= MAPWIDTH + 1; x++)
cout << i2s[gridInfo[1][y][x] + 2];
cout << endl;
}
#endif
}
}
int main()
{
// 加速输入
istream::sync_with_stdio(false);
srand(time(NULL));
init();
int turnID, blockType;
int nextTypeForColor[2];
cin >> turnID;
// 先读入第一回合,得到自己的颜色
// 双方的第一块肯定是一样的
cin >> blockType >> currBotColor;
enemyColor = 1 - currBotColor;
nextTypeForColor[0] = blockType;
nextTypeForColor[1] = blockType;
typeCountForColor[0][blockType]++;
typeCountForColor[1][blockType]++;
// 然后分析以前每回合的输入输出,并恢复状态
// 循环中,color 表示当前这一行是 color 的行为
// 平台保证所有输入都是合法输入
for (int i = 1; i < turnID; i++)
{
int currTypeForColor[2] = { nextTypeForColor[0], nextTypeForColor[1] };
int x, y, o;
// 根据这些输入输出逐渐恢复状态到当前回合
// 先读自己的输出,也就是自己的行为
// 自己的输出是自己的最后一步
// 然后模拟最后一步放置块
cin >> blockType >> x >> y >> o;
// 我当时把上一块落到了 x y o!
Tetris myBlock(currTypeForColor[currBotColor], currBotColor);
myBlock.set(x, y, o).place();
// 我给对方什么块来着?
typeCountForColor[enemyColor][blockType]++;
nextTypeForColor[enemyColor] = blockType;
// 然后读自己的输入,也就是对方的行为
// 裁判给自己的输入是对方的最后一步
cin >> blockType >> x >> y >> o;
// 对方当时把上一块落到了 x y o!
Tetris enemyBlock(currTypeForColor[enemyColor], enemyColor);
enemyBlock.set(x, y, o).place();
// 对方给我什么块来着?
typeCountForColor[currBotColor][blockType]++;
nextTypeForColor[currBotColor] = blockType;
// 检查消去
Util::eliminate(0);
Util::eliminate(1);
// 进行转移
Util::transfer();
}
int blockForEnemy, finalX, finalY, finalO;
// 做出决策(你只需修改以下部分)
// 遇事不决先输出(平台上编译不会输出)
Util::printField();
// 贪心决策
// 从下往上以各种姿态找到第一个位置,要求能够直着落下
Tetris block(nextTypeForColor[currBotColor], currBotColor);
for (int y = 1; y <= MAPHEIGHT; y++)
for (int x = 1; x <= MAPWIDTH; x++)
for (int o = 0; o < 4; o++)
{
if (block.set(x, y, o).isValid() &&
Util::checkDirectDropTo(currBotColor, block.blockType, x, y, o))
{
finalX = x;
finalY = y;
finalO = o;
goto determined;
}
}
determined:
// 再看看给对方什么好
int maxCount = 0, minCount = 99;
for (int i = 0; i < 7; i++)
{
if (typeCountForColor[enemyColor][i] > maxCount)
maxCount = typeCountForColor[enemyColor][i];
if (typeCountForColor[enemyColor][i] < minCount)
minCount = typeCountForColor[enemyColor][i];
}
if (maxCount - minCount == 2)
{
// 危险,找一个不是最大的块给对方吧
for (blockForEnemy = 0; blockForEnemy < 7; blockForEnemy++)
if (typeCountForColor[enemyColor][blockForEnemy] != maxCount)
break;
}
else
{
blockForEnemy = rand() % 7;
}
// 决策结束,输出结果(你只需修改以上部分)
cout << blockForEnemy << " " << finalX << " " << finalY << " " << finalO;
return 0;
}