galaxy 0.0.2
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/.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
|