[2018][ruby][15.1]

sorbet
Alpha Chen 6 years ago
parent df0c1df525
commit cbc8dac1c9

@ -4,4 +4,5 @@ source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
gem "minitest"
gem "pry" gem "pry"

@ -3,6 +3,7 @@ GEM
specs: specs:
coderay (1.1.2) coderay (1.1.2)
method_source (0.9.2) method_source (0.9.2)
minitest (5.11.3)
pry (0.12.2) pry (0.12.2)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.9.0) method_source (~> 0.9.0)
@ -11,6 +12,7 @@ PLATFORMS
ruby ruby
DEPENDENCIES DEPENDENCIES
minitest
pry pry
BUNDLED WITH BUNDLED WITH

@ -1,10 +1,11 @@
require "set" require "set"
require "minitest" require "minitest"
require "minitest/pride"
class Combat class EndCombat < StandardError; end
class EndCombat < StandardError; end
class Combat
def initialize(map) def initialize(map)
@map = map @map = map
end end
@ -12,12 +13,32 @@ class Combat
def fight def fight
return enum_for(__method__) unless block_given? return enum_for(__method__) unless block_given?
turn_order.each do |pos|
end
yield @map yield @map
loop do
turn_order.each do |pos|
next if @map[pos].nil? # don't take a turn for units that just died
turn = Turn.new(@map, pos)
if move = turn.move
unit = @map.delete(pos)
@map[move] = unit
turn.pos = move
end
if attack = turn.attack
unit = @map.fetch(turn.pos)
target = @map.fetch(attack)
target.hp -= unit.attack
@map.delete(attack) if target.hp < 0
end
end
yield @map
end
rescue EndCombat rescue EndCombat
yield @map # yield @map
end end
def turn_order def turn_order
@ -26,7 +47,7 @@ class Combat
end end
class TestCombat < Minitest::Test class TestCombat < Minitest::Test
def test_fight def test_move
map = Map.parse(<<~MAP) map = Map.parse(<<~MAP)
######### #########
#G..G..G# #G..G..G#
@ -40,9 +61,9 @@ class TestCombat < Minitest::Test
MAP MAP
combat = Combat.new(map) combat = Combat.new(map)
rounds = combat.fight rounds = combat.fight
rounds.next # skip the first round
map = rounds.next assert_equal <<~MAP.chomp, rounds.next.to_s
assert_equal <<~MAP.chomp, map.to_s
######### #########
#.G...G.# #.G...G.#
#...G...# #...G...#
@ -53,6 +74,60 @@ class TestCombat < Minitest::Test
#.......# #.......#
######### #########
MAP MAP
assert_equal <<~MAP.chomp, rounds.next.to_s
#########
#..G.G..#
#...G...#
#.G.E.G.#
#.......#
#G..G..G#
#.......#
#.......#
#########
MAP
assert_equal <<~MAP.chomp, rounds.next.to_s
#########
#.......#
#..GGG..#
#..GEG..#
#G..G...#
#......G#
#.......#
#.......#
#########
MAP
end
def test_fight
map = Map.parse(<<~MAP)
#######
#.G...#
#...EG#
#.#.#G#
#..G#E#
#.....#
#######
MAP
combat = Combat.new(map)
rounds = combat.fight
rounds.next # skip the initial state
rounds.next # skip the first found
map = rounds.next # second round
assert_equal 188, map.fetch(Position[2,4]).hp
20.times { rounds.next }
assert_equal <<~MAP.chomp, rounds.next.to_s
#######
#...G.#
#..G.G#
#.#.#G#
#...#E#
#.....#
#######
MAP
end end
def test_turn_order def test_turn_order
@ -71,6 +146,8 @@ class TestCombat < Minitest::Test
end end
class Turn class Turn
attr_accessor :pos
def initialize(map, pos) def initialize(map, pos)
@map, @pos = map, pos @map, @pos = map, pos
@unit = @map.fetch(@pos) @unit = @map.fetch(@pos)
@ -79,6 +156,7 @@ class Turn
def move def move
raise EndCombat if targets.empty? raise EndCombat if targets.empty?
return if can_attack? return if can_attack?
return if nearest.empty?
chosen = nearest.min chosen = nearest.min
haystack = @map.in_range(@pos) haystack = @map.in_range(@pos)
@ -88,12 +166,20 @@ class Turn
.first .first
end end
def attack
attackable.min_by {|pos, target| [target.hp, pos] }&.first
end
def targets def targets
@map.units.select {|_, other| other.is_a?(@unit.enemy) } @map.units.select {|_, other| other.is_a?(@unit.enemy) }
end end
def attackable
targets.select {|pos, _| @pos.adjacent.include?(pos) }
end
def can_attack? def can_attack?
@map.in_range(@pos).any? {|pos| targets.has_key?(pos) } !attackable.empty?
end end
def in_range def in_range
@ -109,6 +195,8 @@ class Turn
reachable.each do |pos, distance| reachable.each do |pos, distance|
nearest[distance] << pos nearest[distance] << pos
end end
return [] if nearest.empty?
nearest.min_by(&:first).last nearest.min_by(&:first).last
end end
@ -199,18 +287,25 @@ class Map
@occupied[pos] @occupied[pos]
end end
def []=(pos, square)
@occupied[pos] = square
end
def fetch(pos) def fetch(pos)
@occupied.fetch(pos) @occupied.fetch(pos)
end end
def delete(pos)
@occupied.delete(pos)
end
def units def units
@occupied.select {|_, sq| sq.is_a?(Unit) } @occupied.select {|_, sq| sq.is_a?(Unit) }
end end
def in_range(pos) def in_range(pos)
[-1, 1] pos
.flat_map {|d| [[pos.y, pos.x+d], [pos.y+d, pos.x]] } .adjacent
.map {|pos| Position[*pos] }
.reject {|pos| @occupied.has_key?(pos) } .reject {|pos| @occupied.has_key?(pos) }
end end
@ -281,12 +376,33 @@ Position = Struct.new(:y, :x) do
[self.y, self.x] <=> [other.y, other.x] [self.y, self.x] <=> [other.y, other.x]
end end
def adjacent
[-1, 1]
.flat_map {|d| [[y, x+d], [y+d, x]] }
.map {|pos| Position[*pos] }
end
def to_a def to_a
[y, x] [y, x]
end end
def to_s
to_a.to_s
end
end end
class Unit class Unit
attr_reader :attack
attr_accessor :hp
def initialize
@attack = 3
@hp = 200
end
def to_s
"#{self.class.name.chars.first}(#{hp})"
end
end end
class Wall class Wall
@ -304,8 +420,85 @@ class Goblin < Unit
end end
end end
def solve(input)
map = Map.parse(input)
combat = Combat.new(map)
map, count = combat.fight.map.with_index
.inject(nil) {|last,(map,count)|
# puts map, map.units.values.map(&:to_s).inspect
[map, count]
}
outcome = map.units.values.map(&:hp).sum * count
end
class TestSolve < Minitest::Test
def test_solve
assert_equal 27730, solve(<<~INPUT)
#######
#.G...#
#...EG#
#.#.#G#
#..G#E#
#.....#
#######
INPUT
assert_equal 36334, solve(<<~INPUT)
#######
#G..#E#
#E#E.E#
#G.##.#
#...#E#
#...E.#
#######
INPUT
assert_equal 39514, solve(<<~INPUT)
#######
#E..EG#
#.#G.E#
#E.##E#
#G..#.#
#..E#.#
#######
INPUT
assert_equal 27755, solve(<<~INPUT)
#######
#E.G#.#
#.#G..#
#G.#.G#
#G..#.#
#...E.#
#######
INPUT
assert_equal 28944, solve(<<~INPUT)
#######
#.E...#
#.#..G#
#.###.#
#E#G#G#
#...#G#
#######
INPUT
assert_equal 18740, solve(<<~INPUT)
#########
#G......#
#.E.#...#
#..##..G#
#...##..#
#...#...#
#.G...G.#
#.....G.#
#########
INPUT
end
end
if __FILE__ == $0 if __FILE__ == $0
require "minitest/autorun" and exit if ENV["TEST"] require "minitest/autorun" and exit if ENV["TEST"]
puts solve(ARGF.read)
end end

Loading…
Cancel
Save