You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
246 lines
6.7 KiB
246 lines
6.7 KiB
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)
|
|
|