ceml 0.5.8 → 0.6.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/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