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.
- data/READTHAT +1 -0
- data/Rakefile +62 -0
- data/THAT +23 -0
- data/examples/basic.rb +71 -0
- data/lib/weewar-ai.rb +14 -0
- data/lib/weewar-ai/__dir__.rb +23 -0
- data/lib/weewar-ai/ai.rb +81 -0
- data/lib/weewar-ai/api.rb +100 -0
- data/lib/weewar-ai/faction.rb +57 -0
- data/lib/weewar-ai/game.rb +173 -0
- data/lib/weewar-ai/hex.rb +112 -0
- data/lib/weewar-ai/map.rb +135 -0
- data/lib/weewar-ai/player.rb +24 -0
- data/lib/weewar-ai/traits.rb +76 -0
- data/lib/weewar-ai/unit.rb +520 -0
- metadata +94 -0
@@ -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
|