Pistos-weewar-ai 2008.06.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,112 @@
1
+ require 'open-uri'
2
+ require 'hpricot'
3
+
4
+ module WeewarAI
5
+
6
+ # The Hex class represents one hex in a Map. You issue the build command
7
+ # through a Hex instance. You can also query whether a Hex is occupied
8
+ # or capturable.
9
+ class Hex
10
+ attr_reader :x, :y, :type
11
+ attr_accessor :faction, :unit
12
+
13
+ SYMBOL_FOR_NAME = {
14
+ 'Airfield' => :airfield,
15
+ 'Base' => :base,
16
+ 'Desert' => :desert,
17
+ 'Harbor' => :harbour,
18
+ 'Mountains' => :mountains,
19
+ 'Plains' => :plains,
20
+ 'Repair patch' => :repairshop,
21
+ 'Swamp' => :swamp,
22
+ 'Water' => :water,
23
+ 'Woods' => :woods,
24
+ }
25
+
26
+ # Downloads the terrain specifications from weewar.com.
27
+ # This is called from WeewarAI::AI.
28
+ # No need to call this yourself.
29
+ def Hex.initialize_specs
30
+ trait[ :terrain_specs ] = Hash.new
31
+ doc = Hpricot( open( 'http://weewar.com/specifications' ) )
32
+ h2 = doc.at( '#Terrains' )
33
+ table = h2.next_sibling
34
+ table.search( 'tr' ).each do |tr|
35
+ name = tr.at( 'b' ).inner_text
36
+ type = SYMBOL_FOR_NAME[ name ]
37
+ if type
38
+ h = trait[ :terrain_specs ][ type ] = {
39
+ :attack => parse_numbers( tr.search( 'td' )[ 2 ].inner_text ),
40
+ :defense => parse_numbers( tr.search( 'td' )[ 3 ].inner_text ),
41
+ :movement => parse_numbers( tr.search( 'td' )[ 4 ].inner_text ),
42
+ }
43
+ else
44
+ raise "Unknown terrain type: #{name}"
45
+ end
46
+ end
47
+ end
48
+
49
+ # No need to call this yourself. Hexes are parsed and built
50
+ # by the Map class.
51
+ def initialize( game, type, x, y )
52
+ @game, @type, @x, @y = game, type, x, y
53
+ end
54
+
55
+ # An internal method used by initialize_specs.
56
+ def Hex.parse_numbers( text )
57
+ retval = Hash.new
58
+ text.scan( /(\w+): (\d+)/ ) do |data|
59
+ retval[ data[ 0 ].to_sym ] = data[ 1 ].to_i
60
+ end
61
+ retval
62
+ end
63
+
64
+ # The terrain_specs Hash.
65
+ def Hex.terrain_specs
66
+ trait[ :terrain_specs ]
67
+ end
68
+
69
+ def to_s
70
+ "#{@type} @ (#{@x},#{@y})"
71
+ end
72
+
73
+ def hex
74
+ self
75
+ end
76
+
77
+ # Comparison for equality with another Hex.
78
+ # A Hex equals another Hex if it has the same coordinates and is
79
+ # of the same type.
80
+ # if one_hex == another_hex
81
+ # puts "The hexes are the same."
82
+ # end
83
+ def ==( other )
84
+ @x == other.x and @y == other.y and @type == other.type
85
+ end
86
+
87
+ # Issues a command to build the given Unit type on this Hex.
88
+ # base.build :linf
89
+ def build( unit_type )
90
+ @game.send "<build x='#{@x}' y='#{@y}' type='#{WeewarAI::Unit::TYPE_FOR_SYMBOL[unit_type]}'/>"
91
+ @game.refresh
92
+ end
93
+
94
+ # Whether or not this Hex is occupied (by a Unit).
95
+ # if not hex.occupied?
96
+ # my_unit.move_to hex
97
+ # end
98
+ def occupied?
99
+ not @unit.nil?
100
+ end
101
+
102
+ # Whether or not this Hex is capturable by Unit s that can capture.
103
+ # if hex.capturable?
104
+ # my_trooper.move_to hex
105
+ # end
106
+ def capturable?
107
+ [ :base, :harbour, :airfield ].include?( @type ) and
108
+ @faction != @game.my_faction
109
+ end
110
+
111
+ end
112
+ end
@@ -0,0 +1,135 @@
1
+ module WeewarAI
2
+
3
+ # Instances of the Map class provide access to the Hex es of a Map,
4
+ # either individually, or by means of iterators or filters.
5
+ class Map
6
+ attr_reader :width, :height, :cols, :units
7
+
8
+ include Enumerable
9
+
10
+ SYMBOL_FOR_TERRAIN = {
11
+ 'Plains' => :plains,
12
+ 'Water' => :water,
13
+ 'Mountains' => :mountains,
14
+ 'Desert' => :desert,
15
+ 'Woods' => :woods,
16
+ 'Swamp' => :swamp,
17
+ 'Base' => :base,
18
+ 'harbor' => :harbour,
19
+ 'repairshop' => :repairshop,
20
+ 'airfield' => :airfield,
21
+ 'red_city' => :red_base,
22
+ 'blue_city' => :blue_base,
23
+ 'purple_city' => :purple_base,
24
+ 'yellow_city' => :yellow_base,
25
+ 'green_city' => :green_base,
26
+ 'white_city' => :white_base,
27
+ 'red_harbor' => :red_harbour,
28
+ 'blue_harbor' => :blue_harbour,
29
+ 'purple_harbor' => :purple_harbour,
30
+ 'yellow_harbor' => :yellow_harbour,
31
+ 'green_harbor' => :green_harbour,
32
+ 'white_harbor' => :white_harbour,
33
+ 'red_airfield' => :red_airfield,
34
+ 'blue_airfield' => :blue_airfield,
35
+ 'purple_airfield' => :purple_airfield,
36
+ 'yellow_airfield' => :yellow_airfield,
37
+ 'green_airfield' => :green_airfield,
38
+ 'white_airfield' => :white_airfield,
39
+ }
40
+
41
+ # Creates a new Map instance, based on the given Game and map ID number.
42
+ # You normally do not need to call this yourself.
43
+ def initialize( game, map_id )
44
+ @game = game
45
+
46
+ map_id = map_id.to_i
47
+ xml = XmlSimple.xml_in(
48
+ WeewarAI::API.get( "/maplayout/#{map_id}" ),
49
+ { 'ForceArray' => [ 'terrain' ], }
50
+ )
51
+
52
+ @width = xml[ 'width' ].to_i
53
+ @height = xml[ 'height' ].to_i
54
+ @cols = Hash.new
55
+ xml[ 'terrains' ][ 'terrain' ].each do |t|
56
+ x = t[ 'x' ].to_i
57
+ @cols[ x ] ||= Hash.new
58
+ y = t[ 'y' ].to_i
59
+ @cols[ x ][ y ] = Hex.new(
60
+ @game,
61
+ SYMBOL_FOR_TERRAIN[ t[ 'type' ] ],
62
+ x, y
63
+ )
64
+ end
65
+ end
66
+
67
+ # The Hex at the given coordinates.
68
+ # the_hex = map.hex( 2, 8 )
69
+ def hex( x, y )
70
+ x = x.to_i
71
+ y = y.to_i
72
+ c = @cols[ x ]
73
+ if c
74
+ c[ y ]
75
+ end
76
+ end
77
+ alias xy hex
78
+
79
+ # A convenience method for obtaining the Hex for a given coordinate pair.
80
+ # hex = my_map[ 3, 7 ]
81
+ def []( *xy )
82
+ hex xy[ 0 ], xy[ 1 ]
83
+ end
84
+
85
+ # The Hex at the given coordinates, with the coordinates given in row-column
86
+ # order (y, x).
87
+ # the_hex = map.rc( 8, 2 )
88
+ def rc( y, x )
89
+ hex( x, y )
90
+ end
91
+
92
+ # An Array of the given Hex's neighbouring Hex es.
93
+ # The Array will not contain any nil elements.
94
+ # surrounding_hexes = map.hex_neighbours( some_hex )
95
+ def hex_neighbours( h )
96
+ if h.y % 2 == 0
97
+ # Even row (not shifted)
98
+ [
99
+ hex( h.x , h.y - 1 ), # NE
100
+ hex( h.x + 1, h.y ), # E
101
+ hex( h.x , h.y + 1 ), # SE
102
+ hex( h.x - 1, h.y + 1 ), # SW
103
+ hex( h.x - 1, h.y ), # W
104
+ hex( h.x - 1, h.y - 1 ), # NW
105
+ ].compact
106
+ else
107
+ # Odd row (shifted right)
108
+ [
109
+ hex( h.x + 1, h.y - 1 ), # NE
110
+ hex( h.x + 1, h.y ), # E
111
+ hex( h.x + 1, h.y + 1 ), # SE
112
+ hex( h.x , h.y + 1 ), # SW
113
+ hex( h.x - 1, h.y ), # W
114
+ hex( h.x , h.y - 1 ), # NW
115
+ ].compact
116
+ end
117
+ end
118
+
119
+ # Iterates over every Hex in the map.
120
+ # Takes a block argument, as per the usual Ruby each method.
121
+ # map.each do |hex|
122
+ # puts hex
123
+ # end
124
+ def each( &block )
125
+ @cols.values.map { |col| col.values }.flatten.compact.each &block
126
+ end
127
+
128
+ # All base Hex es.
129
+ # only_bases = map.bases
130
+ def bases
131
+ find_all { |hex| hex.type == :base }
132
+ end
133
+
134
+ end
135
+ end
@@ -0,0 +1,24 @@
1
+ module WeewarAI
2
+ class Player
3
+ attr_reader :name
4
+
5
+ def self.[]( id )
6
+ #id = id.to_i
7
+ #new(
8
+ #XmlSimple.xml_in(
9
+ #WeewarAI::API.get( "/api1/gamestate/#{id}" ),
10
+ #{ 'ForceArray' => false, }
11
+ #)
12
+ #)
13
+ end
14
+
15
+ def initialize( h )
16
+ @name = h[ 'content' ]
17
+ @current = ( h[ 'current' ] == 'true' )
18
+ end
19
+
20
+ def current?
21
+ @current
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,76 @@
1
+ # Taken from Ramaze ( http://ramaze.net )
2
+ # Copyright (c) 2008 Michael Fellinger m.fellinger@gmail.com
3
+
4
+ Traits = Hash.new{|h,k| h[k] = {}} unless defined?(Traits)
5
+
6
+ # Extensions for Object
7
+
8
+ class Object
9
+
10
+ # Adds a method to Object to annotate your objects with certain traits.
11
+ # It's basically a simple Hash that takes the current object as key
12
+ #
13
+ # Example:
14
+ #
15
+ # class Foo
16
+ # trait :instance => false
17
+ #
18
+ # def initialize
19
+ # trait :instance => true
20
+ # end
21
+ # end
22
+ #
23
+ # Foo.trait[:instance]
24
+ # # false
25
+ #
26
+ # foo = Foo.new
27
+ # foo.trait[:instance]
28
+ # # true
29
+
30
+ def trait hash = nil
31
+ if hash
32
+ Traits[self].merge! hash
33
+ else
34
+ Traits[self]
35
+ end
36
+ end
37
+
38
+ # builds a trait from all the ancestors, closer ancestors
39
+ # overwrite distant ancestors
40
+ #
41
+ # class Foo
42
+ # trait :one => :eins
43
+ # trait :first => :erstes
44
+ # end
45
+ #
46
+ # class Bar < Foo
47
+ # trait :two => :zwei
48
+ # end
49
+ #
50
+ # class Foobar < Bar
51
+ # trait :three => :drei
52
+ # trait :first => :overwritten
53
+ # end
54
+ #
55
+ # Foobar.ancestral_trait
56
+ # {:three=>:drei, :two=>:zwei, :one=>:eins, :first=>:overwritten}
57
+
58
+ def ancestral_trait
59
+ if respond_to?(:ancestors)
60
+ ancs = ancestors
61
+ else
62
+ ancs = self.class.ancestors
63
+ end
64
+ ancs.reverse.inject({}){|s,v| s.merge(v.trait)}.merge(trait)
65
+ end
66
+
67
+ # trait for self.class
68
+
69
+ def class_trait
70
+ if respond_to?(:ancestors)
71
+ trait
72
+ else
73
+ self.class.trait
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,520 @@
1
+ module WeewarAI
2
+
3
+ # An instance of the Unit class corresponds to a single unit in a game.
4
+ #
5
+ # The Unit class provides access to Unit attributes like coordinates (x, y),
6
+ # health (hp), and type (trooper, raider, etc.). Also available are tactical
7
+ # calculation data, such as enemy targets that can be attacked, and hexes that
8
+ # can be reached in the current turn.
9
+ #
10
+ # Unit s can be ordered to move, attack or repair.
11
+ #
12
+ # Read the full method listing to see everything you can do with a Unit.
13
+ class Unit
14
+ attr_reader :faction, :hex, :type
15
+ attr_accessor :hp
16
+
17
+ SYMBOL_FOR_UNIT = {
18
+ 'Trooper' => :linf,
19
+ 'Heavy Trooper' => :hinf,
20
+ 'Raider' => :raider,
21
+ 'Assault Artillery' => :aart,
22
+ 'Tank' => :tank,
23
+ 'Heavy Tank' => :htank,
24
+ 'Berserker' => :bers,
25
+ 'Light Artillery' => :lart,
26
+ 'Heavy Artillery' => :hart,
27
+ 'DFA' => :dfa,
28
+ 'Hovercraft' => :hover,
29
+ #'capturing' => :capturing,
30
+ }
31
+
32
+ TYPE_FOR_SYMBOL = {
33
+ :linf => 'Trooper',
34
+ :hinf => 'Heavy Trooper',
35
+ :raider => 'Raider',
36
+ :tank => 'Tank',
37
+ :htank => 'Heavy Tank',
38
+ :lart => 'Light Artillery',
39
+ :hart => 'Heavy Artillery',
40
+ # TODO: rest
41
+ }
42
+
43
+ UNIT_CLASSES = {
44
+ :linf => :soft,
45
+ :hinf => :soft,
46
+ :raider => :hard,
47
+ :aart => :hard,
48
+ :tank => :hard,
49
+ :htank => :hard,
50
+ :bers => :hard,
51
+ :lart => :hard,
52
+ :hart => :hard,
53
+ :dfa => :hard,
54
+ :capturing => :soft,
55
+ :hover => :amphibic,
56
+ }
57
+
58
+ # <Pistos> These need to be checked, I was just going by memory
59
+ UNIT_COSTS = {
60
+ :linf => 75,
61
+ :hinf => 150,
62
+ :raider => 200,
63
+ :tank => 300,
64
+ :hover => 300,
65
+ :htank => 600,
66
+ :lart => 200,
67
+ :aart => 450,
68
+ :hart => 600,
69
+ :dfa => 1200,
70
+ :bers => 900,
71
+ :sboat => 200,
72
+ :dest => 1100,
73
+ :bship => 2000,
74
+ :sub => 1200,
75
+ :jet => 800,
76
+ :heli => 600,
77
+ :bomber => 900,
78
+ :aa => 300,
79
+ }
80
+
81
+ # <Pistos> These need to be checked, I was just going by memory
82
+ REPAIR_RATE = {
83
+ :linf => 1,
84
+ :hinf => 1,
85
+ :raider => 2,
86
+ :tank => 2,
87
+ :hover => 2,
88
+ :htank => 2,
89
+ :lart => 1,
90
+ :aart => 2,
91
+ :hart => 1,
92
+ :dfa => 1,
93
+ :bers => 1,
94
+ :sboat => 2,
95
+ :dest => 1,
96
+ :bship => 1,
97
+ :sub => 1,
98
+ :jet => 3,
99
+ :heli => 3,
100
+ :bomber => 3,
101
+ :aa => 1,
102
+ }
103
+
104
+ INFINITY = 99999999
105
+
106
+ # Units are created by the Map class. No need to instantiate any on your own.
107
+ def initialize( game, hex, faction, type, hp, finished, capturing = false )
108
+ sym = SYMBOL_FOR_UNIT[ type ]
109
+ if sym.nil?
110
+ raise "Unknown type: '#{type}'"
111
+ end
112
+
113
+ @game, @hex, @faction, @type, @hp, @finished, @capturing =
114
+ game, hex, faction, sym, hp.to_i, finished, capturing
115
+ end
116
+
117
+ def to_s
118
+ "#{@faction} #{@type} @ (#{@hex.x},#{@hex.y})"
119
+ end
120
+
121
+ # The Unit's current x coordinate (column).
122
+ # my_unit.x
123
+ def x
124
+ @hex.x
125
+ end
126
+
127
+ # The Unit's current y coordinate (row).
128
+ # my unit.y
129
+ def y
130
+ @hex.y
131
+ end
132
+
133
+ # Whether or not the unit can be ordered to do anything further.
134
+ # if not my_unit.finished?
135
+ # # do stuff with my_unit
136
+ # end
137
+ def finished?
138
+ @finished
139
+ end
140
+
141
+ # Whether or not the unit is capturing a base at the moment.
142
+ # if not my_trooper.capturing?
143
+ # # do stuff with my_trooper
144
+ # end
145
+ def capturing?
146
+ @capturing
147
+ end
148
+
149
+ # The unit class of this unit. i.e. :soft, :hard, etc.
150
+ # if my_unit.unit_class == :hard
151
+ # # attack some troopers!
152
+ # end
153
+ def unit_class
154
+ UNIT_CLASSES[ @type ]
155
+ end
156
+
157
+ # Comparison for equality with another Unit.
158
+ # A Unit equals another Unit if it is standing on the same Hex,
159
+ # is of the same Faction, and is the same type.
160
+ # if new_unit == old_unit
161
+ # end
162
+ def ==( other )
163
+ @hex == other.hex and
164
+ @faction == other.faction and
165
+ @type == other.type
166
+ end
167
+
168
+ # Whether or not the Unit type can capture bases or not.
169
+ # Be aware that this can return true even if the Unit can no longer
170
+ # take action during the current turn.
171
+ # if my_unit.can_capture?
172
+ # my_unit.move_to enemy_base
173
+ # end
174
+ def can_capture?
175
+ [ :linf, :hinf, :hover ].include? @type
176
+ end
177
+
178
+ # An Array of the Units which this Unit can attack in the current turn.
179
+ # If the optional origin Hex is provided, the target list is calculated
180
+ # as if the unit were on that Hex instead of its current Hex.
181
+ # enemies_in_range = my_unit.targets
182
+ # enemies_in_range_from_there = my_unit.targets possible_attack_position
183
+ def targets( origin = @hex )
184
+ coords = XmlSimple.xml_in(
185
+ @game.send( "<attackOptions x='#{origin.x}' y='#{origin.y}' type='#{TYPE_FOR_SYMBOL[@type]}'/>" )
186
+ )[ 'coordinate' ]
187
+ if coords
188
+ coords.map { |c|
189
+ @game.map[ c[ 'x' ], c[ 'y' ] ].unit
190
+ }.compact
191
+ else
192
+ []
193
+ end
194
+ end
195
+ alias attack_options targets
196
+ alias attackOptions targets
197
+
198
+ # Whether or not the Unit can attack the given target.
199
+ # Returns true iff the Unit can still take action in the current round,
200
+ # and the target is in range.
201
+ # if my_unit.can_attack? enemy_unit
202
+ # my_unit.attack enemy_unit
203
+ # end
204
+ def can_attack?( target )
205
+ not @finished and targets.include?( target )
206
+ end
207
+
208
+ # An Array of the Hex es which the given Unit can move to in the current turn.
209
+ # possible_moves = my_unit.destinations
210
+ def destinations
211
+ coords = XmlSimple.xml_in(
212
+ @game.send( "<movementOptions x='#{x}' y='#{y}' type='#{TYPE_FOR_SYMBOL[@type]}'/>" )
213
+ )[ 'coordinate' ]
214
+ coords.map { |c|
215
+ @game.map[ c[ 'x' ], c[ 'y' ] ]
216
+ }
217
+ end
218
+ alias movement_options destinations
219
+ alias movementOptions destinations
220
+
221
+ # Whether or not the Unit can reach the given Hex in the current turn.
222
+ # if my_unit.can_reach? the_hex
223
+ # my_unit.move_to the_hex
224
+ # end
225
+ def can_reach?( hex )
226
+ destinations.include? hex
227
+ end
228
+
229
+ # An Array of the Unit s of the Game which are on the same side as this Unit.
230
+ # friends = my_unit.allied_units
231
+ def allied_units
232
+ @game.units.find_all { |u| u.faction == @faction }
233
+ end
234
+
235
+ # Whether or not the given unit is an ally of this Unit.
236
+ # if not my_unit.allied_with?( other_unit )
237
+ # my_unit.attack other_unit
238
+ # end
239
+ def allied_with?( unit )
240
+ @faction == unit.faction
241
+ end
242
+
243
+ #-- ----------------------------------------------
244
+ # Travel
245
+ #++
246
+
247
+ # The cost in movement points for the unit to enter the given Hex. This
248
+ # is an internal method used for travel-related calculations; you should not
249
+ # normally need to use this yourself.
250
+ def entrance_cost( hex )
251
+ return nil if hex.nil?
252
+
253
+ specs_for_type = Hex.terrain_specs[ hex.type ]
254
+ if specs_for_type.nil?
255
+ raise "No specs for type '#{hex.type.inspect}': #{Hex.terrain_specs.inspect}"
256
+ end
257
+ specs_for_type[ :movement ][ unit_class ]
258
+ end
259
+
260
+ # The cost in movement points for the unit to travel along the given path.
261
+ # The path given should be an Array of Hexes. This
262
+ # is an internal method used for travel-related calculations; you should not
263
+ # normally need to use this yourself.
264
+ def path_cost( path )
265
+ path.inject( 0 ) { |sum,hex|
266
+ sum + entrance_cost( hex )
267
+ }
268
+ end
269
+
270
+ # The cost in movement points for this unit to travel to the given
271
+ # destination.
272
+ def travel_cost( dest )
273
+ sp = shortest_path( dest )
274
+ path_cost( sp )
275
+ end
276
+
277
+ # The shortest path (as an Array of Hexes) from the
278
+ # Unit's current location to the given destination.
279
+ #
280
+ # If the optional exclusion array is provided, the path will not
281
+ # pass through any Hex in the exclusion array.
282
+ #
283
+ # best_path = my_trooper.shortest_path( enemy_base )
284
+ def shortest_path( dest, exclusions = [] )
285
+ exclusions ||= []
286
+ previous = shortest_paths( exclusions )
287
+ s = []
288
+ u = dest.hex
289
+ while previous[ u ]
290
+ s.unshift u
291
+ u = previous[ u ]
292
+ end
293
+ s
294
+ end
295
+
296
+ # Calculate all shortest paths from the Unit's current Hex to every other
297
+ # Hex, as per Dijkstra's algorithm
298
+ # ( http://en.wikipedia.org/wiki/Dijkstra's_algorithm ).
299
+ # Most AIs will only need to make use of the shortest_path method instead.
300
+ def shortest_paths( exclusions = [] )
301
+ # Initialization
302
+ exclusions ||= []
303
+ source = hex
304
+ dist = Hash.new
305
+ previous = Hash.new
306
+ q = []
307
+ @game.map.each do |h|
308
+ if not exclusions.include? h
309
+ dist[ h ] = INFINITY
310
+ q << h
311
+ end
312
+ end
313
+ dist[ source ] = 0
314
+
315
+ # Work
316
+ while not q.empty?
317
+ u = q.inject { |best,h| dist[ h ] < dist[ best ] ? h : best }
318
+ q.delete u
319
+ @game.map.hex_neighbours( u ).each do |v|
320
+ next if exclusions.include? v
321
+ alt = dist[ u ] + entrance_cost( v )
322
+ if alt < dist[ v ]
323
+ dist[ v ] = alt
324
+ previous[ v ] = u
325
+ end
326
+ end
327
+ end
328
+
329
+ # Results
330
+ previous
331
+ end
332
+
333
+ #-- --------------------------------------------------
334
+ # Actions
335
+ #++
336
+
337
+ # Sends an XML command to the server regarding this Unit. This is an
338
+ # internal method that you should normally not need to call yourself.
339
+ def send( xml )
340
+ command = "<unit x='#{x}' y='#{y}'>#{xml}</unit>"
341
+ response = @game.send command
342
+ doc = Hpricot.XML( response )
343
+ @finished = !! doc.at( 'finished' )
344
+ if not @finished
345
+ $stderr.puts " #{self} NOT FINISHED:\n\t#{response}"
346
+ end
347
+ if not doc.at( 'ok' )
348
+ error = doc.at 'error'
349
+ if error
350
+ message = "ERROR from server: #{error.inner_html}"
351
+ else
352
+ message = "RECEIVED:\n#{response}"
353
+ end
354
+ raise "Failed to execute:\n#{command}\n#{message}"
355
+ end
356
+ response
357
+ end
358
+
359
+ # Moves the given Unit to the given destination if it is reachable
360
+ # in one turn, otherwise moves the Unit towards it using the optimal path.
361
+ #
362
+ # this_unit.move_to some_hex
363
+ # that_unit.move_to enemy_unit
364
+ #
365
+ # If a Unit or an Array of Units is passed as the :also_attack option,
366
+ # those Units will be prioritized for attack after moving, with the Units
367
+ # assumed to be given from highest priority (index 0) to lowest.
368
+ #
369
+ # another_unit.move_to(
370
+ # enemy_unit,
371
+ # :also_attack => [ enemy_unit ] + enemy_artillery )
372
+ # )
373
+ #
374
+ # If an Array of hexes is provided as the :exclusions option, the Unit will
375
+ # not pass through any of the exclusion Hex es on its way to the destination.
376
+ #
377
+ # spy_unit.move_to(
378
+ # enemy_base,
379
+ # :exclusions => well_defended_choke_point_hexes
380
+ # )
381
+ #
382
+ # By default, moving onto a base with a capturing unit will attempt a capture.
383
+ # Set the :no_capture option to true to prevent this.
384
+ #
385
+ # my_trooper.move_to( enemy_base, :no_capture => true )
386
+ #
387
+ # navy_seal.move_to(
388
+ # enemy_base,
389
+ # :also_attack => hard_targets,
390
+ # :exclusions => fortified_hexes,
391
+ # :no_capture => true
392
+ # )
393
+ def move_to( destination, options = {} )
394
+ command = ""
395
+ options[ :exclusions ] ||= []
396
+
397
+ new_hex = @hex
398
+
399
+ if destination != @hex
400
+ # Travel
401
+
402
+ path = shortest_path( destination, options[ :exclusions ] )
403
+ if path.empty?
404
+ $stderr.puts "No path from #{self} to #{destination}"
405
+ else
406
+ dests = destinations
407
+ new_dest = path.pop
408
+ while new_dest and not dests.include?( new_dest )
409
+ new_dest = path.pop
410
+ end
411
+ end
412
+
413
+ if new_dest.nil?
414
+ $stderr.puts " Can't move #{self} to #{destination}"
415
+ else
416
+ o = new_dest.unit
417
+ if o and allied_with?( o )
418
+ # Can't move through allied units
419
+ options[ :exclusions ] << new_dest
420
+ return move_to( destination, options )
421
+ else
422
+ x = new_dest.x
423
+ y = new_dest.y
424
+ new_hex = new_dest
425
+ command << "<move x='#{x}' y='#{y}'/>"
426
+ end
427
+ end
428
+ end
429
+
430
+ target = nil
431
+ also_attack = options[ :also_attack ]
432
+ if also_attack
433
+ enemies = targets( new_hex )
434
+ if not enemies.empty?
435
+ case also_attack
436
+ when Array
437
+ preferred = also_attack & enemies
438
+ else
439
+ preferred = [ also_attack ] & enemies
440
+ end
441
+ target = preferred.first# || enemies.random
442
+
443
+ if target
444
+ command << "<attack x='#{target.x}' y='#{target.y}'/>"
445
+ end
446
+ end
447
+ end
448
+
449
+ if(
450
+ not options[ :no_capture ] and
451
+ can_capture? and
452
+ new_hex == destination and
453
+ new_hex.capturable?
454
+ )
455
+ puts "#{self} capturing #{new_hex}"
456
+ command << "<capture/>"
457
+ end
458
+
459
+ if not command.empty?
460
+ result = send( command )
461
+ puts "Moved #{self} to #{new_hex}"
462
+ @hex.unit = nil
463
+ new_hex.unit = self
464
+ @hex = new_hex
465
+ if target
466
+ #<attack target='[3,4]' damageReceived='2' damageInflicted='7' remainingQuantity='8' />
467
+ process_attack result
468
+ @game.last_attacked = target
469
+ end
470
+
471
+ # Success
472
+ true
473
+ end
474
+ end
475
+ alias move move_to
476
+
477
+ # This is an internal method used to update the Unit attributes after a
478
+ # command is sent to the weewar server. You should not call this yourself.
479
+ def process_attack( xml_text )
480
+ xml = XmlSimple.xml_in( xml_text, { 'ForceArray' => false } )[ 'attack' ]
481
+ if xml[ 'target' ] =~ /\[(\d+),(\d+)\]/
482
+ x, y = $1, $2
483
+ enemy = @game.map[ x, y ].unit
484
+ end
485
+
486
+ if enemy.nil?
487
+ raise "Server says enemy attacked was at (#{x},#{y}), but we have no record of an enemy there."
488
+ end
489
+
490
+ damage_inflicted = xml[ 'damageInflicted' ].to_i
491
+ enemy.hp -= damage_inflicted
492
+
493
+ damage_received = xml[ 'damageReceived' ].to_i
494
+ @hp = xml[ 'remainingQuantity' ].to_i
495
+
496
+ puts " #{self} (-#{damage_received}: #{@hp}) ATTACKED #{enemy} (-#{damage_inflicted}: #{enemy.hp})"
497
+ end
498
+
499
+ # Commands this Unit to attack another Unit. This Unit will not move
500
+ # anywhere in the attempt to attack.
501
+ # Provide either a Unit or a Hex to attack as a method argument.
502
+ # my_unit.attack enemy_unit
503
+ def attack( unit )
504
+ x = unit.x
505
+ y = unit.y
506
+
507
+ result = send "<attack x='#{x}' y='#{y}'/>"
508
+ process_attack result
509
+ @game.last_attacked = @game.map[ x, y ].unit
510
+ true
511
+ end
512
+
513
+ # Commands the Unit to undergo repairs.
514
+ # my_hurt_unit.repair
515
+ def repair
516
+ send "<repair/>"
517
+ @hp += REPAIR_RATE[ @type ]
518
+ end
519
+ end
520
+ end