ceml 0.5.8 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.8
1
+ 0.6.0
data/ceml.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{ceml}
8
- s.version = "0.5.8"
8
+ s.version = "0.6.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Joe Edelman"]
12
- s.date = %q{2011-01-21}
12
+ s.date = %q{2011-01-28}
13
13
  s.description = %q{a language for coordinating real world events}
14
14
  s.email = %q{joe@citizenlogistics.com}
15
15
  s.extra_rdoc_files = [
@@ -32,11 +32,12 @@ Gem::Specification.new do |s|
32
32
  "examples/citizen-investigation.ceml",
33
33
  "examples/high-fives.ceml",
34
34
  "lib/ceml.rb",
35
- "lib/ceml/casting_criterion.rb",
36
35
  "lib/ceml/casting_statement.rb",
36
+ "lib/ceml/confluence.rb",
37
37
  "lib/ceml/driver.rb",
38
38
  "lib/ceml/incident.rb",
39
39
  "lib/ceml/instruction_statements.rb",
40
+ "lib/ceml/role.rb",
40
41
  "lib/ceml/script.rb",
41
42
  "lib/ceml/tt/casting.treetop",
42
43
  "lib/ceml/tt/instructions.treetop",
@@ -1,18 +1,24 @@
1
1
  module CEML
2
+
3
+ class Criteria < Struct.new :plus_tags, :minus_tags, :matching, :radius, :timewindow
4
+ def complexity; plus_tags.size; end
5
+ def =~(candidate)
6
+ candidate[:tags] ||= []
7
+ (plus_tags - candidate[:tags]).empty? and (minus_tags & candidate[:tags]).empty?
8
+ end
9
+ end
10
+
2
11
  module CastingStatement
3
12
  extend Forwardable
4
13
  def_delegators :roles, :names, :[], :min
5
14
  alias_method :rolenames, :names
6
15
 
7
- def criteria(script)
16
+ def roles_to_cast(script)
8
17
  return [] unless type == :await
9
- group_by = if radius then [:city] else [] end
10
- criteria_by_qualifiers = Hash.new do |h,k|
11
- h[k] = CastingCriterion.new(script, k, [], group_by, radius, {})
18
+ roles.list.map do |r|
19
+ c = Criteria.new(r.qualifiers, [], radius ? [:city] : [], radius, timewindow)
20
+ Role.new r.name, c, r.min..r.max, []
12
21
  end
13
-
14
- roles.list.each{ |r| criteria_by_qualifiers[r.qualifiers].role_counts[r.name] = r.min }
15
- criteria_by_qualifiers.values
16
22
  end
17
23
 
18
24
  def type
@@ -23,8 +29,22 @@ module CEML
23
29
  roles.max
24
30
  end
25
31
 
32
+ def within_phrase
33
+ return if modifiers.empty?
34
+ modifiers.elements.select{ |m| m.respond_to? :distance }.first
35
+ end
36
+
37
+ def over_phrase
38
+ return if modifiers.empty?
39
+ modifiers.elements.select{ |m| m.respond_to? :duration }.first
40
+ end
41
+
42
+ def timewindow
43
+ over_phrase && over_phrase.duration.seconds
44
+ end
45
+
26
46
  def radius
27
- within_phrase.empty? ? nil : within_phrase.distance.meters
47
+ within_phrase && within_phrase.distance.meters
28
48
  end
29
49
 
30
50
  def nab?
@@ -0,0 +1,50 @@
1
+ require 'geokit'
2
+ require 'forwardable'
3
+
4
+ module CEML
5
+ class Confluence
6
+ attr_accessor :script, :hash, :created, :dirty, :roles_to_cast, :incident_id, :star
7
+ alias_method :launched?, :incident_id
8
+
9
+ def initialize script, candidate = nil
10
+ @script = script
11
+ @hash = {}
12
+ @created = true
13
+ @roles_to_cast = script.roles_to_cast
14
+ push candidate if candidate
15
+ end
16
+
17
+ def rm *candidates
18
+ @roles_to_cast.each{ |role| role.rm *candidates }
19
+ end
20
+
21
+ def best_role_for candidate
22
+ winner = @roles_to_cast.max_by{ |role| role.affinity(candidate, star) }
23
+ winner unless winner[0] == -1
24
+ end
25
+
26
+ def stage_with_candidate candidate
27
+ best_role = best_role_for(candidate)
28
+ return :uninterested unless best_role
29
+ return :joinable if launched?
30
+ other_roles = @roles_to_cast - [best_role]
31
+ return :launchable if best_role.one_left? and other_roles.all?(&:filled?)
32
+ return :listable
33
+ end
34
+
35
+ def push candidate
36
+ best_role = best_role_for(candidate)
37
+ candidate[:roles] = best_role.name.to_sym
38
+ best_role.casted << candidate
39
+ @dirty = true
40
+ end
41
+
42
+ def full?
43
+ @roles_to_cast.all?{ |role| role.allowed == 0 }
44
+ end
45
+
46
+ def cast
47
+ @roles_to_cast.map{ |r| r.casted }.flatten
48
+ end
49
+ end
50
+ end
data/lib/ceml/driver.rb CHANGED
@@ -21,27 +21,65 @@ module CEML
21
21
  end
22
22
  alias_method :start, :with_incident
23
23
 
24
- LOCATIONS = {}
25
- def ping script, candidate, metadata = {}
26
- LOCATIONS[script] ||= []
27
- script.post candidate, LOCATIONS[script]
28
- LOCATIONS[script].delete_if do |loc|
29
- next unless loc.cast
30
- with_incident nil, script, metadata do |incident, players, metadata|
31
- loc.cast.each{ |guy| subpost incident, players, metadata, guy.initial_state }
24
+ LOCATIONS = Hash.new{ |h,k| h[k] = [] }
25
+ def ping script, candidate
26
+ return unless script.fits? candidate
27
+ candidate[:ts] = Time.now.utc.to_i
28
+ script_id = script.text_value
29
+
30
+ locs = LOCATIONS[script_id].group_by{ |l| l.stage_with_candidate(candidate) }
31
+ if locs[:joinable]
32
+ # puts "joining..."
33
+ first = locs[:joinable].shift
34
+ first.push candidate
35
+ push first.incident_id, nil, candidate
36
+
37
+ elsif locs[:launchable]
38
+ # puts "launching..."
39
+ first = locs[:launchable].shift
40
+ first.push candidate
41
+ cast = first.cast
42
+ push nil, script, *cast
43
+ (locs[:launchable] + (locs[:listable]||[])).each{ |l| l.rm *cast }
44
+
45
+ elsif locs[:listable]
46
+ # puts "listing..."
47
+ locs[:listable].each{ |l| l.push candidate }
48
+
49
+ else
50
+ c = Confluence.new(script)
51
+ case c.stage_with_candidate(candidate)
52
+ when :launchable
53
+ # puts "start-launching..."
54
+ c.push candidate
55
+ push nil, script, candidate
56
+ when :listable
57
+ # puts "start-listing..."
58
+ c.push candidate
59
+ LOCATIONS[script_id] << c
60
+ else raise "what?"
32
61
  end
62
+
33
63
  end
64
+
65
+ LOCATIONS[script_id].delete_if(&:full?)
66
+ # save the changed ones and clear dirty flag
34
67
  end
35
68
 
36
- def post incident_id, player = nil
37
- with_incident incident_id do |incident, players, metadata|
38
- subpost incident, players, metadata, player
69
+ def push incident_id, script, *updated_players
70
+ with_incident incident_id, script do |incident, players, metadata|
71
+ subpost incident, players, metadata, *updated_players
39
72
  end
40
73
  end
74
+
75
+ def post incident_id, *updated_players
76
+ push incident_id, nil, *updated_players
77
+ end
78
+
41
79
  alias_method :run, :post
42
80
 
43
- def subpost incident, players, metadata, player = nil
44
- if player
81
+ def subpost incident, players, metadata, *updated_players
82
+ updated_players.each do |player|
45
83
  player_id = player[:id]
46
84
  player[:roles] = Set.new([*player[:roles] || []])
47
85
  player[:roles] << :agents
data/lib/ceml/role.rb ADDED
@@ -0,0 +1,36 @@
1
+ module CEML
2
+ class Role < Struct.new :name, :criteria, :range, :casted
3
+ # def <=>(b); b.criteria.complexity <=> criteria.complexity; end
4
+ def affinity candidate, star
5
+ return [-1, -1, -1 ] unless fits?(candidate, star)
6
+ [ criteria.complexity, -needed, -allowed ]
7
+ end
8
+
9
+ def filled?; needed == 0; end
10
+ def one_left?; needed == 1; end
11
+
12
+ def rm(*ids); casted.delete_if{ |guy| ids.include? guy[:id] }; end
13
+ def needed; [range.min - casted.size, 0].max; end
14
+ def allowed; [range.max - casted.size, 0].max; end
15
+
16
+ def fits?(candidate, star = nil)
17
+ return false unless criteria =~ candidate
18
+ return false if casted.size >= range.max
19
+ return false if casted.any?{ |guy| guy[:id] == candidate[:id] }
20
+ return true unless star
21
+ c = criteria
22
+ if c.matching
23
+ return unless c.matching.all?{ |g| candidate[:matchables][g] == star[:matchables][g] }
24
+ end
25
+ if c.radius
26
+ c_ll = Geokit::LatLng(candidate[:lat], candidate[:lng])
27
+ s_ll = Geokit::LatLng(star[:lat], star[:lng])
28
+ return unless c_ll.distance_to(s_ll, :meters) <= c.radius
29
+ end
30
+ if c.timewindow
31
+ return unless star.ts - candidate.ts <= c.timewindow
32
+ end
33
+ return true
34
+ end
35
+ end
36
+ end
data/lib/ceml/script.rb CHANGED
@@ -8,16 +8,13 @@ module CEML
8
8
  # = casting =
9
9
  # ===========
10
10
 
11
- # public
12
- def post candidate, locs
13
- return unless candidate = candidate.dup.load(awaited_criteria)
14
- locs.each{ |l| l.push candidate }
15
- locs << CastingLocation.create(self, candidate) if locs.none?(&:added)
11
+ def fits? candidate
12
+ roles_to_cast.any?{ |r| r.fits? candidate }
16
13
  end
17
14
 
18
- def awaited_criteria
15
+ def roles_to_cast
19
16
  return [] unless cast.type == :await
20
- return cast.criteria(self)
17
+ return cast.roles_to_cast(self)
21
18
  end
22
19
 
23
20
  def nabs?
@@ -3,7 +3,15 @@ grammar Casting
3
3
  include Lexer
4
4
 
5
5
  rule casting_statement
6
- ('gather' / 'await' / 'nab') ws roles within_phrase:within_phrase? <CastingStatement>
6
+ ('gather' / 'await' / 'nab') ws roles modifiers:modifier_phrase* <CastingStatement>
7
+ end
8
+
9
+ rule modifier_phrase
10
+ over_phrase / within_phrase
11
+ end
12
+
13
+ rule over_phrase
14
+ ws 'over' ws duration
7
15
  end
8
16
 
9
17
  rule within_phrase
@@ -34,7 +34,7 @@ grammar Lexer
34
34
  end
35
35
 
36
36
  rule reserved_word
37
- 'and' / 'within'
37
+ 'and' / 'within' / 'over'
38
38
  end
39
39
 
40
40
  rule id
@@ -42,11 +42,11 @@ grammar Lexer
42
42
  end
43
43
 
44
44
  rule duration
45
- number ws? time_unit:('seconds' / 'second' / 's' / 'minutes' / 'minute' / 'min')
45
+ number ws? time_unit:('seconds' / 'second' / 's' / 'minutes' / 'minute' / 'min' / 'hours' / 'hour' / 'hr' / 'h')
46
46
  {
47
47
  def seconds
48
48
  number.text_value.to_f * case time_unit.text_value
49
- when /^mi/; 60; else 1; end
49
+ when /^h/; 60*60; when /^mi/; 60; else 1; end
50
50
  end
51
51
  }
52
52
  end
data/lib/ceml.rb CHANGED
@@ -9,7 +9,8 @@ require 'ceml/tt/casting'
9
9
  require 'ceml/tt/instructions'
10
10
  require 'ceml/tt/scripts'
11
11
 
12
- require 'ceml/casting_criterion'
12
+ require 'ceml/role'
13
+ require 'ceml/confluence'
13
14
  require 'ceml/incident'
14
15
  require 'ceml/driver'
15
16
 
@@ -65,9 +65,7 @@ class TestIncident < Test::Unit::TestCase
65
65
  def test_jane
66
66
  s = CEML.parse(:script, JANE_SCRIPT)
67
67
  play do
68
- c = CEML::Candidate.new('fred', ['new'], {})
69
- c.initial_state = { :received => 'freddy' }
70
- ping s, c
68
+ ping s, :id => 'fred', :tags => ['new'], :received => 'freddy'
71
69
  asked 'fred', /Hello freddy. You are Level Zero./
72
70
  says 'fred', "shoeless"
73
71
  asked 'fred', /favorite game/
@@ -97,7 +95,7 @@ class TestIncident < Test::Unit::TestCase
97
95
  def test_signup_1
98
96
  s = CEML.parse(:script, "await 1 new signup\ntell signup: thanks")
99
97
  play do
100
- ping s, CEML::Candidate.new('fred', ['new'], {})
98
+ ping s, :id => 'fred', :tags => ['new']
101
99
  told 'fred', /thanks/
102
100
  end
103
101
  end
@@ -105,11 +103,11 @@ class TestIncident < Test::Unit::TestCase
105
103
  def test_signup_2
106
104
  s = CEML.parse(:script, "await 2 new signups\ntell signups: thanks")
107
105
  play do
108
- ping s, CEML::Candidate.new('fred', ['new'], {})
106
+ ping s, :id => 'fred', :tags => ['new']
109
107
  silent 'fred'
110
- ping s, CEML::Candidate.new('wilma', ['old'], {})
108
+ ping s, :id => 'wilma', :tags => ['old']
111
109
  silent 'fred'
112
- ping s, CEML::Candidate.new('betty', ['new'], {})
110
+ ping s, :id => 'betty', :tags => ['new']
113
111
  told 'fred', /thanks/
114
112
  end
115
113
  end
@@ -117,12 +115,12 @@ class TestIncident < Test::Unit::TestCase
117
115
  def test_await
118
116
  s = CEML.parse(:script, "await a,b,c\ntell a: foo\ntell b: bar\ntell c: baz")
119
117
  play do
120
- ping s, CEML::Candidate.new('fred', [], {})
118
+ ping s, :id => 'fred'
121
119
  silent 'fred'
122
- ping s, CEML::Candidate.new('wilma', [], {})
120
+ ping s, :id => 'wilma'
123
121
  silent 'fred'
124
122
  silent 'wilma'
125
- ping s, CEML::Candidate.new('betty', [], {})
123
+ ping s, :id => 'betty'
126
124
  told 'fred', /foo/
127
125
  told 'betty', /baz/
128
126
  end
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 5
8
- - 8
9
- version: 0.5.8
7
+ - 6
8
+ - 0
9
+ version: 0.6.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Joe Edelman
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2011-01-21 00:00:00 -08:00
17
+ date: 2011-01-28 00:00:00 -08:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -54,11 +54,12 @@ files:
54
54
  - examples/citizen-investigation.ceml
55
55
  - examples/high-fives.ceml
56
56
  - lib/ceml.rb
57
- - lib/ceml/casting_criterion.rb
58
57
  - lib/ceml/casting_statement.rb
58
+ - lib/ceml/confluence.rb
59
59
  - lib/ceml/driver.rb
60
60
  - lib/ceml/incident.rb
61
61
  - lib/ceml/instruction_statements.rb
62
+ - lib/ceml/role.rb
62
63
  - lib/ceml/script.rb
63
64
  - lib/ceml/tt/casting.treetop
64
65
  - lib/ceml/tt/instructions.treetop
@@ -1,90 +0,0 @@
1
- require 'geokit'
2
- require "forwardable"
3
-
4
- module CEML
5
- class Candidate < Struct.new :uid, :tags, :matchables, :lat, :lng, :initial_state
6
- include Geokit::Mappable
7
- attr_reader :criteria
8
-
9
- def load(criteria)
10
- @criteria = criteria.select{ |c| c =~ self }
11
- if @criteria.empty? then nil else self end
12
- end
13
-
14
- def criteria_for_location(star)
15
- criteria.select{ |c| c.fits?(self, star) }
16
- end
17
- end
18
-
19
- class CastingLocation < Struct.new :script, :hash, :created
20
- attr_accessor :added
21
- def star; hash.values.flatten.first; end
22
-
23
- def criteria
24
- @criteria ||= script.awaited_criteria.sort_by{ |c| [-c.complexity, c.min_match] }
25
- end
26
-
27
- def push candidate
28
- matching_criteria = candidate.criteria_for_location(star)
29
- return if matching_criteria.empty?
30
- matching_criteria.each{ |c| (hash[c] ||= []) << candidate }
31
- @added = true
32
- end
33
-
34
- def self.create script, candidate
35
- new(script, {}, true).tap{ |x| x.push candidate }
36
- end
37
-
38
- # this method will miss possible castings and does not handle ranges at all
39
- def cast
40
- [].tap do |casting|
41
- criteria.each do |c|
42
- return nil unless folks = hash[c].dup
43
- folks -= casting
44
- return nil unless folks.size >= c.min_match
45
- c.role_counts.each do |role, minct|
46
- folks.shift(minct).each do |guy|
47
- guy.initial_state ||= {}
48
- guy.initial_state[:id] = guy.uid
49
- guy.initial_state[:roles] = role.to_sym
50
- casting << guy
51
- end
52
- end
53
- end
54
- end
55
- end
56
- end
57
-
58
- class CastingCriterion < Struct.new :script, :plus_tags, :minus_tags, :grouped_by, :radius, :role_counts
59
- def min_match; role_counts.values.reduce(:+); end
60
- def complexity; plus_tags.size; end
61
-
62
- def =~(candidate)
63
- (plus_tags - candidate.tags).empty? and (minus_tags & candidate.tags).empty?
64
- end
65
-
66
- def fits?(candidate, star)
67
- return true unless star
68
- return unless grouped_by.all?{ |g| candidate.matchables[g] == star.matchables[g] }
69
- p radius
70
- !radius or candidate.distance_to(star, :meters) <= radius
71
- end
72
- end
73
- end
74
-
75
- # def satisfied_by_group?(people, already_used)
76
- # if (people - already_used).size >= min_match
77
- # already_used.concat((people - already_used).first(min_match))
78
- # true
79
- # end
80
- # end
81
-
82
- # this method and the one below will miss possible
83
- # castings and do not handle ranges at all
84
- # def complete?(loc)
85
- # people_used = []
86
- # @criteria.all? do |c|
87
- # next unless folks = loc[c.hash]
88
- # c.satisfied_by_group?(folks, people_used)
89
- # end
90
- # end