刚接触go语言不久,前段时间看到一个2048的项目开发教程,于是就试着练了下手。我的环境采用的是Ubuntu Linux环境。
×××:
https://github.com/shiyanlou/golang2048_game.git
http://download.csdn.net/detail/hzy305365977/8067803
项目开发详细教程:
http://www.shiyanlou.com/courses/type/1
一. 2048 游戏设计
《2048》由19岁的意大利人Gabriele Cirulli于2014年3月开发。游戏任务是在一个网格上滑动小方块来进行组合,直到形成一个带有有数字2048的方块。《2048》使用方向键让方块上下左右移动。如果两个带有相同数字的方块在移动中碰撞,则它们会合并为一个方块,且所带数字变为两者之和。每次移动时,会有一个值为2或者4的新方块出现。当值为2048的方块出现时,游戏即胜利。
1. 游戏逻辑设计
2048游戏使用4x4的格子来表示需要移动的数字,这不难想到可以使用一个矩阵来表示这些数字,我们使用type G2048 [4][4]int来表示。每一次使用方向键来移动数字时,对应方向上的数字需要进行移动和合并,也就是移动和合并矩阵中的非零值。当按下不同的方向键时,移动的数字也不同。我们一共会向上、向下、向左、向右四个方向移动数字,可以通过旋转矩阵将向下、向左、向右的移动都转换为向上的移动,这样能一定程度上简化游戏逻辑。大致流程图如下:
2. 界面设计
开发的2048游戏将运行在console下。在console中,我们可以控制每一个字符单元的背景色,以及显示的字符。我们可以根据这一点,在console中绘制中图形,也就是2048游戏的框架:4x4的空白格子,然后每一个格子是4个字符单元,也就是最多能显示四位数字。我们将使用包github.com/nsf/termbox-go进行界面的绘制,termbox-go能很方便的设置字符单元的属性。
三. 2048游戏的实现
2048游戏中的难点有两个地方,一个是矩阵中数字的移动合并,另一个则是矩阵的变换,之所以需要对矩阵进行变换,是为了将2048游戏中向下的移动,向左的移动和向右的移动都转换成向上的移动操作。
1. 矩阵的旋转
矩阵的旋转操作是为了将其他三个方向的移动都转换为向上的移动操作。向下(↓)、向左(←)、向右(→)转换为向上(↑)的操作时,数组需要进行的翻转操作如下所示:
· ↓ → ↑ 此类转换可以有多种方法做到:
o 上下翻转矩阵,然后向上移动合并,再次上下翻转矩阵上下翻转后:martix_new[n-1-x][y]= martix_old[x][y]
o 顺时针翻转180度矩阵,然后向上移动合并,接着逆时针旋转180度此时martix_new[n-1-x]n-1-y]= martix_old[x][y]
· ← → ↑ 此类转换可以将矩阵向右旋转90度后,向上移动合并,接着向左旋转90度完成向右旋转90度后:martix_new[y][n-x-1] = martix_old[x][y] 向左旋转90度后:martix_new[n-y-1][x]= martix_old[x][y]
· → → ↑ 此类转换可以将矩阵向左旋转90度后,向上移动合并,接着向右旋转90度完成
主要代码:
package main
import"fmt"
type g2048 [4][4]int
func (t *g2048)MirrorV() {
tn := new(g2048)
for i, line := range t {
for j, num := range line {
tn[len(t)-i-1][j] =num
}
}
*t = *tn
}
func (t *g2048)Right90() {
tn := new(g2048)
for i, line := range t {
for j, num := range line {
tn[j][len(t)-i-1] = num
}
}
*t = *tn
}
func (t *g2048)Left90() {
tn := new(g2048)
for i, line := range t {
for j, num := range line {
tn[len(line)-j-1][i] =num
}
}
*t = *tn
}
func (g *g2048)R90() {
tn := new(g2048)
for x, line := range g {
for y, _ := range line {
tn[x][y] = g[len(line)-1-y][x]
}
}
*g = *tn
}
func (t *g2048)Right180() {
tn := new(g2048)
for i, line := range t {
for j, num := range line {
tn[len(line)-i-1][len(line)-j-1] = num
}
}
*t = *tn
}
func (t *g2048)Print() {
for _, line := range t {
for _, number := range line {
fmt.Printf("%2d ", number)
}
fmt.Println()
}
fmt.Println()
tn := g2048{{1, 2, 3, 4}, {5, 8}, {9, 10, 11}, {13, 14, 16}}
*t = tn
}
func main() {
fmt.Println("origin")
t := g2048{{1, 2, 3, 4}, {5, 8}, {9, 10, 11}, {13, 14, 16}}
t.Print()
fmt.Println("mirror")
t.MirrorV()
t.Print()
fmt.Println("Left90")
t.Left90()
t.Print()
fmt.Println("Right90")
t.R90()
t.Print()
fmt.Println("Right180")
t.Right180()
t.Print()
}
2. 2048的实现
package g2048
import (
"fmt"
"github.com/nsf/termbox-go"
"math/rand"
"time"
)
var Score int
var step int
//输出字符串
func coverPrintStr(x,y int, str string, fg, bg termbox.Attribute) error {
xx := x
for n, c := rangestr {
if c == '\n' {
y++
xx = x - n - 1
}
termbox.SetCell(xx+n, y,c, fg, bg)
}
termbox.Flush()
return nil
}
//游戏状态
type Status uint
const (
Win Status = iota
Lose
Add
Max = 2048
)
//2048游戏中的16个格子使用4x4二维数组表示
type G2048 [4][4]int
//检查游戏是否已经胜利,没有胜利的情况下随机将值为0的元素
//随机设置为2或者4
func (t *G2048)checkWinOrAdd() Status {
// 判断4x4中是否有元素的值大于(等于)2048,有则获胜利
for _, x := range t {
for _, y := range x {
if y >= Max {
return Win
}
}
}
// 开始随机设置零值元素为2或者4
i := rand.Intn(len(t))
j := rand.Intn(len(t))
for x := 0; x < len(t); x++{
for y := 0; y < len(t); y++{
if t[i%len(t)][j%len(t)] == 0 {
t[i%len(t)][j%len(t)] = 2 <<(rand.Uint32() % 2)
return Add
}
j++
}
i++
}
// 全部元素都不为零(表示已满),则失败
return Lose
}
//初始化游戏界面
func (t G2048)initialize(ox, oy int) error {
fg := termbox.ColorYellow
bg := termbox.ColorBlack
termbox.Clear(fg, bg)
str := " SCORE: " + fmt.Sprint(Score)
for n, c := rangestr {
termbox.SetCell(ox+n, oy-1, c, fg, bg)
}
str = "ESC:exit" + "Enter:replay"
for n, c := rangestr {
termbox.SetCell(ox+n, oy-2, c, fg, bg)
}
str = " PLAY withARROW KEY"
for n, c := rangestr {
termbox.SetCell(ox+n, oy-3, c, fg, bg)
}
fg = termbox.ColorBlack
bg = termbox.ColorGreen
for i := 0; i <=len(t); i++{
for x := 0; x < 5*len(t); x++{
termbox.SetCell(ox+x,oy+i*2, '-', fg, bg)
}
for x := 0; x <=2*len(t); x++{
if x%2 == 0 {
termbox.SetCell(ox+i*5, oy+x, '+', fg, bg)
} else {
termbox.SetCell(ox+i*5, oy+x, '|', fg, bg)
}
}
}
fg = termbox.ColorYellow
bg = termbox.ColorBlack
for i := range t {
for j := range t[i] {
if t[i][j] > 0 {
str := fmt.Sprint(t[i][j])
for n, char := rangestr {
termbox.SetCell(ox+j*5+1+n, oy+i*2+1, char, fg, bg)
}
}
}
}
return termbox.Flush()
}
//翻转二维切片
func (t *G2048)mirrorV() {
tn := new(G2048)
for i, line := range t {
for j, num := range line {
tn[len(t)-i-1][j] =num
}
}
*t = *tn
}
//向右旋转90度
func (t *G2048)right90() {
tn := new(G2048)
for i, line := range t {
for j, num := range line {
tn[j][len(t)-i-1] = num
}
}
*t = *tn
}
//向左旋转90度
func (t *G2048)left90() {
tn := new(G2048)
for i, line := range t {
for j, num := range line {
tn[len(line)-j-1][i] =num
}
}
*t = *tn
}
func (t *G2048)right180() {
tn := new(G2048)
for i, line := range t {
for j, num := range line {
tn[len(line)-i-1][len(line)-j-1] = num
}
}
*t = *tn
}
//向上移动并合并
func (t *G2048)mergeUp() bool {
tl := len(t)
changed := false
notfull := false
for i := 0; i <tl; i++ {
np := tl
n := 0 // 统计每一列中非零值的个数
// 向上移动非零值,如果有零值元素,则用非零元素进行覆盖
for x := 0; x <np; x++ {
if t[x][i] != 0 {
t[n][i] = t[x][i]
if n != x {
changed = true //标示数组的元素是否有变化
}
n++
}
}
if n < tl {
notfull = true
}
np = n
// 向上合并所有相同的元素
for x := 0; x <np-1; x++ {
if t[x][i] == t[x+1][i] {
t[x][i] *= 2
t[x+1][i] = 0
Score += t[x][i] *step // 计算游戏分数
x++
changed = true
}
}
// 合并完相同元素以后,再次向上移动非零元素
n = 0
for x := 0; x <np; x++ {
if t[x][i] != 0 {
t[n][i] = t[x][i]
n++
}
}
for x := n; x <tl; x++ {
t[x][i] = 0
}
}
return changed || !notfull
}
//向下移动合并的操作可以转换向上移动合并:
//1.向右旋转180度矩阵
//2.向上合并
//3.再次向右旋转180度矩阵
func (t *G2048)mergeDwon() bool {
//t.mirrorV()
t.right180()
changed := t.mergeUp()
//t.mirrorV()
t.right180()
return changed
}
//向左移动合并转换为向上移动合并
func (t *G2048)mergeLeft() bool {
t.right90()
changed := t.mergeUp()
t.left90()
return changed
}
///向右移动合并转换为向上移动合并
func (t *G2048)mergeRight() bool {
t.left90()
changed := t.mergeUp()
t.right90()
return changed
}
//检查按键,做出不同的移动动作或者退出程序
func (t *G2048)mrgeAndReturnKey() termbox.Key {
var changed bool
Lable:
changed = false
//ev := termbox.PollEvent()
event_queue := make(chan termbox.Event)
go func() { // 在其他goroutine中开始监听
for {
event_queue <- termbox.PollEvent()// 开始监听键盘事件
}
}()
ev := <-event_queue
switch ev.Type {
case termbox.EventKey:
switch ev.Key {
case termbox.KeyArrowUp:
changed = t.mergeUp()
case termbox.KeyArrowDown:
changed = t.mergeDwon()
case termbox.KeyArrowLeft:
changed = t.mergeLeft()
case termbox.KeyArrowRight:
changed = t.mergeRight()
case termbox.KeyEsc, termbox.KeyEnter:
changed = true
default:
changed = false
}
//如果元素的值没有任何更改,则从新开始循环
if !changed {
goto Lable
}
case termbox.EventResize:
x, y := termbox.Size()
t.initialize(x/2-10, y/2-4)
goto Lable
case termbox.EventError:
panic(ev.Err)
}
step++ // 计算游戏操作数
return ev.Key
}
//重置
func (b *G2048)clear() {
next :=new(G2048)
Score = 0
step = 0
*b = *next
}
//开始游戏
func (b *G2048)Run() {
err := termbox.Init()
if err != nil {
panic(err)
}
defer termbox.Close()
rand.Seed(time.Now().UnixNano())
A:
b.clear()
for { // 进入无限循环
st := b.checkWinOrAdd()
x, y := termbox.Size()
b.initialize(x/2-10, y/2-4) // 初始化游戏界面
switch st {
case Win:
str := "Win!!"
strl := len(str)
coverPrintStr(x/2-strl/2, y/2, str, termbox.ColorMagenta,termbox.ColorYellow)
case Lose:
str := "Lose!!"
strl := len(str)
coverPrintStr(x/2-strl/2, y/2, str, termbox.ColorBlack,termbox.ColorRed)
case Add:
default:
fmt.Print("Err")
}
// 检查用户按键
key := b.mrgeAndReturnKey()
// 如果按键是 Esc 则退出游戏
if key == termbox.KeyEsc{
return
}
// 如果按键是 Enter 则从新开始游戏
if key == termbox.KeyEnter{
goto A
}
}
}