用Go写一个中国象棋(四)| 简单定义
中国象棋的棋盘是9×10的格局,棋子png像素是56×56,很容易得出棋盘的大小。
棋盘大小
打开define.go,增加如下定义:
//窗口
const (
SquareSize = 56
BoardEdge = 8
BoardWidth = BoardEdge + SquareSize*9 + BoardEdge
BoardHeight = BoardEdge + SquareSize*10 + BoardEdge
)
SquareSize 就是棋子所占区域大小
BoardEdge 边界大小
BoardWidth、BoardHeight 就是棋盘的宽和高
//棋盘范围
const (
Top = 3
Bottom = 12
Left = 3
Right = 11
)
修改game.go中Layout
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
return BoardWidth, BoardHeight
}
定义棋盘
中国象棋的棋盘是一个9×10格局,我们把这个棋盘扩大,用一个16×16的数组来表示这个棋盘,在define.go中预置一个常量数组 cucpcStartup[256]:
// 棋盘初始设置
var cucpcStartup = [256]int{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 20, 19, 18, 17, 16, 17, 18, 19, 20, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0,
0, 0, 0, 22, 0, 22, 0, 22, 0, 22, 0, 22, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 14, 0, 14, 0, 14, 0, 14, 0, 14, 0, 0, 0, 0,
0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 12, 11, 10, 9, 8, 9, 10, 11, 12, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
如下图所示:
上图就是9×10的象棋棋盘在16×16的数组中的位置,这样做有很多好处。
首先,在define.go中可以定义一个数组ccInBoard:
//ccInBoard 判断棋子是否在棋盘中的数组
var ccInBoard = [256]int{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
这样很容易的表示哪些格子在棋盘外(填0),哪些格子在棋盘内(填1),所以就没有必要使用 x >= LEFT && x <= RIGHT && y >= TOP && y <= BOTTOM 之类的语句了,取而代之的是 ccInBoard[sq] != 0。
其次,一维数组的上下左右关系非常简明——上面一格减16,下面一格是加16,左面一格是减1,右面一格是加1。
比如马可以跳的点只有8个,终点相对起点的偏移值是固定的:
//马的步长,以帅(将)的步长作为马腿 var ccMaDelta = [4][2]int{{-33, -31}, {-18, 14}, {-14, 18}, {31, 33}}
而对应的马腿的偏移值是:
//帅(将)的步长 var ccJiangDelta = [4]int{-16, -1, 1, 16}
这个数组之所以命名为 ccJiangDelta ,是因为它也是帅(将)的偏移值。
这样找到一个马的所有走法就容易很多了,首先判断某个方向上的马腿是否有子,然后判断该方向上的两个走法是否能走。
用类似的访求可以获得其他棋子的走法。
基本操作
基于上面的棋盘定义,在define.go中增加一些基本操作:
//获得格子的Y
func getY(sq int) int {
return sq >> 4
}
//获得格子的X
func getX(sq int) int {
return sq & 15
}
//根据纵坐标和横坐标获得格子
func squareXY(x, y int) int {
return x + (y << 4)
}
//翻转格子
func squareFlip(sq int) int {
return 254 - sq
}
//X水平镜像
func xFlip(x int) int {
return 14 - x
}
//Y垂直镜像
func yFlip(y int) int {
return 15 - y
}
//获得红黑标记(红子是8,黑子是16)
func sideTag(sd int) int {
return 8 + (sd << 3)
}
//获得对方红黑标记
func oppSideTag(sd int) int {
return 16 - (sd << 3)
}
//获得走法的起点
func src(mv int) int {
return mv & 255
}
//获得走法的终点
func dst(mv int) int {
return mv >> 8
}
//根据起点和终点获得走法
func move(sqSrc, sqDst int) int {
return sqSrc + sqDst*256
}
这里的sq就是cucpcStartup对应的下标,例如sq=52,那对应棋盘的位置就是x=4,y=3。
sd就是棋子的常量值,例如黑马(ImgBlackMa=19)
棋局结构体
在chess文件夹下新建rule.go,定义棋局的结构体:
package chess
//PositionStruct 局面结构
type PositionStruct struct {
sdPlayer int // 轮到谁走,0=红方,1=黑方
ucpcSquares [256]int // 棋盘上的棋子
}
//NewPositionStruct 初始化棋局
func NewPositionStruct() *PositionStruct {
p := &PositionStruct{
}
if p == nil {
return nil
}
return p
}
//startup 初始化棋盘
func (p *PositionStruct) startup() {
p.sdPlayer = 0
for sq := 0; sq < 256; sq++ {
p.ucpcSquares[sq] = cucpcStartup[sq]
}
}
//changeSide 交换走子方
func (p *PositionStruct) changeSide() {
p.sdPlayer = 1 - p.sdPlayer
}
//addPiece 在棋盘上放一枚棋子
func (p *PositionStruct) addPiece(sq, pc int) {
p.ucpcSquares[sq] = pc
}
//delPiece 从棋盘上拿走一枚棋子
func (p *PositionStruct) delPiece(sq int) {
p.ucpcSquares[sq] = 0
}
//movePiece 搬一步棋的棋子
func (p *PositionStruct) movePiece(mv int) {
sqSrc := src(mv)
sqDst := dst(mv)
p.delPiece(sqDst)
pc := p.ucpcSquares[sqSrc]
p.delPiece(sqSrc)
p.addPiece(sqDst, pc);
}
//makeMove 走一步棋
func (p *PositionStruct) makeMove(mv int) {
p.movePiece(mv)
p.changeSide()
}
棋局界面
打开game.go,定义棋局的界面操作,修改Game结构体:
type Game struct {
sqSelected int // 选中的格子
mvLast int // 上一步棋
bFlipped bool //是否翻转棋盘
images map[int]*ebiten.Image //图片资源
audios map[int]*audio.Player //音效
audioContext *audio.Context //音效器
singlePosition *PositionStruct //棋局单例
}
增加sqSelected、mvLast、bFlipped和singlePosition。
修改NewGame函数:
func NewGame() bool {
game := &Game{
images: make(map[int]*ebiten.Image),
audios: make(map[int]*audio.Player),
singlePosition: NewPositionStruct(),
}
if game == nil || game.singlePosition == nil {
return false
}
var err error
//音效器
game.audioContext, err = audio.NewContext(48000)
if err != nil {
fmt.Print(err)
return false
}
//加载资源
if ok := game.loadResource(); !ok {
return false
}
game.singlePosition.startup()
ebiten.SetWindowSize(BoardWidth, BoardHeight)
ebiten.SetWindowTitle("中国象棋")
if err := ebiten.RunGame(game); err != nil {
log.Fatal(err)
return false
}
return true
}
增加资源加载的处理,到这里基本的数据结构已经定义好。
在下一节中,我们将学习如何绘制棋子以及响应鼠标事件。