-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.go
220 lines (200 loc) · 5.11 KB
/
main.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
package doric
import (
"fmt"
"time"
)
// Possible commands coming from the player
const (
// Move the current column left
CommandLeft = iota
// Move the current column right
CommandRight
// Move the current column down
CommandDown
// Rotate tiles in current column
CommandRotate
// Pause / unpause game (for player use)
CommandPauseSwitch
// Pause / unpause game (intended for internal use, e. g. stop game logic while play animation)
CommandWaitSwitch
// Quit game
CommandQuit
)
// Possible returned errors
const (
errorNegativeNumberTilesForNextLevel = "NumberTilesForNextLevel must be equal or greater than 0"
errorLessEqualZeroInitialSpeed = "InitialSpeed must be greater than 0"
errorNegativeSpeedIncrement = "SpeedIncrement must be equal or greater than 0"
errorLessEqualZeroMaxSpeed = "MaxSpeed must be greater than 0"
)
const nanosecond = 1000000000
// Config holds different parameters related with the game
type Config struct {
// How many tiles a player has to destroy to advance to the next level
// Must be equal or greater than zero.
NumberTilesForNextLevel int
// InitialSpeed is the falling speed at the beginning of the game in cells/second.
// Must be greater than zero.
InitialSpeed float64
// SpeedIncrement is how much the speed increases each level in cells/second
// Must be equal or greater than zero.
SpeedIncrement float64
// MaxSpeed is the maximum speed falling columns can reach
// Must be greater than zero.
MaxSpeed float64
}
type game struct {
well Well
column *Column
nextTileset [3]int
level int
paused bool
wait bool
totalRemoved int
cfg Config
events chan interface{}
speed float64
ticker *time.Ticker
build TilesetBuilder
}
// Play starts the game loop in a separate thread, making columns fall to the bottom of the well at gradually quicker speeds
// as level increases.
// Game can be controlled sending command codes to the commands channel. Game updates are communicated as events in the returned
// channel.
// Game ends when no more new columns can enter the well, and this will be signaled with the closing of the
// events channel.
func Play(p Well, builder TilesetBuilder, cfg Config, commands <-chan int) (<-chan interface{}, error) {
game, err := newGame(p, builder, cfg)
if err != nil {
return nil, err
}
go func() {
defer func() {
close(game.events)
game.ticker.Stop()
}()
game.renewColumn()
for {
select {
case comm := <-commands:
if comm == CommandQuit {
return
}
game.execute(comm)
case <-game.ticker.C:
if game.paused || game.wait {
continue
}
if game.column.down(game.well) {
game.events <- EventUpdated{
Column: *game.column,
}
continue
}
game.removeLines()
game.renewColumn()
if game.isOver() {
return
}
}
}
}()
return game.events, nil
}
func newGame(p Well, build TilesetBuilder, cfg Config) (*game, error) {
if err := validateConfig(cfg); err != nil {
return nil, err
}
return &game{
well: p.copy(),
column: &Column{Tileset: [3]int{}},
nextTileset: build(maxTile),
level: 1,
cfg: cfg,
events: make(chan interface{}),
speed: cfg.InitialSpeed,
ticker: time.NewTicker(time.Duration(nanosecond / cfg.InitialSpeed)),
build: build,
}, nil
}
func validateConfig(cfg Config) error {
if cfg.NumberTilesForNextLevel < 0 {
return fmt.Errorf(errorNegativeNumberTilesForNextLevel)
}
if cfg.InitialSpeed <= 0 {
return fmt.Errorf(errorLessEqualZeroInitialSpeed)
}
if cfg.SpeedIncrement < 0 {
return fmt.Errorf(errorNegativeSpeedIncrement)
}
if cfg.MaxSpeed <= 0 {
return fmt.Errorf(errorLessEqualZeroMaxSpeed)
}
return nil
}
func (g *game) execute(comm int) {
if comm == CommandWaitSwitch {
g.wait = !g.wait
return
}
if comm == CommandPauseSwitch {
g.paused = !g.paused
return
}
if !g.paused && !g.wait {
switch comm {
case CommandLeft:
g.column.left(g.well)
case CommandRight:
g.column.right(g.well)
case CommandDown:
g.column.down(g.well)
case CommandRotate:
g.column.rotate()
}
}
g.events <- EventUpdated{
Column: *g.column,
}
}
func (g *game) removeLines() {
g.well.lock(g.column)
removed := g.well.markTilesToRemove()
combo := 1
for removed > 0 {
g.totalRemoved += removed
if g.totalRemoved/g.cfg.NumberTilesForNextLevel > g.level-1 {
g.level++
g.speedUp()
}
g.events <- EventScored{
Well: g.well.copy(),
Combo: combo,
Level: g.level,
Removed: removed,
}
combo++
g.well.settle()
removed = g.well.markTilesToRemove()
}
}
func (g *game) speedUp() {
speed := g.speed + g.cfg.SpeedIncrement
if speed < g.cfg.MaxSpeed {
g.ticker.Stop()
g.speed = speed
g.ticker = time.NewTicker(time.Duration(nanosecond / speed))
}
}
func (g *game) isOver() bool {
return g.well[g.well.width()/2][0] != Empty
}
func (g *game) renewColumn() {
g.column.reset(g.nextTileset, g.well.width()/2)
g.nextTileset = g.build(maxTile)
g.events <- EventRenewed{
Well: g.well.copy(),
Column: *g.column,
NextTileset: g.nextTileset,
}
}