This commit is contained in:
Scott Erickson 2014-02-12 09:55:35 -08:00
commit 59f0d8a93e
26 changed files with 938 additions and 113 deletions

View file

@ -21,6 +21,7 @@ module.exports = class LevelBus extends Bus
'thang-code-ran': 'onCodeRan'
'level-show-victory': 'onVictory'
'tome:spell-changed': 'onSpellChanged'
'tome:spell-created': 'onSpellCreated'
constructor: ->
super(arguments...)
@ -91,12 +92,26 @@ module.exports = class LevelBus extends Bus
code = @session.get('code')
code ?= {}
parts = e.spell.spellKey.split('/')
code[parts[0]] ?= {}
code[parts[0]][parts[1]] = e.spell.getSource()
@changedSessionProperties.code = true
@session.set({'code': code})
@saveSession()
onSpellCreated: (e) ->
return unless @onPoint()
spellTeam = e.spell.team
@teamSpellMap[spellTeam] ?= []
unless e.spell.spellKey in @teamSpellMap[spellTeam]
@teamSpellMap[spellTeam].push e.spell.spellKey
@changedSessionProperties.teamSpells = true
@session.set({'teamSpells': @teamSpellMap})
@saveSession()
onScriptStateChanged: (e) ->
return unless @onPoint()
@fireScriptsRef?.update(e)
@ -220,4 +235,12 @@ module.exports = class LevelBus extends Bus
destroy: ->
super()
@session.off 'change:multiplayer', @onMultiplayerChanged, @
@session.off 'change:multiplayer', @onMultiplayerChanged, @
setTeamSpellMap: (spellMap) ->
@teamSpellMap = spellMap
console.log @teamSpellMap
@changedSessionProperties.teamSpells = true
@session.set({'teamSpells': @teamSpellMap})
@saveSession()

View file

@ -20,6 +20,9 @@ module.exports = class CocoRouter extends Backbone.Router
'db/*path': 'routeToServer'
'file/*path': 'routeToServer'
'play/level/:levelID/leaderboard/:teamID/:startRank/:endRank': 'getPaginatedLevelRank'
'play/level/:levelID/player/:playerID': 'getPlayerLevelInfo'
# most go through here
'*name': 'general'
@ -27,6 +30,13 @@ module.exports = class CocoRouter extends Backbone.Router
general: (name) ->
@openRoute(name)
getPaginatedLevelRank: (levelID,teamID,startRank,endRank) ->
return
getPlayerLevelInfo: (levelID,playerID) ->
return
editorModelView: (modelName, slugOrId, subview) ->
modulePrefix = "views/editor/#{modelName}/"
suffix = subview or (if slugOrId then 'edit' else 'home')

View file

@ -135,7 +135,6 @@ module.exports = class GoalManager extends CocoClass
status: null # should eventually be either 'success', 'failure', or 'incomplete'
keyFrame: 0 # when it became a 'success' or 'failure'
}
@initGoalState(state, [goal.killThangs, goal.saveThangs], 'killed')
@initGoalState(state, [goal.getToLocations?.who, goal.keepFromLocations?.who], 'arrived')
@initGoalState(state, [goal.leaveOffSides?.who, goal.keepFromLeavingOffSides?.who], 'left')

View file

@ -31,21 +31,21 @@ module.exports = nativeDescription: "português do Brasil", englishDescription:
about: "Sobre"
contact: "Contate-nos"
twitter_follow: "Seguir"
# employers: "Employers"
employers: "Empregadores"
# versions:
# save_version_title: "Save New Version"
# new_major_version: "New Major Version"
# cla_prefix: "To save changes, first you must agree to our"
# cla_url: "CLA"
# cla_suffix: "."
# cla_agree: "I AGREE"
versions:
save_version_title: "Salvar nova versão"
new_major_version: "Nova versão principal"
cla_prefix: "Para salvar as modificações, primeiro você deve concordar com nosso"
cla_url: "CLA"
cla_suffix: "."
cla_agree: "EU CONCORDO"
login:
sign_up: "Criar conta"
log_in: "Entrar"
log_out: "Sair"
recover: "recuperar sua conta"
recover: "Recuperar sua conta"
recover:
recover_account_title: "Recuperar conta"
@ -122,7 +122,7 @@ module.exports = nativeDescription: "português do Brasil", englishDescription:
new_password_verify: "Confirmação"
email_subscriptions: "Assinaturas para Notícias por Email"
email_announcements: "Notícias"
# email_notifications_description: "Get periodic notifications for your account."
email_notifications_description: "Recebe notificações periódicas em sua conta."
email_announcements_description: "Receba emails com as últimas notícias e desenvolvimentos do CodeCombat."
contributor_emails: "Emails para as Classes de Contribuidores"
contribute_prefix: "Estamos procurando pessoas para se juntar à nossa turma! Confira a nossa "
@ -191,75 +191,75 @@ module.exports = nativeDescription: "português do Brasil", englishDescription:
tome_select_a_thang: "Selecione alguém para "
tome_available_spells: "Feitiços Disponíveis"
hud_continue: "Continue (tecle Shift+Space)"
# spell_saved: "Spell Saved"
spell_saved: "Feitiço Salvo"
# admin:
# av_title: "Admin Views"
# av_entities_sub_title: "Entities"
# av_entities_users_url: "Users"
# av_entities_active_instances_url: "Active Instances"
# av_other_sub_title: "Other"
# av_other_debug_base_url: "Base (for debugging base.jade)"
# u_title: "User List"
# lg_title: "Latest Games"
admin:
av_title: "Visualização de Administrador"
av_entities_sub_title: "Entidades"
av_entities_users_url: "Usuários"
av_entities_active_instances_url: "Instâncias Ativas"
av_other_sub_title: "Outro"
av_other_debug_base_url: "Base (para debugar base.jade)"
u_title: "Lista de Usuários"
lg_title: "Últimos Jogos"
# editor:
# main_title: "CodeCombat Editors"
# main_description: "Build your own levels, campaigns, units and educational content. We provide all the tools you need!"
# article_title: "Article Editor"
# article_description: "Write articles that give players overviews of programming concepts which can be used across a variety of levels and campaigns."
# thang_title: "Thang Editor"
# thang_description: "Build units, defining their default logic, graphics and audio. Currently only supports importing Flash exported vector graphics."
# level_title: "Level Editor"
# level_description: "Includes the tools for scripting, uploading audio, and constructing custom logic to create all sorts of levels. Everything we use ourselves!"
# security_notice: "Many major features in these editors are not currently enabled by default. As we improve the security of these systems, they will be made generally available. If you'd like to use these features sooner, "
# contact_us: "contact us!"
# hipchat_prefix: "You can also find us in our"
# hipchat_url: "HipChat room."
# level_some_options: "Some Options?"
# level_tab_thangs: "Thangs"
# level_tab_scripts: "Scripts"
# level_tab_settings: "Settings"
# level_tab_components: "Components"
# level_tab_systems: "Systems"
# level_tab_thangs_title: "Current Thangs"
# level_tab_thangs_conditions: "Starting Conditions"
# level_tab_thangs_add: "Add Thangs"
# level_settings_title: "Settings"
# level_component_tab_title: "Current Components"
# level_component_btn_new: "Create New Component"
# level_systems_tab_title: "Current Systems"
# level_systems_btn_new: "Create New System"
# level_systems_btn_add: "Add System"
# level_components_title: "Back to All Thangs"
# level_components_type: "Type"
# level_component_edit_title: "Edit Component"
# level_system_edit_title: "Edit System"
# create_system_title: "Create New System"
# new_component_title: "Create New Component"
# new_component_field_system: "System"
editor:
main_title: "Editores do CodeCombat"
main_description: "Construa seus próprios níveis, campanhas, unidades e conteúdo educacional. Nós fornecemos todas as ferramentas que você precisa!"
article_title: "Editor de Artigo"
article_description: "Escreva artigos que forneçam aos jogadores explicações sobre conceitos de programação que podem ser utilizados em diversos níveis e campanhas."
thang_title: "Editor de Thang"
thang_description: "Construa unidades, definindo sua lógica padrão, gráfico e áudio. Atualmente só é suportado importação de vetores gráficos exportados do Flash."
level_title: "Editor de Niível"
level_description: "Inclui as ferramentas para codificar, fazer o upload de áudio e construir uma lógica diferente para criar todos os tipos de níveis. Tudo o que nós mesmos utilizamos!"
security_notice: "Muitos recursos principais nestes editores não estão habilitados por padrão atualmente. A maneira que melhoramos a segurança desses sistemas, eles serão colocados a disposição. Se você quiser utilizar um desses recursos mais rápido, "
contact_us: "entre em contato!"
hipchat_prefix: "Você também pode nos encontrar na nossa"
hipchat_url: "Sala do HipChat."
level_some_options: "Algumas Opções?"
level_tab_thangs: "Thangs"
level_tab_scripts: "Scripts"
level_tab_settings: "Configurações"
level_tab_components: "Componentes"
level_tab_systems: "Sistemas"
level_tab_thangs_title: "Thangs Atuais"
level_tab_thangs_conditions: "Condições de Início"
level_tab_thangs_add: "Adicionar Thangs"
level_settings_title: "Configurações"
level_component_tab_title: "Componentess Atuais"
level_component_btn_new: "Criar Novo Componente"
level_systems_tab_title: "Sistemas Atuais"
level_systems_btn_new: "Criar Novo Sistema"
level_systems_btn_add: "Adicionar Sistema"
level_components_title: "Voltar para Lista de Thangs"
level_components_type: "Tipo"
level_component_edit_title: "Editar Componente"
level_system_edit_title: "Editar Sistema"
create_system_title: "Criar Novo Sistema"
new_component_title: "Criar Novo Componente"
new_component_field_system: "Sistema"
# article:
# edit_btn_preview: "Preview"
# edit_article_title: "Edit Article"
article:
edit_btn_preview: "Prever"
edit_article_title: "Editar Artigo "
general:
# and: "and"
and: "e"
or: "ou"
name: "Nome"
# body: "Body"
# version: "Version"
# commit_msg: "Commit Message"
# version_history_for: "Version History for: "
# results: "Results"
# description: "Description"
version: "Versão"
commit_msg: "Mensagem do Commit"
version_history_for: "Histórico de Versão para: "
results: "Resultados"
description: "Descrição"
email: "Email"
message: "Mensagem"
# about:
# who_is_codecombat: "Who is CodeCombat?"
# why_codecombat: "Why CodeCombat?"
# who_description_prefix: "together started CodeCombat in 2013. We also created "
about:
who_is_codecombat: "Quem é CodeCombat?"
why_codecombat: "Por que CodeCombat?"
who_description_prefix: "juntos começamos o CodeCombat em 2013. Noós também criamos "
# who_description_suffix: "in 2008, growing it to the #1 web and iOS application for learning to write Chinese and Japanese characters."
# who_description_ending: "Now it's time to teach people to write code."
# why_paragraph_1: "When making Skritter, George didn't know how to program and was constantly frustrated by his inability to implement his ideas. Afterwards, he tried learning, but the lessons were too slow. His housemate, wanting to reskill and stop teaching, tried Codecademy, but \"got bored.\" Each week another friend started Codecademy, then dropped off. We realized it was the same problem we'd solved with Skritter: people learning a skill via slow, intensive lessons when what they need is fast, extensive practice. We know how to fix that."

View file

@ -1,8 +1,8 @@
module.exports = nativeDescription: "简体中文", englishDescription: "Chinese (Simplified)", translation:
common:
loading: "读取中..."
loading: "读取中……"
saving: "保存中……"
sending: "发送中..."
sending: "发送中……"
cancel: "退出"
# save: "Save"
delay_1_sec: "1 秒"
@ -53,33 +53,33 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
signup:
# create_account_title: "Create Account to Save Progress"
description: "这是免费的简单易学:"
description: "这是免费的简单易学:"
email_announcements: "通过邮件接收通知"
coppa: "13 岁以上或非美国用户"
coppa: "13岁以上或非美国用户"
coppa_why: "为什么?"
creating: "账户创建中..."
creating: "账户创建中……"
sign_up: "注册"
log_in: "登录"
home:
slogan: "通过玩儿游戏学到Javascript脚本语言"
no_ie: "抱歉Internet Explorer 9等更旧的预览器打不开此网站"
no_ie: "抱歉Internet Explorer 9等更旧的预览器打不开此网站"
no_mobile: "CodeCombat 不是针对手机设备设计的,所以可能不好用!"
play: ""
play:
choose_your_level: "选取难度"
adventurer_prefix: "你可以选择以下任意关卡,或者讨论以上的关卡 "
adventurer_forum: "冒险论坛"
adventurer_prefix: "你可以选择以下任意关卡,或者讨论以上的关卡"
adventurer_forum: "冒险论坛"
adventurer_suffix: "."
campaign_beginner: "新手作战"
campaign_beginner_description: "...在这里可以学到编程技巧。"
campaign_beginner_description: "……在这里可以学到编程技巧。"
campaign_dev: "随机困难关卡"
campaign_dev_description: "...在这里你可以学到做一些复杂功能的接口。"
campaign_dev_description: "……在这里你可以学到做一些复杂功能的接口。"
campaign_multiplayer: "多人竞技场"
campaign_multiplayer_description: "...在这里你可以和其他玩家们进行代码肉搏战。"
campaign_multiplayer_description: "……在这里你可以和其他玩家们进行代码肉搏战。"
campaign_player_created: "已创建的玩家"
campaign_player_created_description: "...在这里你可以与你的小伙伴的创造力战斗 <a href=\"/contribute#artisan\">技术指导</a>."
campaign_player_created_description: "……在这里你可以与你的小伙伴的创造力战斗 <a href=\"/contribute#artisan\">技术指导</a>."
level_difficulty: "难度"
contact:
@ -90,13 +90,13 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
contribute_suffix: ""
forum_prefix: "对任何公共部分,放手去干吧 "
forum_page: "我们的论坛"
forum_suffix: "代替"
forum_suffix: "代替 "
send: "意见反馈"
diplomat_suggestion:
title: "帮我们翻译CodeCombat"
title: "帮我们翻译 CodeCombat"
sub_heading: "我们需要您的语言技能"
pitch_body: "我们开发了CodeCombat的英文版但是现在我们的玩家遍布全球。很多人想玩中文简体版的却不会说英语所以如果你中英文都会请考虑一下参加我们的翻译工作帮忙把 CodeCombat 网站还有所有的关卡翻译成中文(简体)。"
pitch_body: "我们开发了 CodeCombat 的英文版,但是现在我们的玩家遍布全球。很多人想玩中文(简体)版的,却不会说英语,所以如果你中英文都会,请考虑一下参加我们的翻译工作,帮忙把 CodeCombat 网站还有所有的关卡翻译成中文(简体)。"
missing_translations: "未翻译的文本将显示为英文。"
learn_more: "了解更多有关成为翻译人员的说明"
subscribe_as_diplomat: "提交翻译人员申请"
@ -122,7 +122,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
new_password_verify: "核实"
email_subscriptions: "邮箱验证"
email_announcements: "通知"
# email_notifications_description: "Get periodic notifications for your account."
email_notifications_description: "定期接受来自你的账户的通知。"
email_announcements_description: "接收关于 CodeCombat 最近的新闻和发展的邮件。"
contributor_emails: "贡献者通知"
contribute_prefix: "我们在寻找志同道合的人!请到 "
@ -153,7 +153,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
level_load_error: "关卡不能载入。"
done: "完成"
grid: "格子"
customize_wizard: "自定义巫师"
customize_wizard: "自定义向导"
home: "主页"
guide: "指南"
multiplayer: "多人游戏"
@ -168,7 +168,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
victory_title_suffix: " 完成"
victory_sign_up: "保存进度"
victory_sign_up_poke: "想保存你的代码?创建一个免费账户吧!"
victory_rate_the_level: "评估关卡: "
victory_rate_the_level: "评估关卡"
victory_play_next_level: "下一关"
victory_go_home: "返回主页"
victory_review: "给我们反馈!"
@ -178,7 +178,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
multiplayer_link_description: "把这个链接告诉小伙伴们,一起玩吧。"
multiplayer_hint_label: "提示:"
multiplayer_hint: " 点击全选,然后按 Apple-C苹果电脑或 Ctrl-C 复制链接。"
multiplayer_coming_soon: "多人游戏的更多特性!"
multiplayer_coming_soon: "多人游戏的更多特性"
guide_title: "指南"
tome_minion_spells: "助手的咒语"
tome_read_only_spells: "只读的咒语"
@ -190,7 +190,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
tome_select_spell: "选择一个法术"
tome_select_a_thang: "选择人物来 "
tome_available_spells: "可用的法术"
hud_continue: "继续 (按 shift-空格)"
hud_continue: "继续(按 shift-空格)"
# spell_saved: "Spell Saved"
# admin:

View file

@ -57,7 +57,7 @@ block content
| create an account
span
span(data-i18n="contribute.alert_account_message_suf")
| first.
| first.
label.checkbox(for="translator").well
input(type='checkbox', name="translator", id="translator")

View file

@ -25,4 +25,6 @@ block content
div.homepage_button
a#beginner-campaign(href="/play/level/rescue-mission")
canvas(width="125", height="150")
button(data-i18n="home.play").btn.btn-warning.btn-lg.highlight Play
button(data-i18n="home.play").btn.btn-warning.btn-lg.highlight Play
if me.isAdmin()
button.btn.btn-warning.btn-lg.highlight#simulate-button SIMULATE

View file

@ -2,6 +2,10 @@ View = require 'views/kinds/RootView'
template = require 'templates/home'
WizardSprite = require 'lib/surface/WizardSprite'
ThangType = require 'models/ThangType'
LevelLoader = require 'lib/LevelLoader'
God = require 'lib/God'
GoalManager = require 'lib/world/GoalManager'
module.exports = class HomeView extends View
id: 'home-view'
@ -10,6 +14,7 @@ module.exports = class HomeView extends View
events:
'mouseover #beginner-campaign': 'onMouseOverButton'
'mouseout #beginner-campaign': 'onMouseOutButton'
'click #simulate-button': 'onSimulateButtonClick'
getRenderData: ->
c = super()
@ -97,4 +102,91 @@ module.exports = class HomeView extends View
destroy: ->
super()
@wizardSprite?.destroy()
@wizardSprite?.destroy()
onSimulateButtonClick: (e) =>
$.get "/queue/scoring", (data) =>
levelName = data.sessions[0].levelID
#TODO: Refactor. So much refactor.
world = {}
god = new God()
levelLoader = new LevelLoader(levelName, @supermodel, data.sessions[0].sessionID)
levelLoader.once 'loaded-all', =>
world = levelLoader.world
level = levelLoader.level
levelLoader.destroy()
god.level = level.serialize @supermodel
god.worldClassMap = world.classMap
god.goalManager = new GoalManager(world)
#move goals in here
goalsToAdd = god.goalManager.world.scripts[0].noteChain[0].goals.add
god.goalManager.goals = goalsToAdd
god.goalManager.goalStates =
"destroy-humans":
keyFrame: 0
killed:
"Human Base": false
status: "incomplete"
"destroy-ogres":
keyFrame:0
killed:
"Ogre Base": false
status: "incomplete"
god.spells = @filterProgrammableComponents level.attributes.thangs, @generateSpellToSourceMap data.sessions
god.createWorld()
Backbone.Mediator.subscribe 'god:new-world-created', @onWorldCreated, @
onWorldCreated: (data) ->
console.log "GOAL STATES"
console.log data
filterProgrammableComponents: (thangs, spellToSourceMap) =>
spells = {}
for thang in thangs
isTemplate = false
for component in thang.components
if component.config? and _.has component.config,'programmableMethods'
for methodName, method of component.config.programmableMethods
if typeof method is 'string'
isTemplate = true
break
pathComponents = [thang.id,methodName]
pathComponents[0] = _.string.slugify pathComponents[0]
spellKey = pathComponents.join '/'
spells[spellKey] ?= {}
spells[spellKey].thangs ?= {}
spells[spellKey].name = methodName
thangID = _.string.slugify thang.id
spells[spellKey].thangs[thang.id] ?= {}
spells[spellKey].thangs[thang.id].aether = @createAether methodName, method
if spellToSourceMap[thangID]? then source = spellToSourceMap[thangID][methodName] else source = ""
spells[spellKey].thangs[thang.id].aether.transpile source
if isTemplate
break
spells
createAether : (methodName, method) ->
aetherOptions =
functionName: methodName
protectAPI: false
includeFlow: false
return new Aether aetherOptions
generateSpellToSourceMap: (sessions) ->
spellKeyToSourceMap = {}
spellSources = {}
for session in sessions
teamSpells = session.teamSpells[session.team]
_.merge spellSources, _.pick(session.code, teamSpells)
#merge common ones, this overwrites until the last session
commonSpells = session.teamSpells["common"]
if commonSpells?
_.merge spellSources, _.pick(session.code, commonSpells)
spellSources

View file

@ -26,12 +26,16 @@ module.exports = class Spell
@view.render() # Get it ready and code loaded in advance
@tabView = new SpellListTabEntryView spell: @, supermodel: @supermodel
@tabView.render()
@team = @permissions.readwrite[0] ? "common"
Backbone.Mediator.publish 'tome:spell-created', spell: @
destroy: ->
@view.destroy()
@tabView.destroy()
@thangs = null
addThang: (thang) ->
if @thangs[thang.id]
@thangs[thang.id].thang = thang

View file

@ -62,6 +62,7 @@ module.exports = class TomeView extends View
@spellList = @insertSubView new SpellListView spells: @spells, supermodel: @supermodel
@thangList = @insertSubView new ThangListView spells: @spells, thangs: @options.thangs, supermodel: @supermodel
@castButton = @insertSubView new CastButtonView spells: @spells
@teamSpellMap = @generateTeamSpellMap(@spells)
else
@cast()
console.warn "Warning: There are no Programmable Thangs in this level, which makes it unplayable."
@ -74,6 +75,21 @@ module.exports = class TomeView extends View
@thangList.adjustThangs @spells, thangs
@spellList.adjustSpells @spells
generateTeamSpellMap: (spellObject) ->
teamSpellMap = {}
for spellName, spell of spellObject
teamName = spell.team
teamSpellMap[teamName] ?= []
spellNameElements = spellName.split '/'
thangName = spellNameElements[0]
spellName = spellNameElements[1]
teamSpellMap[teamName].push thangName if thangName not in teamSpellMap[teamName]
return teamSpellMap
createSpells: (programmableThangs, world) ->
pathPrefixComponents = ['play', 'level', @options.levelID, @options.session.id, 'code']
@spells ?= {}

View file

@ -360,6 +360,7 @@ module.exports = class PlayLevelView extends View
register: ->
@bus = LevelBus.get(@levelID, @session.id)
@bus.setSession(@session)
@bus.setTeamSpellMap @tome.teamSpellMap
@bus.connect() if @session.get('multiplayer')
onSessionWillSave: (e) ->
@ -391,4 +392,4 @@ module.exports = class PlayLevelView extends View
@bus?.destroy()
#@instance.save() unless @instance.loading
console.profileEnd?() if PROFILE_ME
@session.off 'change:multiplayer', @onMultiplayerChanged, @
@session.off 'change:multiplayer', @onMultiplayerChanged, @

View file

@ -59,7 +59,9 @@
"express-useragent": "~0.0.9",
"gridfs-stream": "0.4.x",
"stream-buffers": "0.2.x",
"sendwithus": "2.0.x"
"sendwithus": "2.0.x",
"aws-sdk":"~2.0.0",
"bayesian-battle":"0.0.x"
},
"devDependencies": {
"jade": "0.33.x",

View file

@ -25,14 +25,3 @@ createAndConfigureApp = ->
serverSetup.setupRoutes app
app

View file

@ -7,18 +7,22 @@ testing = '--unittest' in process.argv
module.exports.connect = () ->
address = module.exports.generateMongoConnectionString()
winston.info "Connecting to Mongo with connection string #{address}"
mongoose.connect address
mongoose.connection.once 'open', -> Grid.gfs = Grid(mongoose.connection.db, mongoose.mongo)
module.exports.generateMongoConnectionString = ->
if config.mongo.mongoose_replica_string
address = config.mongo.mongoose_replica_string
winston.info "Connecting to replica set: #{address}"
else
dbName = config.mongo.db
dbName += '_unittest' if testing
address = config.mongo.host + ":" + config.mongo.port
if config.mongo.username and config.mongo.password
address = config.mongo.username + ":" + config.mongo.password + "@" + address
# address = config.mongo.username + "@" + address # if connecting to production server
address = "mongodb://#{address}/#{dbName}"
winston.info "Connecting to standalone server #{address}"
mongoose.connect address
mongoose.connection.once 'open', ->
Grid.gfs = Grid(mongoose.connection.db, mongoose.mongo)
return address

View file

@ -33,3 +33,11 @@ module.exports.badInput = (res, message='Unprocessable Entity. Bad Input.') ->
module.exports.serverError = (res, message='Internal Server Error') ->
res.send 500, message
res.end()
module.exports.gatewayTimeoutError = (res, message="Gateway timeout") ->
res.send 504, message
res.end()
module.exports.clientTimeout = (res, message="The server did not recieve the client response in a timely manner") ->
res.send 408, message
res.end()

View file

@ -33,4 +33,5 @@ module.exports.routes =
'routes/languages'
'routes/mail'
'routes/sprites'
'routes/queue'
]

283
server/commons/queue.coffee Normal file
View file

@ -0,0 +1,283 @@
config = require '../../server_config'
log = require 'winston'
mongoose = require 'mongoose'
async = require 'async'
aws = require 'aws-sdk'
db = require './database'
mongoose = require 'mongoose'
events = require 'events'
crypto = require 'crypto'
module.exports.queueClient = undefined
defaultMessageVisibilityTimeoutInSeconds = 20
defaultMessageReceiptTimeout = 10
module.exports.initializeQueueClient = (cb) ->
module.exports.queueClient = generateQueueClient() unless queueClient?
cb?()
generateQueueClient = ->
#if config.queue.accessKeyId
if false #TODO: Change this in production
queueClient = new SQSQueueClient()
else
queueClient = new MongoQueueClient()
class SQSQueueClient
registerQueue: (queueName, options, callback) ->
queueCreationOptions =
QueueName: queueName
@sqs.createQueue queueCreationOptions, (err,data) =>
@_logAndThrowFatalException "There was an error creating a new SQS queue, reason: #{JSON.stringify err}" if err?
newQueue = new SQSQueue queueName, data.QueueUrl, @sqs
callback? err, newQueue
constructor: ->
@_configure()
@sqs = @_generateSQSInstance()
_configure: ->
aws.config.update
accessKeyId: config.queue.accessKeyId
secretAccessKey: config.queue.secretAccessKey
region: config.queue.region
_generateSQSInstance: -> new aws.SQS()
_logAndThrowFatalException: (errorMessage) ->
log.error errorMessage
throw new Error errorMessage
class SQSQueue extends events.EventEmitter
constructor: (@queueName, @queueUrl, @sqs) ->
subscribe: (eventName, callback) -> @on eventName, callback
unsubscribe: (eventName, callback) -> @removeListener eventName, callback
receiveMessage: (callback) ->
queueReceiveOptions =
QueueUrl: @queueUrl
WaitTimeSeconds: defaultMessageReceiptTimeout
@sqs.receiveMessage queueReceiveOptions, (err, data) =>
if err?
@emit 'error',err,originalData
else
originalData = data
data = new SQSMessage originalData, this
@emit 'message',err,data
callback? err,data
deleteMessage: (receiptHandle, callback) ->
queueDeletionOptions =
QueueUrl: @queueUrl
ReceiptHandle: receiptHandle
@sqs.deleteMessage queueDeletionOptions, (err, data) =>
if err? then @emit 'error',err,data else @emit 'message',err,data
callback? err,data
changeMessageVisibilityTimeout: (secondsFromNow, receiptHandle, callback) ->
messageVisibilityTimeoutOptions =
QueueUrl: @queueUrl
ReceiptHandle: receiptHandle
VisibilityTimeout: secondsFromNow
@sqs.changeMessageVisibility messageVisibilityTimeoutOptions, (err, data) =>
if err? then @emit 'error',err,data else @emit 'edited',err,data
callback? err,data
sendMessage: (messageBody, delaySeconds, callback) ->
queueSendingOptions =
QueueUrl: @queueUrl
MessageBody: messageBody
DelaySeconds: delaySeconds
@sqs.sendMessage queueSendingOptions, (err, data) =>
if err? then @emit 'error',err,data else @emit 'sent',err, data
callback? err,data
listenForever: => async.forever (asyncCallback) => @receiveMessage (err, data) -> asyncCallback(null)
class SQSMessage
constructor: (@originalMessage, @parentQueue) ->
isEmpty: -> not @originalMessage.Messages?[0]?
getBody: -> @originalMessage.Messages[0].Body
getID: -> @originalMessage.Messages[0].MessageId
removeFromQueue: (callback) -> @parentQueue.deleteMessage @getReceiptHandle(), callback
requeue: (callback) -> @parentQueue.changeMessageVisibilityTimeout 0, @getReceiptHandle(), callback
changeMessageVisibilityTimeout: (secondsFromFunctionCall, callback) ->
@parentQueue.changeMessageVisibilityTimeout secondsFromFunctionCall,@getReceiptHandle(), callback
getReceiptHandle: -> @originalMessage.Messages[0].ReceiptHandle
class MongoQueueClient
registerQueue: (queueName, options, callback) ->
newQueue = new MongoQueue queueName,options,@messageModel
callback(null, newQueue)
constructor: ->
@_configure()
@_createMongoConnection()
@messageModel = @_generateMessageModel()
_configure: -> @databaseAddress = db.generateMongoConnectionString()
_createMongoConnection: ->
@mongooseConnection = mongoose.createConnection @databaseAddress
@mongooseConnection.on 'error', -> log.error "There was an error connecting to the queue in MongoDB"
@mongooseConnection.once 'open', -> log.info "Successfully connected to MongoDB queue!"
_generateMessageModel: ->
schema = new mongoose.Schema
messageBody: Object,
queue: {type: String, index:true}
scheduledVisibilityTime: {type: Date, index: true}
receiptHandle: {type: String, index: true}
@mongooseConnection.model 'messageQueue',schema
class MongoQueue extends events.EventEmitter
constructor: (queueName, options, messageModel) ->
@Message = messageModel
@queueName = queueName
subscribe: (eventName, callback) -> @on eventName, callback
unsubscribe: (eventName, callback) -> @removeListener eventName, callback
receiveMessage: (callback) ->
conditions =
queue: @queueName
scheduledVisibilityTime:
$lt: new Date()
options =
sort: 'scheduledVisibilityTime'
update =
$set:
receiptHandle: @_generateRandomReceiptHandle()
scheduledVisibilityTime: @_constructDefaultVisibilityTimeoutDate()
@Message.findOneAndUpdate conditions, update, options, (err, data) =>
return @emit 'error',err,data if err?
originalData = data
data = new MongoMessage originalData, this
@emit 'message',err,data
callback? err,data
deleteMessage: (receiptHandle, callback) ->
conditions =
queue: @queueName
receiptHandle: receiptHandle
scheduledVisibilityTime:
$lt: new Date()
@Message.findOneAndRemove conditions, {}, (err, data) =>
if err? then @emit 'error',err,data else @emit 'delete',err,data
callback? err,data
sendMessage: (messageBody, delaySeconds, callback) ->
messageToSend = new @Message
messageBody: messageBody
queue: @queueName
scheduledVisibilityTime: @_constructDefaultVisibilityTimeoutDate delaySeconds
messageToSend.save (err,data) =>
if err? then @emit 'error',err,data else @emit 'sent',err, data
callback? err,data
changeMessageVisibilityTimeout: (secondsFromNow, receiptHandle, callback) ->
conditions =
queue: @queueName
receiptHandle: receiptHandle
scheduledVisibilityTime:
$lt: new Date()
update =
$set:
scheduledVisibilityTime: @_constructDefaultVisibilityTimeoutDate secondsFromNow
@Message.findOneAndUpdate conditions, update, (err, data) =>
if err? then @emit 'error',err,data else @emit 'update',err,data
callback? err, data
listenForever: => async.forever (asyncCallback) => @recieveMessage (err, data) -> asyncCallback(null)
_constructDefaultVisibilityTimeoutDate: (timeoutSeconds) ->
timeoutSeconds ?= defaultMessageVisibilityTimeoutInSeconds
newDate = new Date()
newDate = new Date(newDate.getTime() + 1000 * timeoutSeconds)
newDate
_generateRandomReceiptHandle: -> crypto.randomBytes(20).toString('hex')
class MongoMessage
constructor: (@originalMessage, @parentQueue) ->
isEmpty: -> not @originalMessage
getBody: -> @originalMessage.messageBody
getID: -> @originalMesage._id
removeFromQueue: (callback) -> @parentQueue.deleteMessage @getReceiptHandle(), callbacks
requeue: (callback) -> @parentQueue.changeMessageVisibilityTimeout 0, @getReceiptHandle(), callback
changeMessageVisibilityTimeout: (secondsFromFunctionCall, callback) ->
@parentQueue.changeMessageVisibilityTimeout secondsFromFunctionCall,@getReceiptHandle(), callback
getReceiptHandle: -> @originalMessage.receiptHandle

View file

@ -7,7 +7,7 @@ class LevelSessionHandler extends Handler
modelClass: LevelSession
editableProperties: ['multiplayer', 'players', 'code', 'completed', 'state',
'levelName', 'creatorName', 'levelID', 'screenshot',
'chat']
'chat', 'teamSpells']
getByRelationship: (req, res, args...) ->
return @sendNotFoundError(res) unless args.length is 2 and args[1] is 'active'

View file

@ -55,9 +55,18 @@ _.extend LevelSessionSchema.properties,
# TODO: specify this more
code: { type: 'object' }
teamSpells:
type: 'object'
additionalProperties:
type: 'array'
players: { type: 'object' }
chat: { type: 'array' }
meanStrength: {type: 'number', default: 25}
standardDeviation: {type:'number', default:25/3, minimum: 0}
totalScore: {type: 'number', default: 10}
c.extendBasicProperties LevelSessionSchema, 'level.session'

View file

@ -0,0 +1,306 @@
config = require '../../server_config'
log = require 'winston'
mongoose = require 'mongoose'
async = require 'async'
errors = require '../commons/errors'
aws = require 'aws-sdk'
db = require './../routes/db'
mongoose = require 'mongoose'
queues = require '../commons/queue'
LevelSession = require '../levels/sessions/LevelSession'
TaskLog = require './task/ScoringTask'
bayes = new (require 'bayesian-battle')()
scoringTaskQueue = undefined
scoringTaskTimeoutInSeconds = 400
module.exports.setup = (app) -> connectToScoringQueue()
connectToScoringQueue = ->
queues.initializeQueueClient ->
queues.queueClient.registerQueue "scoring", {}, (err,data) ->
throwScoringQueueRegistrationError(err) if err?
scoringTaskQueue = data
log.info "Connected to scoring task queue!"
throwScoringQueueRegistrationError = (error) ->
log.error "There was an error registering the scoring queue: #{error}"
throw new Error "There was an error registering the scoring queue."
module.exports.createNewTask = (req, res) ->
scoringTaskQueue.sendMessage req.body, 0, (err, data) ->
return errors.badInput res, "There was an error creating the message, reason: #{err}" if err?
res.send data
res.end()
module.exports.dispatchTaskToConsumer = (req, res) ->
userID = getUserIDFromRequest req,res
return errors.forbidden res, "You need to be logged in to simulate games" if isUserAnonymous req
scoringTaskQueue.receiveMessage (taskQueueReceiveError, message) ->
if (not message?) or message.isEmpty() or taskQueueReceiveError?
return errors.gatewayTimeoutError res, "No messages were receieved from the queue. Msg:#{taskQueueReceiveError}"
messageBody = parseTaskQueueMessage req, res, message
return errors.serverError res, "There was an error parsing the queue message" unless messageBody?
constructTaskObject messageBody, (taskConstructionError, taskObject) ->
return errors.serverError res, "There was an error constructing the scoring task" if taskConstructionError?
message.changeMessageVisibilityTimeout scoringTaskTimeoutInSeconds
constructTaskLogObject userID,message.getReceiptHandle(), (taskLogError, taskLogObject) ->
return errors.serverError res, "There was an error creating the task log object." if taskLogError?
setTaskObjectTaskLogID taskObject, taskLogObject._id
sendResponseObject req, res, taskObject
getUserIDFromRequest = (req) -> if req.user? then return req.user._id else return null
isUserAnonymous = (req) -> if req.user? then return req.user.anonymous else return true
parseTaskQueueMessage = (req, res, message) ->
try
if typeof message.getBody() is "object" then return message.getBody()
return messageBody = JSON.parse message.getBody()
catch e
sendResponseObject req, res, {"error":"There was an error parsing the task.Error: #{e}" }
return null
constructTaskObject = (taskMessageBody, callback) ->
async.map taskMessageBody.sessions, getSessionInformation, (err, sessions) ->
return callback err, data if err?
taskObject =
"messageGenerated": Date.now()
"sessions": []
for session in sessions
sessionInformation =
"sessionID": session.sessionID
"sessionChangedTime": session.changed
"team": session.team ? "No team"
"code": session.code
"teamSpells": session.teamSpells ? {}
"levelID": session.levelID
taskObject.sessions.push sessionInformation
callback err, taskObject
getSessionInformation = (sessionIDString, callback) ->
LevelSession.findOne {"_id": sessionIDString }, (err, session) ->
return callback err, {"error":"There was an error retrieving the session."} if err?
session = session.toObject()
sessionInformation =
"sessionID": session._id
"code": _.cloneDeep session.code
"changed": session.changed
"creator": session.creator
"team": session.team
"teamSpells": session.teamSpells
"levelID": session.levelID
callback err, sessionInformation
constructTaskLogObject = (calculatorUserID, messageIdentifierString, callback) ->
taskLogObject = new TaskLog
"createdAt": new Date()
"calculator":calculatorUserID
"sentDate": Date.now()
"messageIdentifierString":messageIdentifierString
taskLogObject.save callback
setTaskObjectTaskLogID = (taskObject, taskLogObjectID) -> taskObject.taskID = taskLogObjectID
sendResponseObject = (req,res,object) ->
res.setHeader('Content-Type', 'application/json')
res.send(object)
res.end()
module.exports.processTaskResult = (req, res) ->
clientResponseObject = verifyClientResponse req.body, res
if clientResponseObject?
TaskLog.findOne {"_id": clientResponseObject.taskID}, (err, taskLog) ->
return errors.serverError res, "There was an error retrieiving the task log object" if err?
taskLogJSON = taskLog.toObject()
return errors.badInput res, "That computational task has already been performed" if taskLogJSON.calculationTimeMS
return handleTimedOutTask req, res, clientResponseObject if hasTaskTimedOut taskLogJSON.sentDate
logTaskComputation clientResponseObject, taskLog, (loggingError) ->
if loggingError?
return errors.serverError res, "There as a problem logging the task computation: #{loggingError}"
updateScores clientResponseObject, (updatingScoresError, newScores) ->
if updatingScoresError?
return errors.serverError res, "There was an error updating the scores.#{updatingScoresError}"
sendResponseObject req, res, {"message":"The scores were updated successfully!"}
hasTaskTimedOut = (taskSentTimestamp) -> taskSentTimestamp + scoringTaskTimeoutInSeconds * 1000 < Date.now()
handleTimedOutTask = (req, res, taskBody) -> errors.clientTimeout res, "The results weren't provided within the timeout"
verifyClientResponse = (responseObject, res) ->
unless typeof responseObject is "object"
errors.badInput res, "The response to that query is required to be a JSON object."
null
else
responseObject
logTaskComputation = (taskObject,taskLogObject, callback) ->
taskLogObject.calculationTimeMS = taskObject.calculationTimeMS
taskLogObject.sessions = taskObject.sessions
taskLogObject.save callback
updateScores = (taskObject,callback) ->
sessionIDs = _.pluck taskObject.sessions, 'sessionID'
async.map sessionIDs, retrieveOldScoreMetrics, (err, oldScores) ->
callback err, {"error": "There was an error retrieving the old scores"} if err?
oldScoreArray = _.toArray putRankingFromMetricsIntoScoreObject taskObject, oldScores
newScoreArray = bayes.updatePlayerSkills oldScoreArray
saveNewScoresToDatabase newScoreArray, callback
saveNewScoresToDatabase = (newScoreArray, callback) ->
async.eachSeries newScoreArray, updateScoreInSession, (err) ->
if err? then callback err, null else callback err, {"message":"All scores were saved successfully."}
updateScoreInSession = (scoreObject,callback) ->
sessionObjectQuery =
"_id": scoreObject.id
LevelSession.findOne sessionObjectQuery, (err, session) ->
return callback err, null if err?
session.meanStrength = scoreObject.meanStrength
session.standardDeviation = scoreObject.standardDeviation
session.totalScore = scoreObject.meanStrength - 1.8 * scoreObject.standardDeviation
log.info "Saving session #{session._id}!"
session.save callback
putRankingFromMetricsIntoScoreObject = (taskObject,scoreObject) ->
scoreObject = _.indexBy scoreObject, 'id'
for session in taskObject.sessions
scoreObject[session.sessionID].gameRanking = session.metrics.rank
scoreObject
retrieveOldScoreMetrics = (sessionID, callback) ->
sessionQuery =
"_id":sessionID
LevelSession.findOne sessionQuery, (err, session) ->
return callback err, {"error":"There was an error retrieving the session."} if err?
defaultScore = (25 - 1.8*(25/3))
defaultStandardDeviation = 25/3
oldScoreObject =
"standardDeviation":session.standardDeviation ? defaultStandardDeviation
"meanStrength":session.meanStrength ? 25
"totalScore":session.totalScore ? defaultScore
"id": sessionID
callback err, oldScoreObject
###Sample Messages
sampleQueueMessage =
{
"sessions": ["52dea9b77e486eeb97000001","52d981a73cf02dcf260003cb"]
}
sampleUndoneTaskObject =
"taskID": "507f191e810c19729de860ea"
"sessions" : [
{
"ID":"52dfeb17c8b5f435c7000025"
"sessionChangedTime": "2014-01-22T16:28:12.450Z"
"team":"humans"
"code": "code goes here"
},
{
"ID":"51eb2714fa058cb20d00fedg"
"sessionChangedTime": "2014-01-22T16:28:12.450Z"
"team":"ogres"
"code": "code goes here"
}
]
sampleResponseObject =
"taskID": "507f191e810c19729de860ea"
"calculationTime":3201
"sessions": [
{
"ID":"52dfeb17c8b5f435c7000025"
"sessionChangedTime": "2014-01-22T16:28:12.450Z"
"metrics": {
"rank":2
}
},
{
"ID":"51eb2714fa058cb20d00fedg"
"sessionChangedTime": "2014-01-22T16:28:12.450Z"
"metrics": {
"rank":1
}
}
]
sampleTaskLogObject=
{
"_id":ObjectId("507f191e810c19729de860ea") #datestamp is built into objectId
"calculatedBy":ObjectId("51eb2714fa058cb20d0006ef")
"calculationTime":3201
timedOut: false
"sessions":[
{
"ID":ObjectId("52dfeb17c8b5f435c7000025")
"metrics": {
"rank":2
}
},
{
"ID":ObjectId("51eb2714fa058cb20d00feda")
"metrics": {
"rank":1
}
}
]
}
###

View file

View file

@ -0,0 +1,12 @@
mongoose = require('mongoose')
ScoringTaskSchema = new mongoose.Schema(
createdAt: {type: Date, expires: 3600} #expire document 1 hour after they are created
calculator: {type:mongoose.Schema.Types.ObjectId}
sentDate: {type: Number}
messageIdentifierString: {type: String}
calculationTimeMS: {type: Number, default: 0}
sessions: {type: Array, default: []}
)
module.exports = mongoose.model('scoringTask', ScoringTaskSchema)

View file

@ -0,0 +1,50 @@
log = require 'winston'
errors = require '../commons/errors'
scoringQueue = require '../queues/scoring'
module.exports.setup = (app) ->
scoringQueue.setup()
app.all '/queue/*', (req, res) ->
setResponseHeaderToJSONContentType res
queueName = getQueueNameFromPath req.path
try
handler = loadQueueHandler queueName
if isHTTPMethodGet req
handler.dispatchTaskToConsumer req,res
else if isHTTPMethodPut req
handler.processTaskResult req,res
else if isHTTPMethodPost req
handler.createNewTask req, res #TODO: do not use this in production
else
sendMethodNotSupportedError req, res
catch error
log.error error
sendQueueError req, res, error
setResponseHeaderToJSONContentType = (res) -> res.setHeader('Content-Type', 'application/json')
getQueueNameFromPath = (path) ->
pathPrefix = '/queue/'
pathAfterPrefix = path[pathPrefix.length..]
partsOfURL = pathAfterPrefix.split '/'
queueName = partsOfURL[0]
queueName
loadQueueHandler = (queueName) -> require ('../queues/' + queueName)
isHTTPMethodGet = (req) -> return req.route.method is 'get'
isHTTPMethodPost = (req) -> return req.route.method is 'post'
isHTTPMethodPut = (req) -> return req.route.method is 'put'
sendMethodNotSupportedError = (req, res) -> errors.badMethod(res,"Queues do not support the HTTP method used." )
sendQueueError = (req,res, error) -> errors.serverError(res, "Route #{req.path} had a problem: #{error}")

View file

@ -1,6 +1,11 @@
config = require '../server_config'
sendwithusAPI = require 'sendwithus'
swuAPIKey = config.mail.sendwithusAPIKey
queues = require './commons/queue'
module.exports.setupRoutes = (app) ->
return
options = { DEBUG: not config.isProduction }
module.exports.api = new sendwithusAPI swuAPIKey, options

View file

@ -31,6 +31,14 @@ config.mail.mailchimpAPIKey = process.env.COCO_MAILCHIMP_API_KEY || '';
config.mail.mailchimpWebhook = process.env.COCO_MAILCHIMP_WEBHOOK || '/mail/webhook';
config.mail.sendwithusAPIKey = process.env.COCO_SENDWITHUS_API_KEY || '';
config.queue = {};
config.queue.accessKeyId = process.env.COCO_AWS_ACCESS_KEY_ID || '';
config.queue.secretAccessKey = process.env.COCO_AWS_SECRET_ACCESS_KEY || '';
config.queue.region = 'us-east-1';
config.queue.simulationQueueName = "simulationQueue";
config.mongoQueue = {};
config.mongoQueue.queueDatabaseName = "coco_queue";
config.salt = process.env.COCO_SALT || 'pepper';
config.cookie_secret = process.env.COCO_COOKIE_SECRET || 'chips ahoy';

View file

@ -80,6 +80,7 @@ setupFacebookCrossDomainCommunicationRoute = (app) ->
exports.setupRoutes = (app) ->
app.use app.router
baseRoute.setup app
setupFacebookCrossDomainCommunicationRoute app
setupFallbackRouteToIndex app