-
Notifications
You must be signed in to change notification settings - Fork 0
/
game.go
249 lines (227 loc) · 6.5 KB
/
game.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
package blokus
import (
"fmt"
)
const (
maxBoardSize = 100
)
var (
neighbors = [4]Coord{
{-1, 0},
{1, 0},
{0, -1},
{0, 1},
}
)
type Game struct {
Players []*Player
Board *Board
// Set of pieces every player starts with.
Pieces []*Piece
// Index of the player whose turn it is.
CurPlayerIndex int
// Moves that have been played.
Moves []*Move
}
func NewGame(size int, pieces []*Piece) (*Game, error) {
if size <= 0 || size > maxBoardSize {
return nil, fmt.Errorf("Board size must be between 1 and %v. Provided: %v", maxBoardSize, size)
}
if len(pieces) == 0 {
return nil, fmt.Errorf("Cannot create game with no pieces")
}
b, err := NewBoard(size)
if err != nil {
return nil, fmt.Errorf("Could not create game board: %v", err)
}
return &Game{
Board: b,
Pieces: pieces,
}, nil
}
func (g *Game) CurrentPlayer() *Player {
return g.Players[g.CurPlayerIndex]
}
func (g *Game) GetNextFreeColor() (Color, error) {
allColors := make([]bool, int(colorEnd))
for _, p := range g.Players {
if !p.Color.IsColored() {
return 0, fmt.Errorf("Player %v has invalid color: %v", p.Name, p.Color)
}
allColors[int(p.Color)] = true
}
for i := 1; i < int(colorEnd); i++ {
if !allColors[i] {
return Color(i), nil
}
}
return 0, fmt.Errorf("No more free colors")
}
func (g *Game) AddPlayer(name string, color Color, startPos Coord) error {
if len(name) == 0 {
return fmt.Errorf("Player name cannot be empty")
}
if color == colorEmpty {
var err error
color, err = g.GetNextFreeColor()
if err != nil {
return err
}
}
if !color.IsColored() {
return fmt.Errorf("Invalid color %v", color)
}
if g.Board.IsOutOfBounds(startPos) {
return fmt.Errorf("Starting position is out of bounds: %v", startPos)
}
for _, p := range g.Players {
if p.Name == name {
return fmt.Errorf("Player %v already in the game", name)
}
if p.Color == color {
return fmt.Errorf("Color %v already taken by player %v", color, p.Name)
}
if p.StartPos == startPos {
return fmt.Errorf("Starting position already occupied by player %v", p.Name)
}
}
p, err := NewPlayer(name, color, startPos, len(g.Pieces))
if err != nil {
return fmt.Errorf("Error adding new player: %v", err)
}
g.Players = append(g.Players, p)
return nil
}
func (g *Game) PassTurn(player *Player) error {
if player == nil {
return fmt.Errorf("Invalid player")
}
// Check if it's this player's turn.
if player != g.Players[g.CurPlayerIndex] {
return fmt.Errorf("Turn belongs to player %v, not player %v", g.Players[g.CurPlayerIndex].Name, player.Name)
}
// Record the move.
g.Moves = append(g.Moves, &Move{
Player: player,
PieceIndex: -1,
})
return nil
}
// Place the piece on the board and record the move, unless there's an error.
// This does not check for winner nor advance player turn.
func (g *Game) PlacePiece(player *Player, pieceIndex int, orient Orientation, loc Coord) error {
if player == nil {
return fmt.Errorf("Invalid player")
}
// Check if it's this player's turn.
if player != g.Players[g.CurPlayerIndex] {
return fmt.Errorf("Turn belongs to player %v, not player %v", g.Players[g.CurPlayerIndex].Name, player.Name)
}
if pieceIndex < 0 || pieceIndex >= len(g.Pieces) {
return fmt.Errorf("Piece index is out of range: %d", pieceIndex)
}
if err := player.CheckPiecePlaceability(pieceIndex); err != nil {
return err
}
piece := g.Pieces[pieceIndex]
if piece == nil {
return fmt.Errorf("Piece at index %d is inexplicably nil", pieceIndex)
}
orientedPiece := &Piece{
Blocks: orient.TransformCoords(piece.Blocks),
corners: orient.TransformCoords(piece.corners),
}
if err := g.checkPiecePlacement(player, orientedPiece, loc); err != nil {
return err
}
// Actually place the piece.
if err := player.placePiece(pieceIndex); err != nil {
return err
}
for _, b := range orientedPiece.Blocks {
g.Board.SetCell(Coord{loc.X + b.X, loc.Y + b.Y}, player.Color)
}
// Record the move.
g.Moves = append(g.Moves, &Move{
Player: player,
PieceIndex: pieceIndex,
Orient: orient,
Loc: loc,
})
return nil
}
// Checks whether piece placement is valid. Returns error if invalid.
// The piece should already be oriented.
func (g *Game) checkPiecePlacement(player *Player, piece *Piece, loc Coord) error {
coversStartPos := false
for _, b := range piece.Blocks {
// Change from relative to absolute coordinate.
b = Coord{b.X + loc.X, b.Y + loc.Y}
// Check that every block is inside the board
if g.Board.IsOutOfBounds(b) {
return fmt.Errorf("Piece placement out of bounds")
}
// Check that every block is on an empty space
if g.Board.Cell(b).IsColored() {
return fmt.Errorf("Cell (%v,%v) is occupied by color %v", b.X, b.Y, g.Board.Cell(b))
}
// Check that every block is not next to a piece of same color
for _, n := range neighbors {
n = Coord{b.X + n.X, b.Y + n.Y}
if g.Board.IsOutOfBounds(n) {
continue
}
if g.Board.Cell(n) == player.Color {
return fmt.Errorf("Piece is next to another %v piece", player.Color)
}
}
// Check if this is the player's starting position.
if b == player.StartPos {
coversStartPos = true
}
}
if coversStartPos {
// Should never reach here for non-first moves, since the first move necessarily must cover the starting position,
// so subsequent moves that cover the starting position will fail the earlier check for occupied cells.
return nil
}
hasValidCorner := false
for _, c := range piece.Corners() {
// Change from relative to absolute coordinate.
c = Coord{c.X + loc.X, c.Y + loc.Y}
if g.Board.IsOutOfBounds(c) {
continue
}
// Check that at least one corner is touching a block of same color.
if g.Board.Cell(c) == player.Color {
hasValidCorner = true
break
}
}
if !hasValidCorner {
// TODO: Make error message more specific.
return fmt.Errorf("Piece has no corner touching another %v piece, and it doesn't cover the player's starting position %v", player.Color, player.StartPos)
}
return nil
}
// Advances the game turn to the next player.
func (g *Game) AdvanceTurn() error {
if len(g.Players) == 0 {
return fmt.Errorf("Cannot advance turn with no players")
}
g.CurPlayerIndex = (g.CurPlayerIndex + 1) % len(g.Players)
return nil
}
// Game ends when all players passed for a round.
func (g *Game) IsGameEnd() bool {
if len(g.Moves) == 0 || len(g.Moves) < len(g.Players) {
return false
}
for _, m := range g.Moves[len(g.Moves)-len(g.Players):] {
// TODO: Refine logic so players who finished early don't have to keep passing.
if !m.IsPass() {
return false
}
}
return true
}