Added boards, including a heart :)

Improved AI slightly
This commit is contained in:
MichaelS11 2018-02-14 12:57:02 -08:00
parent f67b7198e6
commit e9ec8cd1dd
17 changed files with 2038 additions and 786 deletions

View File

@ -15,7 +15,7 @@ go install github.com/MichaelS11/tetris
Then run the binary created, tetris or tetris.exe
## Keys
## Keys during game
| Key | Action |
| --- | --- |
@ -30,6 +30,15 @@ Then run the binary created, tetris or tetris.exe
| q | quit |
| i | toggle AI |
## Keys start screen
| Key | Action |
| --- | --- |
| ← | previous board |
| → | next board |
| spacebar | start game |
| q | quit |
## Features include
@ -38,6 +47,7 @@ Then run the binary created, tetris or tetris.exe
- Next piece
- Ghost piece
- Top scores
- Board choices
## Screenshots
@ -46,4 +56,11 @@ Then run the binary created, tetris or tetris.exe
![alt text](https://raw.githubusercontent.com/MichaelS11/tetris/master/screenshots/screenshot2.png "Golang Tetris")
![alt text](https://raw.githubusercontent.com/MichaelS11/tetris/master/screenshots/screenshot3.png "Tetris Top Scores")
![alt text](https://raw.githubusercontent.com/MichaelS11/tetris/master/screenshots/screenshot3.png "Golang Tetris Heart")
![alt text](https://raw.githubusercontent.com/MichaelS11/tetris/master/screenshots/screenshot4.png "Tetris High Scores")
## To do
* Improve AI speed (slow on large boards)

173
ai.go
View File

@ -1,26 +1,22 @@
package main
const (
aiQueueSize = (boardWidth+1)/2 + 6
aiMoveLength = (boardWidth + 1) / 2
)
type Ai struct {
queue [aiQueueSize]rune
index int
}
func NewAi() *Ai {
ai := Ai{}
for i := 0; i < aiQueueSize; i++ {
ai.queue[i] = 'x'
}
queue := make([]rune, 1)
queue[0] = 'x'
ai.queue = &queue
return &ai
}
func (ai *Ai) ProcessQueue() bool {
func (ai *Ai) ProcessQueue() {
if ai.newQueue != nil {
ai.queue = ai.newQueue
ai.newQueue = nil
ai.index = 0
}
queue := *ai.queue
// wasd + qe keyboard keys
switch ai.queue[ai.index] {
switch queue[ai.index] {
case 'w':
board.MinoDrop()
case 'a':
@ -32,51 +28,31 @@ func (ai *Ai) ProcessQueue() bool {
case 'e':
board.MinoRotateRight()
case 'x':
return false
return
}
ai.index++
if ai.index == aiQueueSize {
ai.index = 0
}
view.RefreshScreen()
return true
}
func (ai *Ai) GetBestQueue() {
ai.addMovesToQueue(ai.getBestQueue())
}
func (ai *Ai) addMovesToQueue(queue []rune) {
insertIndex := ai.index
for _, char := range queue {
ai.queue[insertIndex] = char
insertIndex++
if insertIndex == aiQueueSize {
insertIndex = 0
}
}
}
func (ai *Ai) getBestQueue() []rune {
bestQueue := make([]rune, 0, 0)
bestScore := -9999999
var slideScore int
bestSlide := 6
bestQueue := make([]rune, 0, 0)
currentMino := *board.currentMino
for move1 := 0; move1 <= boardWidth; move1++ {
for rotate1 := 0; rotate1 < 5; rotate1++ {
for slide1 := 0; slide1 <= 5; slide1++ {
for slide1 := 0; slide1 < 5; slide1++ {
for move1 := board.width; move1 >= 0; move1-- {
for rotate1 := 0; rotate1 < 5; rotate1++ {
queue, mino1 := ai.getMovesforMino(rotate1, move1, slide1, nil)
queue, mino1 := board.getMovesforMino(rotate1, move1, slide1, &currentMino, nil)
if mino1 == nil {
continue
}
for move2 := 0; move2 <= boardWidth; move2++ {
for rotate2 := 0; rotate2 < 5; rotate2++ {
for slide2 := 0; slide2 <= 5; slide2++ {
for slide2 := 0; slide2 < 5; slide2++ {
for move2 := board.width; move2 >= 0; move2-- {
for rotate2 := 0; rotate2 < 5; rotate2++ {
_, mino2 := ai.getMovesforMino(rotate2, move2, slide2, mino1)
_, mino2 := board.getMovesforMino(rotate2, move2, slide2, board.previewMino, mino1)
if mino2 == nil {
continue
}
@ -84,16 +60,9 @@ func (ai *Ai) getBestQueue() []rune {
fullLines, holes, bumpy := board.boardStatsWithMinos(mino1, mino2)
score := ai.getScoreFromBoardStats(fullLines, holes, bumpy)
if slide1 < 3 {
slideScore = slide1
} else {
slideScore = slide1 - 2
}
if score > bestScore || (score == bestScore && slideScore < bestSlide) {
if score > bestScore {
bestScore = score
bestQueue = queue
bestSlide = slideScore
}
}
@ -104,100 +73,95 @@ func (ai *Ai) getBestQueue() []rune {
}
}
return bestQueue
if len(bestQueue) < 1 {
bestQueue = append(bestQueue, 'x')
}
ai.newQueue = &bestQueue
}
func (ai *Ai) getMovesforMino(rotate int, move int, slide int, mino1 *Mino) ([]rune, *Mino) {
func (board *Board) getMovesforMino(rotate int, move int, slide int, mino1 *Mino, mino2 *Mino) ([]rune, *Mino) {
queue := make([]rune, 0, 4)
var mino Mino
if mino1 != nil {
mino = *board.previewMino
} else {
mino = *board.currentMino
}
if rotate < 3 {
mino := *mino1
if rotate%2 == 0 {
rotate /= 2
for i := 0; i < rotate; i++ {
mino.RotateRight()
queue = append(queue, 'e')
if !mino.ValidLocation(false) || (mino1 != nil && mino1.minoOverlap(&mino)) {
if !mino.ValidLocation(false) || (mino2 != nil && mino2.minoOverlap(&mino)) {
return queue, nil
}
}
} else {
for i := 0; i < rotate-2; i++ {
rotate = rotate/2 + 1
for i := 0; i < rotate; i++ {
mino.RotateLeft()
queue = append(queue, 'q')
if !mino.ValidLocation(false) || (mino1 != nil && mino1.minoOverlap(&mino)) {
if !mino.ValidLocation(false) || (mino2 != nil && mino2.minoOverlap(&mino)) {
return queue, nil
}
}
}
if move <= aiMoveLength {
move = aiMoveLength - move
if move%2 == 0 {
move /= 2
for i := 0; i < move; i++ {
mino.MoveLeft()
queue = append(queue, 'a')
if !mino.ValidLocation(false) || (mino1 != nil && mino1.minoOverlap(&mino)) {
if !mino.ValidLocation(false) || (mino2 != nil && mino2.minoOverlap(&mino)) {
return queue, nil
}
}
} else {
move = move - aiMoveLength + 1
move = move/2 + 1
for i := 0; i < move; i++ {
mino.MoveRight()
queue = append(queue, 'd')
if !mino.ValidLocation(false) || (mino1 != nil && mino1.minoOverlap(&mino)) {
if !mino.ValidLocation(false) || (mino2 != nil && mino2.minoOverlap(&mino)) {
return queue, nil
}
}
}
for mino.ValidLocation(false) && (mino1 == nil || !mino1.minoOverlap(&mino)) {
for mino.ValidLocation(false) && (mino2 == nil || !mino2.minoOverlap(&mino)) {
mino.MoveDown()
}
mino.MoveUp()
queue = append(queue, 'w')
if slide < 3 {
if slide%2 == 0 {
slide /= 2
for i := 0; i < slide; i++ {
mino.MoveLeft()
queue = append(queue, 'a')
if !mino.ValidLocation(false) || (mino1 != nil && mino1.minoOverlap(&mino)) {
if !mino.ValidLocation(false) || (mino2 != nil && mino2.minoOverlap(&mino)) {
return queue, nil
}
}
} else {
slide = slide - 2
slide = slide/2 + 1
for i := 0; i < slide; i++ {
mino.MoveRight()
queue = append(queue, 'd')
if !mino.ValidLocation(false) || (mino1 != nil && mino1.minoOverlap(&mino)) {
if !mino.ValidLocation(false) || (mino2 != nil && mino2.minoOverlap(&mino)) {
return queue, nil
}
}
}
if !mino.ValidLocation(true) {
return queue, nil
}
queue = append(queue, 'x')
return queue, &mino
}
func (mino *Mino) minoOverlap(mino1 *Mino) bool {
minoBlocks := mino.minoRotation[mino.rotation]
for i := 0; i < mino.length; i++ {
for j := 0; j < mino.length; j++ {
if minoBlocks[i][j] != blankColor {
if mino1.isMinoAtLocation(mino.x+i, mino.y+j) {
return true
}
}
}
}
return false
}
func (board *Board) boardStatsWithMinos(mino1 *Mino, mino2 *Mino) (fullLines int, holes int, bumpy int) {
// fullLines
fullLinesY := make(map[int]bool, 2)
for j := 0; j < boardHeight; j++ {
for j := 0; j < board.height; j++ {
isFullLine := true
for i := 0; i < boardWidth; i++ {
for i := 0; i < board.width; i++ {
if board.colors[i][j] == blankColor && !mino1.isMinoAtLocation(i, j) && !mino2.isMinoAtLocation(i, j) {
isFullLine = false
break
@ -211,10 +175,10 @@ func (board *Board) boardStatsWithMinos(mino1 *Mino, mino2 *Mino) (fullLines int
// holes and bumpy
indexLast := 0
for i := 0; i < boardWidth; i++ {
index := boardHeight
for i := 0; i < board.width; i++ {
index := board.height
indexOffset := 0
for j := 0; j < boardHeight; j++ {
for j := 0; j < board.height; j++ {
if _, found := fullLinesY[j]; found {
indexOffset++
} else {
@ -236,7 +200,7 @@ func (board *Board) boardStatsWithMinos(mino1 *Mino, mino2 *Mino) (fullLines int
indexLast = index + fullLines - indexOffset
index++
for j := index; j < boardHeight; j++ {
for j := index; j < board.height; j++ {
if board.colors[i][j] == blankColor && !mino1.isMinoAtLocation(i, j) && !mino2.isMinoAtLocation(i, j) {
holes++
}
@ -245,24 +209,9 @@ func (board *Board) boardStatsWithMinos(mino1 *Mino, mino2 *Mino) (fullLines int
return
}
func (mino *Mino) isMinoAtLocation(x int, y int) bool {
xIndex := x - mino.x
yIndex := y - mino.y
if xIndex < 0 || xIndex >= mino.length || yIndex < 0 || yIndex >= mino.length {
return false
}
minoBlocks := mino.minoRotation[mino.rotation]
if minoBlocks[xIndex][yIndex] != blankColor {
return true
}
return false
}
func (ai *Ai) getScoreFromBoardStats(fullLines int, holes int, bumpy int) (score int) {
if fullLines == 4 {
score += 256
score += 512
}
score -= 75 * holes
score -= 25 * bumpy

View File

@ -1,494 +1,291 @@
package main
import (
"gopkg.in/inconshreveable/log15.v2"
"os"
"testing"
)
func TestMain(m *testing.M) {
setupForTesting()
retCode := m.Run()
os.Exit(retCode)
type testAiStruct struct {
info string
minos []testMinoStruct
fullLines int
holes int
bumpy int
}
func setupForTesting() {
logger = log15.New()
logger.SetHandler(log15.StreamHandler(os.Stdout, log15.LogfmtFormat()))
func TestAI(t *testing.T) {
// this must be set to the blank 10x20 board
board.boardsIndex = 0
board.Clear()
engine = NewEngine()
tests := []testAiStruct{
{info: "fullLines 2x minoI", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[0], x: 0, y: 18}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 4, y: 18}, // minoI
}, fullLines: 0, holes: 0, bumpy: 1},
{info: "fullLines 2x2 minoI", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[0], x: 0, y: 18}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 4, y: 18}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 0, y: 17}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 4, y: 17}, // minoI
}, fullLines: 0, holes: 0, bumpy: 2},
{info: "fullLines 2x minoI minoO", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[0], x: 0, y: 18}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 4, y: 18}, // minoI
testMinoStruct{minoRotation: minos.minoBag[3], x: 8, y: 18}, // minoO
}, fullLines: 1, holes: 0, bumpy: 1},
{info: "fullLines 2x2 minoI minoO", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[0], x: 0, y: 18}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 4, y: 18}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 0, y: 17}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 4, y: 17}, // minoI
testMinoStruct{minoRotation: minos.minoBag[3], x: 8, y: 18}, // minoO
}, fullLines: 2, holes: 0, bumpy: 0},
{info: "fullLines 5x minoO", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 2, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 4, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 6, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 8, y: 18}, // minoO
}, fullLines: 2, holes: 0, bumpy: 0},
{info: "fullLines 4x4 minoI 2x minoO", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[0], x: 0, y: 18}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 4, y: 18}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 0, y: 17}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 4, y: 17}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 0, y: 16}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 4, y: 16}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 0, y: 15}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 4, y: 15}, // minoI
testMinoStruct{minoRotation: minos.minoBag[3], x: 8, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 8, y: 16}, // minoO
}, fullLines: 4, holes: 0, bumpy: 0},
{info: "holes 2x minoI minoO", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[0], x: 0, y: 18}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 6, y: 18}, // minoI
testMinoStruct{minoRotation: minos.minoBag[3], x: 4, y: 17}, // minoO
}, fullLines: 0, holes: 2, bumpy: 4},
{info: "holes 6x minoO", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 4, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 8, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 2, y: 16}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 6, y: 16}, // minoO
}, fullLines: 0, holes: 8, bumpy: 8},
{info: "holes 4x minoT 2x minoI", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[5], x: 0, y: 18}, // minoT
testMinoStruct{minoRotation: minos.minoBag[5], x: 7, y: 18}, // minoT
testMinoStruct{minoRotation: minos.minoBag[5], x: 3, y: 16}, // minoT
testMinoStruct{minoRotation: minos.minoBag[5], x: 7, y: 16}, // minoT
testMinoStruct{minoRotation: minos.minoBag[0], x: 2, y: 14}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 6, y: 14}, // minoI
}, fullLines: 0, holes: 19, bumpy: 4},
{info: "holes 3x minoZ", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[6], x: 0, y: 18}, // minoZ
testMinoStruct{minoRotation: minos.minoBag[6], x: 3, y: 18}, // minoZ
testMinoStruct{minoRotation: minos.minoBag[6], x: 6, y: 18}, // minoZ
}, fullLines: 0, holes: 3, bumpy: 6},
{info: "bumpy 2x minoT - 1", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[5], x: 0, y: 18}, // minoT
testMinoStruct{minoRotation: minos.minoBag[5], x: 5, y: 18}, // minoT
}, fullLines: 0, holes: 0, bumpy: 7},
{info: "bumpy 2x minoT - 2", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[5], x: 1, y: 18}, // minoT
testMinoStruct{minoRotation: minos.minoBag[5], x: 6, y: 18}, // minoT
}, fullLines: 0, holes: 0, bumpy: 8},
{info: "bumpy 2x minoT - 3", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[5], x: 2, y: 18}, // minoT
testMinoStruct{minoRotation: minos.minoBag[5], x: 7, y: 18}, // minoT
}, fullLines: 0, holes: 0, bumpy: 7},
{info: "bumpy 2x minoJ - 1", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[1], x: 0, y: 18}, // minoJ
testMinoStruct{minoRotation: minos.minoBag[1], x: 5, y: 18}, // minoJ
}, fullLines: 0, holes: 0, bumpy: 6},
{info: "bumpy 2x minoJ - 2", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[1], x: 1, y: 18}, // minoJ
testMinoStruct{minoRotation: minos.minoBag[1], x: 6, y: 18}, // minoJ
}, fullLines: 0, holes: 0, bumpy: 8},
{info: "bumpy 2x minoJ - 2", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[1], x: 2, y: 18}, // minoJ
testMinoStruct{minoRotation: minos.minoBag[1], x: 7, y: 18}, // minoJ
}, fullLines: 0, holes: 0, bumpy: 7},
{info: "bumpy 2x minoL - 1", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[2], x: 0, y: 18}, // minoL
testMinoStruct{minoRotation: minos.minoBag[2], x: 5, y: 18}, // minoL
}, fullLines: 0, holes: 0, bumpy: 7},
{info: "bumpy 2x minoL - 2", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[2], x: 1, y: 18}, // minoL
testMinoStruct{minoRotation: minos.minoBag[2], x: 6, y: 18}, // minoL
}, fullLines: 0, holes: 0, bumpy: 8},
{info: "bumpy 2x minoL - 3", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[2], x: 2, y: 18}, // minoL
testMinoStruct{minoRotation: minos.minoBag[2], x: 7, y: 18}, // minoL
}, fullLines: 0, holes: 0, bumpy: 6},
}
runAiTests(t, tests)
}
func TestBoardStatsFullLines1(t *testing.T) {
board = NewBoard()
func TestBigBoardAI(t *testing.T) {
// this must be set to the blank 20x20 board
board.boardsIndex = 3
board.Clear()
// minoO
mino1 := &Mino{
minoRotation: minoBag[3],
}
mino1.length = len(mino1.minoRotation[0])
mino1.x = 0
mino1.y = 18
mino1.SetOnBoard()
// minoI
mino1 = &Mino{
minoRotation: minoBag[0],
}
mino1.length = len(mino1.minoRotation[0])
mino1.x = 2
mino1.y = 18
// minoI
mino2 := &Mino{
minoRotation: minoBag[0],
}
mino2.length = len(mino2.minoRotation[0])
mino2.x = 6
mino2.y = 18
fullLines, holes, bumpy := board.boardStatsWithMinos(mino1, mino2)
expected := 1
if fullLines != expected {
t.Error("fullLines expected", expected, "got", fullLines)
}
expected = 0
if holes != expected {
t.Error("holes expected", expected, "got", holes)
}
expected = 1
if bumpy != expected {
t.Error("bumpy expected", expected, "got", bumpy)
tests := []testAiStruct{
{info: "fullLines 4x minoI", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[0], x: 0, y: 18}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 4, y: 18}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 8, y: 18}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 12, y: 18}, // minoI
}, fullLines: 0, holes: 0, bumpy: 1},
{info: "fullLines 5x minoI", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[0], x: 0, y: 18}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 4, y: 18}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 8, y: 18}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 12, y: 18}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 16, y: 18}, // minoI
}, fullLines: 1, holes: 0, bumpy: 0},
{info: "fullLines 5x2 minoI", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[0], x: 0, y: 18}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 4, y: 18}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 8, y: 18}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 12, y: 18}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 16, y: 18}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 0, y: 17}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 4, y: 17}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 8, y: 17}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 12, y: 17}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 16, y: 17}, // minoI
}, fullLines: 2, holes: 0, bumpy: 0},
{info: "fullLines 9x minoO", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 2, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 4, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 6, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 8, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 10, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 12, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 14, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 16, y: 18}, // minoO
}, fullLines: 0, holes: 0, bumpy: 2},
{info: "fullLines 10x minoO", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 2, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 4, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 6, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 8, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 10, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 12, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 14, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 16, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 18, y: 18}, // minoO
}, fullLines: 2, holes: 0, bumpy: 0},
{info: "holes 3x minoO 3x minoI", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 6, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[3], x: 12, y: 18}, // minoO
testMinoStruct{minoRotation: minos.minoBag[0], x: 2, y: 16}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 8, y: 16}, // minoI
testMinoStruct{minoRotation: minos.minoBag[0], x: 14, y: 16}, // minoI
}, fullLines: 0, holes: 24, bumpy: 8},
{info: "holes 5x minoZ", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[6], x: 0, y: 18}, // minoZ
testMinoStruct{minoRotation: minos.minoBag[6], x: 4, y: 18}, // minoZ
testMinoStruct{minoRotation: minos.minoBag[6], x: 8, y: 18}, // minoZ
testMinoStruct{minoRotation: minos.minoBag[6], x: 12, y: 18}, // minoZ
testMinoStruct{minoRotation: minos.minoBag[6], x: 16, y: 18}, // minoZ
}, fullLines: 0, holes: 5, bumpy: 18},
{info: "bumpy 4x minoJ - 1", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[1], x: 0, y: 18}, // minoJ
testMinoStruct{minoRotation: minos.minoBag[1], x: 5, y: 18}, // minoJ
testMinoStruct{minoRotation: minos.minoBag[1], x: 10, y: 18}, // minoJ
testMinoStruct{minoRotation: minos.minoBag[1], x: 15, y: 18}, // minoJ
}, fullLines: 0, holes: 0, bumpy: 14},
{info: "bumpy 4x minoJ - 2", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[1], x: 1, y: 18}, // minoJ
testMinoStruct{minoRotation: minos.minoBag[1], x: 6, y: 18}, // minoJ
testMinoStruct{minoRotation: minos.minoBag[1], x: 11, y: 18}, // minoJ
testMinoStruct{minoRotation: minos.minoBag[1], x: 16, y: 18}, // minoJ
}, fullLines: 0, holes: 0, bumpy: 16},
{info: "bumpy 4x minoJ - 3", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[1], x: 2, y: 18}, // minoJ
testMinoStruct{minoRotation: minos.minoBag[1], x: 7, y: 18}, // minoJ
testMinoStruct{minoRotation: minos.minoBag[1], x: 12, y: 18}, // minoJ
testMinoStruct{minoRotation: minos.minoBag[1], x: 17, y: 18}, // minoJ
}, fullLines: 0, holes: 0, bumpy: 15},
{info: "bumpy 4x minoL - 1", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[2], x: 0, y: 18}, // minoL
testMinoStruct{minoRotation: minos.minoBag[2], x: 5, y: 18}, // minoL
testMinoStruct{minoRotation: minos.minoBag[2], x: 10, y: 18}, // minoL
testMinoStruct{minoRotation: minos.minoBag[2], x: 15, y: 18}, // minoL
}, fullLines: 0, holes: 0, bumpy: 15},
{info: "bumpy 4x minoL - 2", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[2], x: 1, y: 18}, // minoL
testMinoStruct{minoRotation: minos.minoBag[2], x: 6, y: 18}, // minoL
testMinoStruct{minoRotation: minos.minoBag[2], x: 11, y: 18}, // minoL
testMinoStruct{minoRotation: minos.minoBag[2], x: 16, y: 18}, // minoL
}, fullLines: 0, holes: 0, bumpy: 16},
{info: "bumpy 4x minoL - 3", minos: []testMinoStruct{
testMinoStruct{minoRotation: minos.minoBag[2], x: 2, y: 18}, // minoL
testMinoStruct{minoRotation: minos.minoBag[2], x: 7, y: 18}, // minoL
testMinoStruct{minoRotation: minos.minoBag[2], x: 12, y: 18}, // minoL
testMinoStruct{minoRotation: minos.minoBag[2], x: 17, y: 18}, // minoL
}, fullLines: 0, holes: 0, bumpy: 14},
}
// for debuging
// mino1.SetOnBoard()
// mino2.SetOnBoard()
// board.drawDebugBoard()
runAiTests(t, tests)
}
func TestBoardStatsFullLines2(t *testing.T) {
board = NewBoard()
func runAiTests(t *testing.T, tests []testAiStruct) {
var mino1 *Mino
var mino2 *Mino
// minoO
mino1 := &Mino{
minoRotation: minoBag[3],
}
mino1.length = len(mino1.minoRotation[0])
mino1.x = 0
mino1.y = 18
mino1.SetOnBoard()
for _, test := range tests {
board.Clear()
// minoO
mino1 = &Mino{
minoRotation: minoBag[3],
}
mino1.length = len(mino1.minoRotation[0])
mino1.x = 2
mino1.y = 18
mino1.SetOnBoard()
for i, minoTest := range test.minos {
mino := NewMino()
mino.minoRotation = minoTest.minoRotation
mino.length = len(mino.minoRotation[0])
mino.x = minoTest.x
mino.y = minoTest.y
if i < len(test.minos)-2 {
mino.SetOnBoard()
} else if i == len(test.minos)-2 {
mino1 = mino
} else {
mino2 = mino
}
}
// minoO
mino1 = &Mino{
minoRotation: minoBag[3],
}
mino1.length = len(mino1.minoRotation[0])
mino1.x = 4
mino1.y = 18
mino1.SetOnBoard()
fullLines, holes, bumpy := board.boardStatsWithMinos(mino1, mino2)
// minoO
mino1 = &Mino{
minoRotation: minoBag[3],
}
mino1.length = len(mino1.minoRotation[0])
mino1.x = 6
mino1.y = 18
mino1.SetOnBoard()
if fullLines != test.fullLines {
mino1.SetOnBoard()
lines := board.getDebugBoardWithMino(mino2)
for i := 0; i < len(lines); i++ {
t.Log(lines[i])
}
t.Errorf("AI fullLines - received: %v - expected: %v - info %v", fullLines, test.fullLines, test.info)
continue
}
if holes != test.holes {
mino1.SetOnBoard()
lines := board.getDebugBoardWithMino(mino2)
for i := 0; i < len(lines); i++ {
t.Log(lines[i])
}
t.Errorf("AI holes - received: %v - expected: %v - info %v", holes, test.holes, test.info)
continue
}
if bumpy != test.bumpy {
mino1.SetOnBoard()
lines := board.getDebugBoardWithMino(mino2)
for i := 0; i < len(lines); i++ {
t.Log(lines[i])
}
t.Errorf("AI bumpy - received: %v - expected: %v - info %v", bumpy, test.bumpy, test.info)
continue
}
// minoO
mino1 = &Mino{
minoRotation: minoBag[3],
}
mino1.length = len(mino1.minoRotation[0])
mino1.x = 8
mino1.y = 18
mino1.SetOnBoard()
// minoO
mino1 = &Mino{
minoRotation: minoBag[3],
}
mino1.length = len(mino1.minoRotation[0])
mino1.x = 0
mino1.y = 16
mino1.SetOnBoard()
// minoI
mino1 = &Mino{
minoRotation: minoBag[0],
}
mino1.length = len(mino1.minoRotation[0])
mino1.x = 2
mino1.y = 16
// minoI
mino2 := &Mino{
minoRotation: minoBag[0],
}
mino2.length = len(mino2.minoRotation[0])
mino2.x = 6
mino2.y = 16
fullLines, holes, bumpy := board.boardStatsWithMinos(mino1, mino2)
expected := 3
if fullLines != expected {
t.Error("fullLines expected", expected, "got", fullLines)
}
expected = 0
if holes != expected {
t.Error("holes expected", expected, "got", holes)
}
expected = 1
if bumpy != expected {
t.Error("bumpy expected", expected, "got", bumpy)
}
// for debuging
// mino1.SetOnBoard()
// mino2.SetOnBoard()
// board.drawDebugBoard()
}
func TestBoardStatsFullLines3(t *testing.T) {
board = NewBoard()
// minoO
mino1 := &Mino{
minoRotation: minoBag[3],
}
mino1.length = len(mino1.minoRotation[0])
mino1.x = 0
mino1.y = 18
mino1.SetOnBoard()
// minoI
mino1 = &Mino{
minoRotation: minoBag[0],
}
mino1.length = len(mino1.minoRotation[0])
mino1.x = 0
mino1.y = 16
mino1.SetOnBoard()
// minoO
mino1 = &Mino{
minoRotation: minoBag[3],
}
mino1.length = len(mino1.minoRotation[0])
mino1.x = 4
mino1.y = 18
mino1.SetOnBoard()
// minoI
mino1 = &Mino{
minoRotation: minoBag[0],
}
mino1.length = len(mino1.minoRotation[0])
mino1.x = 4
mino1.y = 16
mino1.SetOnBoard()
// minoO
mino1 = &Mino{
minoRotation: minoBag[3],
}
mino1.length = len(mino1.minoRotation[0])
mino1.x = 8
mino1.y = 18
// minoO
mino2 := &Mino{
minoRotation: minoBag[3],
}
mino2.length = len(mino2.minoRotation[0])
mino2.x = 8
mino2.y = 16
fullLines, holes, bumpy := board.boardStatsWithMinos(mino1, mino2)
expected := 1
if fullLines != expected {
t.Error("fullLines expected", expected, "got", fullLines)
}
expected = 0
if holes != expected {
t.Error("holes expected", expected, "got", holes)
}
expected = 9
if bumpy != expected {
t.Error("bumpy expected", expected, "got", bumpy)
}
// for debuging
// mino1.SetOnBoard()
// mino2.SetOnBoard()
// board.drawDebugBoard()
}
func TestBoardStatsBumpy1(t *testing.T) {
board = NewBoard()
// minoO
mino1 := &Mino{
minoRotation: minoBag[3],
}
mino1.length = len(mino1.minoRotation[0])
mino1.x = 0
mino1.y = 18
// minoO
mino2 := &Mino{
minoRotation: minoBag[3],
}
mino2.length = len(mino2.minoRotation[0])
mino2.x = 0
mino2.y = 16
fullLines, holes, bumpy := board.boardStatsWithMinos(mino1, mino2)
expected := 0
if fullLines != expected {
t.Error("fullLines expected", expected, "got", fullLines)
}
expected = 0
if holes != expected {
t.Error("holes expected", expected, "got", holes)
}
expected = 4
if bumpy != expected {
t.Error("bumpy expected", expected, "got", bumpy)
}
// for debuging
// mino1.SetOnBoard()
// mino2.SetOnBoard()
// board.drawDebugBoard()
}
func TestBoardStatsBumpy2(t *testing.T) {
board = NewBoard()
// minoO
mino1 := &Mino{
minoRotation: minoBag[3],
}
mino1.length = len(mino1.minoRotation[0])
mino1.x = 0
mino1.y = 18
mino1.SetOnBoard()
// minoI
mino1 = &Mino{
minoRotation: minoBag[0],
}
mino1.length = len(mino1.minoRotation[0])
mino1.x = 0
mino1.y = 16
// minoI
mino2 := &Mino{
minoRotation: minoBag[0],
}
mino2.length = len(mino2.minoRotation[0])
mino2.x = 0
mino2.y = 15
fullLines, holes, bumpy := board.boardStatsWithMinos(mino1, mino2)
expected := 0
if fullLines != expected {
t.Error("fullLines expected", expected, "got", fullLines)
}
expected = 4
if holes != expected {
t.Error("holes expected", expected, "got", holes)
}
expected = 4
if bumpy != expected {
t.Error("bumpy expected", expected, "got", bumpy)
}
// for debuging
// mino1.SetOnBoard()
// mino2.SetOnBoard()
// board.drawDebugBoard()
}
func TestBoardStatsBumpy3(t *testing.T) {
board = NewBoard()
// minoO
mino1 := &Mino{
minoRotation: minoBag[3],
}
mino1.length = len(mino1.minoRotation[0])
mino1.x = 0
mino1.y = 18
mino1.SetOnBoard()
// minoO
mino1 = &Mino{
minoRotation: minoBag[3],
}
mino1.length = len(mino1.minoRotation[0])
mino1.x = 1
mino1.y = 16
// minoO
mino2 := &Mino{
minoRotation: minoBag[3],
}
mino2.length = len(mino2.minoRotation[0])
mino2.x = 2
mino2.y = 14
fullLines, holes, bumpy := board.boardStatsWithMinos(mino1, mino2)
expected := 0
if fullLines != expected {
t.Error("fullLines expected", expected, "got", fullLines)
}
expected = 6
if holes != expected {
t.Error("holes expected", expected, "got", holes)
}
expected = 10
if bumpy != expected {
t.Error("bumpy expected", expected, "got", bumpy)
}
// for debuging
// mino1.SetOnBoard()
// mino2.SetOnBoard()
// board.drawDebugBoard()
}
func TestBoardStatsBumpy4(t *testing.T) {
board = NewBoard()
// minoI
mino1 := &Mino{
minoRotation: minoBag[0],
}
mino1.rotation = 1
mino1.length = len(mino1.minoRotation[0])
mino1.x = 6
mino1.y = 16
// minoI
mino2 := &Mino{
minoRotation: minoBag[0],
}
mino2.rotation = 1
mino2.length = len(mino2.minoRotation[0])
mino2.x = 6
mino2.y = 12
fullLines, holes, bumpy := board.boardStatsWithMinos(mino1, mino2)
expected := 0
if fullLines != expected {
t.Error("fullLines expected", expected, "got", fullLines)
}
expected = 0
if holes != expected {
t.Error("holes expected", expected, "got", holes)
}
expected = 16
if bumpy != expected {
t.Error("bumpy expected", expected, "got", bumpy)
}
// for debuging
// mino1.SetOnBoard()
// mino2.SetOnBoard()
// board.drawDebugBoard()
}
func TestBoardStatsholes1(t *testing.T) {
board = NewBoard()
// minoJ
mino1 := &Mino{
minoRotation: minoBag[1],
}
mino1.rotation = 1
mino1.length = len(mino1.minoRotation[0])
mino1.x = -1
mino1.y = 17
// minoJ
mino2 := &Mino{
minoRotation: minoBag[1],
}
mino2.rotation = 1
mino2.length = len(mino2.minoRotation[0])
mino2.x = 1
mino2.y = 17
fullLines, holes, bumpy := board.boardStatsWithMinos(mino1, mino2)
expected := 0
if fullLines != expected {
t.Error("fullLines expected", expected, "got", fullLines)
}
expected = 4
if holes != expected {
t.Error("holes expected", expected, "got", holes)
}
expected = 3
if bumpy != expected {
t.Error("bumpy expected", expected, "got", bumpy)
}
// for debuging
// mino1.SetOnBoard()
// mino2.SetOnBoard()
// board.drawDebugBoard()
}
func TestBoardStatsholes2(t *testing.T) {
board = NewBoard()
// minoJ
mino1 := &Mino{
minoRotation: minoBag[1],
}
mino1.rotation = 1
mino1.length = len(mino1.minoRotation[0])
mino1.x = -1
mino1.y = 17
// minoJ
mino2 := &Mino{
minoRotation: minoBag[1],
}
mino2.rotation = 1
mino2.length = len(mino2.minoRotation[0])
mino2.x = -1
mino2.y = 14
fullLines, holes, bumpy := board.boardStatsWithMinos(mino1, mino2)
expected := 0
if fullLines != expected {
t.Error("fullLines expected", expected, "got", fullLines)
}
expected = 4
if holes != expected {
t.Error("holes expected", expected, "got", holes)
}
expected = 6
if bumpy != expected {
t.Error("bumpy expected", expected, "got", bumpy)
}
// for debuging
// mino1.SetOnBoard()
// mino2.SetOnBoard()
// board.drawDebugBoard()
}

159
board.go
View File

@ -2,33 +2,49 @@ package main
import (
"fmt"
"github.com/nsf/termbox-go"
"time"
"github.com/nsf/termbox-go"
)
type Board struct {
colors [boardWidth][boardHeight]termbox.Attribute
rotation [boardWidth][boardHeight]int
previewMino *Mino
currentMino *Mino
dropDistance int
func NewBoard() {
board = &Board{}
board.Clear()
}
func NewBoard() *Board {
board := &Board{}
for i := 0; i < boardWidth; i++ {
for j := 0; j < boardHeight; j++ {
board.colors[i][j] = blankColor
}
func (board *Board) Clear() {
board.width = len(boards[board.boardsIndex].colors)
board.height = len(boards[board.boardsIndex].colors[0])
board.colors = make([][]termbox.Attribute, len(boards[board.boardsIndex].colors))
for i := 0; i < len(boards[board.boardsIndex].colors); i++ {
board.colors[i] = make([]termbox.Attribute, len(boards[board.boardsIndex].colors[i]))
copy(board.colors[i], boards[board.boardsIndex].colors[i])
}
for i := 0; i < boardWidth; i++ {
for j := 0; j < boardHeight; j++ {
board.rotation[i][j] = 0
}
board.rotation = make([][]int, len(boards[board.boardsIndex].rotation))
for i := 0; i < len(boards[board.boardsIndex].rotation); i++ {
board.rotation[i] = make([]int, len(boards[board.boardsIndex].rotation[i]))
copy(board.rotation[i], boards[board.boardsIndex].rotation[i])
}
board.previewMino = NewMino()
board.currentMino = NewMino()
return board
}
func (board *Board) PreviousBoard() {
board.boardsIndex--
if board.boardsIndex < 0 {
board.boardsIndex = len(boards) - 1
}
engine.PreviewBoard()
board.Clear()
}
func (board *Board) NextBoard() {
board.boardsIndex++
if board.boardsIndex == len(boards) {
board.boardsIndex = 0
}
engine.PreviewBoard()
board.Clear()
}
func (board *Board) MinoMoveLeft() {
@ -160,7 +176,7 @@ func (board *Board) nextMino() {
board.currentMino = board.previewMino
board.previewMino = NewMino()
engine.ResetAiTimer()
engine.AiGetBestQueue()
engine.ResetTimer(0)
}
@ -180,7 +196,7 @@ func (board *Board) deleteCheck() {
func (board *Board) fullLines() []int {
fullLines := make([]int, 0, 1)
for j := 0; j < boardHeight; j++ {
for j := 0; j < board.height; j++ {
if board.isFullLine(j) {
fullLines = append(fullLines, j)
}
@ -189,7 +205,7 @@ func (board *Board) fullLines() []int {
}
func (board *Board) isFullLine(j int) bool {
for i := 0; i < boardWidth; i++ {
for i := 0; i < board.width; i++ {
if board.colors[i][j] == blankColor {
return false
}
@ -198,16 +214,16 @@ func (board *Board) isFullLine(j int) bool {
}
func (board *Board) deleteLine(line int) {
for i := 0; i < boardWidth; i++ {
for i := 0; i < board.width; i++ {
board.colors[i][line] = blankColor
}
for j := line; j > 0; j-- {
for i := 0; i < boardWidth; i++ {
for i := 0; i < board.width; i++ {
board.colors[i][j] = board.colors[i][j-1]
board.rotation[i][j] = board.rotation[i][j-1]
}
}
for i := 0; i < boardWidth; i++ {
for i := 0; i < board.width; i++ {
board.colors[i][0] = blankColor
}
}
@ -218,7 +234,7 @@ func (board *Board) SetColor(x int, y int, color termbox.Attribute, rotation int
}
func ValidBlockLocation(x int, y int, mustBeOnBoard bool) bool {
if x < 0 || x >= boardWidth || y >= boardHeight {
if x < 0 || x >= board.width || y >= board.height {
return false
}
if mustBeOnBoard {
@ -239,12 +255,12 @@ func ValidBlockLocation(x int, y int, mustBeOnBoard bool) bool {
}
func ValidDisplayLocation(x int, y int) bool {
return x >= 0 && x < boardWidth && y >= 0 && y < boardHeight
return x >= 0 && x < board.width && y >= 0 && y < board.height
}
func (board *Board) DrawBoard() {
for i := 0; i < boardWidth; i++ {
for j := 0; j < boardHeight; j++ {
for i := 0; i < board.width; i++ {
for j := 0; j < board.height; j++ {
if board.colors[i][j] != blankColor {
view.DrawBlock(i, j, board.colors[i][j], board.rotation[i][j])
}
@ -273,9 +289,9 @@ func (board *Board) DrawDropMino() {
}
// for debuging
func (board *Board) drawDebugBoard() {
for j := 0; j < boardHeight; j++ {
for i := 0; i < boardWidth; i++ {
func (board *Board) printDebugBoard() {
for j := 0; j < board.height; j++ {
for i := 0; i < board.width; i++ {
switch board.colors[i][j] {
case blankColor:
fmt.Print(".")
@ -293,8 +309,89 @@ func (board *Board) drawDebugBoard() {
fmt.Print("W")
case termbox.ColorYellow:
fmt.Print("Y")
default:
fmt.Print("U")
}
}
fmt.Println("")
}
}
// for debuging
func (board *Board) getDebugBoard() []string {
lines := make([]string, board.height)
for j := 0; j < board.height; j++ {
for i := 0; i < board.width; i++ {
switch board.colors[i][j] {
case blankColor:
lines[j] += "."
case termbox.ColorBlue:
lines[j] += "B"
case termbox.ColorCyan:
lines[j] += "C"
case termbox.ColorGreen:
lines[j] += "G"
case termbox.ColorMagenta:
lines[j] += "M"
case termbox.ColorRed:
lines[j] += "R"
case termbox.ColorWhite:
lines[j] += "W"
case termbox.ColorYellow:
lines[j] += "Y"
default:
lines[j] += "U"
}
}
}
return lines
}
// for debuging
func (board *Board) getDebugBoardWithMino(mino *Mino) []string {
lines := make([]string, board.height)
for j := 0; j < board.height; j++ {
for i := 0; i < board.width; i++ {
switch mino.getMinoColorAtLocation(i, j) {
case blankColor:
switch board.colors[i][j] {
case blankColor:
lines[j] += "."
case termbox.ColorBlue:
lines[j] += "B"
case termbox.ColorCyan:
lines[j] += "C"
case termbox.ColorGreen:
lines[j] += "G"
case termbox.ColorMagenta:
lines[j] += "M"
case termbox.ColorRed:
lines[j] += "R"
case termbox.ColorWhite:
lines[j] += "W"
case termbox.ColorYellow:
lines[j] += "Y"
default:
lines[j] += "U"
}
case termbox.ColorBlue:
lines[j] += "b"
case termbox.ColorCyan:
lines[j] += "c"
case termbox.ColorGreen:
lines[j] += "g"
case termbox.ColorMagenta:
lines[j] += "m"
case termbox.ColorRed:
lines[j] += "r"
case termbox.ColorWhite:
lines[j] += "w"
case termbox.ColorYellow:
lines[j] += "y"
default:
lines[j] += "u"
}
}
}
return lines
}

1086
boards.go Normal file

File diff suppressed because it is too large Load Diff

30
boards_test.go Normal file
View File

@ -0,0 +1,30 @@
package main
import (
"testing"
)
func TestBoards(t *testing.T) {
for i := 0; i < len(boards); i++ {
b := boards[i]
width := len(b.colors)
height := len(b.colors[0])
for j := 1; j < width; j++ {
if len(b.colors[j]) != height {
t.Fatalf("board height - received: %v - expected: %v - index %v", len(b.colors[j]), height, i)
}
}
if len(b.rotation) != width {
t.Fatalf("rotation width - received: %v - expected: %v - index %v", len(b.rotation), width, i)
}
for j := 0; j < width; j++ {
if len(b.rotation[j]) != height {
t.Fatalf("rotation height - received: %v - expected: %v - index %v", len(b.rotation[j]), height, i)
}
}
}
}

View File

@ -1,29 +1,13 @@
package main
import (
"github.com/nsf/termbox-go"
"time"
"github.com/nsf/termbox-go"
)
type Engine struct {
stopped bool
chanStop chan struct{}
keyInput *KeyInput
ranking *Ranking
timer *time.Timer
tickTime time.Duration
paused bool
gameOver bool
score int
level int
deleteLines int
ai *Ai
aiEnabled bool
aiTimer *time.Timer
}
func NewEngine() *Engine {
return &Engine{
func NewEngine() {
engine = &Engine{
chanStop: make(chan struct{}, 1),
gameOver: true,
tickTime: time.Hour,
@ -42,7 +26,7 @@ func (engine *Engine) Run() {
engine.aiTimer.Stop()
engine.ranking = NewRanking()
board = NewBoard()
board.Clear()
view.RefreshScreen()
engine.keyInput = NewKeyInput()
@ -61,9 +45,8 @@ loop:
case <-engine.timer.C:
engine.tick()
case <-engine.aiTimer.C:
if engine.ai.ProcessQueue() {
engine.aiTimer.Reset(engine.tickTime / 4)
}
engine.ai.ProcessQueue()
engine.aiTimer.Reset(engine.tickTime / 6)
case <-engine.chanStop:
break loop
}
@ -105,15 +88,19 @@ func (engine *Engine) Pause() {
func (engine *Engine) UnPause() {
engine.timer.Reset(engine.tickTime)
if engine.aiEnabled {
engine.aiTimer.Reset(engine.tickTime / 4)
engine.aiTimer.Reset(engine.tickTime / 6)
}
engine.paused = false
}
func (engine *Engine) PreviewBoard() {
engine.previewBoard = true
}
func (engine *Engine) NewGame() {
logger.Info("Engine NewGame start")
board = NewBoard()
board.Clear()
engine.tickTime = 480 * time.Millisecond
engine.score = 0
engine.level = 1
@ -128,6 +115,7 @@ loop:
}
}
engine.previewBoard = false
engine.gameOver = false
if engine.aiEnabled {
engine.ai.GetBestQueue()
@ -153,12 +141,11 @@ func (engine *Engine) ResetTimer(duration time.Duration) {
}
}
func (engine *Engine) ResetAiTimer() {
func (engine *Engine) AiGetBestQueue() {
if !engine.aiEnabled {
return
}
engine.ai.GetBestQueue()
engine.aiTimer.Reset(engine.tickTime / 4)
go engine.ai.GetBestQueue()
}
func (engine *Engine) tick() {
@ -190,8 +177,8 @@ func (engine *Engine) AddDeleteLines(lines int) {
func (engine *Engine) AddScore(add int) {
engine.score += add
if engine.score > 999999 {
engine.score = 999999
if engine.score > 9999999 {
engine.score = 9999999
}
}
@ -222,13 +209,9 @@ func (engine *Engine) GameOver() {
logger.Info("Engine GameOver start")
engine.Pause()
view.ShowGameOverAnimation()
engine.gameOver = true
engine.ranking.InsertScore(uint64(engine.score))
engine.ranking.Save()
view.ShowGameOverAnimation()
loop:
for {
@ -239,21 +222,24 @@ loop:
}
}
engine.ranking.InsertScore(uint64(engine.score))
engine.ranking.Save()
logger.Info("Engine GameOver end")
}
func (engine *Engine) EnabledAi() {
engine.aiEnabled = true
engine.ai.GetBestQueue()
engine.aiTimer.Reset(engine.tickTime / 4)
go engine.ai.GetBestQueue()
engine.aiTimer.Reset(engine.tickTime / 6)
}
func (engine *Engine) DisableAi() {
engine.aiEnabled = false
if !engine.aiTimer.Stop() {
select {
case <-engine.aiTimer.C:
default:
}
}
engine.aiEnabled = false
}

103
globals.go Normal file
View File

@ -0,0 +1,103 @@
package main
import (
"time"
"github.com/nsf/termbox-go"
"gopkg.in/inconshreveable/log15.v2"
)
const (
blankColor = termbox.ColorBlack
boardXOffset = 4
boardYOffset = 2
rankingFileName = "/tetris.db"
MinoPreview MinoType = iota
MinoCurrent = iota
MinoDrop = iota
)
type (
MinoType int
MinoBlocks [][]termbox.Attribute
MinoRotation [4]MinoBlocks
Mino struct {
x int
y int
length int
rotation int
minoRotation MinoRotation
}
Minos struct {
minoBag [7]MinoRotation
bagRand []int
bagIndex int
}
Board struct {
boardsIndex int
width int
height int
colors [][]termbox.Attribute
rotation [][]int
previewMino *Mino
currentMino *Mino
dropDistance int
}
Boards struct {
colors [][]termbox.Attribute
rotation [][]int
}
KeyInput struct {
stopped bool
chanStop chan struct{}
chanKeyInput chan *termbox.Event
}
View struct {
}
Ai struct {
queue *[]rune
newQueue *[]rune
index int
}
Ranking struct {
scores []uint64
}
Engine struct {
stopped bool
chanStop chan struct{}
keyInput *KeyInput
ranking *Ranking
timer *time.Timer
tickTime time.Duration
paused bool
gameOver bool
previewBoard bool
score int
level int
deleteLines int
ai *Ai
aiEnabled bool
aiTimer *time.Timer
}
)
var (
boards []Boards
baseDir string
logger log15.Logger
minos *Minos
board *Board
view *View
engine *Engine
)

View File

@ -1,15 +1,10 @@
package main
import (
"github.com/nsf/termbox-go"
"runtime"
)
type KeyInput struct {
stopped bool
chanStop chan struct{}
chanKeyInput chan *termbox.Event
}
"github.com/nsf/termbox-go"
)
func NewKeyInput() *KeyInput {
return &KeyInput{
@ -29,16 +24,11 @@ loop:
default:
}
event := termbox.PollEvent()
if event.Type == termbox.EventKey {
if event.Type == termbox.EventKey && len(keyInput.chanKeyInput) < 8 {
select {
case <-keyInput.chanStop:
break loop
default:
select {
case keyInput.chanKeyInput <- &event:
case <-keyInput.chanStop:
break loop
}
case keyInput.chanKeyInput <- &event:
}
}
}
@ -47,7 +37,6 @@ loop:
}
func (keyInput *KeyInput) ProcessEvent(event *termbox.Event) {
if event.Key == termbox.KeyCtrlI {
// ctrl i to log stack trace
buffer := make([]byte, 1<<16)
@ -66,8 +55,15 @@ func (keyInput *KeyInput) ProcessEvent(event *termbox.Event) {
}
if engine.gameOver {
if event.Key == termbox.KeySpace {
engine.NewGame()
if event.Ch == 0 {
switch event.Key {
case termbox.KeySpace:
engine.NewGame()
case termbox.KeyArrowLeft:
board.PreviousBoard()
case termbox.KeyArrowRight:
board.NextBoard()
}
}
return
}

81
mino.go
View File

@ -2,47 +2,22 @@ package main
import (
"math/rand"
"time"
"github.com/nsf/termbox-go"
)
type MinoType int
const (
MinoPreview MinoType = iota
MinoCurrent = iota
MinoDrop = iota
)
var (
bagRand []int
bagIndex int
)
func init() {
rand.Seed(time.Now().UnixNano())
bagRand = rand.Perm(7)
}
type Mino struct {
x int
y int
length int
rotation int
minoRotation MinoRotation
}
func NewMino() *Mino {
minoRotation := minoBag[bagRand[bagIndex]]
bagIndex++
if bagIndex > 6 {
bagIndex = 0
bagRand = rand.Perm(7)
minoRotation := minos.minoBag[minos.bagRand[minos.bagIndex]]
minos.bagIndex++
if minos.bagIndex > 6 {
minos.bagIndex = 0
minos.bagRand = rand.Perm(7)
}
mino := &Mino{
minoRotation: minoRotation,
length: len(minoRotation[0]),
}
mino.x = boardWidth/2 - (mino.length+1)/2
mino.x = board.width/2 - (mino.length+1)/2
mino.y = -1
return mino
}
@ -152,3 +127,43 @@ func (mino *Mino) DrawMino(minoType MinoType) {
}
}
}
func (mino *Mino) minoOverlap(mino1 *Mino) bool {
minoBlocks := mino.minoRotation[mino.rotation]
for i := 0; i < mino.length; i++ {
for j := 0; j < mino.length; j++ {
if minoBlocks[i][j] != blankColor {
if mino1.isMinoAtLocation(mino.x+i, mino.y+j) {
return true
}
}
}
}
return false
}
func (mino *Mino) isMinoAtLocation(x int, y int) bool {
xIndex := x - mino.x
yIndex := y - mino.y
if xIndex < 0 || xIndex >= mino.length || yIndex < 0 || yIndex >= mino.length {
return false
}
minoBlocks := mino.minoRotation[mino.rotation]
if minoBlocks[xIndex][yIndex] != blankColor {
return true
}
return false
}
func (mino *Mino) getMinoColorAtLocation(x int, y int) termbox.Attribute {
xIndex := x - mino.x
yIndex := y - mino.y
if xIndex < 0 || xIndex >= mino.length || yIndex < 0 || yIndex >= mino.length {
return blankColor
}
minoBlocks := mino.minoRotation[mino.rotation]
return minoBlocks[xIndex][yIndex]
}

120
mino_test.go Normal file
View File

@ -0,0 +1,120 @@
package main
import (
"math/rand"
"os"
"testing"
"gopkg.in/inconshreveable/log15.v2"
)
type testMinoStruct struct {
minoRotation MinoRotation
x int
y int
}
func TestMain(m *testing.M) {
setupForTesting()
retCode := m.Run()
os.Exit(retCode)
}
func setupForTesting() {
logger = log15.New()
logger.SetHandler(log15.StreamHandler(os.Stdout, log15.LogfmtFormat()))
rand.Seed(1)
NewMinos()
NewBoard()
NewEngine()
}
func TestMinoValidLocation(t *testing.T) {
// this must be set to the blank boards
for _, i := range []int{0, 3} {
board.boardsIndex = i
board.Clear()
tests := []struct {
info string
mino testMinoStruct
changeLocation bool
mustBeOnBoard bool
validLocation bool
}{
{info: "start 0 false", mino: testMinoStruct{minoRotation: minos.minoBag[0]}, mustBeOnBoard: false, validLocation: true},
{info: "start 0 true", mino: testMinoStruct{minoRotation: minos.minoBag[0]}, mustBeOnBoard: true, validLocation: true},
{info: "start 1 false", mino: testMinoStruct{minoRotation: minos.minoBag[1]}, mustBeOnBoard: false, validLocation: true},
{info: "start 1 true", mino: testMinoStruct{minoRotation: minos.minoBag[1]}, mustBeOnBoard: true, validLocation: false},
{info: "start 2 false", mino: testMinoStruct{minoRotation: minos.minoBag[2]}, mustBeOnBoard: false, validLocation: true},
{info: "start 2 true", mino: testMinoStruct{minoRotation: minos.minoBag[2]}, mustBeOnBoard: true, validLocation: false},
{info: "start 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3]}, mustBeOnBoard: false, validLocation: true},
{info: "start 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3]}, mustBeOnBoard: true, validLocation: false},
{info: "start 4 false", mino: testMinoStruct{minoRotation: minos.minoBag[4]}, mustBeOnBoard: false, validLocation: true},
{info: "start 4 true", mino: testMinoStruct{minoRotation: minos.minoBag[4]}, mustBeOnBoard: true, validLocation: false},
{info: "start 5 false", mino: testMinoStruct{minoRotation: minos.minoBag[5]}, mustBeOnBoard: false, validLocation: true},
{info: "start 5 true", mino: testMinoStruct{minoRotation: minos.minoBag[5]}, mustBeOnBoard: true, validLocation: false},
{info: "start 6 false", mino: testMinoStruct{minoRotation: minos.minoBag[6]}, mustBeOnBoard: false, validLocation: true},
{info: "start 6 true", mino: testMinoStruct{minoRotation: minos.minoBag[6]}, mustBeOnBoard: true, validLocation: false},
{info: "top left 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: 0}, changeLocation: true, mustBeOnBoard: false, validLocation: true},
{info: "top left 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: 0}, changeLocation: true, mustBeOnBoard: true, validLocation: true},
{info: "top right 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 2, y: 0}, changeLocation: true, mustBeOnBoard: false, validLocation: true},
{info: "top right 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 2, y: 0}, changeLocation: true, mustBeOnBoard: true, validLocation: true},
{info: "bottom right 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 2, y: board.height - 2}, changeLocation: true, mustBeOnBoard: false, validLocation: true},
{info: "bottom right 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 2, y: board.height - 2}, changeLocation: true, mustBeOnBoard: true, validLocation: true},
{info: "bottom left 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: board.height - 2}, changeLocation: true, mustBeOnBoard: false, validLocation: true},
{info: "bottom left 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: board.height - 2}, changeLocation: true, mustBeOnBoard: true, validLocation: true},
{info: "up 1 top left 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: -1}, changeLocation: true, mustBeOnBoard: false, validLocation: true},
{info: "up 1 top left 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: -1}, changeLocation: true, mustBeOnBoard: true, validLocation: false},
{info: "up 1 top right 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 2, y: -1}, changeLocation: true, mustBeOnBoard: false, validLocation: true},
{info: "up 1 top right 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 2, y: -1}, changeLocation: true, mustBeOnBoard: true, validLocation: false},
{info: "up 2 top left 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: -2}, changeLocation: true, mustBeOnBoard: false, validLocation: true},
{info: "up 2 top left 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: -2}, changeLocation: true, mustBeOnBoard: true, validLocation: false},
{info: "up 2 top right 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 2, y: -2}, changeLocation: true, mustBeOnBoard: false, validLocation: true},
{info: "up 2 top right 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 2, y: -2}, changeLocation: true, mustBeOnBoard: true, validLocation: false},
{info: "up 3 top left 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: -3}, changeLocation: true, mustBeOnBoard: false, validLocation: false},
{info: "up 3 top left 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: -3}, changeLocation: true, mustBeOnBoard: true, validLocation: false},
{info: "up 3 top right 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 2, y: -3}, changeLocation: true, mustBeOnBoard: false, validLocation: false},
{info: "up 3 top right 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 2, y: -3}, changeLocation: true, mustBeOnBoard: true, validLocation: false},
{info: "off top left 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: -1, y: 0}, changeLocation: true, mustBeOnBoard: false, validLocation: false},
{info: "off top left 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: -1, y: 0}, changeLocation: true, mustBeOnBoard: true, validLocation: false},
{info: "off top right 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 1, y: 0}, changeLocation: true, mustBeOnBoard: false, validLocation: false},
{info: "off top right 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 1, y: 0}, changeLocation: true, mustBeOnBoard: true, validLocation: false},
{info: "off bottom right 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 1, y: board.height - 2}, changeLocation: true, mustBeOnBoard: false, validLocation: false},
{info: "off bottom right 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 1, y: board.height - 2}, changeLocation: true, mustBeOnBoard: true, validLocation: false},
{info: "off bottom right 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 2, y: board.height - 1}, changeLocation: true, mustBeOnBoard: false, validLocation: false},
{info: "off bottom right 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 2, y: board.height - 1}, changeLocation: true, mustBeOnBoard: true, validLocation: false},
{info: "off bottom left 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: -1, y: board.height - 2}, changeLocation: true, mustBeOnBoard: false, validLocation: false},
{info: "off bottom left 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: -1, y: board.height - 2}, changeLocation: true, mustBeOnBoard: true, validLocation: false},
{info: "off bottom left 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: board.height - 1}, changeLocation: true, mustBeOnBoard: false, validLocation: false},
{info: "off bottom left 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: board.height - 1}, changeLocation: true, mustBeOnBoard: true, validLocation: false},
}
for _, test := range tests {
mino := NewMino()
mino.minoRotation = test.mino.minoRotation
mino.length = len(mino.minoRotation[0])
if test.changeLocation {
mino.x = test.mino.x
mino.y = test.mino.y
}
validLocation := mino.ValidLocation(test.mustBeOnBoard)
if validLocation != test.validLocation {
lines := board.getDebugBoardWithMino(mino)
for i := 0; i < len(lines); i++ {
t.Log(lines[i])
}
t.Errorf("MinoValidLocation validLocation - received: %v - expected: %v - info %v", validLocation, test.validLocation, test.info)
}
}
}
}

View File

@ -1,16 +1,12 @@
package main
import (
"math/rand"
"github.com/nsf/termbox-go"
)
type MinoBlocks [][]termbox.Attribute
type MinoRotation [4]MinoBlocks
var minoBag [7]MinoRotation
func init() {
func NewMinos() {
minoI := MinoBlocks{
[]termbox.Attribute{blankColor, termbox.ColorCyan, blankColor, blankColor},
[]termbox.Attribute{blankColor, termbox.ColorCyan, blankColor, blankColor},
@ -50,17 +46,17 @@ func init() {
var minoRotationI MinoRotation
minoRotationI[0] = minoI
for i := 1; i < 4; i++ {
minoRotationI[i] = initCloneRotateRight(minoRotationI[i-1])
minoRotationI[i] = minosCloneRotateRight(minoRotationI[i-1])
}
var minoRotationJ MinoRotation
minoRotationJ[0] = minoJ
for i := 1; i < 4; i++ {
minoRotationJ[i] = initCloneRotateRight(minoRotationJ[i-1])
minoRotationJ[i] = minosCloneRotateRight(minoRotationJ[i-1])
}
var minoRotationL MinoRotation
minoRotationL[0] = minoL
for i := 1; i < 4; i++ {
minoRotationL[i] = initCloneRotateRight(minoRotationL[i-1])
minoRotationL[i] = minosCloneRotateRight(minoRotationL[i-1])
}
var minoRotationO MinoRotation
minoRotationO[0] = minoO
@ -70,23 +66,26 @@ func init() {
var minoRotationS MinoRotation
minoRotationS[0] = minoS
for i := 1; i < 4; i++ {
minoRotationS[i] = initCloneRotateRight(minoRotationS[i-1])
minoRotationS[i] = minosCloneRotateRight(minoRotationS[i-1])
}
var minoRotationT MinoRotation
minoRotationT[0] = minoT
for i := 1; i < 4; i++ {
minoRotationT[i] = initCloneRotateRight(minoRotationT[i-1])
minoRotationT[i] = minosCloneRotateRight(minoRotationT[i-1])
}
var minoRotationZ MinoRotation
minoRotationZ[0] = minoZ
for i := 1; i < 4; i++ {
minoRotationZ[i] = initCloneRotateRight(minoRotationZ[i-1])
minoRotationZ[i] = minosCloneRotateRight(minoRotationZ[i-1])
}
minoBag = [7]MinoRotation{minoRotationI, minoRotationJ, minoRotationL, minoRotationO, minoRotationS, minoRotationT, minoRotationZ}
minos = &Minos{
minoBag: [7]MinoRotation{minoRotationI, minoRotationJ, minoRotationL, minoRotationO, minoRotationS, minoRotationT, minoRotationZ},
bagRand: rand.Perm(7),
}
}
func initCloneRotateRight(minoBlocks MinoBlocks) MinoBlocks {
func minosCloneRotateRight(minoBlocks MinoBlocks) MinoBlocks {
length := len(minoBlocks)
newMinoBlocks := make(MinoBlocks, length, length)
for i := 0; i < length; i++ {

View File

@ -8,21 +8,13 @@ import (
"strings"
)
const (
rankingFileName = "/tetris.db"
)
type Ranking struct {
scores []uint64
}
func NewRanking() *Ranking {
ranking := &Ranking{
scores: make([]uint64, 10),
scores: make([]uint64, 9),
}
if _, err := os.Stat(baseDir + rankingFileName); os.IsNotExist(err) {
for i := 0; i < 10; i++ {
for i := 0; i < 9; i++ {
ranking.scores[i] = 0
}
return ranking
@ -35,6 +27,9 @@ func NewRanking() *Ranking {
scoreStrings := strings.Split(string(scoreBytes), ",")
for index, scoreString := range scoreStrings {
if index > 8 {
break
}
score, err := strconv.ParseUint(scoreString, 10, 64)
if err != nil {
logger.Error("NewRanking ParseUint", "error", err.Error())
@ -49,7 +44,7 @@ func NewRanking() *Ranking {
func (ranking *Ranking) Save() {
var buffer bytes.Buffer
for i := 0; i < 10; i++ {
for i := 0; i < 9; i++ {
if i != 0 {
buffer.WriteRune(',')
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
screenshots/screenshot4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -1,24 +1,12 @@
package main
import (
"github.com/nsf/termbox-go"
"gopkg.in/inconshreveable/log15.v2"
"math/rand"
"os"
"path/filepath"
)
"time"
const (
boardWidth = 10
boardHeight = 20
blankColor = termbox.ColorBlack
)
var (
baseDir string
logger log15.Logger
view *View
engine *Engine
board *Board
"gopkg.in/inconshreveable/log15.v2"
)
func main() {
@ -29,8 +17,12 @@ func main() {
logger.SetHandler(log15.Must.FileHandler(baseDir+"/tetris.log", log15.LogfmtFormat()))
}
view = NewView()
engine = NewEngine()
rand.Seed(time.Now().UnixNano())
NewMinos()
NewBoard()
NewView()
NewEngine()
engine.Run()

146
view.go
View File

@ -2,27 +2,20 @@ package main
import (
"fmt"
"github.com/nsf/termbox-go"
"math/rand"
"time"
"github.com/nsf/termbox-go"
)
const (
boardXOffset = 4
boardYOffset = 2
)
type View struct {
drawDropMarkerDisabled bool
}
func NewView() *View {
func NewView() {
err := termbox.Init()
if err != nil {
panic(err)
}
termbox.SetInputMode(termbox.InputEsc)
termbox.Flush()
return &View{}
view = &View{}
}
func (view *View) Stop() {
@ -38,12 +31,18 @@ func (view *View) RefreshScreen() {
view.drawBackground()
view.drawTexts()
board.DrawPreviewMino()
if engine.gameOver {
if engine.previewBoard {
board.DrawBoard()
view.drawGameOver()
} else if engine.gameOver {
view.drawGameOver()
view.drawRankingScores()
} else if engine.paused {
view.drawPaused()
} else {
board.DrawBoard()
board.DrawPreviewMino()
board.DrawDropMino()
board.DrawCurrentMino()
}
@ -55,8 +54,8 @@ func (view *View) drawBackground() {
// playing board
xOffset := boardXOffset
yOffset := boardYOffset
xEnd := boardXOffset + boardWidth*2 + 4
yEnd := boardYOffset + boardHeight + 2
xEnd := boardXOffset + board.width*2 + 4
yEnd := boardYOffset + board.height + 2
for x := xOffset; x < xEnd; x++ {
for y := yOffset; y < yEnd; y++ {
if x == xOffset || x == xOffset+1 || x == xEnd-1 || x == xEnd-2 ||
@ -69,7 +68,7 @@ func (view *View) drawBackground() {
}
// piece preview
xOffset = boardXOffset + boardWidth*2 + 8
xOffset = boardXOffset + board.width*2 + 8
yOffset = boardYOffset
xEnd = xOffset + 14
yEnd = yOffset + 6
@ -87,7 +86,7 @@ func (view *View) drawBackground() {
}
func (view *View) drawTexts() {
xOffset := boardXOffset + boardWidth*2 + 8
xOffset := boardXOffset + board.width*2 + 8
yOffset := boardYOffset + 7
view.drawText(xOffset, yOffset, "SCORE:", termbox.ColorWhite, termbox.ColorBlue)
@ -135,7 +134,7 @@ func (view *View) DrawPreviewMinoBlock(x int, y int, color termbox.Attribute, ro
char1 = ' '
char2 = '▓'
}
xOffset := 2*x + 2*boardWidth + boardXOffset + 11 + (4 - length)
xOffset := 2*x + 2*board.width + boardXOffset + 11 + (4 - length)
termbox.SetCell(xOffset, y+boardYOffset+2, char1, color, color^termbox.AttrBold)
termbox.SetCell(xOffset+1, y+boardYOffset+2, char2, color, color^termbox.AttrBold)
}
@ -160,17 +159,32 @@ func (view *View) DrawBlock(x int, y int, color termbox.Attribute, rotation int)
}
}
func (view *View) drawGameOver() {
xOffset := boardXOffset + 4
yOffset := boardYOffset + 2
func (view *View) drawPaused() {
yOffset := (board.height+1)/2 + boardYOffset
view.drawTextCenter(yOffset, "Paused", termbox.ColorWhite, termbox.ColorBlack)
}
view.drawText(xOffset, yOffset, " GAME OVER", termbox.ColorWhite, termbox.ColorBlack)
func (view *View) drawGameOver() {
yOffset := boardYOffset + 2
view.drawTextCenter(yOffset, " GAME OVER", termbox.ColorWhite, termbox.ColorBlack)
yOffset += 2
view.drawText(xOffset, yOffset, "sbar for new game", termbox.ColorWhite, termbox.ColorBlack)
view.drawTextCenter(yOffset, "sbar for new game", termbox.ColorWhite, termbox.ColorBlack)
if engine.previewBoard {
return
}
yOffset += 2
xOffset += 2
// ascii arrow characters add extra two spaces
view.drawTextCenter(yOffset, "←previous board", termbox.ColorWhite, termbox.ColorBlack)
yOffset += 2
view.drawTextCenter(yOffset, "→next board", termbox.ColorWhite, termbox.ColorBlack)
}
func (view *View) drawRankingScores() {
yOffset := boardYOffset + 10
for index, line := range engine.ranking.scores {
view.drawText(xOffset, yOffset+index, fmt.Sprintf("%2d: %6d", index+1, line), termbox.ColorWhite, termbox.ColorBlack)
view.drawTextCenter(yOffset+index, fmt.Sprintf("%1d: %6d", index+1, line), termbox.ColorWhite, termbox.ColorBlack)
}
}
@ -180,9 +194,14 @@ func (view *View) drawText(x int, y int, text string, fg termbox.Attribute, bg t
}
}
func (view *View) ShowDeleteAnimation(lines []int) {
view.drawDropMarkerDisabled = true
func (view *View) drawTextCenter(y int, text string, fg termbox.Attribute, bg termbox.Attribute) {
xOffset := board.width - (len(text)+1)/2 + boardXOffset + 2
for index, char := range text {
termbox.SetCell(index+xOffset, y, rune(char), fg, bg)
}
}
func (view *View) ShowDeleteAnimation(lines []int) {
view.RefreshScreen()
for times := 0; times < 3; times++ {
@ -195,26 +214,77 @@ func (view *View) ShowDeleteAnimation(lines []int) {
view.RefreshScreen()
time.Sleep(140 * time.Millisecond)
}
view.drawDropMarkerDisabled = false
}
func (view *View) ShowGameOverAnimation() {
view.drawDropMarkerDisabled = true
logger.Info("View ShowGameOverAnimation start")
view.RefreshScreen()
switch rand.Intn(3) {
case 0:
for y := board.height - 1; y >= 0; y-- {
view.colorizeLine(y, termbox.ColorBlack)
termbox.Flush()
time.Sleep(60 * time.Millisecond)
}
for y := boardHeight - 1; y >= 0; y-- {
view.colorizeLine(y, termbox.ColorBlack)
termbox.Flush()
time.Sleep(60 * time.Millisecond)
case 1:
for y := 0; y < board.height; y++ {
view.colorizeLine(y, termbox.ColorBlack)
termbox.Flush()
time.Sleep(60 * time.Millisecond)
}
case 2:
sleepTime := 50 * time.Millisecond
topStartX := boardXOffset + 3
topEndX := board.width*2 + boardXOffset + 1
topY := boardYOffset + 1
rightStartY := boardYOffset + 1
rightEndY := board.height + boardYOffset + 1
rightX := board.width*2 + boardXOffset + 1
bottomStartX := topEndX - 1
bottomEndX := topStartX - 1
bottomY := board.height + boardYOffset
leftStartY := rightEndY - 1
leftEndY := rightStartY - 1
leftX := boardXOffset + 2
for topStartX <= topEndX && rightStartY <= rightEndY {
for x := topStartX; x < topEndX; x++ {
termbox.SetCell(x, topY, ' ', termbox.ColorBlack, termbox.ColorBlack)
}
topStartX++
topEndX--
topY++
for y := rightStartY; y < rightEndY; y++ {
termbox.SetCell(rightX, y, ' ', termbox.ColorBlack, termbox.ColorBlack)
}
rightStartY++
rightEndY--
rightX--
for x := bottomStartX; x > bottomEndX; x-- {
termbox.SetCell(x, bottomY, ' ', termbox.ColorBlack, termbox.ColorBlack)
}
bottomStartX--
bottomEndX++
bottomY--
for y := leftStartY; y > leftEndY; y-- {
termbox.SetCell(leftX, y, ' ', termbox.ColorBlack, termbox.ColorBlack)
}
leftStartY--
leftEndY++
leftX++
termbox.Flush()
time.Sleep(sleepTime)
sleepTime += 4 * time.Millisecond
}
}
view.drawDropMarkerDisabled = false
logger.Info("View ShowGameOverAnimation end")
}
func (view *View) colorizeLine(y int, color termbox.Attribute) {
for x := 0; x < boardWidth; x++ {
for x := 0; x < board.width; x++ {
termbox.SetCell(x*2+boardXOffset+2, y+boardYOffset+1, ' ', termbox.ColorDefault, color)
termbox.SetCell(x*2+boardXOffset+3, y+boardYOffset+1, ' ', termbox.ColorDefault, color)
}