-
-
Notifications
You must be signed in to change notification settings - Fork 141
/
README.md
157 lines (123 loc) · 6.64 KB
/
README.md
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
# Step 10: Ghosts with power ups!
In this lesson you will learn how to:
- Use a timer
- When and how to use a Mutex lock
## Overview
In this lesson we will be adding support for the power up pill to the application. We will update the configuration with the new setups and add code to draw the pill in the maze. We will also manage the process after pacman swallows a pill and collides with a ghost. Finally, we will manipulate cases where pacman tries to swallow a pill while the previous one is active and how to tackle this.
## Task 01: Drawing the Pills
Before we even start we should update the configuration to support power up pills! So for both `config_noemoji.json` and `config.json` we have to add the `ghost_blue` (string) and the `pill_duration_secs` (int) configurations.
Accordingly we update our `Config` struct:
```go
type config struct {
...
GhostBlue string `json:"ghost_blue"`
PillDurationSecs time.Duration `json:"pill_duration_secs"`
}
```
## Task 02: Enable Pill swallowing
To enable the pill swallowing by pacman we should add another case in the `movePlayer` func for the pill case
```go
case 'X':
score += 10
removeDot(player.row, player.col)
go processPill()
```
Where `X` is the pill config character.
Now, before moving to the `processPill` func, we should add some more code for the ghosts to support the 'Blue Ghosts'! We should add a new `GhostStatus` of string type which will hold the status of a ghost. The two statuses we have to support are the `Normal` and the `Blue`.
```go
type GhostStatus string
const (
GhostStatusNormal GhostStatus = "Normal"
GhostStatusBlue GhostStatus = "Blue"
)
```
Now, each ghost should hold alongside with it's current position, the `initialPosition` where it will be spawned after it's been eaten by the pacman and it's current status.
```go
type ghost struct {
position sprite
status GhostStatus
}
```
So, the `loadMaze` func will initially draw the ghosts with the `Normal` status and store it's initial position.
```go
ghosts = append(ghosts, &ghost{sprite{row, col, row, col}, GhostStatusNormal})
```
The `printScreen` func should be updated as well to support printing ghost of both types - Normal and Blue ghosts!
```go
for _, g := range ghosts {
moveCursor(g.position.row, g.position.col)
if g.status == GhostStatusNormal {
fmt.Printf(cfg.Ghost)
} else if g.status == GhostStatusBlue {
fmt.Printf(cfg.GhostBlue)
}
}
```
The last thing that has left is the `processPill` func we added just before. This func should change all Ghosts' status to `Blue` for the defined period by the `PillDurationSecs` config.
For the pill processing we are going to use a `Timer` from the ['time' package](https://golang.org/pkg/time/). We will use the `NewTimer` func which creates a new Timer that will send the current time on its channel after at least the specified duration.
The processPill code changes all ghosts' statuses to `GhostStatusBlue`, then it blocks for `PillDurationSecs` and then changes back all ghosts' statuses back to `GhostStatusNormal`.
```go
var pillTimer *time.Timer
func processPill() {
for _, g := range ghosts {
g.status = GhostStatusBlue
}
pillTimer = time.NewTimer(time.Second * cfg.PillDurationSecs)
<-pillTimer.C
for _, g := range ghosts {
g.status = GhostStatusNormal
}
}
```
## Task 03: Support simultaneous pill swallowing
The `processPill` function we discussed just before has a simple issue. Imagine what can happen if pacman tries to swallow a power-up pill while another pill is still active! Currently, with the proposed `processPill` function, when a second pill is being swallowed by the pacman, while the first on is still active, when the 1st pill's effect ends (after PillDurationSecs) all ghosts will turn back to Normal. In order to overcome this, we should check if a pill is already active by checking the timer and then stopping it and re-initializing it if it's already active.
```go
var pillTimer *time.Timer
func processPill() {
updateGhosts(ghosts, GhostStatusBlue)
if pillTimer != nil {
pillTimer.Stop()
}
pillTimer = time.NewTimer(time.Second * cfg.PillDurationSecs)
<-pillTimer.C
pillTimer.Stop()
updateGhosts(ghosts, GhostStatusNormal)
}
```
## Task 04: Avoiding Race Conditions
In our scenarios there are two possible race conditions. The first one is about the pill timer we mentioned just before. The `processPill` function is called asynchronously. So in the case that the first `processPill` function is just after the `pillTimer.Stop()` while the second one is inside the `if pillTimer != nil {` block. In this rare case it seems that while one pill is active, consuming a next one while code is at this point we might loose the second pill as Ghosts will come back to normal.
For this reason we are introducing a pillMx Mutex lock which we are going to acquire at the beginning of the `processPill` function and release just before starting to wait on the timer channel. Also we are going to acquire it just after the blocking function and release it at the end of the function.
```go
var pillTimer *time.Timer
var pillMx sync.Mutex
func processPill() {
pillMx.Lock()
updateGhosts(ghosts, GhostStatusBlue)
if pillTimer != nil {
pillTimer.Stop()
}
pillTimer = time.NewTimer(time.Second * cfg.PillDurationSecs)
pillMx.Unlock()
<-pillTimer.C
pillMx.Lock()
pillTimer.Stop()
updateGhosts(ghosts, GhostStatusNormal)
pillMx.Unlock()
}
```
Another possible race condition that might arise during execution is when we update the ghosts' status. For this purpose we are going to use a RWMutex lock. We have to acquire the lock whenever we read or update a ghost's status. RWMutex supports locking even for read or write access. So we are introducing the `var ghostsStatusMx sync.RWMutex` and a `updateGhosts` function that updates one or more ghost's status.
```go
var ghostsStatusMx sync.RWMutex
func updateGhosts(ghosts []*Ghost, ghostStatus GhostStatus) {
ghostsStatusMx.Lock()
defer ghostsStatusMx.Unlock()
for _, g := range ghosts {
g.status = ghostStatus
}
}
```
Also we have to acquire a RLock whenever we read a ghost's status. Multiple read locks can be acquire simultaneously but only one write lock can be acquired. We are going to use the `ghostsStatusMx.RLock()` and `ghostsStatusMx.RUnlock()` while reading the ghosts' status. We have to always unlock the RLock before updating a ghost's status otherwise a deadlock will occur.
Now we have a more challenging pacman! Happy gaming/coding! :)
## That's All Folks!
Congratulations! You've completed all the steps of the tutorial.
But your journey must not end here. If you are interested in contributing with a new step, have a look at the [TODO list](../TODO.md) or any open issues and submit a PR!