mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-13 09:11:22 -05:00
d72e4eb750
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
372 lines
14 KiB
CoffeeScript
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
|