galaxy 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/.idea/.rakeTasks +7 -0
- data/.idea/encodings.xml +5 -0
- data/.idea/galaxy.iml +10 -0
- data/.idea/misc.xml +14 -0
- data/.idea/modules.xml +9 -0
- data/.idea/vcs.xml +8 -0
- data/.idea/workspace.xml +486 -0
- data/LICENSE +20 -0
- data/README.rdoc +36 -0
- data/Rakefile +62 -0
- data/VERSION +1 -0
- data/doc/plan.txt +5 -0
- data/doc/pseudo.txt +43 -0
- data/features/galaxy.feature +9 -0
- data/features/step_definitions/galaxy_steps.rb +0 -0
- data/features/support/env.rb +4 -0
- data/galaxy.gemspec +95 -0
- data/galaxy/.loadpath +5 -0
- data/galaxy/.project +17 -0
- data/galaxy/.settings/org.eclipse.mylyn.tasks.ui.prefs +4 -0
- data/galaxy/.settings/org.eclipse.wst.sse.core.prefs +5 -0
- data/galaxy/experiments.rb +26 -0
- data/lib/galaxy.rb +8 -0
- data/lib/galaxy/models/bombing.rb +64 -0
- data/lib/galaxy/models/fleet.rb +62 -0
- data/lib/galaxy/models/group.rb +178 -0
- data/lib/galaxy/models/models.rb +16 -0
- data/lib/galaxy/models/planet.rb +181 -0
- data/lib/galaxy/models/product.rb +84 -0
- data/lib/galaxy/models/race.rb +112 -0
- data/lib/galaxy/models/route.rb +60 -0
- data/lib/galaxy/order.rb +24 -0
- data/lib/galaxy/report.rb +176 -0
- data/lib/galaxy/section.rb +226 -0
- data/lib/galaxy/utils.rb +109 -0
- data/lib/galaxy/virtual_base.rb +165 -0
- data/spec/spec_helper.rb +9 -0
- data/test/test_helper.rb +4 -0
- data/test/unit/models_test.rb +1469 -0
- data/test/unit/report_test.rb +187 -0
- data/test/unit/utils_test.rb +421 -0
- data/test/unit/virtual_base_test.rb +224 -0
- metadata +123 -0
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'galaxy/models/models'
|
2
|
+
|
3
|
+
class Race < ActiveRecord::Base
|
4
|
+
#:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean.
|
5
|
+
virtual
|
6
|
+
tableless :columns => [
|
7
|
+
[ :name, :string ],
|
8
|
+
[ :drive, :float ],
|
9
|
+
[ :weapons, :float ],
|
10
|
+
[ :shields, :float ],
|
11
|
+
[ :cargo, :float ],
|
12
|
+
[ :pop, :float ],
|
13
|
+
[ :ind, :float ],
|
14
|
+
[ :num_planets, :integer ],
|
15
|
+
[ :relation, :string ],
|
16
|
+
[ :vote, :float ]
|
17
|
+
]
|
18
|
+
has_many_linked :products
|
19
|
+
has_many_linked :planets
|
20
|
+
has_many_linked :fleets
|
21
|
+
has_many_linked :routes
|
22
|
+
has_many_linked :groups
|
23
|
+
has_many_linked :bombings
|
24
|
+
has_many_linked :incoming_bombings, :class_name => 'Bombing', :foreign_key => 'victim_id'
|
25
|
+
|
26
|
+
attr_accessor :order
|
27
|
+
|
28
|
+
def kill
|
29
|
+
products.each {|m| m.kill}
|
30
|
+
planets.each {|m| m.kill}
|
31
|
+
fleets.each {|m| m.kill}
|
32
|
+
routes.each {|m| m.kill}
|
33
|
+
groups.each {|m| m.kill}
|
34
|
+
bombings.each {|m| m.kill}
|
35
|
+
incoming_bombings.each {|m| m.kill}
|
36
|
+
super #puts "Warning: kill attempt failed for #{self}" unless
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize match, state
|
40
|
+
case match.size
|
41
|
+
when 10 then
|
42
|
+
super({:name=>match[0], :drive=>match[1].to_f, :weapons=>match[2].to_f, :shields=>match[3].to_f, :cargo=>match[4].to_f,
|
43
|
+
:pop=>match[5], :ind=>match[6].to_f, :num_planets=>match[7].to_i, :relation=>match[8], :vote=>match[9].to_f})
|
44
|
+
when 0 then # All init data must be given in a state hash
|
45
|
+
super state
|
46
|
+
end
|
47
|
+
add if self.class.dataset # Add instantiated model to dataset if it is defined
|
48
|
+
@order = Order.new self, 'zelikaka', state[:game], state[:turn] if your?
|
49
|
+
end
|
50
|
+
|
51
|
+
def key; name end
|
52
|
+
|
53
|
+
# Special collection accessors
|
54
|
+
def sciences; products.find_all {|p| p and p.science?} || [] end # TODO redefine find_all in HashArray?
|
55
|
+
def ships; products.find_all {|p| p and p.ship?} || [] end
|
56
|
+
alias designs ships
|
57
|
+
alias ship_types ships
|
58
|
+
|
59
|
+
|
60
|
+
def battle_groups ; groups.find_all {|g| g and g.from_battle?} || [] end
|
61
|
+
|
62
|
+
# Boolean tests on Races
|
63
|
+
def rip? ; name.split("_")[-1] == 'RIP' end
|
64
|
+
|
65
|
+
def enemy? ; relation == 'War' end
|
66
|
+
alias war? enemy?
|
67
|
+
|
68
|
+
def friend? ; not enemy? end
|
69
|
+
alias ally? friend?
|
70
|
+
alias peace? friend?
|
71
|
+
|
72
|
+
def your? ; relation == '-' end # Redefining TODO or test if this is your other controlled Race (aka 3ombies)
|
73
|
+
|
74
|
+
def <=>(other)
|
75
|
+
case other
|
76
|
+
when nil then 1
|
77
|
+
when Race then num_planets == other.num_planets ? key <=> other.key : num_planets <=> other.num_planets
|
78
|
+
when Product, Planet, Group, Bombing, Route, Fleet then - other <=> self
|
79
|
+
when Integer then num_planets <=> other
|
80
|
+
when String then self <=> other.downcase.to_sym
|
81
|
+
when Symbol then
|
82
|
+
return rip? ? 0 : 1 if other == :rip or other == :dead
|
83
|
+
return !rip? ? 0 : 1if other == :alive or other == :active
|
84
|
+
return enemy? ? 0 :1 if other == :enemy or other == :war
|
85
|
+
return friend? ? 0 : 1 if other == :friend or other == :ally or other == :peace
|
86
|
+
return your? ? 0 : 1 if other == :your or other == :yours or other == :controlled or other == :my or other == :mine
|
87
|
+
return 0 if name.downcase.include? other.to_s
|
88
|
+
key <=> other.to_s
|
89
|
+
else raise ArgumentError, 'Comparison with a wrong type'
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class Order
|
95
|
+
attr_accessor :race, :text
|
96
|
+
|
97
|
+
def initialize( race, password, game, turn )
|
98
|
+
@race = race
|
99
|
+
@password = password
|
100
|
+
@game = game
|
101
|
+
@turn = turn
|
102
|
+
@text = "#order #@game #{@race.name}_#@password turn #@turn\n#end\n"
|
103
|
+
end
|
104
|
+
|
105
|
+
def add_line line
|
106
|
+
@text[-6] = "\n" + line + "\n"
|
107
|
+
end
|
108
|
+
|
109
|
+
def recalc_order
|
110
|
+
puts "Unable to recalculate: method under construction!"
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'galaxy/models/models'
|
2
|
+
|
3
|
+
class Route < ActiveRecord::Base
|
4
|
+
virtual
|
5
|
+
tableless :columns => [
|
6
|
+
[ :cargo, :string ]
|
7
|
+
]
|
8
|
+
belongs_to :race
|
9
|
+
belongs_to :planet
|
10
|
+
belongs_to :target, :class_name => "Planet"
|
11
|
+
|
12
|
+
def initialize match, state # match is not really used, remains for consistency with other models
|
13
|
+
|
14
|
+
return if match.join == 'N$MCE' # Skip header
|
15
|
+
match[1..4].each_with_index do |m,i|
|
16
|
+
if m != '-'
|
17
|
+
super :cargo => ['cap', 'mat', 'col', 'empty'][i]
|
18
|
+
self.target = Planet.new_or_update [m], state.merge({:race=>nil,:product=>nil,:created_by=>self}) unless self.target = Planet.lookup(m)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
planet = Planet.new_or_update [match[0]], state.merge({:race=>nil,:product=>nil,:created_by=>self}) unless planet = Planet.lookup(match[0])
|
22
|
+
race = Race.lookup(state[:owner])
|
23
|
+
|
24
|
+
race.routes << self
|
25
|
+
planet.routes << self
|
26
|
+
target.incoming_routes << self
|
27
|
+
add if self.class.dataset # Add instantiated model to dataset if it is defined
|
28
|
+
end
|
29
|
+
|
30
|
+
def kill
|
31
|
+
if result = super
|
32
|
+
race.routes.delete self if race
|
33
|
+
planet.routes.delete self if planet and planet == self
|
34
|
+
target.incoming_routes.delete self if target and target == self
|
35
|
+
self.planet = nil
|
36
|
+
self.target = nil
|
37
|
+
end
|
38
|
+
result
|
39
|
+
end
|
40
|
+
|
41
|
+
def key ; [planet.num, target.num, cargo].join('.') end
|
42
|
+
|
43
|
+
def <=>(other)
|
44
|
+
case other
|
45
|
+
when nil then 1
|
46
|
+
when Route then key <=> other.key
|
47
|
+
when Race then race <=> other
|
48
|
+
when Planet then planet == other ? 0 : target <=> other
|
49
|
+
when Integer, Float then planet.distance(target) <=> other
|
50
|
+
when String then self <=> other.downcase.to_sym
|
51
|
+
when Symbol then
|
52
|
+
return 0 if race == other
|
53
|
+
return 0 if planet == other
|
54
|
+
return 0 if target == other
|
55
|
+
return 0 if cargo.downcase.include? other.to_s
|
56
|
+
key.downcase <=> other.to_s
|
57
|
+
else raise ArgumentError, 'Comparison with a wrong type'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end #Route
|
data/lib/galaxy/order.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# A <race> - declare peace
|
2
|
+
# B <Num> [amount] - break group, also for fleets
|
3
|
+
# D <Name><drive><ammo><weap><defence><crg> - design a ship
|
4
|
+
# D <Name> - delete a ship
|
5
|
+
# G <Num><Race>[amount] - gift a group
|
6
|
+
# H <Name><D><W><S><C> - design a science
|
7
|
+
# H <Name> - delete a science
|
8
|
+
# J <Num><Fleet>[amount] - join group Num to Fleet
|
9
|
+
# J <Fleet1><Fleet2> - join F1 to F2
|
10
|
+
# K <Num><amount> - take group to pieces
|
11
|
+
# L <Num><CargoType>[ShipAmount,[CargoAmount]] - load group
|
12
|
+
# N <Planet><NewName> - rename a planet
|
13
|
+
# O - options
|
14
|
+
# P <Planet><Prod> - set production type
|
15
|
+
# Q <GameName><Race><Pass> - quit after 3 turns
|
16
|
+
# R <Src><CargoType>[Dst] - route
|
17
|
+
# S <Num><Dst>[Amount] - send a group
|
18
|
+
# S <Fleet><Dst> - send a fleet
|
19
|
+
# T <TypeName><NewName> - rename type
|
20
|
+
# U <Num>[ShipAmount[CargoAmount]] - unload group
|
21
|
+
# V <Race> - vote
|
22
|
+
# W <Race> - declare war
|
23
|
+
# X <Num><Tech>[ShipAmount[MaxTech]] - upgrade group
|
24
|
+
# Z [parameters] - order a report
|
@@ -0,0 +1,176 @@
|
|
1
|
+
#TAG report.rb tasks are on
|
2
|
+
require 'galaxy/section.rb'
|
3
|
+
|
4
|
+
# Module Gamedata provides G+ game-related data elements and collections
|
5
|
+
# and defines basic accessors to them for any Class including this module
|
6
|
+
module Gamedata
|
7
|
+
require 'galaxy/utils.rb'
|
8
|
+
require 'galaxy/models/models.rb'
|
9
|
+
|
10
|
+
attr_accessor :order # Order (Object?) associated with this report
|
11
|
+
attr_accessor :owner # Report owner race
|
12
|
+
attr_accessor :game # Report game name
|
13
|
+
attr_accessor :turn # Report turn number
|
14
|
+
attr_accessor :time # Report time (from server)
|
15
|
+
attr_accessor :server # Report server version (string)
|
16
|
+
|
17
|
+
# Establish Data Collections
|
18
|
+
attr_accessor :races # Races in Report
|
19
|
+
attr_accessor :products # Sciences in Report
|
20
|
+
attr_accessor :bombings # Bombings in Report
|
21
|
+
attr_accessor :planets # Planets in Report
|
22
|
+
attr_accessor :routes # Routes in Report
|
23
|
+
attr_accessor :fleets # Fleets in Report
|
24
|
+
attr_accessor :groups # Groups in Report
|
25
|
+
attr_accessor :battles #FIXME Add Battles (based on Battle Protocols) later
|
26
|
+
|
27
|
+
def initialize *args
|
28
|
+
# When object of any including Class is instantiated (and calling super),
|
29
|
+
# modify ActiveRecord:Base and point @@dataset attribute to this object
|
30
|
+
ActiveRecord::Base.establish_dataset(self)
|
31
|
+
|
32
|
+
# Initialize G+ collections
|
33
|
+
@races = HashArray.new
|
34
|
+
@bombings = HashArray.new
|
35
|
+
@planets = HashArray.new
|
36
|
+
@routes = HashArray.new
|
37
|
+
@fleets = HashArray.new
|
38
|
+
@groups = HashArray.new
|
39
|
+
@products = HashArray.new
|
40
|
+
|
41
|
+
# Generate Products that are present in each G+ report by default
|
42
|
+
@products['Drive_Research'] = Product.new [], {:name=>'Drive', :prod_type=>'research'}
|
43
|
+
@products['Weapons_Research'] = Product.new [], {:name=>'Weapons', :prod_type=>'research'}
|
44
|
+
@products['Shields_Research'] = Product.new [], {:name=>'Shields', :prod_type=>'research'}
|
45
|
+
@products['Cargo_Research'] = Product.new [], {:name=>'Cargo', :prod_type=>'research'}
|
46
|
+
@products['Capital'] = Product.new [], {:name=>'Capital', :prod_type=>'cap'}
|
47
|
+
@products['Materials'] = Product.new [], {:name=>'Materials', :prod_type=>'mat'}
|
48
|
+
|
49
|
+
super *args
|
50
|
+
end
|
51
|
+
|
52
|
+
def sciences
|
53
|
+
@products.find_all {|p| p and p.race and p.science?} || [] #redefine find_all in HashArray?
|
54
|
+
end
|
55
|
+
|
56
|
+
def ships
|
57
|
+
@products.find_all {|p| p and p.race and p.ship?} || [] #redefine find_all in HashArray?
|
58
|
+
end
|
59
|
+
alias designs ships
|
60
|
+
|
61
|
+
def battle_groups
|
62
|
+
@groups.find_all {|g| g and g.from_battle?} || []
|
63
|
+
end
|
64
|
+
|
65
|
+
def incoming_groups
|
66
|
+
@groups.find_all {|g| g and g.incoming?} || []
|
67
|
+
end
|
68
|
+
|
69
|
+
def your_groups
|
70
|
+
@groups.find_all {|g| g and g.your?} || []
|
71
|
+
end
|
72
|
+
|
73
|
+
def your_active_groups
|
74
|
+
@groups.find_all {|g| g and g.your_active?} || []
|
75
|
+
end
|
76
|
+
|
77
|
+
def unidentified_groups
|
78
|
+
@groups.find_all {|g| g and g.unidentified?} || []
|
79
|
+
end
|
80
|
+
alias unknown_groups unidentified_groups
|
81
|
+
|
82
|
+
def your_planets
|
83
|
+
@planets.find_all {|p| p and p.your?} || []
|
84
|
+
end
|
85
|
+
|
86
|
+
def uninhabited_planets
|
87
|
+
@planets.find_all {|p| p and p.uninhabited? } || []
|
88
|
+
end
|
89
|
+
|
90
|
+
def unidentified_planets
|
91
|
+
@planets.find_all {|p| p and p.unidentified? } || []
|
92
|
+
end
|
93
|
+
alias unknown_planets unidentified_planets
|
94
|
+
end
|
95
|
+
|
96
|
+
# Describes Galaxy Plus Report (as received from server) data structures and provides
|
97
|
+
# 'parse' method for extracting data it.
|
98
|
+
# Initially, Report "contains" extracted data (through included module Gamedata),
|
99
|
+
# but later on these data containers should be moved to new Game class
|
100
|
+
class Report < Section
|
101
|
+
include Gamedata
|
102
|
+
|
103
|
+
# Describes G+ Report data structure, opens report file if given a valid file name
|
104
|
+
def initialize (*args)
|
105
|
+
|
106
|
+
# Define Proc for Report Header processing TODO
|
107
|
+
report_proc = lambda do |match, state|
|
108
|
+
@owner = state[:owner] = match[1]
|
109
|
+
@game = state[:game] = match[2]
|
110
|
+
@turn = state[:turn] = match[3].to_i
|
111
|
+
@time = state[:time] = match[4]
|
112
|
+
@server = state[:server] = match[5]
|
113
|
+
end
|
114
|
+
|
115
|
+
# Define G+ Report Sections
|
116
|
+
@sections = [
|
117
|
+
:races,
|
118
|
+
{:name=>:science_products, :mult=>true},
|
119
|
+
{:name=>:ship_products, :mult=>true},
|
120
|
+
{:name=>:battle_planets, :footer => 'Battle Protocol',
|
121
|
+
:sections => [Section.new(:name=>:battle_groups, :mult=>true)], :mult=>true },
|
122
|
+
:bombings,
|
123
|
+
{:header => 'Maps_header', :skip=>true},
|
124
|
+
:incoming_groups,
|
125
|
+
:your_planets,
|
126
|
+
:production_planets,
|
127
|
+
:routes,
|
128
|
+
{:name=>:planets, :mult=>true},
|
129
|
+
:uninhabited_planets,
|
130
|
+
:unidentified_planets,
|
131
|
+
:fleets,
|
132
|
+
:your_groups,
|
133
|
+
{:name=>:groups, :mult=>true},
|
134
|
+
:unidentified_groups
|
135
|
+
].map do |init| Section.new init end
|
136
|
+
|
137
|
+
# Initialize main Section and data Collections (from module Gamedata)
|
138
|
+
super :name=>:reports, :header_proc=>report_proc, :sections => @sections
|
139
|
+
|
140
|
+
# Checking arguments
|
141
|
+
case args.size
|
142
|
+
when 0 then # Do nothing
|
143
|
+
when 1 then open *args
|
144
|
+
else # Wrong number of arguments, initializer failed
|
145
|
+
puts "Usage: Report.new or Report.new(file_name)"
|
146
|
+
raise ArgumentError, "Wrong number of arguments in Report initialization"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Validates file name and reads everything from file into Report's @text property
|
151
|
+
def open file_name
|
152
|
+
if file_name =~ /\A[[:punct:]\w\d]+.rep\z/ and File.exists? file_name
|
153
|
+
# This is a valid and existing rep file name
|
154
|
+
elsif file_name += '.rep' and file_name =~ /\A[[:punct:]\w\d]+.rep\z/ and File.exists? file_name
|
155
|
+
# This is a valid and existing rep file (without rep suffix)
|
156
|
+
else
|
157
|
+
raise ArgumentError, "Can't open: Invalid Report file #{file_name}"
|
158
|
+
end
|
159
|
+
# puts "Initializing Report from rep file: " + file_name
|
160
|
+
# Open file and pass file stream into block to read from it into Report's @text property
|
161
|
+
# File closing is automatic upon execution of the block
|
162
|
+
File.open(file_name, "r") do |f| @text = f.read end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Method returns status string of the Report
|
166
|
+
def status
|
167
|
+
return "
|
168
|
+
Report: #@owner #@game #@turn #@time #@server
|
169
|
+
Races: #{@races.size} Sciences: #{sciences.size} Types: #{designs.size} BattleGroups: #{battle_groups.size} \
|
170
|
+
Bombings: #{@bombings.size} Incomings: #{incoming_groups.size} Your Planets: #{your_planets.size} \
|
171
|
+
Ships in Production: #{@productions} Routes: #{@routes.size}
|
172
|
+
Planets: #{@planets.size} Uninhabited Planets: #{uninhabited_planets.size} Unidentified Planets: #{unidentified_planets.size} \
|
173
|
+
Fleets: #{@fleets.size} Your Groups: #{your_groups.size} Groups: #{@groups.size} Unidentified Groups: #{unidentified_groups.size}"
|
174
|
+
end
|
175
|
+
|
176
|
+
end #class Report
|
@@ -0,0 +1,226 @@
|
|
1
|
+
#TAG section.rb tasks are on
|
2
|
+
#require 'oniguruma'
|
3
|
+
|
4
|
+
# Regexen module provides pattern Constants that are used by Sections to describe its data structure
|
5
|
+
module Regexen
|
6
|
+
#Defining basic regex patterns...
|
7
|
+
Name = '([[:punct:]\w]+)'
|
8
|
+
Sname = ' +?' + Name
|
9
|
+
Fname = '^ *?' + Name #first Name in line (may be preceded by spaces, or not)
|
10
|
+
Num = '(\d+(?:\.\d+)?)(?=\s)'
|
11
|
+
Snum = ' +?' + Num
|
12
|
+
Fnum = '^ *?' + Num
|
13
|
+
Int = '(\d+)'
|
14
|
+
Sint = ' +?' + Int
|
15
|
+
Fint = '^ *?' + Int #first int in line (may be preceded by spaces, or not)
|
16
|
+
Line = '([[:punct:]\w ]+)'
|
17
|
+
Scargo = ' +?(COL|CAP|MAT|-)'
|
18
|
+
Sstatus = ' +?(War|Peace|-|In_Battle|Out_Battle|In_Space|In_Orbit|Upgrade|Launched|Transfer_Status|Damaged|Wiped)' #may need further refining for productivity
|
19
|
+
Group = Sname + Snum * 4 + Scargo + Snum #Different from actual Groups?! Have to check...
|
20
|
+
Header = '\n\n\s*?([PIRVNDAWSCM#TQLOXY$EGF]\s+?)'
|
21
|
+
|
22
|
+
#Defining section header/footer patterns
|
23
|
+
Reports_header = Name + ' Report for Galaxy PLUS' + Sname + ' Turn' + Sint + ' ' + Line + '$\s*' + Line
|
24
|
+
#+ '$$\s*Size:' + Snum + '\s*Planets:' + Snum + '\s*Players:'+ Snum
|
25
|
+
Races_header = '^\s*?Status of Players .+?' + Snum + ' votes\)' + Header
|
26
|
+
Science_products_header = '^\s*?' + Name + ' Sciences' + Header
|
27
|
+
Ship_products_header = '^\s*?' + Name + ' Ship Types' + Header
|
28
|
+
Battle_planets_header = '^\s*?Battle at \(#' + Int + '\) ' + Name
|
29
|
+
Bombings_header = '^\s*?Bombings' + Header
|
30
|
+
Maps_header = '^\s*Map around'
|
31
|
+
Incoming_groups_header = '^\s*?(Incoming) Groups' + Header
|
32
|
+
Your_planets_header = '^\s*?(Your) Planets' + Header
|
33
|
+
Planets_header = '^\s*?' + Name + ' Planets' + Header
|
34
|
+
Production_planets_header = '^\s*?Ships In Production' + Header
|
35
|
+
Routes_header = '^\s*?(Your) Routes' + Header
|
36
|
+
Uninhabited_planets_header = '^\s*?(Uninhabited) Planets' + Header
|
37
|
+
Unidentified_planets_header = '^\s*?(Unidentified) Planets' + Header
|
38
|
+
Fleets_header = '^\s*?(Your) Fleets' + Header
|
39
|
+
Your_groups_header = '^\s*?(Your) Groups' + Header
|
40
|
+
Groups_header = '^\s*?' + Name + ' Groups(?<!Your Groups)(?<!Unidentified Groups)\s+?' #' Groups(?<!Your Groups)(?<!Unidentified Groups)\s+?' + Header
|
41
|
+
Battle_groups_header = Groups_header
|
42
|
+
Unidentified_groups_header = '^\s*(Unidentified) Groups' + Header
|
43
|
+
Default_footer ='(\n\n)|(\z)'
|
44
|
+
|
45
|
+
#Defining data record patterns
|
46
|
+
Races_record = Fname + Snum * 7 + Sstatus + Snum
|
47
|
+
Science_products_record = Fname + Snum * 4
|
48
|
+
Ship_products_record = Fname + Snum * 6
|
49
|
+
#Battles_line = '^' + Line + '(fires on)' + Line + '($)'
|
50
|
+
Battle_planets_record = Battle_planets_header
|
51
|
+
Battle_groups_record = Fint + Group + Sint + Sstatus
|
52
|
+
Bombings_record = Fname + Sname + Sint + Sname + Snum * 2 + Sname + Snum * 4 + Sstatus
|
53
|
+
Incoming_groups_record = Fname + Sname + Snum * 3
|
54
|
+
Unidentified_planets_record = Fint + Snum * 2
|
55
|
+
Uninhabited_planets_record = Unidentified_planets_record + Sname + Snum * 4
|
56
|
+
Planets_record = Uninhabited_planets_record + Sname + Snum * 4
|
57
|
+
Your_planets_record = Planets_record
|
58
|
+
Production_planets_record = Fint + Sname * 2 + Snum * 3
|
59
|
+
Routes_record = Fname + Sname * 4 #Attention! routes_record captures column headers line
|
60
|
+
Fleets_record = Fint + Sname + Sint + Sname * 2 + Snum * 2 + Sstatus
|
61
|
+
Your_groups_record = Fint + Sint + Group + Sname * 2 + Snum * 3 + Sname + Sstatus
|
62
|
+
Groups_record = Fint + Group + Sname + Snum * 2
|
63
|
+
Unidentified_groups_record = Fnum + Snum
|
64
|
+
# FIXME Broadcasts not defined
|
65
|
+
|
66
|
+
#Defining data processing Procs
|
67
|
+
Default_header_proc = lambda do |match, state|
|
68
|
+
state[:race] = match[1] == 'Your' ? Race.lookup(state[:owner]) : Race.lookup(match[1]) if match[1]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Section is a piece of text that contains data structured in a certain way (possibly with sub-sections),
|
73
|
+
# Section describes this data structure and provides methods for parsing the text and extracting data records
|
74
|
+
# By default, Section calls new_or_update method on class defined by Section name
|
75
|
+
# Example: Section(:name=>:battle_groups) calls Group.new_or_update(match, state) on each found /Battle_group_record/ match
|
76
|
+
# by default, unless :record_proc is provided to Section
|
77
|
+
# 'state' hash is used to provide context for multi-section parsing
|
78
|
+
class Section
|
79
|
+
include Regexen
|
80
|
+
|
81
|
+
attr_accessor :name # Name of this Section (also used to auto-generate properties)
|
82
|
+
attr_accessor :text # Source text of this Section (raw material for data extraction)
|
83
|
+
attr_accessor :header # Regex identifying start of this Section (obligatory!)
|
84
|
+
attr_accessor :footer # Regex identifying end of this Section (end of EACH Multisections)
|
85
|
+
attr_accessor :record # Regex matching data Record (or an array of such regexen)
|
86
|
+
attr_accessor :header_proc # Proc object to be run on Header match
|
87
|
+
attr_accessor :footer_proc # Proc object to be run on Footer match
|
88
|
+
attr_accessor :record_proc # Proc object to be run on each Record match (or an array of Procs)
|
89
|
+
attr_accessor :sections # (Sub)sections (possibly) contained inside this Section
|
90
|
+
attr_accessor :skip # Flag indicating that this Sections contains no data (should be skipped)
|
91
|
+
attr_accessor :mult # Flag indicating that this is a "Multisection" (several Sections with similar headers one after another)
|
92
|
+
|
93
|
+
# New Section is created by using the following syntax:
|
94
|
+
# Section.new {:header => header, :footer => footer, :record =>[rec1,rec2], :sections => [sec1,sec2,sec3]}
|
95
|
+
# Section.new {:name => name} -> extracted as {:header => Name_header, :footer => Name_footer, :record =>Name_record }
|
96
|
+
# Section.new :symbol -> extracted as {:header => Symbol_header, :footer => Symbol_footer, :record =>Symbol_record }
|
97
|
+
def initialize args
|
98
|
+
case args # Parsing (named) arguments
|
99
|
+
when Symbol, String # Symbol represents Section name, appropriately named Constants MUST be defined in Regexen module
|
100
|
+
@name = args.to_s.downcase.capitalize
|
101
|
+
when Hash
|
102
|
+
@name = args[:name].to_s.downcase.capitalize if args[:name]
|
103
|
+
@text = args[:text]
|
104
|
+
@skip = args[:skip]
|
105
|
+
@mult = args[:mult]
|
106
|
+
@sections = args[:sections]
|
107
|
+
|
108
|
+
# Header/footer/record patterns and appropriate processing Procs can be:
|
109
|
+
# 1) given as a Constant name (should be defined in Regexen module),
|
110
|
+
# 2) given as a direct value (escaped pattern string literal or Proc, respectively), or
|
111
|
+
# 3) not given at all, appropriate values should be inferred from :name argument
|
112
|
+
@header = Regexen.const_get(args[:header]) rescue args[:header]
|
113
|
+
@footer = Regexen.const_get(args[:footer]) rescue args[:footer]
|
114
|
+
@record = Regexen.const_get(args[:record]) rescue args[:record]
|
115
|
+
@header_proc = Regexen.const_get(args[:header_proc]) rescue args[:header_proc]
|
116
|
+
@footer_proc = Regexen.const_get(args[:footer_proc]) rescue args[:footer_proc]
|
117
|
+
@record_proc = Regexen.const_get(args[:record_proc]) rescue args[:record_proc]
|
118
|
+
end #case
|
119
|
+
|
120
|
+
# Try to auto-generate Section's Patterns and Procs from @name (if they are not already given)
|
121
|
+
# First we try to find Regexen constants derived from name, if not found then we look for defaults
|
122
|
+
@header = @header || Regexen.const_get(@name + '_header') rescue
|
123
|
+
if Regexen.const_defined?('Default_header') then Regexen.const_get('Default_header') end
|
124
|
+
@footer = @footer || Regexen.const_get(@name + '_footer') rescue
|
125
|
+
if Regexen.const_defined?('Default_footer') then Regexen.const_get('Default_footer') end
|
126
|
+
@record = @record || Regexen.const_get(@name + '_record') rescue
|
127
|
+
if Regexen.const_defined?('Default_record') then Regexen.const_get('Default_record') end
|
128
|
+
@header_proc = @header_proc || Regexen.const_get(@name + '_header_proc') rescue
|
129
|
+
if Regexen.const_defined?('Default_header_proc') then Regexen.const_get('Default_header_proc') end
|
130
|
+
@footer_proc = @footer_proc || Regexen.const_get(@name + '_footer_proc') rescue
|
131
|
+
if Regexen.const_defined?('Default_footer_proc') then Regexen.const_get('Default_footer_proc') end
|
132
|
+
@record_proc = @record_proc || Regexen.const_get(@name + '_record_proc') rescue
|
133
|
+
if Regexen.const_defined?('Default_record_proc') then Regexen.const_get('Default_record_proc') end
|
134
|
+
|
135
|
+
# This is a G+ specific piece of code overriding general Section functionality (Default_record_proc)
|
136
|
+
# Needed to speed up calculations and avoid class evaluations on each record
|
137
|
+
# Class name of the Object (described by Record), e.g "Group"
|
138
|
+
if @name and not @record_proc
|
139
|
+
klass_name = @name.split("_")[-1][0..-2].capitalize
|
140
|
+
if Object.const_defined?(klass_name)
|
141
|
+
klass = Object.const_get(klass_name)
|
142
|
+
@record_proc ||= lambda do |match, state|
|
143
|
+
klass.new_or_update match[1..-1], state
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end #initialize
|
148
|
+
|
149
|
+
# Returns (relatively) deep copy of self
|
150
|
+
def copy
|
151
|
+
secs = @sections ? @sections.map {|s| s.copy} : nil
|
152
|
+
Section.new :name=>@name, :header=>@header, :footer=>@footer, :record=>@record, :header_proc=>@header_proc,
|
153
|
+
:footer_proc=>@footer_proc, :record_proc=>@record_proc, :sections=>secs, :skip=>@skip, :mult=>@mult, :text=>@text
|
154
|
+
end
|
155
|
+
|
156
|
+
# Recursively parse Section, extract data records
|
157
|
+
def parse state={}
|
158
|
+
state[:section] = @name
|
159
|
+
if @mult
|
160
|
+
#puts "Mults: #{self.name} #{self.header}"
|
161
|
+
# Multisection: Find out if this Section is actually a collection of sections with similar headers
|
162
|
+
# If it is, clone an Array of multisections and call parse on each (data extraction happens downstream)
|
163
|
+
scan_text(@header) do |match|
|
164
|
+
start = match.begin
|
165
|
+
finish = -1 unless finish = find_text(@footer, match.end) # Find end of Section (after Header END)
|
166
|
+
s = self.copy # Create a copy of Section (to be used as child multisection template)
|
167
|
+
s.mult = false
|
168
|
+
s.text = @text[start..finish] # Set text property for found multisection
|
169
|
+
s.parse state # Recursively call parse on each found multisection
|
170
|
+
end
|
171
|
+
else
|
172
|
+
# Process Section Header, Records and Footer (if any)
|
173
|
+
find_text(@header) {|match| @header_proc.call match, state} if @header and @header_proc
|
174
|
+
scan_text(@record) {|match| @record_proc.call match, state} if @record and @record_proc
|
175
|
+
find_text(@footer) {|match| @footer_proc.call match, state} if @footer and @footer_proc
|
176
|
+
|
177
|
+
if @sections
|
178
|
+
#puts "Sections: #{self.name} #{self.header}"
|
179
|
+
# Process Sections array against @text, skipping empty/skippable Sections, recursively
|
180
|
+
# calling parse on found Sections and moving forward position cursor pos
|
181
|
+
# TODO Generalize for UNORDERED Sections (position cursor should not work in this case)
|
182
|
+
finish = 0
|
183
|
+
@sections.each_with_index do |s, i|
|
184
|
+
next if s.skip #Skip non-data Section
|
185
|
+
if start = find_text(s.header, finish) # Find Section Header
|
186
|
+
finish = nil # Needed for last Section (no next section to find)
|
187
|
+
@sections[i+1..-1].each do |sn| # Find finish by cycling through next Section Headers
|
188
|
+
break if finish = find_text(sn.header, start) # Find first of next Section Header
|
189
|
+
end
|
190
|
+
finish = -1 unless finish # If finish not found, set it to the end of @text
|
191
|
+
#Start and finish defined, assign text to this Section and recursively parse it
|
192
|
+
s.text = @text[start..finish]
|
193
|
+
s.parse state
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end #parse
|
199
|
+
|
200
|
+
# Safely matches given regex to @text (starting at position pos),
|
201
|
+
# returns initial offset of match or nil if regex not found, yield match to given block (if any)
|
202
|
+
def find_text regex, pos=0
|
203
|
+
return nil if @text == nil
|
204
|
+
return nil if regex == nil
|
205
|
+
text = pos == 0 ? @text : @text[pos..-1]
|
206
|
+
match = Oniguruma::ORegexp.new(regex).match(text)
|
207
|
+
return nil unless match
|
208
|
+
yield match if block_given?
|
209
|
+
pos + match.begin # Return initial match offset (corrected for position pos)
|
210
|
+
end #find_text
|
211
|
+
|
212
|
+
# Scans @text for Data Records matching given regex pattern, returns array of matching Data Records
|
213
|
+
# (as MatchData or String array), yields each found match object to given block (if any)
|
214
|
+
def scan_text regex, pos=0
|
215
|
+
text = pos == 0 ? @text : @text[pos..-1]
|
216
|
+
if block_given?
|
217
|
+
# Scan Section for regex matches, yield each match to given block, return array of MATCH objects
|
218
|
+
Oniguruma::ORegexp.new(regex).scan(text) {|match| yield match }
|
219
|
+
else
|
220
|
+
# Scan Section for regex matches, return array of matches converted into string arrays
|
221
|
+
results=[]
|
222
|
+
Oniguruma::ORegexp.new(regex).scan(text) {|match| results << match[1..-1].to_a }
|
223
|
+
results
|
224
|
+
end
|
225
|
+
end #scan_text
|
226
|
+
end #class Section
|