snapshpt
This commit is contained in:
commit
968acca766
|
@ -0,0 +1,195 @@
|
|||
###
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2016, Reactive Sets
|
||||
|
||||
equals( a, b )
|
||||
|
||||
Returns true if a and b are deeply equal, false otherwise.
|
||||
|
||||
Parameters:
|
||||
- a (Any type): value to compare to b
|
||||
- b (Any type): value compared to a
|
||||
|
||||
Implementation:
|
||||
'a' is considered equal to 'b' if all scalar values in a and b are strictly equal as
|
||||
compared with operator '==' except for these two special cases:
|
||||
- 0 == -0 but are not equal.
|
||||
- NaN is not == to itself but is equal.
|
||||
|
||||
RegExp objects are considered equal if they have the same lastIndex, i.e. both regular
|
||||
expressions have matched the same number of times.
|
||||
|
||||
Functions must be identical, so that they have the same closure context.
|
||||
"undefined" is a valid value, including in Objects
|
||||
###
|
||||
equals = ( a, b ) ->
|
||||
return a == b && # strict equality should be enough unless zero
|
||||
a != 0 || # because 0 == -0, requires test by _equals()
|
||||
_equals( a, b ) # handles not strictly equal or zero values
|
||||
|
||||
_equals = ( a, b ) ->
|
||||
# a and b have already failed test for strict equality or are zero
|
||||
# They should have the same toString() signature
|
||||
if ( ( s = toString.call( a ) ) != toString.call( b ) )
|
||||
return false
|
||||
|
||||
switch s
|
||||
when '[object Number]'
|
||||
# Converts Number instances into primitive values
|
||||
# This is required also for NaN test bellow
|
||||
a = +a
|
||||
b = +b
|
||||
|
||||
if a
|
||||
return a == b
|
||||
else
|
||||
if a == a # a is 0 or -O
|
||||
return 1/a == 1/b # 1/0 != 1/-0 because Infinity != -Infinity
|
||||
return b != b # NaN, the only Number not equal to itself!
|
||||
when '[object RegExp]'
|
||||
return a.source == b.source &&
|
||||
a.global == b.global &&
|
||||
a.ignoreCase == b.ignoreCase &&
|
||||
a.multiline == b.multiline &&
|
||||
a.lastIndex == b.lastIndex
|
||||
when '[object Function]'
|
||||
return false # functions should be strictly equal because of closure context
|
||||
when '[object Array]'
|
||||
if ( ( l = a.length ) != b.length )
|
||||
return false
|
||||
# Both have as many elements
|
||||
|
||||
while ( l-- )
|
||||
if ( ( x = a[ l ] ) == ( y = b[ l ] ) && x != 0 || _equals( x, y ) )
|
||||
continue
|
||||
return false
|
||||
return true
|
||||
when '[object Object]'
|
||||
l = 0 # counter of own properties
|
||||
|
||||
for p in a
|
||||
if ( a.hasOwnProperty( p ) )
|
||||
++l
|
||||
|
||||
if ( ( x = a[ p ] ) == ( y = b[ p ] ) && x != 0 || _equals( x, y ) )
|
||||
continue
|
||||
|
||||
return false
|
||||
|
||||
# Check if 'b' has as not more own properties than 'a'
|
||||
for p in b
|
||||
if ( b.hasOwnProperty( p ) && --l < 0 )
|
||||
return false
|
||||
|
||||
return true
|
||||
else # Boolean, Date, String
|
||||
return a.valueOf() == b.valueOf()
|
||||
|
||||
partial = (func) ->
|
||||
boundArgs = arguments #slice.call(arguments, 1)
|
||||
bound = () ->
|
||||
position = 0
|
||||
length = boundArgs.length
|
||||
args = Array(length)
|
||||
for i in [0..length]
|
||||
args[i] = boundArgs[i]
|
||||
while (position < arguments.length)
|
||||
args.push(arguments[position++]);
|
||||
return executeBound(func, bound, this, this, args)
|
||||
return bound
|
||||
|
||||
TAG_COMPARISON =
|
||||
TOTAL: 1
|
||||
PARTIAL: 0
|
||||
MISMATCH: -1
|
||||
|
||||
compareTags = (a, b) ->
|
||||
# Returns a TAG_COMPARISON value
|
||||
if (equals(a, b))
|
||||
return TAG_COMPARISON.TOTAL
|
||||
# If the tags are unequal but have the same length, it stands to reason
|
||||
# there is a mismatch.
|
||||
if (a.length == b.length)
|
||||
return TAG_COMPARISON.MISMATCH
|
||||
if a < b
|
||||
[shorter, longer] = [a, b]
|
||||
else
|
||||
[shorter, longer] = [b, a]
|
||||
for x, index in shorter
|
||||
if x != longer[index]
|
||||
return TAG_COMPARISON.MISMATCH
|
||||
return TAG_COMPARISON.PARTIAL
|
||||
|
||||
# Ensures that the group and model don't have any mismatched tags
|
||||
mismatchFilterSub = (group, model) ->
|
||||
if not group.tags?
|
||||
return 0
|
||||
result = group.tags.find((groupTag) ->
|
||||
# Look for a mismatch.
|
||||
matched = model.tags.find (modelTag) =>
|
||||
modelTag[0] == groupTag[0]
|
||||
if not matched?
|
||||
return false
|
||||
if compareTags(groupTag, matched) == TAG_COMPARISON.MISMATCH
|
||||
return true;
|
||||
return false
|
||||
)
|
||||
if not result?
|
||||
return 0
|
||||
return null
|
||||
|
||||
bonusCompare = (mode, bonus = 1, cumulative = false) ->
|
||||
return (group, model) ->
|
||||
results = group.tags.filter((groupTag) ->
|
||||
matched = model.tags.find((modelTag) =>
|
||||
modelTag[0] == groupTag[0]
|
||||
)
|
||||
if not matched?
|
||||
return false
|
||||
if compareTags(groupTag, matched) == mode
|
||||
return true
|
||||
return false
|
||||
)
|
||||
if (results.length)
|
||||
if cumulative
|
||||
return bonus * results.length
|
||||
else
|
||||
return bonus
|
||||
return 0
|
||||
|
||||
filters = {}
|
||||
|
||||
filters.mismatchFilter = () ->
|
||||
return mismatchFilterSub
|
||||
|
||||
filters.partialBonus = partial(bonusCompare, TAG_COMPARISON.PARTIAL)
|
||||
|
||||
filters.fullBonus = partial(bonusCompare, TAG_COMPARISON.TOTAL)
|
||||
|
||||
filters.dryness = () ->
|
||||
return (group) ->
|
||||
that = this
|
||||
newPhrases = []
|
||||
for phrase in group.phrases
|
||||
if (that.history.indexOf(phrase) == -1 && phrase != null)
|
||||
newPhrases.push(phrase)
|
||||
newGroup = Object.create(group)
|
||||
newGroup.phrases = newPhrases
|
||||
return [0, newGroup]
|
||||
|
||||
filters.unmentioned = (bonus = 1) ->
|
||||
return (group) ->
|
||||
if (!Array.isArray(group.tags))
|
||||
return 0
|
||||
if (group.tags.length == 0)
|
||||
return 0
|
||||
that = this
|
||||
for t in group.tags
|
||||
# Return true if the tag is "novel".
|
||||
for u in that.tagHistory
|
||||
if u[0] == t[0]
|
||||
return bonus
|
||||
return 0
|
||||
|
||||
Improv.filters = filters
|
|
@ -0,0 +1,246 @@
|
|||
defaults =
|
||||
filters: []
|
||||
reincorporate: false
|
||||
persistence: true
|
||||
audit: false
|
||||
salienceFormula: (a) ->
|
||||
return a
|
||||
|
||||
class Improv
|
||||
constructor: (snippets, options = {}) ->
|
||||
# Constructor for Improv generators.
|
||||
# We don't want to mutate the options object we've been given we don't know
|
||||
# where it's been.
|
||||
spec = {}
|
||||
$.extend(spec, defaults)
|
||||
$.extend(spec, options)
|
||||
@snippets = snippets
|
||||
@filters = spec.filters
|
||||
@reincorporate = Boolean(spec.reincorporate)
|
||||
@persistence = Boolean(spec.persistence)
|
||||
@audit = Boolean(spec.audit)
|
||||
@salienceFormula = spec.salienceFormula
|
||||
@builtins = spec.builtins
|
||||
@history = []
|
||||
@tagHistory = []
|
||||
|
||||
if @audit
|
||||
@instantiateAuditData()
|
||||
|
||||
###
|
||||
Actually generate text. Separate from #gen() because we don't want to clear
|
||||
history or error-handling data while a call to #gen() hasn't finished
|
||||
returning.
|
||||
###
|
||||
__gen: (snippet, model) ->
|
||||
###
|
||||
For the sake of better error handling, we try to keep an accurate record
|
||||
of what snippet is being generated at any given time.
|
||||
###
|
||||
if (typeof model.bindings == 'object' && model.bindings[snippet])
|
||||
# The snippet already exists in the model's bindings.
|
||||
return model.bindings[snippet]
|
||||
|
||||
@__currentSnippet = snippet
|
||||
|
||||
chosenPhrase = @selectPhrase(@scoreFilter(@applyFilters(snippet, model)), model)
|
||||
@history.unshift(chosenPhrase)
|
||||
|
||||
if @audit
|
||||
phraseTotal = @__phraseAudit.get(snippet).get(chosenPhrase)
|
||||
@__phraseAudit.get(snippet).set(chosenPhrase, phraseTotal + 1)
|
||||
|
||||
output = template(chosenPhrase, model, @__gen.bind(this), this)
|
||||
|
||||
if @snippets[snippet].bind
|
||||
model.bindings ?= {}
|
||||
model.bindings[snippet] = output
|
||||
return output
|
||||
|
||||
###
|
||||
This is a getter so that the internals of how auditing data is stored
|
||||
and calculated can change without changing the API.
|
||||
###
|
||||
phraseAudit: () ->
|
||||
if !@audit
|
||||
throw new Error('Tried retriving audit from generator not in auditing mode.')
|
||||
return @__phraseAudit
|
||||
|
||||
###
|
||||
Generate text (user-facing API). Since this function can recur, most of
|
||||
the heavy lifting is done in __gen().
|
||||
###
|
||||
gen: (snippet, model = {}) ->
|
||||
# Make sure the model has a tag property.
|
||||
model.tags ?= []
|
||||
|
||||
if not @snippets[snippet]?
|
||||
throw new Error("Tried generating snippet \"#{snippet}\", but no such snippet exists in spec")
|
||||
|
||||
output = @__gen(snippet, model)
|
||||
|
||||
if !@persistence
|
||||
@clearHistory()
|
||||
@clearTagHistory()
|
||||
|
||||
return output
|
||||
|
||||
# Add a group's tags to the model, for reincorporation.
|
||||
mergeTags: (model, groupTags) ->
|
||||
mergeTag = (a, b) ->
|
||||
if a.length < b.length
|
||||
return b
|
||||
return a
|
||||
|
||||
for a in groupTags
|
||||
# Find the corresponding tag in the model.
|
||||
site = model.tags.findIndex (b) =>
|
||||
a[0] == b[0]
|
||||
if site == -1
|
||||
# No such tag simply add the group's tags to the model.
|
||||
model.tags = model.tags.concat([a])
|
||||
else
|
||||
model.tags[site] = mergeTag(model.tags[site], a)
|
||||
return model
|
||||
|
||||
# Once we have a list of suitable groups, finally select a phrase at random.
|
||||
selectPhrase: (groups, model) ->
|
||||
log = () =>
|
||||
if @audit
|
||||
console.log(groups)
|
||||
console.log(model)
|
||||
throw new Error("Ran out of phrases in #{groups} while generating #{@__currentSnippet}")
|
||||
if groups.length == 0
|
||||
log()
|
||||
|
||||
phrases = @flattenGroups(groups)
|
||||
if phrases.length == 0
|
||||
log()
|
||||
|
||||
if !this
|
||||
console.log('not this either!')
|
||||
|
||||
chosen = salet.rnd.randomElement(phrases)
|
||||
|
||||
if (@reincorporate)
|
||||
@mergeTags(model, chosen[1])
|
||||
|
||||
if Array.isArray(chosen[1])
|
||||
@tagHistory = chosen[1].concat(@tagHistory)
|
||||
|
||||
return chosen[0]
|
||||
|
||||
###
|
||||
Run the filters through an individual group.
|
||||
###
|
||||
applyFiltersToGroup: (group, model) ->
|
||||
output = { score: 0 }
|
||||
|
||||
# Make sure the group has tags.
|
||||
group.tags ?= []
|
||||
|
||||
# Since we might return a different group than we got, we use a variable.
|
||||
currentGroup = group
|
||||
|
||||
for cb in @filters
|
||||
if not output.score?
|
||||
return
|
||||
|
||||
cbOutput = cb.call(this, currentGroup, model)
|
||||
|
||||
scoreOffset = undefined
|
||||
|
||||
if Array.isArray(cbOutput)
|
||||
# We got a tuple, meaning the filter wants to modify the group before
|
||||
# moving on.
|
||||
scoreOffset = cbOutput[0]
|
||||
currentGroup = cbOutput[1]
|
||||
else
|
||||
scoreOffset = cbOutput
|
||||
|
||||
if scoreOffset == null
|
||||
output.score = null
|
||||
return
|
||||
|
||||
output.score += scoreOffset
|
||||
|
||||
output.group = currentGroup
|
||||
return output
|
||||
|
||||
###
|
||||
Starting with the raw list of groups, return a filtered list with
|
||||
scores.
|
||||
###
|
||||
applyFilters: (snippetName, model) ->
|
||||
if not @snippets[snippetName]?
|
||||
throw new Error("Missing snippet object for snippet #{snippetName}.")
|
||||
|
||||
groups = @snippets[snippetName].groups
|
||||
if !Array.isArray(groups)
|
||||
throw new Error("Missing or bad groups array for snippet #{snippetName} was #{typeof groups}")
|
||||
if @audit
|
||||
console.log "Eligible groups for applyFilters, snippet #{@__currentSnippet}:"
|
||||
console.log groups
|
||||
|
||||
validGroups = []
|
||||
for group in groups
|
||||
rgr = @applyFiltersToGroup(group, model)
|
||||
if rgr? and rgr.score?
|
||||
group.score = rgr.score
|
||||
validGroups.push(group)
|
||||
return validGroups
|
||||
|
||||
###
|
||||
Starting with the scored list from applyFilters(), return a list that has
|
||||
invalid groups scrubbed out and only includes groups with a score past
|
||||
the threshold.
|
||||
###
|
||||
scoreFilter: (groups) ->
|
||||
if @audit
|
||||
console.log "Eligible groups for scoreFilter, snippet #{@__currentSnippet}: "
|
||||
console.log groups
|
||||
# Filter out groups emptied out by dryness()
|
||||
if groups.length == 0
|
||||
return groups
|
||||
validGroups = []
|
||||
for g in groups
|
||||
if g.phrases.length > 0
|
||||
validGroups.push(g)
|
||||
maxScore = Number.NEGATIVE_INFINITY
|
||||
for group in validGroups
|
||||
if group.score > maxScore
|
||||
maxScore = group.score
|
||||
scoreThreshold = @salienceFormula(maxScore)
|
||||
return validGroups.filter (o) =>
|
||||
o.score >= scoreThreshold
|
||||
|
||||
###
|
||||
Starting with a list of scored groups, flatten them into a simple list
|
||||
of phrases.
|
||||
###
|
||||
flattenGroups: (groups) ->
|
||||
if groups.length == 0
|
||||
return Array()
|
||||
return ```groups
|
||||
.map(o => o.phrases.map(i => [i, o.tags]))
|
||||
.reduce((a, b) => a.concat(b), []);
|
||||
```
|
||||
|
||||
clearHistory: () ->
|
||||
@history = []
|
||||
|
||||
clearTagHistory: () ->
|
||||
@tagHistory = []
|
||||
|
||||
instantiateAuditData: () ->
|
||||
###
|
||||
Create and fill audit maps with starter data, ie zeroes.
|
||||
###
|
||||
@__phraseAudit = new Map()
|
||||
self = this
|
||||
|
||||
for key in Object.keys(self.snippets)
|
||||
self.__phraseAudit.set(key, new Map())
|
||||
for group in self.snippets[key].groups
|
||||
for phrase in group.phrases
|
||||
self.phraseAudit().get(key).set(phrase, 0)
|
|
@ -0,0 +1,91 @@
|
|||
TEMPLATE_BUILTINS =
|
||||
a: (text) ->
|
||||
if text.match(/^[aeioAEIO]/)
|
||||
return "an #{text}"
|
||||
return "a #{text}"
|
||||
an: (text) ->
|
||||
return this.a(text)
|
||||
cap: (text) ->
|
||||
return "#{text[0].toUpperCase()}#{text.slice(1)}"
|
||||
A: (text) ->
|
||||
return this.cap(this.a(text))
|
||||
An: (text) ->
|
||||
return this.A(text)
|
||||
|
||||
mergeInTag = (tags, tag) ->
|
||||
# Find the matching tag...
|
||||
|
||||
i = tags.findIndex(t => t[0] == tag[0])
|
||||
|
||||
if i == -1
|
||||
return tags.concat([tag])
|
||||
|
||||
# Otherwise:
|
||||
# This is supposed to be a non-destructive operation
|
||||
newTags = tags.concat()
|
||||
newTags[i] = tag
|
||||
return newTags
|
||||
|
||||
processDirective = (rawDirective, model, cb, generator) ->
|
||||
directive = rawDirective.trim()
|
||||
|
||||
if directive[0] == directive.slice(-1) && directive[0] == "'"
|
||||
# This is a literal directive.
|
||||
return directive.slice(1, -1)
|
||||
|
||||
if directive.indexOf(' ') != -1
|
||||
# The directive contains a space, which means it's a chained directive.
|
||||
funcName = directive.split(' ')[0]
|
||||
rest = directive.slice(directive.indexOf(' ') + 1)
|
||||
|
||||
if TEMPLATE_BUILTINS.hasOwnProperty(funcName)
|
||||
return "#{TEMPLATE_BUILTINS[funcName](processDirective(rest, model, cb, generator))}"
|
||||
|
||||
if generator && generator.builtins && generator.builtins[funcName]
|
||||
return "#{generator.builtins[funcName](processDirective(rest, model, cb, generator))}"
|
||||
|
||||
if typeof model[funcName] != 'function'
|
||||
throw new Error("Builtin or model property \"#{funcName}\" is not a function.")
|
||||
|
||||
return "#{model[funcName](processDirective(rest, model, cb, generator))}"
|
||||
|
||||
if directive[0] == '|'
|
||||
[tagStr, snippet] = directive.split(':')
|
||||
# Disregard the first |
|
||||
newTag = tagStr.slice(1).split('|')
|
||||
newModel = Object.create(model)
|
||||
|
||||
newModel.tags = mergeInTag(model.tags, newTag)
|
||||
|
||||
return cb(snippet, newModel)
|
||||
|
||||
if directive[0] == ':'
|
||||
return cb(directive.slice(1), model)
|
||||
|
||||
if directive[0] == '#'
|
||||
args = directive.slice(1).split('-')
|
||||
return salet.rnd.randRange(parseInt(args[0], 10), parseInt(args[1], 10))
|
||||
|
||||
if directive.indexOf('.') != -1
|
||||
propChain = directive.split('.')
|
||||
return propChain.reduce((obj, prop) =>
|
||||
return [obj[prop], model] ## TODO
|
||||
)
|
||||
|
||||
return "#{model[directive]}"
|
||||
|
||||
template = (phrase, model, cb, generator) ->
|
||||
[openBracket, closeBracket] = [phrase.indexOf('['), phrase.indexOf(']')]
|
||||
if openBracket == -1
|
||||
return phrase
|
||||
if closeBracket == -1
|
||||
throw new Error("Missing close bracket in phrase: #{phrase}")
|
||||
before = phrase.slice(0, openBracket)
|
||||
after = phrase.slice(closeBracket + 1)
|
||||
directive = phrase.substring(openBracket + 1, closeBracket)
|
||||
return template(
|
||||
"#{before}#{processDirective(directive, model, cb, generator)}#{after}",
|
||||
model,
|
||||
cb,
|
||||
generator
|
||||
)
|
Loading…
Reference in New Issue