游戏介绍
华容道游戏共有10个棋子, 在5*4的棋盘上. 其中曹操占2*2格, 张飞,关羽, 赵云, 黄忠, 马超 占2*1格, 这些棋子有横向和竖向的区别. 4个卒各占1格. 目标: 将曹操这枚棋子移动到棋盘正下方. 棋子不能横跨棋子,每次只移动一格.
设计思路
因为没有图形界面, 于是将每个棋子和移动方向编号.
* 棋子编号: 0.曹操 1.张飞 2.关羽 3.赵云 4.黄忠 5.马超 6.兵 7.卒 8.勇 9.士
* 移动方向编号: 1.上 2.下 3.左 4.右
用户输入两个整数, 第一个选择棋子, 第二个选择方向.
如: 0, 1 将曹操向上移动一格.
将棋盘看作一个对象, 它有5*4的一个二维数组当作棋盘.
有10个棋子组成的map.
棋子结构体定义如下:
// 棋子
type Chessman struct {
Name string `desc:"棋子的名字"`
Code string `desc:"棋子在棋盘上的标志符"`
StartPoint [2]int `desc:"起点"`
GridNums int `desc:"格子数"`
Direction int `desc:"方向, 0 代表竖着, 1 代表横着"`
}
棋盘的定义如下:
// 华容道
type Klotski struct {
Array *[5][4]string `desc:"5*4的地图"`
Chessmen map[int]*Chessman `desc:"存放棋子的字典"`
}
因为棋盘的摆放位置多种多样, 本程序没有模拟这个随机算法.只选取其中一种摆放位置.
// 用于初始化华容道地图
// 初始地图有很多种,所以将这部分提取出来,以后可扩展
func initKlotski() *Klotski {
return &Klotski{
Array: &[5][4]string{},
Chessmen: map[int]*Chessman{
0: {Name: "曹操", Code: "曹", StartPoint: [2]int{0, 1}, GridNums: 4},
1: {Name: "张飞", Code: "张", StartPoint: [2]int{0, 0}, GridNums: 2, Direction: 0},
2: {Name: "关羽", Code: "关", StartPoint: [2]int{2, 1}, GridNums: 2, Direction: 1},
3: {Name: "赵云", Code: "赵", StartPoint: [2]int{0, 3}, GridNums: 2, Direction: 0},
4: {Name: "黄忠", Code: "黄", StartPoint: [2]int{2, 0}, GridNums: 2, Direction: 0},
5: {Name: "马超", Code: "马", StartPoint: [2]int{2, 3}, GridNums: 2, Direction: 0},
6: {Name: "兵", Code: "兵", StartPoint: [2]int{3, 1}, GridNums: 1},
7: {Name: "卒", Code: "卒", StartPoint: [2]int{3, 2}, GridNums: 1},
8: {Name: "勇", Code: "勇", StartPoint: [2]int{4, 0}, GridNums: 1},
9: {Name: "士", Code: "士", StartPoint: [2]int{4, 3}, GridNums: 1},
},
}
}
摆放完棋子后, 要初始化二维数组,一则方便呈现给用户看, 二则可以用来判断棋子的位置, 周围的情况.
const BLANK = " " // 空白
// 根据棋子的位置,初始化华容道对象中的数组
// 将棋子的code填入棋盘上对应的位置中
func (this *Klotski) initArray() {
arr := this.Array
// 0-9 棋子
for i := 0; i < 10; i++ {
chessman := this.Chessmen[i]
// 占据2个格子的棋子,根据棋子方向,填入code
if chessman.GridNums == 2 {
arr[chessman.StartPoint[0]][chessman.StartPoint[1]] = chessman.Code
if chessman.Direction == 0 {
// 总坐标 + 1
arr[chessman.StartPoint[0] + 1][chessman.StartPoint[1]] = chessman.Code
}else {
// 横坐标 + 1
arr[chessman.StartPoint[0]][chessman.StartPoint[1] + 1] = chessman.Code
}
}
// 占据1个格子的棋子,在起点位置填入code
if chessman.GridNums == 1 {
arr[chessman.StartPoint[0]][chessman.StartPoint[1]] = chessman.Code
}
// 占据4个格子的棋子,填入code
if chessman.GridNums == 4 {
arr[chessman.StartPoint[0]][chessman.StartPoint[1]] = chessman.Code
arr[chessman.StartPoint[0] + 1][chessman.StartPoint[1]] = chessman.Code
arr[chessman.StartPoint[0]][chessman.StartPoint[1] + 1] = chessman.Code
arr[chessman.StartPoint[0] + 1][chessman.StartPoint[1] + 1] = chessman.Code
}
}
// 填入空白
for i := 0; i < len(arr); i++ {
for j := 0; j < len(arr[0]); j++ {
if arr[i][j] == "" {
arr[i][j] = BLANK
}
}
}
}
常量BLANK的目的是代表2个空格, 让棋盘显示的好看一点.
开始游戏前要初始化,返回一个棋盘对象
// 返回一个华容道
func NewKlotski() *Klotski {
// 1. 初始化棋子的位置
klotski := initKlotski()
// 2. 初始化数组
klotski.initArray()
return klotski
}
接收到用户的输入后,要移动棋子.
移动棋子时要注意会不会越界, 可不可以移动, 移动后的二维数组又会变成什么样.
move方法没有分解, 代码很长,这里就只给出函数签名, 最后会附上源码.
move时华容道对象的一个方法. 接收棋子的ID和移动方向.返回值是是否移动了棋子.
// 移动棋子的方法
func (this *Klotski) move(chessID, direct int) bool
源码
package main
import (
"fmt"
"bytes"
"os"
)
const BLANK = " " // 空白
// 棋子
type Chessman struct {
Name string `desc:"棋子的名字"`
Code string `desc:"棋子在棋盘上的标志符"`
StartPoint [2]int `desc:"起点"`
GridNums int `desc:"格子数"`
Direction int `desc:"方向, 0 代表竖着, 1 代表横着"`
}
// 华容道
type Klotski struct {
Array *[5][4]string `desc:"5*4的地图"`
Chessmen map[int]*Chessman `desc:"存放棋子的字典"`
}
// 移动棋子的方法
func (this *Klotski) move(chessID, direct int) bool {
arr := this.Array
chessman := this.Chessmen[chessID]
x, y := chessman.StartPoint[0], chessman.StartPoint[1]
// 1. 输入校验
if direct == 1 && x == 0 {
// 向上移动时,x坐标要大于0
return false
}
if direct == 2 && x == 4 {
// 向下移动时,x坐标要小于4
return false
}
if direct == 3 && y == 0 {
// 向左移动时, y的坐标要大于0
return false
}
if direct == 4 && y == 3 {
// 向左移动时, y的坐标要小于3
return false
}
// 2. 判断是否可以移动, 可以就移动
// 占据2个格子的棋子,根据棋子方向, 移动
isMove := false
if chessman.GridNums == 2 {
switch direct {
case 1: // 上
if chessman.Direction == 0 { // 竖着
if arr[x-1][y] == BLANK {
this.switchGrid(x, y, x-1, y)
this.switchGrid(x+1, y, x, y)
isMove = true
}
}else { // 横着
if arr[x-1][y] == BLANK && arr[x-1][y+1] == BLANK {
this.switchGrid(x, y, x-1, y)
this.switchGrid(x, y+1, x-1, y+1)
isMove = true
}
}
case 2: // 下
if chessman.Direction == 0 { // 竖着
if arr[x+2][y] == BLANK {
this.switchGrid(x+1, y, x+2, y)
this.switchGrid(x, y, x+1, y)
isMove = true
}
}else { // 横着
if arr[x+1][y] == BLANK && arr[x+1][y+1] == BLANK {
this.switchGrid(x, y, x+1, y)
this.switchGrid(x, y+1, x+1, y+1)
isMove = true
}
}
case 3: // 左
if chessman.Direction == 0 { // 竖着
if arr[x][y-1] == BLANK && arr[x+1][y-1] == BLANK {
this.switchGrid(x, y, x, y-1)
this.switchGrid(x+1, y, x+1, y-1)
isMove = true
}
}else { // 横着
if arr[x][y-1] == BLANK {
this.switchGrid(x, y, x, y-1)
this.switchGrid(x, y+1, x, y)
isMove = true
}
}
case 4: // 右
if chessman.Direction == 0 { // 竖着
if arr[x][y+1] == BLANK && arr[x+1][y+1] == BLANK {
this.switchGrid(x, y, x, y+1)
this.switchGrid(x+1, y, x+1, y+1)
isMove = true
}
}else { // 横着
if arr[x][y+2] == BLANK {
this.switchGrid(x, y+1, x, y+2)
this.switchGrid(x, y, x, y+1)
isMove = true
}
}
}
}
// 占据1个格子的棋子, 移动
if chessman.GridNums == 1 {
switch direct {
case 1: // 上
if arr[x-1][y] == BLANK {
arr[x][y], arr[x-1][y] = arr[x-1][y], arr[x][y]
isMove = true
}
case 2: // 下
if arr[x+1][y] == BLANK {
arr[x][y], arr[x+1][y] = arr[x+1][y], arr[x][y]
isMove = true
}
case 3: // 左
if arr[x][y-1] == BLANK {
arr[x][y], arr[x][y-1] = arr[x][y-1], arr[x][y]
isMove = true
}
case 4: // 右
if arr[x][y+1] == BLANK {
arr[x][y], arr[x][y+1] = arr[x][y+1], arr[x][y]
isMove = true
}
}
}
// 占据4个格子的棋子, 移动
if chessman.GridNums == 4 {
switch direct {
case 1: // 上
if arr[x-1][y] == BLANK && arr[x-1][y+1] == BLANK {
this.switchGrid(x, y, x-1, y)
this.switchGrid(x, y+1, x-1, y+1)
this.switchGrid(x+1, y, x, y)
this.switchGrid(x+1, y+1, x, y+1)
isMove = true
}
case 2: // 下
if arr[x+2][y] == BLANK && arr[x+2][y+1] == BLANK {
this.switchGrid(x+1, y, x+2, y)
this.switchGrid(x+1, y+1, x+2, y+1)
this.switchGrid(x, y, x+1, y)
this.switchGrid(x, y+1, x+1, y+1)
isMove = true
}
case 3: // 左
if arr[x][y-1] == BLANK && arr[x+1][y] == BLANK {
this.switchGrid(x, y, x, y-1)
this.switchGrid(x+1, y, x+1, y-1)
this.switchGrid(x, y+1, x, y)
this.switchGrid(x+1, y+1, x+1, y)
isMove = true
}
case 4: // 右
if arr[x][y+2] == BLANK && arr[x+1][y+2] == BLANK {
this.switchGrid(x, y+1, x, y+2)
this.switchGrid(x+1, y+1, x+1, y+2)
this.switchGrid(x, y, x, y+1)
this.switchGrid(x+1, y, x+1, y+1)
isMove = true
}
}
}
// 3. 如果移动,修改棋子起点位置
if isMove {
switch direct {
case 1: // 上
chessman.StartPoint[0] = x - 1
case 2: // 下
chessman.StartPoint[0] = x + 1
case 3: // 左
chessman.StartPoint[1] = y - 1
case 4: // 右
chessman.StartPoint[1] = y + 1
}
}
// 4. 判断用户是否胜利
if this.Array[4][1] == "曹" && this.Array[4][2] == "曹" {
fmt.Println("恭喜您胜利了!")
fmt.Println("游戏退出.")
os.Exit(0)
}
return isMove
}
// 交换数组中的(a, b) 和 (c, d)
func (this *Klotski) switchGrid(a, b, c, d int) {
this.Array[a][b], this.Array[c][d] = this.Array[c][d], this.Array[a][b]
}
// 将华容道地图变成可打印的字符串
func (this *Klotski) String() string {
var buffer bytes.Buffer
for i := 0; i < 5; i++ {
for j := 0; j < 4; j++ {
buffer.WriteString(this.Array[i][j])
buffer.WriteString(" ")
}
buffer.WriteString("\n")
}
return buffer.String()
}
// 返回一个华容道
func NewKlotski() *Klotski {
// 1. 初始化棋子的位置
klotski := initKlotski()
// 2. 初始化数组
klotski.initArray()
return klotski
}
// 用于初始化华容道地图
// 初始地图有很多种,所以将这部分提取出来,以后可扩展
func initKlotski() *Klotski {
return &Klotski{
Array: &[5][4]string{},
Chessmen: map[int]*Chessman{
0: {Name: "曹操", Code: "曹", StartPoint: [2]int{0, 1}, GridNums: 4},
1: {Name: "张飞", Code: "张", StartPoint: [2]int{0, 0}, GridNums: 2, Direction: 0},
2: {Name: "关羽", Code: "关", StartPoint: [2]int{2, 1}, GridNums: 2, Direction: 1},
3: {Name: "赵云", Code: "赵", StartPoint: [2]int{0, 3}, GridNums: 2, Direction: 0},
4: {Name: "黄忠", Code: "黄", StartPoint: [2]int{2, 0}, GridNums: 2, Direction: 0},
5: {Name: "马超", Code: "马", StartPoint: [2]int{2, 3}, GridNums: 2, Direction: 0},
6: {Name: "兵", Code: "兵", StartPoint: [2]int{3, 1}, GridNums: 1},
7: {Name: "卒", Code: "卒", StartPoint: [2]int{3, 2}, GridNums: 1},
8: {Name: "勇", Code: "勇", StartPoint: [2]int{4, 0}, GridNums: 1},
9: {Name: "士", Code: "士", StartPoint: [2]int{4, 3}, GridNums: 1},
},
}
}
// 根据棋子的位置,初始化华容道对象中的数组
// 将棋子的code填入棋盘上对应的位置中
func (this *Klotski) initArray() {
arr := this.Array
// 0-9 棋子
for i := 0; i < 10; i++ {
chessman := this.Chessmen[i]
// 占据2个格子的棋子,根据棋子方向,填入code
if chessman.GridNums == 2 {
arr[chessman.StartPoint[0]][chessman.StartPoint[1]] = chessman.Code
if chessman.Direction == 0 {
// 总坐标 + 1
arr[chessman.StartPoint[0] + 1][chessman.StartPoint[1]] = chessman.Code
}else {
// 横坐标 + 1
arr[chessman.StartPoint[0]][chessman.StartPoint[1] + 1] = chessman.Code
}
}
// 占据1个格子的棋子,在起点位置填入code
if chessman.GridNums == 1 {
arr[chessman.StartPoint[0]][chessman.StartPoint[1]] = chessman.Code
}
// 占据4个格子的棋子,填入code
if chessman.GridNums == 4 {
arr[chessman.StartPoint[0]][chessman.StartPoint[1]] = chessman.Code
arr[chessman.StartPoint[0] + 1][chessman.StartPoint[1]] = chessman.Code
arr[chessman.StartPoint[0]][chessman.StartPoint[1] + 1] = chessman.Code
arr[chessman.StartPoint[0] + 1][chessman.StartPoint[1] + 1] = chessman.Code
}
}
// 填入空白
for i := 0; i < len(arr); i++ {
for j := 0; j < len(arr[0]); j++ {
if arr[i][j] == "" {
arr[i][j] = BLANK
}
}
}
}
// 游戏开始提示
func init() {
fmt.Println("华容道游戏开始...")
fmt.Println("操作提示: 输入棋子的序号和移动方向,然后回车. 如 0 1 代表将曹操向上移动一格. 输入0 0或其他字符退出游戏.")
fmt.Println("0.曹操 1.张飞 2.关羽 3.赵云 4.黄忠 5.马超 6.兵 7.卒 8.勇 9.士")
fmt.Println("1.上 2.下 3.左 4.右")
fmt.Println()
}
// 用户输入
func input() (int, int) {
fmt.Print("(棋子序号, 移动方向): ")
chessID, direct := 0, 0
fmt.Scan(&chessID, &direct)
// 判断游戏是否结束
if chessID == 0 && direct == 0 {
fmt.Println("游戏退出.")
os.Exit(0)
}
fmt.Println(chessID, direct)
return chessID, direct
}
func main() {
klotski := NewKlotski()
fmt.Println(klotski.String())
fmt.Println()
for {
chessID, direct := input()
isMove := klotski.move(chessID, direct)
if isMove {
fmt.Println(klotski.String())
}else {
fmt.Println("被包围了,不能移动")
}
}
}
设计一个文件格式保存华容道?
- 使用json, 将华容道klotski这个对象进行序列化.
golang的json模块可以做到. - txt文件, 保存棋子的ID和起点位置. 注: 起点位置是棋子最左上角的坐标.
如何自动完成处于某个状态的华容道游戏?
华容道游戏的下棋步骤分支很多, 有点像树的结构. 因为游戏是要尽可能找到最少步骤, 要求出最优解.所以分支限界算法可以处理这个问题.