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 +1 -1
- data/ceml.gemspec +4 -3
- data/lib/ceml/casting_statement.rb +28 -8
- data/lib/ceml/confluence.rb +50 -0
- data/lib/ceml/driver.rb +51 -13
- data/lib/ceml/role.rb +36 -0
- data/lib/ceml/script.rb +4 -7
- data/lib/ceml/tt/casting.treetop +9 -1
- data/lib/ceml/tt/lexer.treetop +3 -3
- data/lib/ceml.rb +2 -1
- data/test/test_incident.rb +8 -10
- metadata +6 -5
- data/lib/ceml/casting_criterion.rb +0 -90
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
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.
|
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-
|
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
|
16
|
+
def roles_to_cast(script)
|
8
17
|
return [] unless type == :await
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
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
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
37
|
-
with_incident incident_id do |incident, players, metadata|
|
38
|
-
subpost incident, players, metadata,
|
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,
|
44
|
-
|
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
|
-
|
12
|
-
|
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
|
15
|
+
def roles_to_cast
|
19
16
|
return [] unless cast.type == :await
|
20
|
-
return cast.
|
17
|
+
return cast.roles_to_cast(self)
|
21
18
|
end
|
22
19
|
|
23
20
|
def nabs?
|
data/lib/ceml/tt/casting.treetop
CHANGED
@@ -3,7 +3,15 @@ grammar Casting
|
|
3
3
|
include Lexer
|
4
4
|
|
5
5
|
rule casting_statement
|
6
|
-
('gather' / 'await' / 'nab') ws roles
|
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
|
data/lib/ceml/tt/lexer.treetop
CHANGED
@@ -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
data/test/test_incident.rb
CHANGED
@@ -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
|
-
|
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,
|
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,
|
106
|
+
ping s, :id => 'fred', :tags => ['new']
|
109
107
|
silent 'fred'
|
110
|
-
ping s,
|
108
|
+
ping s, :id => 'wilma', :tags => ['old']
|
111
109
|
silent 'fred'
|
112
|
-
ping s,
|
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,
|
118
|
+
ping s, :id => 'fred'
|
121
119
|
silent 'fred'
|
122
|
-
ping s,
|
120
|
+
ping s, :id => 'wilma'
|
123
121
|
silent 'fred'
|
124
122
|
silent 'wilma'
|
125
|
-
ping s,
|
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
|
-
-
|
8
|
-
-
|
9
|
-
version: 0.
|
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-
|
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
|