codecombat/app/core/utils.coffee
Matt Lott d72e4eb750 Practice levels Ux and next level algorithm
Update classroom and gameplay Ux to surface practice levels as 3a, 3b,
etc.
Update next level logic to leverage practice levels based on per level
completion playtime thresholds.
Patrol buster and patrol buster A are live for testing.
Fix a few classroom Ux progress hover bubble info bugs.

Closes #3767
2016-06-27 14:05:42 -07:00

372 lines
14 KiB
CoffeeScript

module.exports.clone = (obj) ->
return obj if obj is null or typeof (obj) isnt 'object'
temp = obj.constructor()
for key of obj
temp[key] = module.exports.clone(obj[key])
temp
module.exports.combineAncestralObject = (obj, propertyName) ->
combined = {}
while obj?[propertyName]
for key, value of obj[propertyName]
continue if combined[key]
combined[key] = value
if obj.__proto__
obj = obj.__proto__
else
# IE has no __proto__. TODO: does this even work? At most it doesn't crash.
obj = Object.getPrototypeOf(obj)
combined
module.exports.normalizeFunc = (func_thing, object) ->
# func could be a string to a function in this class
# or a function in its own right
object ?= {}
if _.isString(func_thing)
func = object[func_thing]
if not func
console.error "Could not find method #{func_thing} in object", object
return => null # always return a func, or Mediator will go boom
func_thing = func
return func_thing
module.exports.objectIdToDate = (objectID) ->
new Date(parseInt(objectID.toString().slice(0,8), 16)*1000)
module.exports.hexToHSL = (hex) ->
rgbToHsl(hexToR(hex), hexToG(hex), hexToB(hex))
hexToR = (h) -> parseInt (cutHex(h)).substring(0, 2), 16
hexToG = (h) -> parseInt (cutHex(h)).substring(2, 4), 16
hexToB = (h) -> parseInt (cutHex(h)).substring(4, 6), 16
cutHex = (h) -> (if (h.charAt(0) is '#') then h.substring(1, 7) else h)
module.exports.hslToHex = (hsl) ->
'#' + (toHex(n) for n in hslToRgb(hsl...)).join('')
toHex = (n) ->
h = Math.floor(n).toString(16)
h = '0'+h if h.length is 1
h
module.exports.i18n = (say, target, language=me.get('preferredLanguage', true), fallback='en') ->
generalResult = null
fallBackResult = null
fallForwardResult = null # If a general language isn't available, the first specific one will do.
fallSidewaysResult = null # If a specific language isn't available, its sibling specific language will do.
matches = (/\w+/gi).exec(language)
generalName = matches[0] if matches
for localeName, locale of say.i18n
continue if localeName is '-'
if target of locale
result = locale[target]
else continue
return result if localeName is language
generalResult = result if localeName is generalName
fallBackResult = result if localeName is fallback
fallForwardResult = result if localeName.indexOf(language) is 0 and not fallForwardResult?
fallSidewaysResult = result if localeName.indexOf(generalName) is 0 and not fallSidewaysResult?
return generalResult if generalResult?
return fallForwardResult if fallForwardResult?
return fallSidewaysResult if fallSidewaysResult?
return fallBackResult if fallBackResult?
return say[target] if target of say
null
module.exports.getByPath = (target, path) ->
throw new Error 'Expected an object to match a query against, instead got null' unless target
pieces = path.split('.')
obj = target
for piece in pieces
return undefined unless piece of obj
obj = obj[piece]
obj
module.exports.isID = (id) -> _.isString(id) and id.length is 24 and id.match(/[a-f0-9]/gi)?.length is 24
module.exports.round = _.curry (digits, n) ->
n = +n.toFixed(digits)
positify = (func) -> (params) -> (x) -> if x > 0 then func(params)(x) else 0
# f(x) = ax + b
createLinearFunc = (params) ->
(x) -> (params.a or 1) * x + (params.b or 0)
# f(x) = ax² + bx + c
createQuadraticFunc = (params) ->
(x) -> (params.a or 1) * x * x + (params.b or 1) * x + (params.c or 0)
# f(x) = a log(b (x + c)) + d
createLogFunc = (params) ->
(x) -> if x > 0 then (params.a or 1) * Math.log((params.b or 1) * (x + (params.c or 0))) + (params.d or 0) else 0
# f(x) = ax^b + c
createPowFunc = (params) ->
(x) -> (params.a or 1) * Math.pow(x, params.b or 1) + (params.c or 0)
module.exports.functionCreators =
linear: positify(createLinearFunc)
quadratic: positify(createQuadraticFunc)
logarithmic: positify(createLogFunc)
pow: positify(createPowFunc)
# Call done with true to satisfy the 'until' goal and stop repeating func
module.exports.keepDoingUntil = (func, wait=100, totalWait=5000) ->
waitSoFar = 0
(done = (success) ->
if (waitSoFar += wait) <= totalWait and not success
_.delay (-> func done), wait) false
module.exports.grayscale = (imageData) ->
d = imageData.data
for i in [0..d.length] by 4
r = d[i]
g = d[i+1]
b = d[i+2]
v = 0.2126*r + 0.7152*g + 0.0722*b
d[i] = d[i+1] = d[i+2] = v
imageData
# Deep compares l with r, with the exception that undefined values are considered equal to missing values
# Very practical for comparing Mongoose documents where undefined is not allowed, instead fields get deleted
module.exports.kindaEqual = compare = (l, r) ->
if _.isObject(l) and _.isObject(r)
for key in _.union Object.keys(l), Object.keys(r)
return false unless compare l[key], r[key]
return true
else if l is r
return true
else
return false
# Return UTC string "YYYYMMDD" for today + offset
module.exports.getUTCDay = (offset=0) ->
day = new Date()
day.setDate(day.getUTCDate() + offset)
partYear = day.getUTCFullYear()
partMonth = (day.getUTCMonth() + 1)
partMonth = "0" + partMonth if partMonth < 10
partDay = day.getUTCDate()
partDay = "0" + partDay if partDay < 10
"#{partYear}#{partMonth}#{partDay}"
# Fast, basic way to replace text in an element when you don't need much.
# http://stackoverflow.com/a/4962398/540620
if document?.createElement
dummy = document.createElement 'div'
dummy.innerHTML = 'text'
TEXT = if dummy.textContent is 'text' then 'textContent' else 'innerText'
module.exports.replaceText = (elems, text) ->
elem[TEXT] = text for elem in elems
null
# Add a stylesheet rule
# http://stackoverflow.com/questions/524696/how-to-create-a-style-tag-with-javascript/26230472#26230472
# Don't use wantonly, or we'll have to implement a simple mechanism for clearing out old rules.
if document?.createElement
module.exports.injectCSS = ((doc) ->
# wrapper for all injected styles and temp el to create them
wrap = doc.createElement("div")
temp = doc.createElement("div")
# rules like "a {color: red}" etc.
return (cssRules) ->
# append wrapper to the body on the first call
unless wrap.id
wrap.id = "injected-css"
wrap.style.display = "none"
doc.body.appendChild wrap
# <br> for IE: http://goo.gl/vLY4x7
temp.innerHTML = "<br><style>" + cssRules + "</style>"
wrap.appendChild temp.children[1]
return
)(document)
module.exports.getQueryVariable = getQueryVariable = (param, defaultValue) ->
query = document.location.search.substring 1
pairs = (pair.split('=') for pair in query.split '&')
for pair in pairs when pair[0] is param
return {'true': true, 'false': false}[pair[1]] ? decodeURIComponent(pair[1])
defaultValue
module.exports.getSponsoredSubsAmount = getSponsoredSubsAmount = (price=999, subCount=0, personalSub=false) ->
# 1 100%
# 2-11 80%
# 12+ 60%
# TODO: make this less confusing
return 0 unless subCount > 0
offset = if personalSub then 1 else 0
if subCount <= 1 - offset
price
else if subCount <= 11 - offset
Math.round((1 - offset) * price + (subCount - 1 + offset) * price * 0.8)
else
Math.round((1 - offset) * price + 10 * price * 0.8 + (subCount - 11 + offset) * price * 0.6)
module.exports.getCourseBundlePrice = getCourseBundlePrice = (coursePrices, seats=20) ->
totalPricePerSeat = coursePrices.reduce ((a, b) -> a + b), 0
if coursePrices.length > 2
pricePerSeat = Math.round(totalPricePerSeat / 2.0)
else
pricePerSeat = parseInt(totalPricePerSeat)
seats * pricePerSeat
module.exports.getCoursePraise = getCoursePraise = ->
praise = [
{
quote: "The kids love it."
source: "Leo Joseph Tran, Athlos Leadership Academy"
},
{
quote: "My students have been using the site for a couple of weeks and they love it."
source: "Scott Hatfield, Computer Applications Teacher, School Technology Coordinator, Eastside Middle School"
},
{
quote: "Thanks for the captivating site. My eighth graders love it."
source: "Janet Cook, Ansbach Middle/High School"
},
{
quote: "My students have started working on CodeCombat and love it! I love that they are learning coding and problem solving skills without them even knowing it!!"
source: "Kristin Huff, Special Education Teacher, Webb City School District"
},
{
quote: "I recently introduced Code Combat to a few of my fifth graders and they are loving it!"
source: "Shauna Hamman, Fifth Grade Teacher, Four Peaks Elementary School"
},
{
quote: "Overall I think it's a fantastic service. Variables, arrays, loops, all covered in very fun and imaginative ways. Every kid who has tried it is a fan."
source: "Aibinder Andrew, Technology Teacher"
},
{
quote: "I love what you have created. The kids are so engaged."
source: "Desmond Smith, 4KS Academy"
},
{
quote: "My students love the website and I hope on having content structured around it in the near future."
source: "Michael Leonard, Science Teacher, Clearwater Central Catholic High School"
}
]
praise[_.random(0, praise.length - 1)]
module.exports.getPrepaidCodeAmount = getPrepaidCodeAmount = (price=0, users=0, months=0) ->
return 0 unless users > 0 and months > 0
total = price * users * months
total
startsWithVowel = (s) -> s[0] in 'aeiouAEIOU'
module.exports.filterMarkdownCodeLanguages = (text, language) ->
return '' unless text
currentLanguage = language or me.get('aceConfig')?.language or 'python'
excludedLanguages = _.without ['javascript', 'python', 'coffeescript', 'clojure', 'lua', 'java', 'io'], currentLanguage
# Exclude language-specific code blocks like ```python (... code ...)``` for each non-target language.
codeBlockExclusionRegex = new RegExp "```(#{excludedLanguages.join('|')})\n[^`]+```\n?", 'gm'
# Exclude language-specific images like ![python - image description](image url) for each non-target language.
imageExclusionRegex = new RegExp "!\\[(#{excludedLanguages.join('|')}) - .+?\\]\\(.+?\\)\n?", 'gm'
text = text.replace(codeBlockExclusionRegex, '').replace(imageExclusionRegex, '')
commonLanguageReplacements =
python: [
['true', 'True'], ['false', 'False'], ['null', 'None'],
['object', 'dictionary'], ['Object', 'Dictionary'],
['array', 'list'], ['Array', 'List'],
]
lua: [
['null', 'nil'],
['object', 'table'], ['Object', 'Table'],
['array', 'table'], ['Array', 'Table'],
]
for [from, to] in commonLanguageReplacements[currentLanguage] ? []
# Convert JS-specific keywords and types to Python ones, if in simple `code` tags.
# This won't cover it when it's not in an inline code tag by itself or when it's not in English.
text = text.replace ///`#{from}`///g, "`#{to}`"
# Now change "An `dictionary`" to "A `dictionary`", etc.
if startsWithVowel(from) and not startsWithVowel(to)
text = text.replace ///(\ a|A)n(\ `#{to}`)///g, "$1$2"
if not startsWithVowel(from) and startsWithVowel(to)
text = text.replace ///(\ a|A)(\ `#{to}`)///g, "$1n$2"
return text
module.exports.aceEditModes = aceEditModes =
'javascript': 'ace/mode/javascript'
'coffeescript': 'ace/mode/coffee'
'python': 'ace/mode/python'
'java': 'ace/mode/java'
'lua': 'ace/mode/lua'
'java': 'ace/mode/java'
module.exports.initializeACE = (el, codeLanguage) ->
contents = $(el).text().trim()
editor = ace.edit el
editor.setOptions maxLines: Infinity
editor.setReadOnly true
editor.setTheme 'ace/theme/textmate'
editor.setShowPrintMargin false
editor.setShowFoldWidgets false
editor.setHighlightActiveLine false
editor.setHighlightActiveLine false
editor.setBehavioursEnabled false
editor.renderer.setShowGutter false
editor.setValue contents
editor.clearSelection()
session = editor.getSession()
session.setUseWorker false
session.setMode aceEditModes[codeLanguage]
session.setWrapLimitRange null
session.setUseWrapMode true
session.setNewLineMode 'unix'
return editor
module.exports.capitalLanguages = capitalLanguages =
'javascript': 'JavaScript'
'coffeescript': 'CoffeeScript'
'python': 'Python'
'java': 'Java'
'lua': 'Lua'
module.exports.createLevelNumberMap = (levels) ->
levelNumberMap = {}
practiceLevelTotalCount = 0
practiceLevelCurrentCount = 0
for level, i in levels
levelNumber = i - practiceLevelTotalCount + 1
if level.practice
levelNumber = i - practiceLevelTotalCount + String.fromCharCode('a'.charCodeAt(0) + practiceLevelCurrentCount)
practiceLevelTotalCount++
practiceLevelCurrentCount++
else
practiceLevelCurrentCount = 0
levelNumberMap[level.key] = levelNumber
levelNumberMap
module.exports.findNextLevel = (levels, currentIndex, needsPractice) ->
# levels = [{practice: true/false, complete: true/false}]
index = currentIndex
index++
if needsPractice
if levels[currentIndex].practice or index < levels.length and levels[index].practice
# Needs practice, on practice or next practice, choose next incomplete level
# May leave earlier practice levels incomplete and reach end of course
index++ while index < levels.length and levels[index].complete
else
# Needs practice, on required, next required, choose first incomplete level of previous practice chain
index--
index-- while index >= 0 and not levels[index].practice
if index >= 0
index-- while index >= 0 and levels[index].practice
if index >= 0
index++
index++ while index < levels.length and levels[index].practice and levels[index].complete
if levels[index].practice and not levels[index].complete
return index
index = currentIndex + 1
index++ while index < levels.length and levels[index].complete
else
# No practice needed, next required incomplete level
index++ while index < levels.length and (levels[index].practice or levels[index].complete)
index
module.exports.needsPractice = (playtime=0, threshold=2) ->
playtime / 60 > threshold