-
Notifications
You must be signed in to change notification settings - Fork 0
/
context.cr
234 lines (201 loc) · 6.94 KB
/
context.cr
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
require "json"
# A BattleSnake::Context is the representation of the game as it arrives from
# the [Webhook API](https://docs.battlesnake.com/api) request to `src/app.cr`
# endpoints.
#
# The context's key method is `#valid_moves`
class BattleSnake::Context
include JSON::Serializable
@[JSON::Field(key: "game")]
property game : Game
@[JSON::Field(key: "turn")]
property turn : Int32
@[JSON::Field(key: "board")]
property board : Board
@[JSON::Field(key: "you")]
property you : Snake
def dup
new_context = Context.from_json(to_json)
new_context.turn = turn + 1
new_context
end
def enemies
board.snakes.reject { |snake| snake.id == you.id }
end
# Returns a hash with all the valid `:moves` and `:neighbors` available from
# a given `BattleSnake::Point`.
#
# `:moves` is an `Array(BattleSnake::Point)` that containts the directions
# from the given `#point` that are valid to move without dying
# (`"up"/"left"/"down"/"right"`).
#
# `:neighbors` is a `{} of String => BattleSnake::Point` that contains those
# directions' coordinates.
#
# Example:
#
# ```
# context.valid_moves(Point.new(1,1))
# => {
# moves: [ "up", "right" ],
# neighbors: { Point.new(2,1), Point.new(1,2) }
# }
# ```
#
# NOTE: A common method to help manipulate the results is
# `BattleSnake::Point#move?`. An example of this in practice is the
# `Strategy::Utils.a_star` method implementation.
#
# TODO: Take into account the last point of snakes that will move on next
# turn, which would be in fact valid moves (not counted at the moment).
def valid_moves(point : Point)
moves = [] of String
neighbors = {} of String => Point
up = point.up
if up.y < board.height && !board.snake_points.includes?(up)
moves << "up"
neighbors["up"] = up
end
left = point.left
if left.x >= 0 && !board.snake_points.includes?(left)
moves << "left"
neighbors["left"] = left
end
down = point.down
if down.y >= 0 && !board.snake_points.includes?(down)
moves << "down"
neighbors["down"] = down
end
right = point.right
if right.x < board.width && !board.snake_points.includes?(right)
moves << "right"
neighbors["right"] = right
end
{ moves: moves, neighbors: neighbors }
end
# Similar to `BattleSnake::Context#valid_moves` but considers all valid
# moves from enemies. Returns a hash with all the valid `:moves`,
# `:neighbors` and `:risky_moves` (we might collide with enemy) available
# for `you`.
#
# `:moves` is an `Array(BattleSnake::Point)` that containts the directions
# from the given `#point` that are valid to move without dying
# (`"up"/"left"/"down"/"right"`).
#
# `:risky_moves` is an `Array(BattleSnake::Point)` that containts the
# directions from the given `#point` that are valid to move but there's a
# chance we could die (`"up"/"left"/"down"/"right"`).
#
# `:neighbors` is a `{} of String => BattleSnake::Point` that contains those
# directions' coordinates.
def blast_valid_moves!
moves = [] of String
risky_moves = [] of String
# Remove last body point from `you`, taking into account both
# `context.you` and `board.snakes` before starting work
you.body.pop
index = board.snakes.index! { |snake| snake.id == you.id }
board.snakes[index].body.pop
possible_moves = valid_moves(you.head)
# Get `valid_moves` for each enemy on the board
enemy_valid_moves = {} of String => Array(String)
enemies.each_with_index do |snake, index|
snake_moves = valid_moves(snake.head)[:moves]
enemy_valid_moves[snake.id] = snake_moves unless snake_moves.empty?
end
# Without enemy valid moves we can fallback to our possible_moves as valid
return {
moves: possible_moves[:moves],
neighbors: possible_moves[:neighbors],
risky_moves: risky_moves
} if enemy_valid_moves.empty?
# Build contexts for all possible enemy `valid_moves` permutations
contexts = [] of BattleSnake::Context
permutations = enemy_valid_moves.values
.map(&.size)
.reduce { |acc, i| acc * i }
permutations.times { contexts << self.dup }
contexts.each_with_index do |context, index|
# Perform each enemy corresponding move per permutation
offset = 1
enemy_valid_moves.each do |snake_id, moves|
direction = moves[(index / offset).floor.to_i % moves.size]
context.move(snake_id, direction)
offset = offset * moves.size
end
end
possible_moves[:moves].each do |direction|
target = possible_moves[:neighbors][direction]
collision = contexts.find do |context|
context.board.snake_points.includes?(target)
end
if collision.nil?
moves << direction
else
risky_moves << direction
end
end
{
moves: moves,
neighbors: possible_moves[:neighbors],
risky_moves: risky_moves
}
end
# Simulate a move of a snake by id in some direction
def move(snake_id, direction)
index = board.snakes.index! { |snake| snake.id == snake_id }
# delete last body point
deleted_point = board.snakes[index].body.pop
# Move head
board.snakes[index].head = board.snakes[index].head.move(direction)
board.snakes[index].body.unshift(board.snakes[index].head)
# Update You if necessary
@you = board.snakes[index] if @you.id == snake_id
# Update the board's snake_points
board.snake_points.clear
board.find_snake_points
end
# Checks collisions from snakes on the board and removes snakes that die
def check_collisions
collisions = [] of String
# Find collisions
board.snakes.each_with_index do |snake, i|
# Boundary collision
if snake.head.x < 0 || snake.head.y < 0 || snake.head.x > board.width || snake.head.y > board.height
collisions << snake.id
next # Don't run any more checks
end
# Self-snake-collision
if snake.body.count { |point| point == snake.head } > 1
collisions << snake.id
next # Don't run any more checks
end
board.snakes.each_with_index do |opponent, k|
# Don't check self
next if opponent.id == snake.id
# Head on head collision
if snake.head == opponent.head
case snake.body.size <=> opponent.body.size
when .negative?
# snake eliminated
collisions << snake.id
when .positive?
# opponent eliminated
collisions << opponent.id
else
# Both eliminated
collisions << snake.id
collisions << opponent.id
end
end
# Ran into another snake
collisions << snake.id if opponent.body.includes?(snake.head)
end
end
# Resolve collisions
collisions.uniq.each do |id|
snake = board.snakes.find { |snake| snake.id == id }
board.snakes.delete(snake)
end
end
end