Skip to content

Commit 970971f

Browse files
committed
✨ (mine-sweeper): add restart logic and test
1 parent ea47c35 commit 970971f

File tree

3 files changed

+149
-30
lines changed

3 files changed

+149
-30
lines changed

internal/game/game_restart_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package game
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestGameRestart(t *testing.T) {
10+
const (
11+
rows = 5
12+
cols = 5
13+
mineCount = 5
14+
)
15+
16+
predicableMineShuffler := func(coords []coord) {
17+
// not shuffler
18+
}
19+
newGameWithPredictableMines := func() *Game {
20+
game := NewGame(rows, cols, mineCount)
21+
game.Board.minePositionShuffler = predicableMineShuffler
22+
game.Board.cells = make([][]*Cell, rows)
23+
for r := range game.Board.cells {
24+
game.Board.cells[r] = make([]*Cell, cols)
25+
for c := range game.Board.cells[r] {
26+
game.Board.cells[r][c] = &Cell{}
27+
}
28+
}
29+
game.Board.mineCoords = []coord{}
30+
game.Board.PlaceMines(mineCount)
31+
game.Board.CalculateAdjacentMines()
32+
return game
33+
}
34+
// 1. create an initial game state for comparison
35+
gameInitial := newGameWithPredictableMines()
36+
37+
// 2. create another game to manipulate
38+
gamePlaying := newGameWithPredictableMines()
39+
40+
// 3. Change the state of gamePlaying
41+
// Let's reveal a safe cell (a cell that is not a mine)
42+
// With our predictable shuffler, mines are at (0,0), (0,1), (0,2), (0,3), (0,4)
43+
// Let's reveal cell (4,4) which is safe
44+
gamePlaying.Board.Reveal(4, 4)
45+
46+
assert.NotEqual(t, gameInitial.Board.cells, gamePlaying.Board.cells)
47+
48+
// 5 Simulate a restart by create a new game
49+
restartedGame := NewGame(rows, cols, mineCount)
50+
restartedGame.Board.minePositionShuffler = predicableMineShuffler
51+
restartedGame.Board.minePositionShuffler = predicableMineShuffler
52+
restartedGame.Board.cells = make([][]*Cell, rows)
53+
for r := range restartedGame.Board.cells {
54+
restartedGame.Board.cells[r] = make([]*Cell, cols)
55+
for c := range restartedGame.Board.cells[r] {
56+
restartedGame.Board.cells[r][c] = &Cell{}
57+
}
58+
}
59+
restartedGame.Board.mineCoords = []coord{}
60+
restartedGame.Board.PlaceMines(mineCount)
61+
restartedGame.Board.CalculateAdjacentMines()
62+
63+
// verify that the restarted game state is identical to intial game state
64+
assert.Equal(t, gameInitial.Board.cells, restartedGame.Board.cells)
65+
assert.Equal(t, gameInitial.IsGameOver, restartedGame.IsGameOver)
66+
assert.Equal(t, gameInitial.Board.GetRemainingFlags(), restartedGame.Board.GetRemainingFlags())
67+
}

internal/layout/font.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ func init() {
3232
const (
3333
IsMine = iota + 100
3434
IsFlag
35+
IsButtonIcon
3536
)
3637

3738
func getTileColor(value int) color.Color {
@@ -40,7 +41,7 @@ func getTileColor(value int) color.Color {
4041
return color.RGBA{0x77, 0x6e, 0x65, 0xff}
4142
case IsFlag:
4243
return color.RGBA{0xf9, 0xf6, 0xf2, 0xff}
43-
case IsMine:
44+
case IsMine, IsButtonIcon:
4445
return color.Black
4546
default:
4647
return color.RGBA{0xf9, 0xf6, 0xf2, 0xff}

internal/layout/layout.go

Lines changed: 80 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package layout
22

33
import (
44
"fmt"
5+
"image"
56
"image/color"
67

78
"github.com/hajimehoshi/ebiten/v2"
@@ -15,14 +16,16 @@ const (
1516
gridSize = 32
1617
Rows = 10
1718
Cols = 10
18-
PanelHeight = 36 // 上方面板高度
19-
PaddingX = 140 // 面板內文字左邊距
20-
PaddingY = 20 // 面板
19+
PanelHeight = 36 // 上方面板高度
20+
PaddingX = 32 // 面板內文字左邊距
21+
PaddingY = 20 // 面板
2122
ScreenHeight = PanelHeight + gridSize*Rows
2223
ScreenWidth = gridSize * Cols
2324
MineCounts = 9
2425
)
2526

27+
var buttonRectRelativePos = image.Rect(0, 0, 32, 32) // 一個方格大小的 button
28+
2629
type Coord struct {
2730
Row int
2831
Col int
@@ -37,6 +40,16 @@ func NewGameLayout(gameInstance *game.Game) *GameLayout {
3740
}
3841

3942
func (g *GameLayout) Update() error {
43+
// 偵測 restart icon 有被點擊
44+
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
45+
xPos, yPos := ebiten.CursorPosition()
46+
if xPos >= ((ScreenWidth-1.5*gridSize)/2+buttonRectRelativePos.Min.X) &&
47+
xPos <= (ScreenWidth)/2+buttonRectRelativePos.Max.X+0.5*gridSize &&
48+
yPos >= buttonRectRelativePos.Min.Y &&
49+
yPos <= buttonRectRelativePos.Max.Y+3 {
50+
g.Restart()
51+
}
52+
}
4053
// 當狀態為遊戲結束
4154
if g.gameInstance.IsGameOver || g.gameInstance.IsPlayerWin {
4255
return nil
@@ -207,61 +220,94 @@ func (g *GameLayout) drawBoard(screen *ebiten.Image) {
207220
}
208221
}
209222

210-
// drawRemainFlag
211-
func (g *GameLayout) drawRemainFlag(screen *ebiten.Image) {
212-
status, bgColor := g.getColorStatus()
223+
// drawGamePanel - 繪製遊戲狀態面板
224+
func (g *GameLayout) drawGamePanel(screen *ebiten.Image) {
225+
emojiIcon, bgColor := g.getColorStatus()
213226
panel := ebiten.NewImage(ScreenWidth, PanelHeight)
214227
panel.Fill(bgColor)
215228
screen.DrawImage(panel, nil)
216-
// 畫旗子面板(固定在上方)
217-
textValue := fmt.Sprintf("Flags: %d/%d, Status: %s", g.gameInstance.Board.GetRemainingFlags(), MineCounts, status)
218-
textXPos := PaddingX
229+
// 畫旗子面板(固定在左方)
230+
g.drawRemainingFlagInfo(screen)
231+
// 畫顯示狀態 button
232+
g.drawButtonWithIcon(screen, emojiIcon)
233+
}
234+
235+
// drawButtonWithIcon - 繪製 buttonIcon
236+
func (g *GameLayout) drawButtonWithIcon(screen *ebiten.Image, emojiIcon string) {
237+
vector.DrawFilledRect(screen,
238+
float32((ScreenWidth-1.5*gridSize)/2+buttonRectRelativePos.Min.X),
239+
float32(buttonRectRelativePos.Min.Y),
240+
float32(buttonRectRelativePos.Dx()+0.5*gridSize),
241+
float32(buttonRectRelativePos.Dy()+3),
242+
color.RGBA{120, 120, 120, 255},
243+
true,
244+
)
245+
vector.DrawFilledCircle(screen, ScreenWidth/2, gridSize/2, 16,
246+
color.RGBA{180, 180, 0, 255},
247+
true,
248+
)
249+
emojiValue := emojiIcon
250+
emojiXPos := (ScreenWidth) / 2
251+
emojiYPos := PaddingY
252+
emojiOpts := &text.DrawOptions{}
253+
emojiOpts.ColorScale.ScaleWithColor(getTileColor(IsButtonIcon))
254+
emojiOpts.PrimaryAlign = text.AlignCenter
255+
emojiOpts.SecondaryAlign = text.AlignCenter
256+
emojiOpts.GeoM.Translate(float64(emojiXPos), float64(emojiYPos))
257+
text.Draw(screen, emojiValue, &text.GoTextFace{
258+
Source: emojiFaceSource,
259+
Size: 32,
260+
}, emojiOpts)
261+
}
262+
263+
// drawRemainingFlagInfo
264+
func (g *GameLayout) drawRemainingFlagInfo(screen *ebiten.Image) {
265+
// 畫旗子面板(固定在左方)
266+
textValue := fmt.Sprintf("%03d", g.gameInstance.Board.GetRemainingFlags())
267+
textXPos := PaddingX + len(textValue)
219268
textYPos := PaddingY
220269
textOpts := &text.DrawOptions{}
221270
textOpts.ColorScale.ScaleWithColor(getTileColor(-1))
222-
textOpts.PrimaryAlign = text.AlignCenter
271+
textOpts.PrimaryAlign = text.AlignStart
223272
textOpts.SecondaryAlign = text.AlignCenter
224273
textOpts.GeoM.Translate(float64(textXPos), float64(textYPos))
225274
text.Draw(screen, textValue, &text.GoTextFace{
226275
Source: mplusFaceSource,
227276
Size: 20,
228277
}, textOpts)
278+
emojiValue := "🚩"
279+
emojiXPos := len(emojiValue)
280+
emojiYPos := PaddingY
281+
emojiOpts := &text.DrawOptions{}
282+
emojiOpts.ColorScale.ScaleWithColor(getTileColor(-1))
283+
emojiOpts.PrimaryAlign = text.AlignStart
284+
emojiOpts.SecondaryAlign = text.AlignCenter
285+
emojiOpts.GeoM.Translate(float64(emojiXPos), float64(emojiYPos))
286+
text.Draw(screen, emojiValue, &text.GoTextFace{
287+
Source: emojiFaceSource,
288+
Size: 20,
289+
}, emojiOpts)
229290
}
230291

231292
func (g *GameLayout) Draw(screen *ebiten.Image) {
232293
g.drawBoard(screen)
233-
g.drawRemainFlag(screen)
234-
if g.gameInstance.IsGameOver {
235-
g.drawCoverOnGameOver(screen)
236-
}
294+
g.drawGamePanel(screen)
237295
}
238296

239297
func (g *GameLayout) Layout(outsideWidth, outsideHeight int) (int, int) {
240298
return ScreenWidth, ScreenHeight
241299
}
242300

243-
// drawCoverOnGameOver - 畫出無法操作的灰色遮罩
244-
func (g *GameLayout) drawCoverOnGameOver(screen *ebiten.Image) {
245-
w, h := ScreenWidth, ScreenHeight-PanelHeight
246-
vector.DrawFilledRect(
247-
screen,
248-
0, PanelHeight, // x, y
249-
float32(w), float32(h), // width, height
250-
color.RGBA{0, 0, 0, 128}, // 半透明黑色 (128 = 約 50% 透明)
251-
false,
252-
)
253-
}
254-
255301
// getColorStatus - 根據 IsGameOver 與 IsPlayerWin 來找出對 message, bgColor
256302
func (g *GameLayout) getColorStatus() (string, color.RGBA) {
257303
bgColor := color.RGBA{100, 100, 0x10, 0xFF}
258-
status := "playing"
304+
status := "😀"
259305
if g.gameInstance.IsGameOver {
260-
status = "game over"
306+
status = "😵"
261307
bgColor = color.RGBA{150, 0, 0x10, 0xFF}
262308
}
263309
if g.gameInstance.IsPlayerWin {
264-
status = "you win"
310+
status = "😎"
265311
bgColor = color.RGBA{200, 200, 0, 0xFF}
266312
}
267313
return status, bgColor
@@ -281,3 +327,8 @@ func (g *GameLayout) handlePositionClickEvent(listenHandler func(row, col int))
281327
}
282328
}
283329
}
330+
331+
// Restart - 重新建立 Game 狀態
332+
func (g *GameLayout) Restart() {
333+
g.gameInstance = game.NewGame(Rows, Cols, MineCounts)
334+
}

0 commit comments

Comments
 (0)