冲子,一款简单的棋牌游戏

一、玩法

冲子,小时候我们乡下玩的最多的小游戏之一,只需要在土地上画个简单的棋盘,然后捡些个小石子、小木棒什么的就行了,就像五子棋一样,规则也很简单。
棋盘主要分为以下两种:

  • 4 * 4图,就是4格*4格的棋盘
  • 3 * 3图,就是3格*3格的棋盘

下法分为以下两种:

  • 带初始固定棋子,就是3*3图双方各有一排棋子,4*4图双方各有两排棋子,每当吃掉对方一个棋子,就将该位置的换成自己的棋子,谁的子先被吃完,谁输。
  • 不带初始固定棋子,就是空棋盘,一对一个的下子,当棋盘下满以后,互相去掉对方的一个棋子,然后就一步一步的走棋,吃掉一个算一个,不再落子,谁先被吃完,谁输。

走法呢,只能是一步一步的走。
这些说起来麻烦,走起来以后就会感觉规则很简单。

二、规则

无规矩不成方圆,吃子的方式如下图:

  • 1、二冲一,如图(1)

如果红方落子后出现红红蓝的排列,就会吃掉蓝方棋子。
假如棋盘上有了红1蓝1两个棋子,这时候红方下了一个棋子在红2处,就会吃掉蓝1

  • 2、二冲二,如图(2)

如果红方落子后出现红红蓝蓝的排列,就会吃掉蓝方的两个棋子。
假如棋盘上有了红1蓝1蓝2三个棋子,这时候红方下了一个棋子在红2处,就会吃掉蓝1蓝2

  • 3、一冲三,如图(3)

如果红方落子后出现红蓝蓝蓝的排列,就会吃掉蓝方的三个棋子。
假如棋盘上有了蓝1蓝2蓝3三个棋子,这时候红方下了一个棋子在红1处,就会吃掉蓝1蓝2蓝3

  • 4、带尾不冲

在可吃掉棋子的排列的前面或者后面有任意一方的棋子,本次吃子都无效。
如图,绿色代表红蓝任意一方的棋子,虽然有红红蓝的吃子的排列,但是只需要绿1绿2存在任意一个就无法吃子。

三、实现

1、定义角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// 游戏玩家
///
/// - blue: 蓝方
/// - red: 红方
enum FightUser:Int {
case blue = 1
case red = 2
/// 身份翻转,用于一些角色计算的切换等
///
/// - Returns: 翻转后的角色身份
func revers() -> FightUser {
return self == .blue ? .red : .blue
}
}

2、定义棋盘

5 * 5的棋盘,CoreGraphics的一些简单的API即能轻松绘制。

棋盘上棋子的坐标用一个类似于iOS屏幕坐标象限来算:

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
/// 棋子在棋盘上的坐标,坐标值都为整数
/// 坐标系为类iOS视图坐标系
///
/// 0 -----------→ x
/// | - - -
/// | - - -
/// | - - -
/// y ↓
struct ChessLocation {
/// x坐标
var x:Int
/// y坐标
var y:Int
/// 初始化方法
///
/// - Parameters:
/// - x: x坐标
/// - y: y坐标
init(x:Int, y:Int) {
self.x = x
self.y = y
}
func isEqual(to location:ChessLocation) -> Bool {
return location.x == self.x && location.y == self.y
}
}

当玩家在棋盘上下棋以后,各个位置的状态可以用一个二维数组来表示:

1
var chessBoard:[[Int]] = []

没有玩家的位置都设置成0,有玩家的地方,设置成对应的数值。

3、下棋,排列检测

每当玩家下完棋以后都需要判断一下是否可以吃掉对方的棋子,从规则里,我们可以知道:

  1. 只有三种可以吃掉对方棋子的排列,
  2. 每种排列都是连续的
  3. 每种排列最多有四个棋子,最少有3个棋子
    然后我们就可以对用户下完棋以后棋盘上的排列做一个筛选过滤了:
  4. 连续少于3个棋子的过滤掉
  5. 连续多余4个棋子的过滤掉

下面是各个方向的棋子连续坐标过滤:

1)、水平方向

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
/// 计算当前点所在的从左到右的连线,线上的点的位置是一个从小到大的关系
/// 在坐标系中的方程式是: y = b
/// 此处的坐标系是类iOS中视图位置的坐标系
///
/// 0 -----------→ x
/// | - - -
/// | o o o
/// | - - -
/// y ↓
///
/// - Parameter location: 要处理的点
private func horiLineWith(location:ChessLocation) {
var hori:[ChessLocation] = [location]
// 0...当前点
if location.x > 0 {
for i in 1...location.x {
if chessBoard[location.y][location.x - i] == 0 {
break
} else {
hori.insert(ChessLocation(x: location.x - i, y: location.y), at: 0)
}
}
}
if location.x < 4 {
for i in (location.x + 1)..<5 {
if chessBoard[location.y][i] == 0 {
break
} else {
hori.append(ChessLocation(x: i, y: location.y))
}
}
}
wipedOut(With: hori)
}

2)、竖直方向

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
/// 计算当前点所在的从上角到下的连线,线上的点的位置是一个从小到大的关系
/// 在坐标系中的方程式是: x = b
/// 此处的坐标系是类iOS中视图位置的坐标系
///
/// 0 -----------→ x
/// | - o -
/// | - o -
/// | - o -
/// y ↓
///
/// - Parameter location: 要处理的点
private func vertLineWith(location:ChessLocation) {
var vert:[ChessLocation] = [location]
if location.y > 0 {
for i in 1...location.y {
if chessBoard[location.y - i][location.x] == 0 {
break
} else {
vert.insert(ChessLocation(x: location.x, y: location.y - i), at: 0)
}
}
}
if location.y < 4 {
for i in (location.y + 1)..<5 {
if chessBoard[i][location.x] == 0 {
break
} else {
vert.append(ChessLocation(x: location.x, y: i))
}
}
}
wipedOut(With: vert)
}

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
37
38
39
40
41
42
/// 计算当前点所在的从左上角到右下角的连线,线上的点的位置是一个从小到大的关系
/// 在坐标系中的方程式是: y = x + b
/// 此处的坐标系是类iOS中视图位置的坐标系
///
/// 0 -----------→ x
/// | o - -
/// | - o -
/// | - - o
/// y ↓
///
/// - Parameter location: 要处理的点
private func ltrbWith(location:ChessLocation) {
var ltrb:[ChessLocation] = [location]
let b = location.y - location.x
if location.x > 0 {
for i in 1...location.x {
let y = location.y - i // b + (location.x - i)
if y >= 0 {
if chessBoard[y][location.x - i] == 0 {
break
} else {
ltrb.insert(ChessLocation(x: location.x - i, y: y), at: 0)
}
}
}
}
if location.x < 4 {
for i in (location.x + 1)..<5 {
let y = i + b
if y < 5 {
if chessBoard[y][i] == 0 {
break
} else {
ltrb.append(ChessLocation(x: i, y: y))
}
}
}
}
wipedOut(With: ltrb)
}

4)、从右上到左下

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
39
40
41
42
/// 计算当前点所在的从左下角到右上角的连线,线上的点的位置是一个从大到小的关系
/// 在坐标系中的方程式是: y = -x + b
/// 此处的坐标系是类iOS中视图位置的坐标系
///
/// 0 -----------→ x
/// | - - o
/// | - o -
/// | o - -
/// y ↓
///
/// - Parameter location: 要处理的点
private func lbrtWith(location:ChessLocation) {
var lbrt:[ChessLocation] = [location]
let b = location.x + location.y
if location.x > 0 {
for i in 1...location.x {
let y = location.y + i // b - (location.x - i)
if y >= 0 && y < 5 {
if chessBoard[y][location.x - i] == 0 {
break
} else {
lbrt.insert(ChessLocation(x: location.x - i, y: y), at: 0)
}
}
}
}
if location.x < 4 {
for i in (location.x + 1)..<5 {
let y = b - i
if y >= 0 && y < 5 {
if chessBoard[y][i] == 0 {
break
} else {
lbrt.append(ChessLocation(x: i, y: y))
}
}
}
}
wipedOut(With: lbrt)
}

4、吃子

在过滤完各个方向的连续棋子坐标后,我们先把他们转换成一种比较容易判断的标志字符串:

1
2
3
4
5
6
7
8
9
10
11
/// 将棋子连线转换成用于匹配的01标志字符串,比如`110`,这样就说明可以把`0`位置的棋子给冲掉
/// 0 - 不是自己的
/// 1 - 是自己的子
///
/// - Parameter line: 棋子连线上的各点
/// - Returns: 标志的字符串
func symbolTrans(With line:[ChessLocation]) -> String! {
return line.reduce(into: "") { (res, location) in
return res.append("\(self.chessBoard[location.y][location.x] == self.curUser.rawValue ? 1 : 0)")
}
}

然后在把二冲一二冲二一冲三三种情况编码为1101100011001110000001六种,依次判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// 根据棋子连线的标志来判断被冲掉的棋子
///
/// - Parameter line: 连线的棋子的坐标数组
private func wipedOut(With line:[ChessLocation]) {
if line.count == 3 || line.count == 4 {
if let lineSymbol = symbolTrans(With: line) {
if lineSymbol.elementsEqual("110") {
self.replace(At: [line.last!])
} else if lineSymbol.elementsEqual("1100") {
self.replace(At: [line[2], line[3]])
} else if lineSymbol.elementsEqual("1000") {
self.replace(At: [line[1], line[2], line[3]])
} else if lineSymbol.elementsEqual("011") {
self.replace(At: [line.first!])
} else if lineSymbol.elementsEqual("0011") {
self.replace(At: [line[0], line[1]])
} else if lineSymbol.elementsEqual("0001") {
self.replace(At: [line[0], line[1], line[2]])
} else {
print("\(lineSymbol) 不符合")
}
}
}
}

5、判断输赢

当一方棋子没有可走的情况或者一方棋子都被吃完的情况下算输。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
private func finished() -> Bool {
if chessBoard.contains(where: { xs -> Bool in
return xs.contains(where: { x -> Bool in
return x == curUser.revers().rawValue
})
}) == false {
// 不包含对手,说明下完了,产生了胜利的人
return true
}
for y in 0..<self.chessBoard.count {
for x in 0..<self.chessBoard[y].count {
if self.chessBoard[y][x] == curUser.revers().rawValue {
// 如果这个子是活的,就说明对手还没有被困死,这盘棋还没有结束
if self.isActive(location: ChessLocation(x: x, y: y)) {
return false
}
}
}
}
return true
}
/// 判断棋子是否是活的
private func isActive(location:ChessLocation) -> Bool {
func boxValidateWith(x: Int, y: Int) -> Bool {
// 不能超过棋盘的界限
if x < 0 || x > 4 || y < 0 || y > 4 {
return false
}
// 只有当这个点为0的时候才说明要判断的子是活的
return self.chessBoard[y][x] == 0
}
let lx = location.x - 1
let rx = location.x + 1
let ty = location.y - 1
let by = location.y + 1
if boxValidateWith(x: lx, y: location.y) {
return true
}
if boxValidateWith(x: rx, y: location.y) {
return true
}
if boxValidateWith(x: location.x, y: ty) {
return true
}
if boxValidateWith(x: location.x, y: by) {
return true
}
return false
}

四、最后效果