Merge branch 'master' into production

This commit is contained in:
Nick Winter 2015-01-07 15:04:54 -08:00
commit 3ba746094a
24 changed files with 370 additions and 210 deletions

View file

@ -131,6 +131,18 @@ module.exports.kindaEqual = compare = (l, r) ->
else
return false
# Return UTC string "YYYY-MM-DD" for today + offset
module.exports.getUTCDay = (offset=0) ->
# TODO: Move to utility
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?

View file

@ -159,6 +159,10 @@
date: "Date"
body: "Body"
version: "Version"
pending: "Pending"
accepted: "Accepted"
rejected: "Rejected"
withdrawn: "Withdrawn"
submitter: "Submitter"
submitted: "Submitted"
commit_msg: "Commit Message"

View file

@ -145,13 +145,13 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
fork: "Bifurcar"
play: "Jugar" # When used as an action verb, like "Play next level"
retry: "Reintentar"
# actions: "Actions"
actions: "Acciones"
# info: "Info"
# help: "Help"
help: "Ayuda"
watch: "Mirar"
unwatch: "Pasar"
submit_patch: "Mandar Parche"
# submit_changes: "Submit Changes"
submit_patch: "Enviar Parche"
submit_changes: "Enviar Cambios"
general:
and: "y"
@ -165,10 +165,10 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
# review: "Review"
version_history: "Historial de versión"
version_history_for: "Historial de las versiones de: "
# select_changes: "Select two changes below to see the difference."
# undo_prefix: "Undo"
select_changes: "Selecciona dos cambios más abajo para ver la diferencia."
undo_prefix: "Deshacer"
# undo_shortcut: "(Ctrl+Z)"
# redo_prefix: "Redo"
redo_prefix: "Rehacer"
# redo_shortcut: "(Ctrl+Shift+Z)"
# play_preview: "Play preview of current level"
result: "Resultado"
@ -233,7 +233,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
reload_title: "¿Recargar todo el código?"
reload_really: "¿Estas seguro que quieres reiniciar el nivel?"
reload_confirm: "Recargarlo todo"
# victory: "Victory"
victory: "Victoria"
victory_title_prefix: "¡"
victory_title_suffix: " Completado!"
victory_sign_up: "Regístrate para recibir actualizaciones."
@ -246,8 +246,8 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
victory_review: "¡Cuéntanos más!" # Only in old-style levels.
victory_hour_of_code_done: "¿Ya terminaste?"
victory_hour_of_code_done_yes: "Si, ¡He terminado con mi hora de código!"
# victory_experience_gained: "XP Gained"
# victory_gems_gained: "Gems Gained"
victory_experience_gained: "XP Conseguida"
victory_gems_gained: "Gemas Conseguidas"
guide_title: "Guía"
tome_minion_spells: "Los hechizos de tus súbditos" # Only in old-style levels.
tome_read_only_spells: "Hechizos de solo lectura" # Only in old-style levels.
@ -271,7 +271,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
loading_ready: "¡Listo!"
loading_start: "Iniciar Nivel"
problem_alert_title: "Arregla tu código"
# problem_alert_help: "Help"
problem_alert_help: "Ayuda"
time_current: "Ahora:"
time_total: "Máx:"
time_goto: "Ir a:"
@ -304,7 +304,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
tip_talk_is_cheap: "Hablar es fácil. Enséñame el código. - Linus Torvalds"
tip_first_language: "La cosa más desastrosa que puedes aprender es tu primer lenguaje de programación. - Alan Kay"
tip_hardware_problem: "P: Cuantos programadores hacen falta para cambiar una bombilla? R: Ninguno, es un problema de hardware."
# tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law."
tip_hofstadters_law: "Ley de Hofstadter: Siempre lleva más tiempo de lo que esperas, incluso cuando tienes en cuenta la Ley de Hofstadter."
tip_premature_optimization: "La optimizacion prematura es la raiz de todo mal. - Donald Knuth"
tip_brute_force: "Cuando haya dudas, usa la fuerza bruta. - Ken Thompson"
tip_extrapolation: "Existen solo dos clases de personas: aquellos que pueden extrapolar desde información incompleta..."

View file

@ -68,7 +68,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
change_hero: "重新选择英雄" # Go back from choose inventory to choose hero
choose_inventory: "装备道具"
buy_gems: "购买宝石"
# campaign_desert: "Desert Campaign"
campaign_desert: "沙漠战役"
campaign_forest: "森林战役"
campaign_dungeon: "地牢战役"
subscription_required: "需订阅"
@ -106,9 +106,9 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
load_profile: "载入 G+ 档案"
load_email: "载入 G+ 电子邮件"
finishing: "完成..."
# sign_in_with_facebook: "Sign in with Facebook"
# sign_in_with_gplus: "Sign in with G+"
# signup_switch: "Want to create an account?"
sign_in_with_facebook: "Facebook账号登录"
sign_in_with_gplus: " G+ 账号登录"
signup_switch: "是否创建新账户?"
signup:
email_announcements: "通过邮件接收通知"
@ -117,7 +117,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
log_in: "登录"
social_signup: "或者你可以通过Facebook或G+注册:"
required: "在做这件事情之前你必须先注册。"
# login_switch: "Already have an account?"
login_switch: "已经注册过账户?"
recover:
recover_account_title: "找回账户"
@ -145,13 +145,13 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
fork: "派生"
play: "开始" # When used as an action verb, like "Play next level"
retry: "重试"
# actions: "Actions"
# info: "Info"
actions: "行为"
info: "信息"
help: "帮助"
watch: "关注"
unwatch: "取消关注"
submit_patch: "提交补丁"
# submit_changes: "Submit Changes"
submit_changes: "提交更新"
general:
and: ""
@ -159,18 +159,18 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
date: "日期"
body: "正文"
version: "版本"
# submitter: "Submitter"
# submitted: "Submitted"
submitter: "提交者"
submitted: "已提交"
commit_msg: "提交信息"
# review: "Review"
review: "查看"
version_history: "版本历史"
version_history_for: "版本历史: "
# select_changes: "Select two changes below to see the difference."
# undo_prefix: "Undo"
# undo_shortcut: "(Ctrl+Z)"
# redo_prefix: "Redo"
# redo_shortcut: "(Ctrl+Shift+Z)"
# play_preview: "Play preview of current level"
select_changes: "选择下面两项更新来查看其不同。"
undo_prefix: "取消"
undo_shortcut: "(Ctrl+Z)"
redo_prefix: "重做"
redo_shortcut: "(Ctrl+Shift+Z)"
play_preview: "当前关卡预览"
result: "结果"
results: "结果"
description: "描述"
@ -233,7 +233,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
reload_title: "重载所有代码?"
reload_really: "确定重载这一关,返回开始处吗?"
reload_confirm: "重载所有"
# victory: "Victory"
victory: "胜利"
victory_title_prefix: ""
victory_title_suffix: " 完成"
victory_sign_up: "保存进度"
@ -246,8 +246,8 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
victory_review: "给我们反馈!" # Only in old-style levels.
victory_hour_of_code_done: "你完成了吗?"
victory_hour_of_code_done_yes: "是的, 完成了!"
# victory_experience_gained: "XP Gained"
# victory_gems_gained: "Gems Gained"
victory_experience_gained: "获得经验"
victory_gems_gained: "获得宝石"
guide_title: "指南"
tome_minion_spells: "助手的咒语" # Only in old-style levels.
tome_read_only_spells: "只读的咒语" # Only in old-style levels.
@ -271,7 +271,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
loading_ready: "载入完成!"
loading_start: "开战"
problem_alert_title: "修正你的代码"
# problem_alert_help: "Help"
problem_alert_help: "帮助"
time_current: "现在:"
time_total: "最大:"
time_goto: "跳到:"
@ -307,7 +307,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
tip_hofstadters_law: "侯世达定律:做事所花费的时间总是比你预期的要长,即使你的预期中考虑了侯世达定律。"
tip_premature_optimization: "过早的优化是万恶之源。 - 高德纳"
tip_brute_force: "拿不准时就用穷举法。 - Ken Thompson"
# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..."
tip_extrapolation: "世界上只有两类人:一类人能够根据不完整的数据进行推断……"
# tip_superpower: "Coding is the closest thing we have to a superpower."
game_menu:
@ -315,8 +315,8 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
save_load_tab: "保存/打开"
options_tab: "设置"
guide_tab: "使用向导"
# guide_video_tutorial: "Video Tutorial"
# guide_tips: "Tips"
guide_video_tutorial: "视频教程"
guide_tips: "小技巧"
multiplayer_tab: "多人游戏"
auth_tab: "注册"
inventory_caption: "装备你的英雄"
@ -330,7 +330,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
inventory:
choose_inventory: "装备道具"
equipped_item: "已装备"
# required_purchase_title: "Required"
required_purchase_title: "需要"
available_item: "可用"
restricted_title: "被限制"
should_equip: "(双击装备此道具)"
@ -350,7 +350,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
prompt_title: "没有足够数量的宝石"
prompt_body: "还需要更多吗?"
prompt_button: "进入商店"
# recovered: "Previous gems purchase recovered. Please refresh the page."
recovered: "之前购买的宝石已恢复。请刷新页面。"
subscribe:
subscribe_title: "订阅"
@ -391,7 +391,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
regeneration: "恢复"
range: "攻击范围" # As in "attack or visual range"
blocks: "格挡" # As in "this shield blocks this much damage"
# backstab: "Backstab" # As in "this dagger does this much backstab damage"
backstab: "背刺" # As in "this dagger does this much backstab damage"
skills: "技能"
available_for_purchase: "可以购买" # Shows up when you have unlocked, but not purchased, a hero in the hero store
level_to_unlock: "未解锁关卡:" # Label for which level you have to beat to unlock a particular hero (click a locked hero in the store to see)
@ -574,10 +574,10 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
classes:
archmage_title: "大法师"
archmage_title_description: "(代码编写人员)"
# archmage_summary: "If you are a developer interested in coding educational games, become an archmage to help us build CodeCombat!"
archmage_summary: "如果你是对教育类游戏感兴趣的开发者,那么就选择大法师来帮我们为 CodeCombat编写代码吧"
artisan_title: "工匠"
artisan_title_description: "(关卡建立人员)"
# artisan_summary: "Build and share levels for you and your friends to play. Become an Artisan to learn the art of teaching others to program."
artisan_summary: "建立游戏关卡并分享给朋友们。那么就选择工匠职业来教其他人编程吧。"
adventurer_title: "冒险家"
adventurer_title_description: "(关卡测试人员)"
# adventurer_summary: "Get our new levels (even our subscriber content) for free one week early and help us work out bugs before our public release."
@ -659,7 +659,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
# achievement_query_goals: "Key achievement off of level goals"
level_completion: "关卡完成"
pop_i18n: "填写 I18N"
# tasks: "Tasks"
tasks: "任务"
article:
edit_btn_preview: "预览"
@ -829,7 +829,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
# service_apple: "Apple"
# service_web: "Web"
# paid_on: "Paid On"
# service: "Service"
service: "服务"
price: "价格"
gems: "宝石"
# active: "Active"
@ -934,7 +934,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
practices_title: "尊重最佳实践"
practices_description: "这是我们对您的承诺,即玩家,尽管这在法律用语中略显不足。"
privacy_title: "隐私"
# privacy_description: "We will not sell any of your personal information."
privacy_description: "我们不会泄露您的个人信息。"
security_title: "安全"
security_description: "我们竭力保证您的个人信息安全性。作为一个开源项目,任何人都可以检讨并改善我们自由开放的网站的安全性。"
email_title: "电子邮件"
@ -1026,7 +1026,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
next_photo: "添加一张职业的照片(可选)。"
next_active: "将自己标记为正在寻求工作机会以使自己的名字出现在搜索结果中。"
example_blog: "你的博客"
# example_personal_site: "Personal Site"
example_personal_site: "个人主页"
links_header: "个人网站链接"
links_blurb: "链接任何你希望展示的网站或介绍例如你的Github你的领英档案或是你的博客。"
links_name: "链接名称"
@ -1088,7 +1088,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
education_description: "描述"
education_description_help: "展示任何与你的教育经历相关的信息。140个字符选填"
our_notes: "我们的评注"
# remarks: "Remarks"
remarks: "评价"
projects: "项目"
projects_header: "添加3个项目"
projects_header_2: "项目前3个"
@ -1101,7 +1101,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
project_picture_help: "上传一站230x115像素或更大的图片来展示这个项目。"
project_link: "链接"
project_link_help: "项目的链接。"
# player_code: "Player Code"
player_code: "玩家代码"
employers:
# deprecation_warning_title: "Sorry, CodeCombat is not recruiting right now."
@ -1165,6 +1165,6 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
av_other_sub_title: "其他"
av_other_debug_base_url: "Base用于调试 base.jade"
u_title: "用户列表"
# ucp_title: "User Code Problems"
ucp_title: "用户代码的问题"
lg_title: "最新的游戏"
clas: "贡献者许可协议"

View file

@ -84,7 +84,7 @@ module.exports = class User extends CocoModel
gemsEarned = @get('earned')?.gems ? 0
gemsPurchased = @get('purchased')?.gems ? 0
gemsSpent = @get('spent') ? 0
gemsEarned + gemsPurchased - gemsSpent
Math.floor gemsEarned + gemsPurchased - gemsSpent
heroes: ->
heroes = (me.get('purchased')?.heroes ? []).concat([ThangType.heroes.captain, ThangType.heroes.knight])
@ -139,7 +139,7 @@ module.exports = class User extends CocoModel
else
@subscribeCopyGroup = 'original'
@subscribeCopyGroup
getVideoTutorialStylesIndex: (numVideos=0)->
# A/B Testing video tutorial styles
# Not a constant number of videos available (e.g. could be 0, 1, 3, or 4 currently)

View file

@ -34,7 +34,7 @@ defaultTasks = [
'Write a loading tip, if needed.'
'Click the Populate i18n button.'
'Mark whether it requires a subscription (after adventurer week).'
'Mark whether it requires a subscription.'
'Release to everyone via MailChimp.'
'Check completion/engagement/problem analytics.'

View file

@ -227,7 +227,7 @@ me.RewardSchema = (descriptionFragment='earned by achievements') ->
me.stringID(links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], title: 'Item ThangType', description: 'A reference to the earned item ThangType.', format: 'thang-type')
levels: me.array {uniqueItems: true, description: "Levels #{descriptionFragment}."},
me.stringID(links: [{rel: 'db', href: '/db/level/{($)}/version'}], title: 'Level', description: 'A reference to the earned Level.', format: 'latest-version-original-reference')
gems: me.int {description: "Gems #{descriptionFragment}."}
gems: me.float {description: "Gems #{descriptionFragment}."}
me.task = me.object {title: 'Task', description: 'A task to be completed', format: 'task', default: {name: 'TODO', complete: false}},
name: {title: 'Name', description: 'What must be done?', type: 'string'}

View file

@ -30,7 +30,7 @@ block content
h4(data-i18n="contribute.how_to_join") How to Join
p
a(title='Contact', tabindex=-1, data-toggle="coco-modal", data-target="core/ContactModal", data-i18n="contribute.contact_us_url")
a(tabindex=-1, data-toggle="coco-modal", data-target="core/ContactModal", data-i18n="contribute.contact_us_url")
| Contact us
span ,
span(data-i18n="contribute.ambassador_join_desc")

View file

@ -41,13 +41,13 @@ block content
p
span(data-i18n="contribute.join_desc_1")
| Anyone can help out! Just check out our
a(title='GitHub', href="https://github.com/codecombat/codecombat", tabindex=-1)
a(href="https://github.com/codecombat/codecombat", tabindex=-1)
| GitHub
span
span(data-i18n="contribute.join_desc_2")
| to get started, and check the box below to mark yourself as a brave Archmage and get the latest news by email.
| Want to chat about what to do or how to get more deeply involved?
a(title='Contact', tabindex=-1, data-toggle="coco-modal", data-target="core/ContactModal", data-i18n="contribute.join_url_email")
a(tabindex=-1, data-toggle="coco-modal", data-target="core/ContactModal", data-i18n="contribute.join_url_email")
| Email us
span(data-i18n="contribute.join_desc_3")
| , or find us in our

View file

@ -36,7 +36,7 @@ block content
h4(data-i18n="contribute.how_to_join") How To Join
p
a(title='Contact', tabindex=-1, data-toggle="coco-modal", data-target="core/ContactModal", data-i18n="contribute.contact_us_url")
a(tabindex=-1, data-toggle="coco-modal", data-target="core/ContactModal", data-i18n="contribute.contact_us_url")
| Contact us
span ,
span(data-i18n="contribute.scribe_join_description")

View file

@ -65,7 +65,7 @@
h4 Recent Sessions
if recentSessions
div(style='font-size:10pt') Latest 10 sessions for this level
div(style='font-size:10pt') Latest #{recentSessions.length} sessions for this level
div(style='font-size:10pt') Double-click row to open player and session
table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt')
thead

View file

@ -1,15 +1,15 @@
.btn-group(data-toggle="buttons").status-buttons
label.btn.btn-default.pending
input(type="radio", name="status", value="pending")
input(type="radio", name="status", value="pending", data-i18n="general.pending")
| Pending
label.btn.btn-default.accepted
input(type="radio", name="status", value="accepted")
input(type="radio", name="status", value="accepted", data-i18n="general.accepted")
| Accepted
label.btn.btn-default.rejected
input(type="radio", name="status", value="rejected")
input(type="radio", name="status", value="rejected", data-i18n="general.rejected")
| Rejected
label.btn.btn-default.withdrawn
input(type="radio", name="status", value="withdrawn")
input(type="radio", name="status", value="withdrawn", data-i18n="general.withdrawn")
| Withdrawn
if patches.loading

View file

@ -243,9 +243,7 @@ module.exports = class CampaignEditorView extends RootView
getCampaignCompletions: =>
# Fetch last 7 days of campaign drop-off rates
startDay = new Date()
startDay.setDate(startDay.getUTCDate() - 6)
startDay = startDay.getUTCFullYear() + '-' + (startDay.getUTCMonth() + 1) + '-' + startDay.getUTCDate()
startDay = utils.getUTCDay -6
success = (data) =>
return if @destroyed

View file

@ -3,6 +3,7 @@ Level = require 'models/Level'
LevelSession = require 'models/LevelSession'
ModelModal = require 'views/modal/ModelModal'
User = require 'models/User'
utils = require 'core/utils'
module.exports = class CampaignLevelView extends CocoView
id: 'campaign-level-view'
@ -47,9 +48,7 @@ module.exports = class CampaignLevelView extends CocoView
getCommonLevelProblems: ->
# Fetch last 30 days of common level problems
startDay = new Date()
startDay.setDate(startDay.getUTCDate() - 29)
startDay = startDay.getUTCFullYear() + '-' + (startDay.getUTCMonth() + 1) + '-' + startDay.getUTCDate()
startDay = utils.getUTCDay -29
success = (data) =>
return if @destroyed
@ -77,9 +76,7 @@ module.exports = class CampaignLevelView extends CocoView
@levelCompletions = _.map data, mapFn, @
@render()
startDay = new Date()
startDay.setDate(startDay.getUTCDate() - 6)
startDay = startDay.getUTCFullYear() + '-' + (startDay.getUTCMonth() + 1) + '-' + startDay.getUTCDate()
startDay = utils.getUTCDay -6
# TODO: Why do we need this url dash?
request = @supermodel.addRequestResource 'level_completions', {
@ -97,10 +94,8 @@ module.exports = class CampaignLevelView extends CocoView
@levelPlaytimes = data.sort (a, b) -> if a.created < b.created then 1 else -1
@render()
startDay = new Date()
startDay.setDate(startDay.getUTCDate() - 6)
startDay = startDay.getUTCFullYear() + '-' + (startDay.getUTCMonth() + 1) + '-' + startDay.getUTCDate()
startDay = utils.getUTCDay -6
# TODO: Why do we need this url dash?
request = @supermodel.addRequestResource 'playtime_averages', {
url: '/db/level/-/playtime_averages'
@ -111,7 +106,7 @@ module.exports = class CampaignLevelView extends CocoView
request.load()
getRecentSessions: ->
limit = 10
limit = 100
success = (data) =>
return if @destroyed

View file

@ -110,6 +110,8 @@ module.exports = class HeroVictoryModal extends ModalView
# rewards = achievement.get('rewards') or {}
# rewards.gems *= (index + 1)
# TODO: use earned achievement worths or somehow pull in recalculated exp/gems
c.thangTypes = @thangTypes
c.me = me
c.readyToRank = @level.get('type', true) is 'hero-ladder' and @session.readyToRank()

View file

@ -0,0 +1,82 @@
# Calculate level completion rates via mixpanel export API
# TODO: unique users
# TODO: align output
# TODO: order output
import sys
from mixpanel import Mixpanel
try:
import json
except ImportError:
import simplejson as json
# NOTE: mixpanel dates are by day and inclusive
# E.g. '2014-12-08' is any date that day, up to 2014-12-09 12am
if __name__ == '__main__':
if not len(sys.argv) is 3:
print "Script format: <script> <api_key> <api_secret>"
else:
api_key = sys.argv[1]
api_secret = sys.argv[2]
api = Mixpanel(
api_key = api_key,
api_secret = api_secret
)
startDate = '2014-12-31'
endDate = '2015-01-05'
print("Requesting data for {0} to {1}".format(startDate, endDate))
data = api.request(['export'], {
'event' : ['Started Level', 'Saw Victory'],
'from_date' : startDate,
'to_date' : endDate
})
levelRates = {}
lines = data.split('\n')
print "Received %d entries" % len(lines)
for line in lines:
try:
if len(line) is 0: continue
eventData = json.loads(line)
eventName = eventData['event']
if not eventName in ['Started Level', 'Saw Victory']:
print 'Unexpected event ' + eventName
break
properties = eventData['properties']
if 'levelID' in properties:
levelID = properties['levelID']
elif 'level' in properties:
levelID = properties['level'].lower().replace(' ', '-')
else:
print("Unkonwn levelID for", eventName)
print(properties)
break
if not levelID in levelRates:
levelRates[levelID] = {'started': 0, 'finished': 0}
if eventName == 'Started Level':
levelRates[levelID]['started'] += 1
elif eventName == 'Saw Victory':
levelRates[levelID]['finished'] += 1
else:
print("Unknown event name", eventName)
print(eventData)
break
except:
print "Unexpected error:", sys.exc_info()[0]
print line
break
# print(levelRates)
for levelID in levelRates:
started = levelRates[levelID]['started']
finished = levelRates[levelID]['finished']
# if not levelID == 'endangered-burl':
# continue
if started > 0:
print("{0}\t{1}\t{2}\t{3}%".format(levelID, started, finished, float(finished) / started * 100))
else:
print("{0}\t{1}\t{2}".format(levelID, started, finished))

View file

@ -20,12 +20,15 @@ print("Today is " + today);
var todayMinus6 = new Date();
todayMinus6.setDate(todayMinus6.getUTCDate() - 6);
var startDate = todayMinus6.toISOString().substr(0, 10) + "T00:00:00.000Z";
// startDate = "2014-12-01T00:00:00.000Z";
// startDate = "2014-12-31T00:00:00.000Z";
print("Start date is " + startDate)
// var endDate = "2015-01-06T00:00:00.000Z";
// print("End date is " + endDate)
var cursor = db['analytics.log.events'].find({
$and: [
{"created": { $gte: ISODate(startDate)}},
// {"created": { $lt: ISODate(endDate)}},
{$or: [ {"event" : 'Started Level'}, {"event" : 'Saw Victory'}]}
]
});
@ -115,6 +118,7 @@ var campaigns = {
// Bucketize events by user
print("Getting event data...");
var userProgression = {};
var userLevelEventMap = {}; // Only want unique users per-level/event
while (cursor.hasNext()) {
var doc = cursor.next();
var created = doc.created;
@ -124,12 +128,17 @@ while (cursor.hasNext()) {
if (level) {
if (level.length > longestLevelName) longestLevelName = level.length;
var user = doc.user.valueOf();
if (!userProgression[user]) userProgression[user] = [];
userProgression[user].push({
created: created,
event: event,
level: level
});
if (!userLevelEventMap[user]) userLevelEventMap[user] = {};
if (!userLevelEventMap[user][level]) userLevelEventMap[user][level] = {};
if (!userLevelEventMap[user][level][event]) {
userLevelEventMap[user][level][event] = true;
if (!userProgression[user]) userProgression[user] = [];
userProgression[user].push({
created: created,
event: event,
level: level
});
}
}
}
longestLevelName += 2;
@ -224,7 +233,7 @@ for (campaign in campaigns) {
print("\n" + campaign);
var level = "level";
var levelSpacer = new Array(longestLevelName - level.length).join(' ');
print(level + levelSpacer + "started\tdropped\t\tfinished dropped");
print(level + levelSpacer + "started\tdropped\t\tfinished dropped\tcompletion");
for (var i = 0; i < campaignRates[campaign].levels.length; i++) {
var level = campaignRates[campaign].levels[i].level;
var started = campaignRates[campaign].levels[i].started;
@ -232,13 +241,13 @@ for (campaign in campaigns) {
var finished = campaignRates[campaign].levels[i].finished;
var finishDropped = campaignRates[campaign].levels[i].finishDropped;
var levelSpacer = new Array(longestLevelName - level.length).join(' ');
print(level + levelSpacer + started + "\t" + (started < 100 ? "\t" : "") + startDropped + "\t" + (startDropped / started * 100).toFixed(2) + "%\t" + finished + "\t" + finishDropped + "\t" + (finishDropped / finished * 100).toFixed(2) + "%");
print(level + levelSpacer + started + "\t" + (started < 100 ? "\t" : "") + startDropped + "\t" + (startDropped / started * 100).toFixed(2) + "%\t" + finished + "\t" + finishDropped + "\t" + (finishDropped / finished * 100).toFixed(2) + "%" + "\t" + (finished / started * 100).toFixed(2) + "%");
}
var level = 'Overall';
var started = campaignRates[campaign].overall.started;
var startDropped = campaignRates[campaign].overall.startDropped;
var finished = campaignRates[campaign].overall.finished;
var finishDropped = campaignRates[campaign].overall.finishDropped;
var levelSpacer = new Array(longestLevelName - level.length).join(' ');
print(level + levelSpacer + started + "\t" + (started < 100 ? "\t" : "") + startDropped + "\t" + (startDropped / started * 100).toFixed(2) + "%\t" + finished + "\t" + finishDropped + "\t" + (finishDropped / finished * 100).toFixed(2) + "%");
// var level = 'Overall';
// var started = campaignRates[campaign].overall.started;
// var startDropped = campaignRates[campaign].overall.startDropped;
// var finished = campaignRates[campaign].overall.finished;
// var finishDropped = campaignRates[campaign].overall.finishDropped;
// var levelSpacer = new Array(longestLevelName - level.length).join(' ');
// print(level + levelSpacer + started + "\t" + (started < 100 ? "\t" : "") + startDropped + "\t" + (startDropped / started * 100).toFixed(2) + "%\t" + finished + "\t" + finishDropped + "\t" + (finishDropped / finished * 100).toFixed(2) + "%");
}

View file

@ -16,7 +16,7 @@ EarnedAchievementSchema.pre 'save', (next) ->
EarnedAchievementSchema.index({user: 1, achievement: 1}, {unique: true, name: 'earned achievement index'})
EarnedAchievementSchema.index({user: 1, changed: -1}, {name: 'latest '})
EarnedAchievementSchema.statics.createForAchievement = (achievement, doc, originalDocObj, done) ->
EarnedAchievementSchema.statics.createForAchievement = (achievement, doc, originalDocObj=null, previouslyEarnedAchievement=null, done) ->
User = require '../users/User'
userObjectID = doc.get(achievement.get('userField'))
userID = if _.isObject userObjectID then userObjectID.toHexString() else userObjectID # Standardize! Use strings, not ObjectId's
@ -27,18 +27,20 @@ EarnedAchievementSchema.statics.createForAchievement = (achievement, doc, origin
achievementName: achievement.get 'name'
earnedRewards: achievement.get 'rewards'
worth = achievement.get('worth') ? 10
pointWorth = achievement.get('worth') ? 10
gemWorth = achievement.get('rewards')?.gems ? 0
earnedPoints = 0
earnedGems = 0
wrapUp = (earnedAchievementDoc) ->
# Update user's experience points
update = {$inc: {points: earnedPoints}}
update = {$inc: {points: earnedPoints, 'earned.gems': earnedGems}}
for rewardType, rewards of achievement.get('rewards') ? {}
if rewardType is 'gems'
update.$inc['earned.gems'] = rewards if rewards
else if rewards.length
continue if rewardType is 'gems'
if rewards.length
update.$addToSet ?= {}
update.$addToSet["earned.#{rewardType}"] = $each: rewards
User.update {_id: userID}, update, {}, (err, count) ->
User.update {_id: mongoose.Types.ObjectId(userID)}, update, {}, (err, count) ->
log.error err if err?
done?(earnedAchievementDoc)
@ -46,29 +48,40 @@ EarnedAchievementSchema.statics.createForAchievement = (achievement, doc, origin
if isRepeatable
#log.debug 'Upserting repeatable achievement called \'' + (achievement.get 'name') + '\' for ' + userID
proportionalTo = achievement.get 'proportionalTo'
originalAmount = if originalDocObj then util.getByPath(originalDocObj, proportionalTo) or 0 else 0
docObj = doc.toObject()
newAmount = docObj[proportionalTo]
newAmount = util.getByPath(docObj, proportionalTo) or 0
if previouslyEarnedAchievement
originalAmount = previouslyEarnedAchievement.get('achievedAmount') or 0
else if originalDocObj # This branch could get buggy if unchangedCopy tracking isn't working.
originalAmount = util.getByPath(originalDocObj, proportionalTo) or 0
else
originalAmount = 0
#console.log 'original amount is', originalAmount, 'and new amount is', newAmount, 'for', proportionalTo, 'with doc', docObj, 'and previously earned achievement amount', previouslyEarnedAchievement?.get('achievedAmount'), 'because we had originalDocObj', originalDocObj
if originalAmount isnt newAmount
expFunction = achievement.getExpFunction()
earned.notified = false
earned.achievedAmount = newAmount
earned.earnedPoints = (expFunction(newAmount) - expFunction(originalAmount)) * worth
#console.log 'earnedPoints is', (expFunction(newAmount) - expFunction(originalAmount)) * pointWorth, 'was', earned.earnedPoints, earned.previouslyAchievedAmount, 'got exp function for new amount', newAmount, expFunction(newAmount), 'for original amount', originalAmount, expFunction(originalAmount), 'with point worth', pointWorth
earnedPoints = earned.earnedPoints = (expFunction(newAmount) - expFunction(originalAmount)) * pointWorth
earnedGems = earned.earnedGems = (expFunction(newAmount) - expFunction(originalAmount)) * gemWorth
earned.previouslyAchievedAmount = originalAmount
EarnedAchievement.update {achievement: earned.achievement, user: earned.user}, earned, {upsert: true}, (err) ->
return log.debug err if err?
earnedPoints = earned.earnedPoints
#log.debug earnedPoints
wrapUp()
wrapUp(new EarnedAchievement(earned))
else
done?()
else # not alreadyAchieved
#log.debug 'Creating a new earned achievement called \'' + (achievement.get 'name') + '\' for ' + userID
earned.earnedPoints = worth
earned.earnedPoints = pointWorth
earned.earnedGems = gemWorth
(new EarnedAchievement(earned)).save (err, doc) ->
return log.error err if err?
earnedPoints = worth
earnedPoints = pointWorth
earnedGems = gemWorth
wrapUp(doc)
User.saveActiveUser userID, "achievement"

View file

@ -6,6 +6,7 @@ EarnedAchievement = require './EarnedAchievement'
User = require '../users/User'
Handler = require '../commons/Handler'
LocalMongo = require '../../app/lib/LocalMongo'
util = require '../../app/core/utils'
class EarnedAchievementHandler extends Handler
modelClass: EarnedAchievement
@ -65,6 +66,10 @@ class EarnedAchievementHandler extends Handler
return @sendNotFoundError(res, 'Could not find achievement.')
else if not trigger
return @sendNotFoundError(res, 'Could not find trigger.')
else if achievement.get('proportionalTo') and earned
EarnedAchievement.createForAchievement(achievement, trigger, null, earned, (earnedAchievementDoc) =>
@sendCreated(res, (earnedAchievementDoc or earned)?.toObject())
)
else if earned
achievementEarned = achievement.get('rewards')
actuallyEarned = earned.get('earnedRewards')
@ -82,10 +87,8 @@ class EarnedAchievementHandler extends Handler
return @sendDatabaseError(res, err) if err
return @sendSuccess(res, earned.toObject())
)
else if achievement.get('proportionalTo')
return @sendBadInputError(res, 'Cannot currently do this to repeatable docs...')
else
EarnedAchievement.createForAchievement(achievement, trigger, null, (earnedAchievementDoc) =>
EarnedAchievement.createForAchievement(achievement, trigger, null, null, (earnedAchievementDoc) =>
@sendCreated(res, earnedAchievementDoc.toObject())
)
)
@ -221,13 +224,15 @@ class EarnedAchievementHandler extends Handler
notified: achievement._id in alreadyEarnedIDs
if isRepeatable
earned.achievedAmount = something.get(achievement.get 'proportionalTo')
earned.achievedAmount = util.getByPath(something.toObject(), achievement.get 'proportionalTo') or 0
earned.previouslyAchievedAmount = 0
expFunction = achievement.getExpFunction()
newPoints = expFunction(earned.achievedAmount) * achievement.get('worth') ? 10
newGems = expFunction(earned.achievedAmount) * (achievement.get('rewards')?.gems ? 0)
else
newPoints = achievement.get('worth') ? 10
newGems = achievement.get('rewards')?.gems ? 0
earned.earnedPoints = newPoints
newTotalPoints += newPoints
@ -235,7 +240,10 @@ class EarnedAchievementHandler extends Handler
earned.earnedRewards = achievement.get('rewards')
for rewardType in ['heroes', 'items', 'levels']
newTotalRewards[rewardType] = newTotalRewards[rewardType].concat(achievement.get('rewards')?[rewardType] ? [])
newTotalRewards.gems += achievement.get('rewards')?.gems ? 0
if isRepeatable and earned.earnedRewards
earned.earnedRewards = _.clone earned.earnedRewards
earned.earnedRewards.gems = newGems
newTotalRewards.gems += newGems
EarnedAchievement.update {achievement:earned.achievement, user:earned.user}, earned, {upsert: true}, (err) ->
doneWithAchievement err

View file

@ -33,6 +33,7 @@ class AnalyticsLogEventHandler extends Handler
# endDay - Exclusive, optional, e.g. '2014-12-16'
# TODO: An uncached call takes about 15s locally
# TODO: Use unique users
levelSlug = req.query.slug or req.body.slug
startDay = req.query.startDay or req.body.startDay
@ -105,11 +106,12 @@ class AnalyticsLogEventHandler extends Handler
# startDay - Inclusive, optional, e.g. '2014-12-14'
# endDay - Exclusive, optional, e.g. '2014-12-16'
# TODO: Must be a better way to organize this series of 3 database calls (campaigns, levels, analytics)
# TODO: Must be a better way to organize this series of database calls (campaigns, levels, analytics)
# TODO: An uncached call can take over 30s locally
# TODO: Returns all the campaigns
# TODO: Calculate overall campaign stats
# TODO: Assumes db campaign levels are in progression order. Should build this based on actual progression.
# TODO: Remove earliest duplicate event so our dropped counts will be more accurate.
campaignSlug = req.query.slug or req.body.slug
startDay = req.query.startDay or req.body.startDay
@ -128,95 +130,111 @@ class AnalyticsLogEventHandler extends Handler
cacheKey += 'e' + endDay if endDay?
return @sendSuccess res, campaignDropOffs if campaignDropOffs = @campaignDropOffsCache[cacheKey]
getCompletions = (campaigns) =>
getCompletions = (campaigns, userProgression) =>
# Calculate campaign drop off rates
# Input:
# campaigns - per-campaign dictionary of ordered level slugs
# userProgression - per-user event lists
# Remove duplicate user events
for user of userProgression
userProgression[user] = _.uniq userProgression[user], false, (val, index, arr) -> val.event + val.level
# Order user progression by created
for user of userProgression
userProgression[user].sort (a,b) -> if a.created < b.created then return -1 else 1
# Per-level start/drop/finish/drop
levelProgression = {}
for user of userProgression
for i in [0...userProgression[user].length]
event = userProgression[user][i].event
level = userProgression[user][i].level
levelProgression[level] ?=
started: 0
startDropped: 0
finished: 0
finishDropped: 0
if event is 'Started Level'
levelProgression[level].started++
levelProgression[level].startDropped++ if i is userProgression[user].length - 1
else if event is 'Saw Victory'
levelProgression[level].finished++
levelProgression[level].finishDropped++ if i is userProgression[user].length - 1
# Put in campaign order
completions = {}
for level of levelProgression
for campaign of campaigns
if level in campaigns[campaign]
started = levelProgression[level].started
startDropped = levelProgression[level].startDropped
finished = levelProgression[level].finished
finishDropped = levelProgression[level].finishDropped
completions[campaign] ?=
levels: []
# overall:
# started: 0,
# startDropped: 0,
# finished: 0,
# finishDropped: 0
completions[campaign].levels.push
level: level
started: started
startDropped: startDropped
finished: finished
finishDropped: finishDropped
break
# Sort level data by campaign order
for campaign of completions
completions[campaign].levels.sort (a, b) ->
if campaigns[campaign].indexOf(a.level) < campaigns[campaign].indexOf(b.level) then return -1 else 1
# Return all campaign data for simplicity
# Cache other individual campaigns too, since we have them
@campaignDropOffsCache[cacheKey] = completions
for campaign of completions
cacheKey = campaign
cacheKey += 's' + startDay if startDay?
cacheKey += 'e' + endDay if endDay?
@campaignDropOffsCache[cacheKey] = completions
@sendSuccess res, completions
getUserEventData = (campaigns) =>
# Gather user start and finish event data
# Input:
# campaigns - per-campaign dictionary of ordered level slugs
# Output:
# userProgression - per-user event lists
userProgression = {}
queryParams = {$and: [{$or: [ {"event" : 'Started Level'}, {"event" : 'Saw Victory'}]}]}
queryParams["$and"].push created: {$gte: new Date(startDay + "T00:00:00.000Z")} if startDay?
queryParams["$and"].push created: {$lt: new Date(endDay + "T00:00:00.000Z")} if endDay?
AnalyticsLogEvent.find(queryParams).select('created event properties user').exec (err, data) =>
if err? then return @sendDatabaseError res, err
# Bucketize events by user
userProgression = {}
for item in data
created = item.get('created')
event = item.get('event')
if event is 'Saw Victory'
level = item.get('properties.level').toLowerCase().replace new RegExp(' ', 'g'), '-'
else
level = item.get('properties.levelID')
continue unless level?
user = item.get('user')
userProgression[user] ?= []
userProgression[user].push
created: created
event: event
level: level
# Order user progression by created
for user in userProgression
userProgression[user].sort (a,b) -> if a.created < b.created then return -1 else 1
# Per-level start/drop/finish/drop
levelProgression = {}
for user of userProgression
for i in [0...userProgression[user].length]
event = userProgression[user][i].event
level = userProgression[user][i].level
levelProgression[level] ?=
started: 0
startDropped: 0
finished: 0
finishDropped: 0
if event is 'Started Level'
levelProgression[level].started++
levelProgression[level].startDropped++ if i is userProgression[user].length - 1
else if event is 'Saw Victory'
levelProgression[level].finished++
levelProgression[level].finishDropped++ if i is userProgression[user].length - 1
# Put in campaign order
completions = {}
for level of levelProgression
for campaign of campaigns
if level in campaigns[campaign]
started = levelProgression[level].started
startDropped = levelProgression[level].startDropped
finished = levelProgression[level].finished
finishDropped = levelProgression[level].finishDropped
completions[campaign] ?=
levels: []
# overall:
# started: 0,
# startDropped: 0,
# finished: 0,
# finishDropped: 0
completions[campaign].levels.push
level: level
started: started
startDropped: startDropped
finished: finished
finishDropped: finishDropped
break
# Sort level data by campaign order
for campaign of completions
completions[campaign].levels.sort (a, b) ->
if campaigns[campaign].indexOf(a.level) < campaigns[campaign].indexOf(b.level) then return -1 else 1
# Return all campaign data for simplicity
# Cache other individual campaigns too, since we have them
@campaignDropOffsCache[cacheKey] = completions
for campaign of completions
cacheKey = campaign
cacheKey += 's' + startDay if startDay?
cacheKey += 'e' + endDay if endDay?
@campaignDropOffsCache[cacheKey] = completions
@sendSuccess res, completions
# Query stream is better for large results
# http://mongoosejs.com/docs/api.html#query_Query-stream
stream = AnalyticsLogEvent.find(queryParams).select('created event properties user').stream()
stream.on 'data', (item) =>
created = item.get('created')
event = item.get('event')
if event is 'Saw Victory'
level = item.get('properties.level').toLowerCase().replace new RegExp(' ', 'g'), '-'
else
level = item.get('properties.levelID')
return unless level?
user = item.get('user')
userProgression[user] ?= []
userProgression[user].push
created: created
event: event
level: level
.on 'error', (err) =>
return @sendDatabaseError res, err
.on 'close', () =>
getCompletions campaigns, userProgression
getLevelData = (campaigns, campaignLevelIDs) =>
# Get level data and replace levelIDs with level slugs in campaigns
@ -240,7 +258,7 @@ class AnalyticsLogEventHandler extends Handler
mapFn = (item) -> levelSlugMap[item]
campaigns[campaign] = _.map campaigns[campaign], mapFn, @
getCompletions campaigns
getUserEventData campaigns
getCampaignData = () =>
# Get campaign data

View file

@ -39,7 +39,7 @@ CampaignHandler = class CampaignHandler extends Handler
getRelatedLevels: (req, res, campaign, projection) ->
extraProjectionProps = []
if projection
unless _.isEmpty(projection)
# Make sure that permissions and version are fetched, but not sent back if they didn't ask for them.
extraProjectionProps.push 'permissions' unless projection.permissions
extraProjectionProps.push 'version' unless projection.version

View file

@ -14,7 +14,8 @@ AchievablePlugin = (schema, options) ->
# Keep track the document before it's saved
schema.post 'init', (doc) ->
doc.unchangedCopy = doc.toObject()
unless doc.unchangedCopy
doc.unchangedCopy = doc.toObject()
# Check if an achievement has been earned
schema.post 'save', (doc) ->

View file

@ -575,8 +575,8 @@ sendLadderUpdateEmail = (session, now, daysAgo) ->
unless user.get('email') and allowNotes and not session.unsubscribed
#log.info "Not sending email to #{user.get('email')} #{user.get('name')} because they only want emails about #{user.get('emailSubscriptions')}, #{user.get('emails')} - session unsubscribed: #{session.unsubscribed}"
return
unless session.levelName
#log.info "Not sending email to #{user.get('email')} #{user.get('name')} because the session had no levelName in it."
unless session.levelName and session.team
#log.info "Not sending email to #{user.get('email')} #{user.get('name')} because the session had levelName #{session.levelName} or team #{session.team} in it."
return
name = if user.get('firstName') and user.get('lastName') then "#{user.get('firstName')}" else user.get('name')
name = 'Wizard' if not name or name is 'Anoner'

View file

@ -21,6 +21,8 @@ repeatable =
userField: '_id'
proportionalTo: 'simulatedBy'
recalculable: true
rewards:
gems: 1
diminishing =
name: 'Simulated2'
@ -169,19 +171,23 @@ describe 'Achieving Achievements', ->
it 'verify that a repeatable achievement has been earned', (done) ->
unittest.getNormalJoe (joe) ->
EarnedAchievement.find {achievementName: repeatable.name}, (err, docs) ->
expect(err).toBeNull()
expect(docs.length).toBe(1)
achievement = docs[0]
if achievement
expect(achievement.get 'achievement').toBe repeatable._id
expect(achievement.get 'user').toBe joe._id.toHexString()
expect(achievement.get 'notified').toBeFalsy()
expect(achievement.get 'earnedPoints').toBe 2 * repeatable.worth
expect(achievement.get 'achievedAmount').toBe 2
expect(achievement.get 'previouslyAchievedAmount').toBeFalsy()
done()
User.findById(joe.get('_id')).exec (err, joe2) ->
expect(joe2.get('earned').gems).toBe(2)
EarnedAchievement.find {achievementName: repeatable.name}, (err, docs) ->
expect(err).toBeNull()
expect(docs.length).toBe(1)
achievement = docs[0]
if achievement
expect(achievement.get 'achievement').toBe repeatable._id
expect(achievement.get 'user').toBe joe._id.toHexString()
expect(achievement.get 'notified').toBeFalsy()
expect(achievement.get 'earnedPoints').toBe 2 * repeatable.worth
expect(achievement.get 'achievedAmount').toBe 2
expect(achievement.get 'previouslyAchievedAmount').toBeFalsy()
done()
it 'verify that the repeatable achievement with complex exp has been earned', (done) ->
unittest.getNormalJoe (joe) ->
@ -196,6 +202,17 @@ describe 'Achieving Achievements', ->
done()
it 'increases gems proportionally to changes made', (done) ->
unittest.getNormalJoe (joe) ->
User.findById(joe.get('_id')).exec (err, joe2) ->
joe2.set('simulatedBy', 4)
joe2.save (err, joe3) ->
expect(err).toBeNull()
User.findById(joe3.get('_id')).exec (err, joe4) ->
expect(joe4.get('earned').gems).toBe(4) # Crap, it's 6 TODO
done()
describe 'Recalculate Achievements', ->
EarnedAchievementHandler = require '../../../server/achievements/earned_achievement_handler'
@ -241,7 +258,8 @@ describe 'Recalculate Achievements', ->
unittest.getNormalJoe (joe) ->
User.findById joe.get('id'), (err, guy) ->
expect(err).toBeNull()
expect(guy.get 'points').toBe unlockable.worth + 2 * repeatable.worth + (Math.log(.5 * (2 + .5)) + 1) * diminishing.worth
expect(guy.get 'points').toBe unlockable.worth + 4 * repeatable.worth + (Math.log(.5 * (4 + .5)) + 1) * diminishing.worth
expect(guy.get('earned').gems).toBe 4 * repeatable.rewards.gems
done()
it 'cleaning up test: deleting all Achievements and related', (done) ->