Merge remote-tracking branch 'codecombat/master'

This commit is contained in:
AkaKaras 2015-08-21 11:28:33 -04:00
commit f6e3800970
57 changed files with 1545 additions and 999 deletions

View file

@ -239,6 +239,21 @@ particleKinds['level-dungeon-replayable'] = particleKinds['level-dungeon-replaya
colorMiddle: hsl 0.17, 0.75, 0.5
colorEnd: hsl 0.17, 0.75, 0.3
particleKinds['level-dungeon-premium-item'] = ext particleKinds['level-dungeon-gate'],
emitter:
particleCount: 2000
radius: 2.5
acceleration: vec 0, 8, 1
opacityStart: 0
opacityMiddle: 0.5
opacityEnd: 0.75
colorStart: hsl 0.5, 0.75, 0.9
colorMiddle: hsl 0.5, 0.75, 0.7
colorEnd: hsl 0.5, 0.75, 0.3
colorStartSpread: vec 1, 1, 1
colorMiddleSpread: vec 1.5, 1.5, 1.5
colorEndSpread: vec 2.5, 2.5, 2.5
particleKinds['level-forest-premium-hero'] = ext particleKinds['level-forest-premium'],
emitter:
particleCount: 200

View file

@ -104,6 +104,7 @@ module.exports = class CocoRouter extends Backbone.Router
'multiplayer': go('MultiplayerView')
'play': go('play/CampaignView')
'play/ladder/:levelID/:leagueType/:leagueID': go('ladder/LadderView')
'play/ladder/:levelID': go('ladder/LadderView')
'play/ladder': go('ladder/MainLadderView')
'play/level/:levelID': go('play/level/PlayLevelView')

View file

@ -78,7 +78,11 @@ module.exports = class Label extends CocoClass
o.fontSize = {D: 25, S: 12, N: 24}[st]
fontFamily = {D: 'Arial', S: 'Arial', N: 'Arial'}[st]
o.fontDescriptor = "#{o.fontWeight} #{o.fontSize}px #{fontFamily}"
o.fontColor = {D: '#000', S: '#FFF', N: '#00a'}[st]
o.fontColor = {D: '#000', S: '#FFF', N: '#0a0'}[st]
if @style is 'name' and @sprite?.thang?.team is 'humans'
o.fontColor = '#a00'
else if @style is 'name' and @sprite?.thang?.team is 'ogres'
o.fontColor = '#00a'
o.backgroundFillColor = {D: 'white', S: 'rgba(0,0,0,0.4)', N: 'rgba(255,255,255,0.5)'}[st]
o.backgroundStrokeColor = {D: 'black', S: 'rgba(0,0,0,0.6)', N: 'rgba(0,0,0,0)'}[st]
o.backgroundStrokeStyle = {D: 2, S: 1, N: 1}[st]

View file

@ -476,8 +476,9 @@ module.exports = Lank = class Lank extends CocoClass
bar.scaleX = healthPct / @options.floatingLayer.resolutionFactor
if @thang.showsName
@setNameLabel(if @thang.health <= 0 then '' else @thang.id)
else if @options.playerName
@setNameLabel @options.playerName
# Let's try just using the DuelStatsView instead of this.
#else if @options.playerName
# @setNameLabel @options.playerName
configureMouse: ->
@sprite.cursor = 'pointer' if @thang?.isSelectable

View file

@ -234,43 +234,43 @@ module.exports = nativeDescription: "български език", englishDescri
control_bar_join_game: "Присъединяване"
reload: "Презареди"
reload_title: "Презареди целият код?"
# reload_really: "Are you sure you want to reload this level back to the beginning?"
reload_really: "Сигурен ли сте, че искате да презаредите нивото и да започнете отначало?"
reload_confirm: "Презареди всички"
victory: "Победа"
# victory_title_prefix: ""
# victory_title_suffix: " Complete"
victory_title_prefix: "Ниво "
victory_title_suffix: " завършено!"
victory_sign_up: "Регистрирай се за да запишеш напредъка си"
# victory_sign_up_poke: "Want to save your code? Create a free account!"
# victory_rate_the_level: "Rate the level: " # Only in old-style levels.
# victory_return_to_ladder: "Return to Ladder"
victory_sign_up_poke: "Регистрирайте се безплатно за да запазите прогреса си!"
victory_rate_the_level: "Оценете нивото: " # Only in old-style levels.
victory_return_to_ladder: "Обратно към Стълбата"
victory_play_continue: "Продължи"
victory_saving_progress: "Записване на напредъка"
# victory_go_home: "Go Home"
# victory_review: "Tell us more!"
# victory_review_placeholder: "How was the level?"
victory_go_home: "На Главната"
victory_review: "Разкажи ни повече!"
victory_review_placeholder: "Как беше нивото?"
victory_hour_of_code_done: "Готов ли си?"
victory_hour_of_code_done_yes: "Да аз съм готов с моят Hour of Code™!"
victory_experience_gained: "Спечелен опит"
victory_gems_gained: "Спечелени скъпоценни камъни"
# victory_new_item: "New Item"
# victory_viking_code_school: "Holy smokes, that was a hard level you just beat! If you aren't already a software developer, you should be. You just got fast-tracked for acceptance with Viking Code School, where you can take your skills to the next level and become a professional web developer in 14 weeks."
# victory_become_a_viking: "Become a Viking"
victory_new_item: "Нов Предмет"
victory_viking_code_school: "О да - това ниво беше наистина тежко! Ти или си програмист, или обезателно трябва да станеш такъв! Току що се доближи до приемането си във Викингското Училище по Програмиране, където ще научиш много нови неща и ще станеш професионален уеб програмист за 14 седмици."
victory_become_a_viking: "Стани Викинг"
guide_title: "Упътване"
# tome_minion_spells: "Your Minions' Spells" # Only in old-style levels.
# tome_read_only_spells: "Read-Only Spells" # Only in old-style levels.
# tome_other_units: "Other Units" # Only in old-style levels.
# tome_cast_button_run: "Run"
# tome_cast_button_running: "Running"
# tome_cast_button_ran: "Ran"
# tome_submit_button: "Submit"
# tome_reload_method: "Reload original code for this method" # Title text for individual method reload button.
# tome_select_method: "Select a Method"
# tome_see_all_methods: "See all methods you can edit" # Title text for method list selector (shown when there are multiple programmable methods).
# tome_select_a_thang: "Select Someone for "
# tome_available_spells: "Available Spells"
tome_minion_spells: "Заклинания на вашите Миньони' Spells" # Only in old-style levels.
tome_read_only_spells: "Read-Only Заклинания" # Only in old-style levels.
tome_other_units: "Други Модули" # Only in old-style levels.
tome_cast_button_run: "Стартиране"
tome_cast_button_running: "В Процес..."
tome_cast_button_ran: "Стартирано"
tome_submit_button: "Изпращане"
tome_reload_method: "Презареди оригиналния код за този метод" # Title text for individual method reload button.
tome_select_method: "Избери Метод"
tome_see_all_methods: "Виж всички методи, които можеш да редактираш" # Title text for method list selector (shown when there are multiple programmable methods).
tome_select_a_thang: "Избери някого за "
tome_available_spells: "Достъпни Заклинания"
tome_your_skills: "Твоите Умения"
tome_help: "Помощ"
# tome_current_method: "Current Method"
tome_current_method: "Текущ Метод"
hud_continue_short: "Продължи"
code_saved: "Кодът е записан"
skip_tutorial: "Пропусни (esc)"
@ -280,43 +280,43 @@ module.exports = nativeDescription: "български език", englishDescri
problem_alert_title: "Оправи си кода."
problem_alert_help: "Помощ"
time_current: "Текущо време:"
# time_total: "Max:"
# time_goto: "Go to:"
# non_user_code_problem_title: "Unable to Load Level"
# infinite_loop_title: "Infinite Loop Detected"
# infinite_loop_description: "The initial code to build the world never finished running. It's probably either really slow or has an infinite loop. Or there might be a bug. You can either try running this code again or reset the code to the default state. If that doesn't fix it, please let us know."
# check_dev_console: "You can also open the developer console to see what might be going wrong."
# check_dev_console_link: "(instructions)"
time_total: "Максимално:"
time_goto: "Иди на:"
non_user_code_problem_title: "Нивото не може да се зареди"
infinite_loop_title: "Открит е безкраен цикъл"
infinite_loop_description: "Кодът за сътворение на света никога не свършва. Или е много бавен, или има безкраен цикъл. Или може да има бъг. Можете да опитате да стартирате този код отново, или да нулирате кода до изходното му състояние. Ако нещата не се оправят, моля, съобщете ни."
check_dev_console: "Също така можете да отворите конзолата за разработчици, за да видите какво не е наред."
check_dev_console_link: "(инструкции)"
infinite_loop_try_again: "Пробвай отново"
infinite_loop_reset_level: "Ресетване на Ниво"
infinite_loop_reset_level: "Нулиране на Ниво"
infinite_loop_comment_out: "Коментирай моят Код"
# tip_toggle_play: "Toggle play/paused with Ctrl+P."
# tip_scrub_shortcut: "Use Ctrl+[ and Ctrl+] to rewind and fast-forward."
# tip_guide_exists: "Click the guide, inside game menu (at the top of the page), for useful info."
tip_toggle_play: "Превключвайте възпроизвеждане/пауза с Ctrl+P."
tip_scrub_shortcut: "Използвайте Ctrl+[ и Ctrl+] за бързо превъртане напред и назад."
tip_guide_exists: "Кликнете на ръководството в менюто(в горната част на страницата), за полезна информация."
tip_open_source: "CodeCombat e 100% проект с отворен код!"
# tip_tell_friends: "Enjoying CodeCombat? Tell your friends about us!"
tip_tell_friends: "Насладихте ли се на CodeCombat? Разкажете на приятелите си за нас!"
tip_beta_launch: "CodeCombat стартира своята beta през Октомври, 2013."
tip_think_solution: "Помисли върху решението,не проблема."
# tip_theory_practice: "In theory, there is no difference between theory and practice. But in practice, there is. - Yogi Berra"
# tip_error_free: "There are two ways to write error-free programs; only the third one works. - Alan Perlis"
# tip_debugging_program: "If debugging is the process of removing bugs, then programming must be the process of putting them in. - Edsger W. Dijkstra"
# tip_forums: "Head over to the forums and tell us what you think!"
# tip_baby_coders: "In the future, even babies will be Archmages."
# tip_morale_improves: "Loading will continue until morale improves."
# tip_all_species: "We believe in equal opportunities to learn programming for all species."
# tip_reticulating: "Reticulating spines."
# tip_harry: "Yer a Wizard, "
# tip_great_responsibility: "With great coding skill comes great debug responsibility."
# tip_munchkin: "If you don't eat your vegetables, a munchkin will come after you while you're asleep."
# tip_binary: "There are only 10 types of people in the world: those who understand binary, and those who don't."
# tip_commitment_yoda: "A programmer must have the deepest commitment, the most serious mind. ~ Yoda"
# tip_no_try: "Do. Or do not. There is no try. - Yoda"
# tip_patience: "Patience you must have, young Padawan. - Yoda"
# tip_documented_bug: "A documented bug is not a bug; it is a feature."
# tip_impossible: "It always seems impossible until it's done. - Nelson Mandela"
# tip_talk_is_cheap: "Talk is cheap. Show me the code. - Linus Torvalds"
# tip_first_language: "The most disastrous thing that you can ever learn is your first programming language. - Alan Kay"
# tip_hardware_problem: "Q: How many programmers does it take to change a light bulb? A: None, it's a hardware problem."
tip_think_solution: "Помисли върху решението, не проблема."
tip_theory_practice: "На теория няма разлика между теорията и практиката. Но на практика има. - Yogi Berra"
tip_error_free: "Има само два начина да напишеш безгрешна програма; само третия работи. - Alan Perlis"
tip_debugging_program: "Ако дебъгването е процес на премахване на бъгове, тогава програмирането трябва да е процес на поставянето им. - Edsger W. Dijkstra"
tip_forums: "Идете на форумите и кажете какво мислите!"
tip_baby_coders: "В бъдещето дори бебетата ще са Архимагове."
tip_morale_improves: "Зареждането ще продължи докато бойният дух не се възстанови."
tip_all_species: "Ние вярваме в равните възможности на всички видове да се научат да програмират."
tip_reticulating: "Да замрежим бодлите!"
tip_harry: "Ти си Магьосник, "
tip_great_responsibility: "С големите програмистки умения идват големите отговорности по дебъга."
tip_munchkin: "Ако не си изядеш зеленчуците, Торбалан ще дойде и ще те вземе, когато заспиш."
tip_binary: "Има само 10 типа хора по света - тези, които разбират двоичната система, и тези, които не я разбират."
tip_commitment_yoda: "Програмистът на прининципите верен трябва да е - и със ум сериозен. ~ Йода"
tip_no_try: "Прави. Или не прави. Недей опитва. - Йода"
tip_patience: "Търпение да имаш трябва, млади Падуане. - Йода"
tip_documented_bug: "Документирания бъг не е бъг - той е фичър."
tip_impossible: "Винаги изглежда невъзможно - докато не се направи. - Нелсън Мандела"
tip_talk_is_cheap: "Приказките са вятър и мъгла. Покажи ми кода. - Линус Торвалдс"
tip_first_language: "Най-пагубното нещо, което можеш да научиш е първият ти език за програмиране. - Alan Kay"
tip_hardware_problem: "Въпр.: Колко програмиста са нужни, за да сменят електрическа крушка? Отг.: Николко, това е хардуерен проблем."
# tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law."
# tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth"
# tip_brute_force: "When in doubt, use brute force. - Ken Thompson"

View file

@ -820,6 +820,7 @@
latest_achievement: "Latest Achievement"
playtime: "Playtime"
last_played: "Last played"
leagues_explanation: "Play in a league against other clan members in these multiplayer arena instances."
classes:
archmage_title: "Archmage"
@ -1009,6 +1010,7 @@
my_matches: "My Matches"
simulate: "Simulate"
simulation_explanation: "By simulating games you can get your game ranked faster!"
simulation_explanation_leagues: "You will mainly help simulate games for allied players in your clans and courses."
simulate_games: "Simulate Games!"
simulate_all: "RESET AND SIMULATE GAMES"
games_simulated_by: "Games simulated by you:"
@ -1059,6 +1061,7 @@
tournament_blurb_blog: "on our blog"
rules: "Rules"
winners: "Winners"
league: "League"
user:
stats: "Stats"

View file

@ -634,28 +634,28 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
sys_requirements_2: "CodeCombat no está soportado en iPad aún."
teachers_survey:
# title: "Teacher Survey"
# must_be_logged: "You must be logged in first. Please create an account or log in from the menu above."
# retrieving: "Retrieving information..."
# being_reviewed_1: "Your application for a free trial subscription is being"
# being_reviewed_2: "reviewed."
# approved_1: "Your application for a free trial subscription was"
title: "Encuesta para profesores"
must_be_logged: "Debe iniiciar sesión primero. Por favor cree una cuenta o inicie sesión desde el menú en la parte superior."
retrieving: "Obteniendo información..."
being_reviewed_1: "Su solicitud para una prueba gratuita de subscripción está siendo"
being_reviewed_2: "revisada."
approved_1: "Su solicitud para una prueba gratuita de subscripción fue" #since about 1993 fué can use no tilde
approved_2: "Aprobada."
# approved_3: "Further instructions have been sent to"
# denied_1: "Your application for a free trial subscription has been"
denied_2: "denegadae."
contact_1: "Porfavor contactarse"
# contact_2: "if you have further questions."
approved_3: "Instruccciones posteriores han sido enviadas a"
denied_1: "Su solicitud para una prueba gratuita de subscripción fue"
denied_2: "denegada."
contact_1: "Por favor contáctenos"
contact_2: "si tiene más preguntas."
# description_1: "We offer free subscriptions to teachers for evaluation purposes. You can find more information on our"
# description_2: "teachers"
# description_3: "page."
description_2: "página"
description_3: "de maestros."
# description_4: "Please fill out this quick survey and well email you setup instructions."
email: "Dirección de email"
school: "Nombre del colegio"
location: "Nombre de la ciudad"
age_students: "¿Qué edad tienen tus estudiantes?"
# under: "Under"
# other: "Other:"
under: "Menor" #under like underage = menor as menor de edad
other: "Otro:"
amount_students: "¿A cuantos alumnos les enseñas?"
hear_about: "¿Donde escuchaste sobre CodeCombat?"
fill_fields: "Porfavor llenar todos los campos."
@ -707,7 +707,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
new_password: "Nueva Contraseña"
new_password_verify: "Verificar"
type_in_email: "Ingrese su correo electrónico para confirmar la eliminación" # {change}
# type_in_password: "Also, type in your password."
type_in_password: "También, escribe tu password."
email_subscriptions: "Suscripciones de Email"
email_subscriptions_none: "No tienes suscripciones."
email_announcements: "Noticias"
@ -782,7 +782,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
make_private: "Hacer clan privado"
subs_only: "solo suscriptores"
create_clan: "Crear nuevo clan"
# private_preview: "Preview"
private_preview: "Previsualizar"
public_clans: "Clanes publicos"
my_clans: "Mis Clanes"
clan_name: "Nombre del clan"
@ -794,7 +794,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
edit_name: "editar nombre"
edit_description: "editar descripción"
private: "(privado)"
# summary: "Summary"
summary: "Resumen"
average_level: "Nivel Promedio"
average_achievements: "Logros Promedio"
delete_clan: "Borrar Clan"
@ -853,8 +853,8 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
indoor: "Interior"
desert: "Desierto"
grassy: "Herboso"
# mountain: "Mountain"
# glacier: "Glacier"
mountain: "Mountaña"
glacier: "Glaciar"
small: "Pequeño"
large: "Grande"
fork_title: "Fork de Nueva Versión"

View file

@ -492,7 +492,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
blocks: "Bloqueo" # As in "this shield blocks this much damage"
# backstab: "Backstab" # As in "this dagger does this much backstab damage"
skills: "Habilidades"
# attack_1: "Deals"
# attack_1: "Deals" #like an offer?
# attack_2: "of listed"
# attack_3: "weapon damage."
# health_1: "Gains"
@ -579,7 +579,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
# josh_blurb: "Floor Is Lava"
jose_title: "Música"
# jose_blurb: "Taking Off"
# retrostyle_title: "Illustration"
retrostyle_title: "Illustración"
# retrostyle_blurb: "RetroStyle Games"
teachers:
@ -774,9 +774,9 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
social_hipchat: "Habla con nosotros en el chat publico de CodeCombat HipChat room"
contribute_to_the_project: "Contribuye al proyecto"
# clans:
# clan: "Clan"
# clans: "Clans"
clans:
clan: "Clan"
clans: "Clanes"
# new_name: "New clan name"
# new_description: "New clan description"
# make_private: "Make clan private"
@ -809,8 +809,8 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
# complete_1: "complete"
# exp_levels: "Expand levels"
# rem_hero: "Remove Hero"
# status: "Status"
# complete_2: "Complete"
status: "Estado"
complete_2: "Completo"
# started_2: "Started"
# not_started_2: "Not Started"
# view_solution: "Click to view solution."
@ -850,11 +850,11 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
revert_models: "Revertir Modelos"
pick_a_terrain: "Escoge un Terreno"
# dungeon: "Dungeon"
# indoor: "Indoor"
indoor: "Interior"
desert: "Desierto"
grassy: "Cubierto de hierba"
# mountain: "Mountain"
# glacier: "Glacier"
mountain: "Mountaña"
glacier: "Glaciar"
small: "Pequeño"
# large: "Large"
fork_title: "Bifurcar nueva versión"
@ -878,7 +878,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
level_tab_thangs_conditions: "Condiciones de inicio"
level_tab_thangs_add: "Añadir Objetos"
# level_tab_thangs_search: "Search thangs"
# add_components: "Add Components"
add_components: "Agregar Componentes"
# component_configs: "Component Configurations"
# config_thang: "Double click to configure a thang"
delete: "Borrar"
@ -1046,7 +1046,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
fight: "¡Pelea!"
watch_victory: "Ver tu victoria"
defeat_the: "Vence a"
# tournament_started: ", started"
tournament_started: ", iniciado"
tournament_ends: "El torneo termina"
tournament_ended: "El torneo ha terminado"
tournament_rules: "Reglas del Torneo"
@ -1095,7 +1095,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
recently_played: "Jugado Recientemente"
no_recent_games: "No he jugado juegos en las ultimas dos semanas."
payments: "Pagos"
# purchased: "Purchased"
purchased: "Adquirido"
# subscription: "Subscription"
# invoices: "Invoices"
service_apple: "Apple"
@ -1105,9 +1105,9 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
price: "Precio"
gems: "Joyas"
active: "Activo"
# subscribed: "Subscribed"
subscribed: "Suscrito"
# unsubscribed: "Unsubscribed"
# active_until: "Active Until"
active_until: "Activo Hasta"
cost: "Costo"
next_payment: "Siguiente Pago"
card: "Tarjeta"
@ -1177,7 +1177,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
user_remarks: "Observaciones de Usuario"
versions: "Versiones"
items: "Objetos"
# hero: "Hero"
hero: "Héroe"
heroes: "Heroes"
achievement: "Logro"
clas: "Clasess"
@ -1188,10 +1188,10 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
poll: "Encuesta"
# user_polls_record: "Poll Voting History"
# concepts:
concepts:
# advanced_strings: "Advanced Strings"
# algorithms: "Algorithms"
# arguments: "Arguments"
arguments: "Argumentos"
# arithmetic: "Arithmetic"
# arrays: "Arrays"
# basic_syntax: "Basic Syntax"
@ -1199,16 +1199,16 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
# break_statements: "Break Statements"
# classes: "Classes"
# for_loops: "For Loops"
# functions: "Functions"
functions: "Funciones"
# if_statements: "If Statements"
# input_handling: "Input Handling"
# math_operations: "Math Operations"
# object_literals: "Object Literals"
# strings: "Strings"
# variables: "Variables"
# vectors: "Vectors"
# while_loops: "Loops"
# recursion: "Recursion"
strings: "Cadenas"
variables: "Variables"
vectors: "Vectores"
while_loops: "Ciclos"
recursion: "Recursividad"
delta:
added: "Añadido"

View file

@ -247,7 +247,7 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
victory_saving_progress: "Salvataggio progressi"
victory_go_home: "Torna alla pagina iniziale"
victory_review: "Dicci di più!"
# victory_review_placeholder: "How was the level?"
victory_review_placeholder: "Come è stato il livello?"
victory_hour_of_code_done: "Finito?"
victory_hour_of_code_done_yes: "Si, ho finito la mia ora di programmazione!"
victory_experience_gained: "Punti XP guadagnati"
@ -322,7 +322,7 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
tip_brute_force: "Quando sei in dubbio, usa la forza bruta. - Ken Thompson"
tip_extrapolation: "Ci sono soltanto due tipi di persone: quelli che possono estrapolare da dati incompleti..."
tip_superpower: "La programmazione è la cosa che più si avvicina a un superpotere."
# tip_control_destiny: "In real open source, you have the right to control your own destiny. - Linus Torvalds"
tip_control_destiny: "Nel vero open source, hai il diritto di controllare il propio destino. - Linus Torvalds"
tip_no_code: "Nessun codice è più veloce di nessun codice."
tip_code_never_lies: "Il codice non mente mai, ma i commenti a volte lo fanno. — Ron Jeffries"
# tip_reusable_software: "Before software can be reusable it first has to be usable."
@ -412,14 +412,14 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
# unsubscribe: "Unsubscribe"
# confirm_unsubscribe: "Confirm Unsubscribe"
# never_mind: "Never Mind, I Still Love You"
# thank_you_months_prefix: "Thank you for supporting us these last"
# thank_you_months_suffix: "months."
# thank_you: "Thank you for supporting CodeCombat."
# thank_you_months_prefix: "Grazie for supporting us these last"
thank_you_months_suffix: "mesi."
# thank_you: "Grazie for supporting CodeCombat."
# sorry_to_see_you_go: "Sorry to see you go! Please let us know what we could have done better."
# unsubscribe_feedback_placeholder: "O, what have we done?"
# parent_button: "Ask your parent"
parent_button: "Chiedi i tuoi genitori"
# parent_email_description: "We'll email them so they can buy you a CodeCombat subscription."
# parent_email_input_invalid: "Email address invalid."
# parent_email_input_invalid: "Indirizzo Email invalido."
# parent_email_input_label: "Parent email address"
# parent_email_input_placeholder: "Enter parent email"
parent_email_send: "Invia email"
@ -428,17 +428,17 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
parents: "Per i genitori"
# parents_title: "Dear Parent: Your child is learning to code. Will you help them continue?"
# parents_blurb1: "Your child has played __nLevels__ levels and learned programming basics. Help cultivate their interest and buy them a subscription so they can keep playing."
# parents_blurb1a: "Computer programming is an essential skill that your child will undoubtedly use as an adult. By 2020, basic software skills will be needed by 77% of jobs, and software engineers are in high demand across the world. Did you know that Computer Science is the highest-paid university degree?"
# parents_blurb1a: "Computer programming è una capacità essenziale che your child will undoubtedly use as an adult. By 2020, basic software skills will be needed by 77% of jobs, and software engineers are in high demand across the world. Did you know that Computer Science is the highest-paid university degree?"
# parents_blurb2: "For $9.99 USD/mo, your child will get new challenges every week and personal email support from professional programmers."
# parents_blurb3: "No Risk: 100% money back guarantee, easy 1-click unsubscribe."
# payment_methods: "Payment Methods"
# payment_methods_title: "Accepted Payment Methods"
# parents_blurb3: "Senza Rischio: 100% money back guarantee, easy 1-click unsubscribe."
payment_methods: "Metodi di Pagamento"
payment_methods_title: "Metodi di Pagamento Accetati"
# payment_methods_blurb1: "We currently accept credit cards and Alipay."
# payment_methods_blurb2: "If you require an alternate form of payment, please contact"
# stripe_description: "Monthly Subscription"
stripe_description: "Sottoscrizione mensile"
# subscription_required_to_play: "You'll need a subscription to play this level."
# unlock_help_videos: "Subscribe to unlock all video tutorials."
# personal_sub: "Personal Subscription" # Accounts Subscription View below
# unlock_help_videos: "Sottoscribe to unlock all video tutorials."
personal_sub: "Sottoscrizione Personale" # Accounts Subscription View below
# loading_info: "Loading subscription information..."
# managed_by: "Managed by"
# will_be_cancelled: "Will be cancelled on"
@ -453,13 +453,13 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
# group_discounts_1st: "1st subscription"
# group_discounts_full: "Full price"
# group_discounts_2nd: "Subscriptions 2-11"
# group_discounts_20: "20% off"
group_discounts_20: "20% disconto"
# group_discounts_12th: "Subscriptions 12+"
# group_discounts_40: "40% off"
group_discounts_40: "40% di sconto"
# subscribing: "Subscribing..."
# recipient_emails_placeholder: "Enter email address to subscribe, one per line."
# subscribe_users: "Subscribe Users"
# users_subscribed: "Users subscribed:"
subscribe_users: "Sottoscrivere Utenti"
users_subscribed: "Utenti sottoscritti:"
# no_users_subscribed: "No users subscribed, please double check your email addresses."
# current_recipients: "Current Recipients"
# unsubscribing: "Unsubscribing..."
@ -582,8 +582,8 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
retrostyle_title: "Illustratore"
retrostyle_blurb: "Giochi retrò"
# teachers:
# title: "CodeCombat: Info for Teachers"
teachers:
title: "CodeCombat: Informazzione per Professori"
# intro_1: "CodeCombat is an online game that teaches programming. Students write code in real programming languages."
# intro_2: "No experience required!"
# free_title: "How much does it cost?"
@ -597,7 +597,7 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
# sub_includes_title: "What is included in the subscription?"
# sub_includes_1: "In addition to the 100+ basic levels, students with a monthly subscription get access to these additional features:"
# sub_includes_2: "70+ practice levels"
# sub_includes_3: "Video tutorials"
sub_includes_3: "Video tutoriali"
# sub_includes_4: "Premium email support"
# sub_includes_5: "10 new heroes with unique skills to master"
# sub_includes_6: "3500 bonus gems every month"
@ -610,7 +610,7 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
# monitor_progress_5: "After they join, you will see a summary of the student's progress on your Clan's page."
# private_clans_1: "Private Clans provide increased privacy and detailed progress information for each student."
# private_clans_2: "To create a private Clan, check the 'Make clan private' checkbox when creating a"
# private_clans_3: "."
private_clans_3: "."
# who_for_title: "Who is CodeCombat for?"
# who_for_1: "We recommend CodeCombat for students aged 9 and up. No prior programming experience is needed."
# who_for_2: "We've designed CodeCombat to appeal to both boys and girls."
@ -626,14 +626,14 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
# how_much_5: "We accept discounted one-time purchases and yearly subscription purchases for groups, such as a class or school. Please contact"
# how_much_6: "for more details."
# more_info_title: "Where can I find more information?"
# more_info_1: "Our"
more_info_1: "Il nostro"
# more_info_2: "teachers forum"
# more_info_3: "is a good place to connect with fellow educators who are using CodeCombat."
# more_info_3: "è un buon posto to connect with fellow educators who are using CodeCombat."
# sys_requirements_title: "System Requirements"
# sys_requirements_1: "A modern web browser. Newer versions of Chrome, Firefox, or Safari. Internet Explorer 9 or later."
# sys_requirements_2: "CodeCombat is not supported on iPad yet."
# teachers_survey:
teachers_survey:
# title: "Teacher Survey"
# must_be_logged: "You must be logged in first. Please create an account or log in from the menu above."
# retrieving: "Retrieving information..."
@ -647,15 +647,15 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
# contact_1: "Please contact"
# contact_2: "if you have further questions."
# description_1: "We offer free subscriptions to teachers for evaluation purposes. You can find more information on our"
# description_2: "teachers"
description_2: "professori"
# description_3: "page."
# description_4: "Please fill out this quick survey and well email you setup instructions."
# email: "Email Address"
# school: "Name of School"
# location: "Name of City"
school: "Nome di Scuola"
location: "Nome di Città"
# age_students: "How old are your students?"
# under: "Under"
# other: "Other:"
other: "Altri:"
# amount_students: "How many students do you teach?"
# hear_about: "How did you hear about CodeCombat?"
# fill_fields: "Please fill out all fields."
@ -806,7 +806,7 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
# progress: "Progress"
# not_started_1: "not started"
# started_1: "started"
# complete_1: "complete"
complete_1: "finito"
# exp_levels: "Expand levels"
# rem_hero: "Remove Hero"
# status: "Status"
@ -883,7 +883,7 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
config_thang: "Doppio click per configurare un thang"
delete: "Cancella"
duplicate: "Duplica"
# stop_duplicate: "Stop Duplicate"
stop_duplicate: "Ferma Duplicazzione"
rotate: "Ruota"
level_settings_title: "Impostazioni"
level_component_tab_title: "Componenti esistenti"
@ -894,7 +894,7 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
level_components_title: "Torna all'elenco thangs"
level_components_type: "Tipo"
level_component_edit_title: "Modifica componente"
# level_component_config_schema: "Config Schema"
level_component_config_schema: "Schema di configurazione"
level_component_settings: "Impostazioni"
level_system_edit_title: "Modifica sistema"
create_system_title: "Crea nuovo sistema"
@ -1039,21 +1039,21 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
tutorial_play_first: "Prima di tutto gioca al Tutorial."
simple_ai: "IA semplice"
warmup: "Allenamento"
# friends_playing: "Friends Playing"
friends_playing: "Amici Giocando"
log_in_for_friends: "Accedi per giocare con i tuoi amici!"
social_connect_blurb: "Connettiti e gioca contro i tuoi amici!"
invite_friends_to_battle: "Invita i tuoi amici a unirsi a te nella battaglia!"
fight: "Combatti!"
# watch_victory: "Watch your victory"
# defeat_the: "Defeat the"
# tournament_started: ", started"
# tournament_ends: "Tournament ends"
tournament_started: ", ha cominciato"
tournament_ends: "Torneo conclude"
tournament_ended: "Torneo concluso"
tournament_rules: "Regole torneo"
# tournament_blurb: "Write code, collect gold, build armies, crush foes, win prizes, and upgrade your career in our $40,000 Greed tournament! Check out the details"
# tournament_blurb_criss_cross: "Win bids, construct paths, outwit opponents, grab gems, and upgrade your career in our Criss-Cross tournament! Check out the details"
# tournament_blurb_zero_sum: "Unleash your coding creativity in both gold gathering and battle tactics in this alpine mirror match between red sorcerer and blue sorcerer. The tournament began on Friday, March 27 and will run until Monday, April 6 at 5PM PDT. Compete for fun and glory! Check out the details"
# tournament_blurb_blog: "on our blog"
tournament_blurb_blog: "nel nostro blog"
rules: "Regole"
winners: "Vincitori"
@ -1148,7 +1148,7 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
gplus_friends: "Amici G+"
gplus_friend_sessions: "Sessioni amici G+"
leaderboard: "Classifica"
# user_schema: "User Schema"
user_schema: "Schema Utenti"
user_profile: "Profilo utente"
# patch: "Patch"
# patches: "Patches"
@ -1188,9 +1188,9 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
poll: "Sondaggio"
# user_polls_record: "Poll Voting History"
# concepts:
concepts:
# advanced_strings: "Advanced Strings"
# algorithms: "Algorithms"
algorithms: "Algoritmi"
# arguments: "Arguments"
# arithmetic: "Arithmetic"
# arrays: "Arrays"
@ -1318,14 +1318,14 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
# not_featured: "Not Featured"
# looking_for: "Looking for:"
last_updated: "Ultimo aggiornamento:"
# contact: "Contact"
contact: "Contatto"
# active: "Looking for interview offers now"
# inactive: "Not looking for offers right now"
# complete: "complete"
# next: "Next"
# next_city: "city?"
next_city: "città?"
# next_country: "pick your country."
# next_name: "name?"
next_name: "nome?"
# next_short_description: "write a short description."
# next_long_description: "describe your desired position."
# next_skills: "list at least five skills."
@ -1335,7 +1335,7 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
# next_links: "add any personal or social links."
# next_photo: "add an optional professional photo."
# next_active: "mark yourself open to offers to show up in searches."
# example_blog: "Blog"
example_blog: "Blog"
# example_personal_site: "Personal Site"
# links_header: "Personal Links"
# links_blurb: "Link any other sites or profiles you want to highlight, like your GitHub, your LinkedIn, or your blog."
@ -1347,9 +1347,9 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
# basics_active_help: "Want interview offers right now?"
# basics_job_title: "Desired Job Title"
# basics_job_title_help: "What role are you looking for?"
# basics_city: "City"
basics_city: "Città"
# basics_city_help: "City you want to work in (or live in now)."
# basics_country: "Country"
basics_country: "Paese"
# basics_country_help: "Country you want to work in (or live in now)."
# basics_visa: "US Work Status"
# basics_visa_help: "Are you authorized to work in the US, or do you need visa sponsorship? (If you live in Canada or Australia, mark authorized.)"
@ -1382,14 +1382,14 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
# work_employer_help: "Name of your employer."
# work_role: "Job Title"
# work_role_help: "What was your job title or role?"
# work_duration: "Duration"
work_duration: "Durazione"
# work_duration_help: "When did you hold this gig?"
# work_description: "Description"
# work_description_help: "What did you do there? (140 chars; optional)"
# education: "Education"
# education_header: "Recount your academic ordeals"
# education_blurb: "List your academic ordeals."
# education_school: "School"
education_school: "Scuola"
# education_school_help: "Name of your school."
# education_degree: "Degree"
# education_degree_help: "What was your degree and field of study?"
@ -1409,11 +1409,11 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
# project_description_help: "Briefly describe the project."
# project_picture: "Picture"
# project_picture_help: "Upload a 230x115px or larger image showing off the project."
# project_link: "Link"
project_link: "Link"
# project_link_help: "Link to the project."
# player_code: "Player Code"
# employers:
employers:
# deprecation_warning_title: "Sorry, CodeCombat is not recruiting right now."
# deprecation_warning: "We are focusing on beginner levels instead of finding expert developers for the time being."
# hire_developers_not_credentials: "Hire developers, not credentials." # We are not actively recruiting right now, so there's no need to add new translations for the rest of this section.
@ -1447,16 +1447,16 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
# what_blurb: "CodeCombat is a multiplayer browser programming game. Players write code to control their forces in battle against other developers. Our players have experience with all major tech stacks."
# cost: "How much do we charge?"
# cost_blurb: "We charge 15% of first year's salary and offer a 100% money back guarantee for 90 days. We don't charge for candidates who are already actively being interviewed at your company."
# candidate_name: "Name"
candidate_name: "Nome"
# candidate_location: "Location"
# candidate_looking_for: "Looking For"
# candidate_role: "Role"
# candidate_top_skills: "Top Skills"
# candidate_years_experience: "Yrs Exp"
# candidate_last_updated: "Last Updated"
# candidate_who: "Who"
candidate_who: "Chi"
# featured_developers: "Featured Developers"
# other_developers: "Other Developers"
other_developers: "Altri Sviluppatori"
# inactive_developers: "Inactive Developers"
admin:

View file

@ -247,7 +247,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
victory_saving_progress: "保存进度"
victory_go_home: "返回主页"
victory_review: "给我们反馈!"
# victory_review_placeholder: "How was the level?"
victory_review_placeholder: "关卡如何?"
victory_hour_of_code_done: "你完成了吗?"
victory_hour_of_code_done_yes: "是的, 完成了!"
victory_experience_gained: "获得经验"
@ -294,7 +294,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
tip_scrub_shortcut: "用 Ctrl+[ 和 Ctrl+] 来倒退和快进。" # {change}
tip_guide_exists: "点击页面上方的指南, 可以获得更多有用信息。"
tip_open_source: "「CodeCombat」是100%开源的!"
# tip_tell_friends: "Enjoying CodeCombat? Tell your friends about us!"
tip_tell_friends: "喜欢Codecombat那就赶快把它安利给朋友"
tip_beta_launch: "CodeCombat开始于2013的10月份。"
tip_think_solution: "思考如何解决, 而不是思考问题。"
tip_theory_practice: "在理论上,理论和实践之间是没有区别的。但在实践上,它们是有区别的。 - Yogi Berra"
@ -336,7 +336,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
tip_hate_computers: "那些认为他们讨厌电脑的人,其实他们讨厌的是垃圾程序编写员。- Larry Niven"
tip_open_source_contribute: "你可以帮助「CodeCombat」提高"
tip_recurse: "迭代为人,递归为神 - L. Peter Deutsch"
tip_free_your_mind: "丢掉一切私心杂念,丢掉害怕、疑问和拒信,解放你的思想。 - Morpheus"
tip_free_your_mind: "丢掉一切私心杂念,丢掉害怕、疑问和拒信,解放你的思想。 - Morpheus黑客帝国"
tip_strong_opponents: "即使是最强大的对手也是有弱点的. - Itachi Uchiha"
# tip_paper_and_pen: "Before you start coding, you can always plan with a sheet of paper and a pen."

View file

@ -1,4 +1,4 @@
module.exports = nativeDescription: "中文", englishDescription: "Chinese (Traditional)", translation:
module.exports = nativeDescription: "中文", englishDescription: "Chinese (Traditional)", translation:
home:
slogan: "玩遊戲學程式"
no_ie: "抱歉Internet Explorer 8 等舊的瀏覽器打不開此網站" # Warning that only shows up in IE8 and older
@ -42,7 +42,7 @@ module.exports = nativeDescription: "繁体中文", englishDescription: "Chinese
diplomat_suggestion:
title: "幫我們翻譯CodeCombat" # This shows up when a player switches to a non-English language using the language selector.
sub_heading: "我們需要您的語言技能"
pitch_body: "我們開發了CodeCombat的英文版但是現在我們的玩家遍佈全球。很多人想玩中文版的卻不會說英文所以如果您中英文都會請考慮一下參加我們的翻譯工作幫忙把 CodeCombat 網站還有所有的關卡翻譯成中文(繁)。"
pitch_body: "我們開發了CodeCombat的英文版但是現在我們的玩家遍佈全球。很多人想玩中文版的卻不會說英文所以如果您中英文都會請考慮一下參加我們的翻譯工作幫忙把 CodeCombat 網站還有所有的關卡翻譯成中文(繁)。"
missing_translations: "直至所有正體中文的翻譯完畢,當無法提供正體中文時還會以英文顯示。"
learn_more: "關於成為外交官"
subscribe_as_diplomat: "註冊成為外交官"
@ -79,8 +79,8 @@ module.exports = nativeDescription: "繁体中文", englishDescription: "Chinese
adjust_volume: "調整音量"
campaign_multiplayer: "多人競技場"
campaign_multiplayer_description: "...在這裡您可以和其他玩家進行對戰。"
# campaign_old_multiplayer: "(Deprecated) Old Multiplayer Arenas"
# campaign_old_multiplayer_description: "Relics of a more civilized age. No simulations are run for these older, hero-less multiplayer arenas."
campaign_old_multiplayer: "(過時的)舊多人競技場"
campaign_old_multiplayer_description: "多個文明時代的遺跡。已沒有模擬運行這些陳舊、蕪絕英雄多人競技場。"
share_progress_modal:
blurb: "您正在建立優秀的進度! 告訴別人您已經從CodeCombat學習到多少東西." # {change}
@ -147,7 +147,7 @@ module.exports = nativeDescription: "繁体中文", englishDescription: "Chinese
unwatch: "取消關注"
submit_patch: "送出修補"
submit_changes: "送出修改"
# save_changes: "Save Changes"
save_changes: "保存更改"
general:
and: ""
@ -247,14 +247,14 @@ module.exports = nativeDescription: "繁体中文", englishDescription: "Chinese
victory_saving_progress: "儲存進度"
victory_go_home: "返回首頁"
victory_review: "給我們回饋!"
# victory_review_placeholder: "How was the level?"
victory_review_placeholder: "關卡如何?"
victory_hour_of_code_done: "您完成了嗎?"
victory_hour_of_code_done_yes: "是的,我完成了我的程式碼!"
victory_experience_gained: "取得經驗值"
victory_gems_gained: "取得寶石"
# victory_new_item: "New Item"
# victory_viking_code_school: "Holy smokes, that was a hard level you just beat! If you aren't already a software developer, you should be. You just got fast-tracked for acceptance with Viking Code School, where you can take your skills to the next level and become a professional web developer in 14 weeks."
# victory_become_a_viking: "Become a Viking"
victory_new_item: "新的物品"
victory_viking_code_school: "太厲害了, 你剛完成了非常困難的關卡! 如果你想成為一個軟件開發人員你就應該去試一下Viking Code School。在這裡你可以把你的知識增長到另一個台階。只需要14個星期你就能成為一個專業的網頁開發人員。"
victory_become_a_viking: "成為一個維京人。"
guide_title: "指南"
tome_minion_spells: "助手的咒語" # Only in old-style levels.
tome_read_only_spells: "唯讀的咒語" # Only in old-style levels.
@ -282,11 +282,11 @@ module.exports = nativeDescription: "繁体中文", englishDescription: "Chinese
time_current: "現在:"
time_total: "最大值:"
time_goto: "前往:"
# non_user_code_problem_title: "Unable to Load Level"
# infinite_loop_title: "Infinite Loop Detected"
# infinite_loop_description: "The initial code to build the world never finished running. It's probably either really slow or has an infinite loop. Or there might be a bug. You can either try running this code again or reset the code to the default state. If that doesn't fix it, please let us know."
# check_dev_console: "You can also open the developer console to see what might be going wrong."
# check_dev_console_link: "(instructions)"
non_user_code_problem_title: "無法加載關卡"
infinite_loop_title: "檢測到無限循環"
infinite_loop_description: "建立世界的初始代碼還沒有運行完畢。這可能是真的很慢或出現無限循環或者存在一個bug。你可以嘗試再次運行這段代碼或重置代碼為默認狀態。如果還是解決不了問題請聯繫我們。."
check_dev_console: "你也可以打開開發者界面看一下有什麼可能出錯了。"
check_dev_console_link: "(說明)"
infinite_loop_try_again: "再試一次"
infinite_loop_reset_level: "重置關卡"
infinite_loop_comment_out: "在我的程式碼中加入注解"
@ -294,7 +294,7 @@ module.exports = nativeDescription: "繁体中文", englishDescription: "Chinese
tip_scrub_shortcut: "Ctrl+[ 快退; Ctrl+] 快進." # {change}
tip_guide_exists: "點擊頁面上方的指南,可獲得更多有用的訊息."
tip_open_source: "「CodeCombat」100% 開源!"
# tip_tell_friends: "Enjoying CodeCombat? Tell your friends about us!"
tip_tell_friends: "喜歡Codecombat那就把它介紹給朋友!"
tip_beta_launch: "「CodeCombat」在2013年10月進入 BETA 測試。"
tip_think_solution: "思考解決方法而不是問題."
tip_theory_practice: "理論上, 理論和實作之間是沒有區別. 但是實作上, 這兩者是有區別的. - Yogi Berra"
@ -335,8 +335,8 @@ module.exports = nativeDescription: "繁体中文", englishDescription: "Chinese
tip_adding_evil: "增加一個邪惡之捏."
tip_hate_computers: "關於自我覺得恨透電腦的那群人. 其實他們真正應該恨的事情是糟糕的程序員. - Larry Niven"
tip_open_source_contribute: "你可以幫助「CodeCombat」提高"
# tip_recurse: "To iterate is human, to recurse divine. - L. Peter Deutsch"
# tip_free_your_mind: "You have to let it all go, Neo. Fear, doubt, and disbelief. Free your mind. - Morpheus"
tip_recurse: "迭代者人也,遞歸者神也 - L. Peter Deutsch"
tip_free_your_mind: "放下一切私心雜念,丟棄害怕、疑問和拒信,解放你的思維。 - 莫菲斯(駭客任務)"
# tip_strong_opponents: "Even the strongest of opponents always has a weakness. - Itachi Uchiha"
# tip_paper_and_pen: "Before you start coding, you can always plan with a sheet of paper and a pen."

View file

@ -294,6 +294,14 @@ _.extend LevelSessionSchema.properties,
simulator: {type: 'object', description: 'Holds info on who simulated the match, and with what tools.'}
randomSeed: {description: 'Stores the random seed that was used during this match.'}
leagues:
c.array {description: 'Multiplayer data for the league corresponding to Clans and CourseInstances the player is a part of.'},
c.object {},
leagueID: {type: 'string', description: 'The _id of a Clan or CourseInstance the user belongs to.'}
stats: c.object {description: 'Multiplayer match statistics corresponding to this entry in the league.'}
LevelSessionSchema.properties.leagues.items.properties.stats.properties = _.pick LevelSessionSchema.properties, 'meanStrength', 'standardDeviation', 'totalScore', 'numberOfWinsAndTies', 'numberOfLosses', 'scoreHistory', 'matches'
c.extendBasicProperties LevelSessionSchema, 'level.session'
c.extendPermissionsProperties LevelSessionSchema, 'level.session'

View file

@ -17,6 +17,9 @@
h1
text-align: center
&.league-header
margin: 15px 0 20px 0
.tournament-blurb
margin-top: -10px
margin-bottom: 10px

View file

@ -45,9 +45,12 @@ $level-resize-transition-time: 0.5s
.team-gold
font-size: 2vw
line-height: 2vw
img
width: 1.8vw
heighT: 1.8vw
img
width: 1.8vw
height: 1.8vw
#duel-stats-view
right: calc(1% + 100px)
bottom: 50px
#control-bar-view .title
left: 20%
width: 60%

View file

@ -0,0 +1,154 @@
@import "app/styles/mixins"
@import "app/styles/bootstrap/variables"
#duel-stats-view
position: absolute
right: 44.3%
bottom: 133px
z-index: 3
@include transition(opacity .2s linear)
@include user-select(none)
padding: 4px 10px 0 4px
background: transparent url(/images/level/hud_background.png) no-repeat
background-size: 100% auto
border-radius: 4px
width: 500px
height: 60px
display: flex
flex-direction: row
&:hover
opacity: 0.1
.player-container
width: 50%
height: 50px
display: flex
flex-direction: row
align-items: center
text-transform: uppercase
font-family: $headings-font-family
font-weight: bold
font-size: 18px
color: hsla(4,80%,51%,1)
text-shadow: 0px 1px 0px black, 0px -1px 0px black, 1px 0px 0px black, -1px 0px 0px black
&.team-humans
padding-right: 10px
.player-power
margin-right: 5px
&.team-ogres
padding-left: 10px
flex-direction: row-reverse
color: hsla(205,100%,51%,1)
.name-and-power
flex-direction: row-reverse
.player-name
text-align: right
.player-power
margin-left: 5px
flex-direction: row-reverse
text-align: right
.player-health
flex-direction: row-reverse
.health-bar-container .health-bar
background: hsla(205,100%,51%,1)
.name-and-power
display: flex
flex-direction: row
.player-portrait
margin: 0 12px
.thang-avatar-view
width: 30px
.player-name
height: 50%
text-align: left
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
max-width: 130px
$iconSize: 16px
.health-icon, .power-icon
display: inline-block
width: $iconSize
height: $iconSize
background: transparent url(/images/level/hud_info_icons.png) no-repeat
background-size: auto $iconSize
.player-health, .player-power
height: 50%
display: flex
flex-direction: row
height: 18px
.player-health
line-height: 16px
.health-bar-container
width: 100px
display: inline-block
margin: 1px 5px
height: 16px
background: rgb(32, 27, 21)
padding: 4px
border-radius: 8px
border: 0
overflow: hidden
.health-bar
background: rgb(234, 35, 45)
width: 100%
height: 8px
border-radius: 4px
.health-value
vertical-align: top
.player-power
.power-icon
margin-top: 4px
background-position: (-5 * $iconSize) 0px
.power-value
min-width: 20px
margin: 0px 5px
//&.team-humans .team-gold
// color: hsla(4,80%,51%,1)
//
//&.team-ogres .team-gold
// color: hsla(205,100%,51%,1)
//
//.team-gold
// font-size: 1.4vw
// line-height: 1.4vw
// margin: 0
// color: hsla(205,0%,51%,1)
// display: inline-block
// padding: 0px 4px
//
// img
// width: 1.2vw
// height: 1.2vw
// border-radius: 2px
// padding: 0.1vw
// margin-top: -0.2vw
// margin-right: 0.1vw
//
// .gold-amount
// display: inline-block
// min-width: 20px

View file

@ -385,6 +385,9 @@
.offer
display: none
img
margin: 5px 10px
p
color: white

View file

@ -12,6 +12,10 @@
display: none
#gold-view
right: 1%
#duel-stats-view
right: 230px
bottom: 80px
@include scale(2)
#control-bar-view
width: 100%

View file

@ -26,52 +26,62 @@ block content
button.btn(data-dismiss='modal', data-i18n="modal.close") Close
button.btn.edit-description-save-btn(data-i18n="common.save_changes") Save changes
if clan
h1 #{clan.get('name')}
if clan.get('type') === 'private'
small(data-i18n="clans.private") (private)
if clan.get('ownerID') === me.id
span.spl
button.btn.btn-xs.edit-name-btn(data-toggle='modal', data-target='#editNameModal', data-i18n="clans.edit_name") edit name
.row
.col-lg-6
if clan
h1 #{clan.get('name')}
if clan.get('type') === 'private'
small(data-i18n="clans.private") (private)
if clan.get('ownerID') === me.id
span.spl
button.btn.btn-xs.edit-name-btn(data-toggle='modal', data-target='#editNameModal', data-i18n="clans.edit_name") edit name
if clan.get('description')
.clan-description
each line in clan.get('description').split('\n')
p= line
if clan.get('ownerID') === me.id
button.btn.btn-xs.edit-description-btn(data-toggle='modal', data-target='#editDescriptionModal', data-i18n="clans.edit_description") edit description
h5(data-i18n="clans.summary") Summary
table.table.table-condensed.stats-table
if owner
tr
td
span.spr(data-i18n="clans.chieftain") Chieftain
td
span.spr.player-hero-icon(data-memberid="#{clan.get('ownerID')}")
a(href="/user/#{clan.get('ownerID')}")= owner.get('name')
if stats.averageLevel
tr
td(data-i18n="clans.average_level") Average Level
td= stats.averageLevel
if stats.averageAchievements && clan.get('type') === 'public'
tr
td(data-i18n="clans.average_achievements") Average Achievements
td= stats.averageAchievements
p
if isOwner
button.btn.btn-xs.btn-warning.delete-clan-btn(data-i18n="clans.delete_clan") Delete Clan
else if isMember
button.btn.btn-xs.btn-warning.leave-clan-btn(data-i18n="clans.leave_clan") Leave Clan
else
button.btn.btn-lg.btn-success.join-clan-btn(data-i18n="clans.join_clan") Join Clan
if clan.get('ownerID') === me.id || clan.get('type') === 'public'
div
span.spl.spr.join-link-prompt(data-i18n="clans.invite_1") Invite:
input.join-clan-link(type="text", readonly, value="#{joinClanLink}")
.small(data-i18n="clans.invite_2") *Invite players to this Clan by sending them this link.
if clan.get('description')
.clan-description
each line in clan.get('description').split('\n')
p= line
if clan.get('ownerID') === me.id
button.btn.btn-xs.edit-description-btn(data-toggle='modal', data-target='#editDescriptionModal', data-i18n="clans.edit_description") edit description
h5(data-i18n="clans.summary") Summary
table.table.table-condensed.stats-table
if owner
tr
td
span.spr(data-i18n="clans.chieftain") Chieftain
td
span.spr.player-hero-icon(data-memberid="#{clan.get('ownerID')}")
a(href="/user/#{clan.get('ownerID')}")= owner.get('name')
if stats.averageLevel
tr
td(data-i18n="clans.average_level") Average Level
td= stats.averageLevel
if stats.averageAchievements && clan.get('type') === 'public'
tr
td(data-i18n="clans.average_achievements") Average Achievements
td= stats.averageAchievements
p
if isOwner
button.btn.btn-xs.btn-warning.delete-clan-btn(data-i18n="clans.delete_clan") Delete Clan
else if isMember
button.btn.btn-xs.btn-warning.leave-clan-btn(data-i18n="clans.leave_clan") Leave Clan
else
button.btn.btn-lg.btn-success.join-clan-btn(data-i18n="clans.join_clan") Join Clan
if clan.get('ownerID') === me.id || clan.get('type') === 'public'
div
span.spl.spr.join-link-prompt(data-i18n="clans.invite_1") Invite:
input.join-clan-link(type="text", readonly, value="#{joinClanLink}")
.small(data-i18n="clans.invite_2") *Invite players to this Clan by sending them this link.
if arenas && arenas.length
.col-lg-6
h2(data-i18n="play.campaign_multiplayer")
p(data-i18n="clans.leagues_explanation")
for arena in arenas
h3
a(href="/play/ladder/#{arena.slug}/clan/#{clan.id}")= i18n(arena, 'name')
if members
h3

View file

@ -17,6 +17,8 @@ if campaign
a(href=level.type == 'hero' ? '#' : level.disabled ? "/play" : "/play/#{level.levelPath || 'level'}/#{level.slug}", disabled=level.disabled, data-level-slug=level.slug, data-level-path=level.levelPath || 'level', data-level-name=level.name)
if level.slug == 'lost-viking'
img.star(src="/file/db/thang.type/5441c3144e9aeb727cc97111/portrait.png")
else if level.slug == 'robot-ragnarok'
img.star(src="/file/db/thang.type/54ea35fd2b7506e891ca70d5/portrait.png")
else if level.requiresSubscription
img.star(src="/images/pages/play/star.png")
if levelStatusMap[level.slug] === 'complete'

View file

@ -23,12 +23,13 @@ div#columns.row
- if(!showJustTop && topSessions.length == 20) topSessions = topSessions.slice(0, 10);
for session, rank in topSessions
- var myRow = session.get('creator') == me.id
- var sessionStats = league ? (_.find(session.get('leagues') || [], {leagueID: league.id}) || {}).stats || {} : session.attributes;
tr(class=myRow ? "success" : "", data-player-id=session.get('creator'), data-session-id=session.id)
td.code-language-cell(style="background-image: url(/images/common/code_languages/" + session.get('submittedCodeLanguage') + "_icon.png)" title=capitalize(session.get('submittedCodeLanguage')))
if level.get('type', true) == 'hero-ladder'
td.hero-portrait-cell(style="background-image: url(/file/db/thang.type/#{(session.get('heroConfig') || {}).thangType || '529ffbf1cf1818f2be000001'}/portrait.png)")
td.rank-cell= rank + 1
td.score-cell= Math.round(session.get('totalScore') * 100)
td.score-cell= Math.round(sessionStats.totalScore * 100)
td.name-col-cell= session.get('creatorName') || "Anonymous"
td.fight-cell
a(href="/play/level/#{level.get('slug') || level.id}?team=#{team.otherTeam}&opponent=#{session.id}")
@ -41,12 +42,13 @@ div#columns.row
td(colspan=4).ellipsis-row ...
for session in team.leaderboard.nearbySessions()
- var myRow = session.get('creator') == me.id
- var sessionStats = league ? (_.find(session.get('leagues'), {leagueID: league.id}) || {}).stats || {} : session.attributes;
tr(class=myRow ? "success" : "", data-player-id=session.get('creator'), data-session-id=session.id)
td.code-language-cell(style="background-image: url(/images/common/code_languages/" + session.get('submittedCodeLanguage') + "_icon.png)")
if level.get('type', true) == 'hero-ladder'
td.hero-portrait-cell(style="background-image: url(/file/db/thang.type/#{(session.get('heroConfig') || {}).thangType || '529ffbf1cf1818f2be000001'}/portrait.png)")
td.rank-cell= session.rank
td.score-cell= Math.round(session.get('totalScore') * 100)
td.score-cell= Math.round(sessionStats.totalScore * 100)
td.name-col-cell= session.get('creatorName') || "Anonymous"
td.fight-cell
a(href="/play/level/#{level.get('slug') || level.id}?team=#{team.otherTeam}&opponent=#{session.id}")

View file

@ -10,6 +10,11 @@ block content
else
h1= level.get('name')
if league
h1.league-header
a(href="/#{leagueType == 'clan' ? 'clans' : leagueType}/#{league.id}")= league.get('name')
span.spl(data-i18n="ladder.league") League
if level.get('name') == 'Greed'
.tournament-blurb
h2
@ -70,7 +75,7 @@ block content
a(href="http://discourse.codecombat.com/") the forum
| and discuss your strategies, your triumphs, and your turmoils.
if level.get('name') == 'Zero Sum'
if level.get('name') == 'Zero Sum' && !league
.tournament-blurb
h2
span(data-i18n="ladder.tournament_ended") Tournament ended

View file

@ -3,6 +3,7 @@ p(id="simulation-status-text")
| #{simulationStatus}
else
span(data-i18n="ladder.simulation_explanation") By simulating games you can get your game ranked faster!
span.spl(data-i18n="ladder.simulation_explanation_leagues") You will mainly help simulate games for allied players in your clans and courses.
p
button(data-i18n="ladder.simulate_games").btn.btn-warning.btn-lg.highlight#simulate-button Simulate Games!

View file

@ -29,6 +29,8 @@
#multiplayer-status-view
#duel-stats-view
#playback-view
#thang-hud

View file

@ -0,0 +1,21 @@
for player in players
div(class="player-container team-" + player.team)
.player-portrait
.thang-avatar-placeholder
.player-info
.name-and-power
.player-power
.power-icon
.power-value
.player-name= player.name
.player-health
.health-icon
.health-bar-container
.health-bar
.health-value
//.player-gold
// .team-gold
// img(src="/images/level/gold_icon.png", alt="", draggable="false")
// .gold-amount

View file

@ -122,7 +122,7 @@ block modal-footer-content
button.btn.btn-illustrated.btn-primary.btn-lg.world-map-button.continue-from-offer-button(data-i18n="play_level.victory_become_a_viking") Become a Viking
.offer.a-mayhem-of-munchkins
p
img.pull-left(src="/file/db/level/55ca29439bc1892c835b0137/bloc-mentor.png")
img.pull-right(src="/file/db/level/55ca29439bc1892c835b0137/bloc-logo-square-100x100-white.png")
img.pull-left(src="/file/db/level/55ca29439bc1892c835b0137/bloc_warrior.png")
img.pull-right(src="/file/db/level/55ca29439bc1892c835b0137/bloc_logo.png")
span(data-i18n="play_level.victory_bloc")
button.btn.btn-illustrated.btn-primary.btn-lg.world-map-button.continue-from-offer-button(data-i18n="play_level.victory_bloc_cta")

View file

@ -1,3 +1,3 @@
div(class="team-gold team-" + team)
img(src="/images/level/gold_icon.png", alt="")
img(src="/images/level/gold_icon.png", alt="", draggable="false")
div(class="gold-amount team-" + team)

View file

@ -9,5 +9,6 @@
#canvas-top-gradient.gradient
#gold-view.secret.expanded
#level-chat-view
#duel-stats-view
#playback-view
#thang-hud

View file

@ -10,6 +10,7 @@ LevelSession = require 'models/LevelSession'
SubscribeModal = require 'views/core/SubscribeModal'
ThangType = require 'models/ThangType'
User = require 'models/User'
utils = require 'core/utils'
# TODO: Add message for clan not found
# TODO: Progress visual for premium levels?
@ -60,7 +61,7 @@ module.exports = class ClanDetailsView extends RootView
@listenTo @memberAchievements, 'sync', @onMemberAchievementsSync
@listenTo @memberSessions, 'sync', @onMemberSessionsSync
@supermodel.loadModel @campaigns, 'clan', cache: false
@supermodel.loadModel @campaigns, 'campaigns', cache: false
@supermodel.loadModel @clan, 'clan', cache: false
@supermodel.loadCollection(@members, 'members', {cache: false})
@supermodel.loadCollection(@memberAchievements, 'member_achievements', {cache: false})
@ -120,6 +121,8 @@ module.exports = class ClanDetailsView extends RootView
context.lastUserCampaignLevelMap = lastUserCampaignLevelMap
context.showExpandedProgress = maxLastUserCampaignLevel <= 30 or @showExpandedProgress
context.userConceptsMap = userConceptsMap
context.arenas = @arenas
context.i18n = utils.i18n
context
afterRender: ->
@ -179,21 +182,24 @@ module.exports = class ClanDetailsView extends RootView
return unless @campaigns.loaded
@campaignLevelProgressions = []
@conceptsProgression = []
@arenas = []
for campaign in @campaigns.models
continue if campaign.get('slug') is 'auditions'
campaignLevelProgression =
ID: campaign.id
slug: campaign.get('slug')
name: campaign.get('fullName') or campaign.get('name')
name: utils.i18n(campaign.attributes, 'fullName') or utils.i18n(campaign.attributes, 'name')
levels: []
for levelID, level of campaign.get('levels')
campaignLevelProgression.levels.push
ID: levelID
slug: level.slug
name: level.name
name: utils.i18n level, 'name'
if level.concepts?
for concept in level.concepts
@conceptsProgression.push concept unless concept in @conceptsProgression
if level.type == 'hero-ladder'
@arenas.push level
@campaignLevelProgressions.push campaignLevelProgression
@render?()

View file

@ -43,11 +43,14 @@ module.exports = class LadderPlayModal extends ModalView
# PART 1: Load challengers from the db unless some are in the matches
startLoadingChallengersMaybe: ->
matches = @session?.get('matches')
if @options.league
matches = _.find(@session?.get('leagues'), leagueID: @options.league.id)?.stats.matches
else
matches = @session?.get('matches')
if matches?.length then @loadNames() else @loadChallengers()
loadChallengers: ->
@challengersCollection = new ChallengersData(@level, @team, @otherTeam, @session)
@challengersCollection = new ChallengersData(@level, @team, @otherTeam, @session, @options.league)
@listenTo(@challengersCollection, 'sync', @loadNames)
# PART 2: Loading the names of the other users
@ -156,7 +159,10 @@ module.exports = class LadderPlayModal extends ModalView
mediumInfo = @challengeInfoFromSession(@challengersCollection.mediumPlayer.models[0])
hardInfo = @challengeInfoFromSession(@challengersCollection.hardPlayer.models[0])
else
matches = @session.get('matches')
if @options.league
matches = _.find(@session?.get('leagues'), leagueID: @options.league.id)?.stats.matches
else
matches = @session?.get('matches')
won = (m for m in matches when m.metrics.rank < m.opponents[0].metrics.rank)
lost = (m for m in matches when m.metrics.rank > m.opponents[0].metrics.rank)
tied = (m for m in matches when m.metrics.rank is m.opponents[0].metrics.rank)
@ -195,18 +201,26 @@ module.exports = class LadderPlayModal extends ModalView
}
class ChallengersData
constructor: (@level, @team, @otherTeam, @session) ->
constructor: (@level, @team, @otherTeam, @session, @league) ->
_.extend @, Backbone.Events
score = @session?.get('totalScore') or 25
@easyPlayer = new LeaderboardCollection(@level, {order: 1, scoreOffset: score - 5, limit: 1, team: @otherTeam})
@easyPlayer.fetch cache: false
@listenToOnce(@easyPlayer, 'sync', @challengerLoaded)
@mediumPlayer = new LeaderboardCollection(@level, {order: 1, scoreOffset: score, limit: 1, team: @otherTeam})
@mediumPlayer.fetch cache: false
@listenToOnce(@mediumPlayer, 'sync', @challengerLoaded)
@hardPlayer = new LeaderboardCollection(@level, {order: -1, scoreOffset: score + 5, limit: 1, team: @otherTeam})
@hardPlayer.fetch cache: false
@listenToOnce(@hardPlayer, 'sync', @challengerLoaded)
if @league
score = _.find(@session?.get('leagues'), leagueID: @league.id)?.stats?.totalScore or 10
else
score = @session?.get('totalScore') or 10
for player in [
{type: 'easyPlayer', order: 1, scoreOffset: score - 5}
{type: 'mediumPlayer', order: 1, scoreOffset: score}
{type: 'hardPlayer', order: -1, scoreOffset: score + 5}
]
playerResource = @[player.type] = new LeaderboardCollection(@level, @collectionParameters(order: player.order, scoreOffset: player.scoreOffset))
playerResource.fetch cache: false
@listenToOnce playerResource, 'sync', @challengerLoaded
collectionParameters: (parameters) ->
parameters.team = @otherTeam
parameters.limit = 1
parameters['leagues.leagueID'] = @league.id if @league
parameters
challengerLoaded: ->
if @allLoaded()

View file

@ -154,7 +154,7 @@ module.exports = class LadderTabView extends CocoView
@supermodel.removeModelResource oldLeaderboard
oldLeaderboard.destroy()
teamSession = _.find @sessions.models, (session) -> session.get('team') is team.id
@leaderboards[team.id] = new LeaderboardData(@level, team.id, teamSession, @ladderLimit)
@leaderboards[team.id] = new LeaderboardData(@level, team.id, teamSession, @ladderLimit, @options.league)
@leaderboardRes = @supermodel.addModelResource(@leaderboards[team.id], 'leaderboard', {cache: false}, 3)
@leaderboardRes.load()
@ -166,7 +166,9 @@ module.exports = class LadderTabView extends CocoView
team = _.find @teams, name: histogramWrapper.data('team-name')
histogramData = null
$.when(
$.get "/db/level/#{@level.get('slug')}/histogram_data?team=#{team.name.toLowerCase()}", {cache: false}, (data) -> histogramData = data
url = "/db/level/#{@level.get('slug')}/histogram_data?team=#{team.name.toLowerCase()}"
url += '&leagues.leagueID=' + @options.league.id if @options.league
$.get url, {cache: false}, (data) -> histogramData = data
).then =>
@generateHistogram(histogramWrapper, histogramData, team.name.toLowerCase()) unless @destroyed
@ -181,6 +183,8 @@ module.exports = class LadderTabView extends CocoView
ctx.onFacebook = @facebookStatus is 'connected'
ctx.onGPlus = application.gplusHandler.loggedIn
ctx.capitalize = _.string.capitalize
ctx.league = @options.league
ctx._ = _
ctx
generateHistogram: (histogramElement, histogramData, teamName) ->
@ -227,8 +231,11 @@ module.exports = class LadderTabView extends CocoView
.attr('x', 1)
.attr('width', width/20)
.attr('height', (d) -> height - y(d.y))
if @leaderboards[teamName].session?
playerScore = @leaderboards[teamName].session.get('totalScore') * 100
if session = @leaderboards[teamName].session
if @options.league
playerScore = (_.find(session.get('leagues'), {leagueID: @options.league.id})?.stats.totalScore or 10) * 100
else
playerScore = session.get('totalScore') * 100
scorebar = svg.selectAll('.specialbar')
.data([playerScore])
.enter().append('g')
@ -245,10 +252,13 @@ module.exports = class LadderTabView extends CocoView
message = "#{histogramData.length} players"
if @leaderboards[teamName].session?
if @leaderboards[teamName].myRank <= histogramData.length
message="##{@leaderboards[teamName].myRank} of #{histogramData.length}"
if @options.league
# TODO: fix server handler to properly fetch myRank with a leagueID
message = "#{histogramData.length} players in league"
else if @leaderboards[teamName].myRank <= histogramData.length
message = "##{@leaderboards[teamName].myRank} of #{histogramData.length}"
else
message='Rank your session!'
message = 'Rank your session!'
svg.append('g')
.append('text')
.attr('class', rankClass)
@ -301,24 +311,36 @@ module.exports.LeaderboardData = LeaderboardData = class LeaderboardData extends
Consolidates what you need to load for a leaderboard into a single Backbone Model-like object.
###
constructor: (@level, @team, @session, @limit) ->
constructor: (@level, @team, @session, @limit, @league) ->
super()
collectionParameters: (parameters) ->
parameters.team = @team
parameters['leagues.leagueID'] = @league.id if @league
parameters
fetch: ->
console.warn 'Already have top players on', @ if @topPlayers
@topPlayers = new LeaderboardCollection(@level, {order: -1, scoreOffset: HIGHEST_SCORE, team: @team, limit: @limit})
@topPlayers = new LeaderboardCollection(@level, @collectionParameters(order: -1, scoreOffset: HIGHEST_SCORE, limit: @limit))
promises = []
promises.push @topPlayers.fetch cache: false
if @session
score = @session.get('totalScore') or 10
@playersAbove = new LeaderboardCollection(@level, {order: 1, scoreOffset: score, limit: 4, team: @team})
promises.push @playersAbove.fetch cache: false
@playersBelow = new LeaderboardCollection(@level, {order: -1, scoreOffset: score, limit: 4, team: @team})
promises.push @playersBelow.fetch cache: false
level = "#{@level.get('original')}.#{@level.get('version').major}"
success = (@myRank) =>
promises.push $.ajax("/db/level/#{level}/leaderboard_rank?scoreOffset=#{@session.get('totalScore')}&team=#{@team}", cache: false, success: success)
if @league
score = _.find(@session.get('leagues'), {leagueID: @league.id})?.stats.totalScore
else
score = @session.get('totalScore')
if score
@playersAbove = new LeaderboardCollection(@level, @collectionParameters(order: 1, scoreOffset: score, limit: 4))
promises.push @playersAbove.fetch cache: false
@playersBelow = new LeaderboardCollection(@level, @collectionParameters(order: -1, scoreOffset: score, limit: 4))
promises.push @playersBelow.fetch cache: false
level = "#{@level.get('original')}.#{@level.get('version').major}"
success = (@myRank) =>
loadURL = "/db/level/#{level}/leaderboard_rank?scoreOffset=#{score}&team=#{@team}"
loadURL += '&leagues.leagueID=' + @league.id if @league
promises.push $.ajax(loadURL, cache: false, success: success)
@promise = $.when(promises...)
@promise.then @onLoad
@promise.fail @onFail
@ -340,7 +362,11 @@ module.exports.LeaderboardData = LeaderboardData = class LeaderboardData extends
return me.id in (session.attributes.creator for session in @topPlayers.models)
nearbySessions: ->
return [] unless @session?.get('totalScore')
if @league
score = _.find(@session?.get('leagues'), {leagueID: @league.id})?.stats.totalScore
else
score = @session?.get('totalScore')
return [] unless score
l = []
above = @playersAbove.models
l = l.concat(above)

View file

@ -12,6 +12,9 @@ SimulateTabView = require './SimulateTabView'
LadderPlayModal = require './LadderPlayModal'
CocoClass = require 'core/CocoClass'
Clan = require 'models/Clan'
#CourseInstance = require 'models/CourseInstance'
HIGHEST_SCORE = 1000000
class LevelSessionsCollection extends CocoCollection
@ -35,12 +38,19 @@ module.exports = class LadderView extends RootView
'click a:not([data-toggle])': 'onClickedLink'
'click .spectate-button': 'onClickSpectateButton'
constructor: (options, @levelID) ->
constructor: (options, @levelID, @leagueType, @leagueID) ->
super(options)
@level = @supermodel.loadModel(new Level(_id: @levelID), 'level').model
@sessions = @supermodel.loadCollection(new LevelSessionsCollection(@levelID), 'your_sessions', {cache: false}).model
@teams = []
@loadLeague()
loadLeague: ->
@leagueID = @leagueType = null unless @leagueType in ['clan'] #, 'course']
return unless @leagueID
modelClass = if @leagueType is 'clan' then Clan else null# else CourseInstance
resourceString = if @leagueType is 'clan' then 'clans.clan' else null# else 'courses.course'
@league = @supermodel.loadModel(new modelClass(_id: @leagueID), resourceString).model
onLoaded: ->
@teams = teamDataFromLevel @level
@ -53,6 +63,8 @@ module.exports = class LadderView extends RootView
ctx.teams = @teams
ctx.levelID = @levelID
ctx.levelDescription = marked(@level.get('description')) if @level.get('description')
ctx.leagueType = @leagueType
ctx.league = @league
ctx._ = _
if tournamentEndDate = {greed: 1402444800000, 'criss-cross': 1410912000000, 'zero-sum': 1428364800000}[@levelID]
ctx.tournamentTimeLeft = moment(new Date(tournamentEndDate)).fromNow()
@ -64,9 +76,9 @@ module.exports = class LadderView extends RootView
afterRender: ->
super()
return unless @supermodel.finished()
@insertSubView(@ladderTab = new LadderTabView({}, @level, @sessions))
@insertSubView(@myMatchesTab = new MyMatchesTabView({}, @level, @sessions))
@insertSubView(@simulateTab = new SimulateTabView())
@insertSubView(@ladderTab = new LadderTabView({league: @league}, @level, @sessions))
@insertSubView(@myMatchesTab = new MyMatchesTabView({league: @league}, @level, @sessions))
@insertSubView(@simulateTab = new SimulateTabView(league: @league))
@refreshInterval = setInterval(@fetchSessionsAndRefreshViews.bind(@), 60 * 1000)
hash = document.location.hash[1..] if document.location.hash
if hash and not (hash in ['my-matches', 'simulate', 'ladder', 'prizes', 'rules', 'winners'])
@ -101,7 +113,7 @@ module.exports = class LadderView extends RootView
showPlayModal: (teamID) ->
session = (s for s in @sessions.models when s.get('team') is teamID)[0]
modal = new LadderPlayModal({}, @level, session, teamID)
modal = new LadderPlayModal({league: @league}, @level, session, teamID)
@openModalView modal
onClickedLink: (e) ->

View file

@ -24,7 +24,8 @@ module.exports = class MyMatchesTabView extends CocoView
# Only fetch the names for the userIDs we don't already have in @nameMap
ids = []
for session in @sessions.models
for match in (session.get('matches') or [])
matches = @statsFromSession(session).matches or []
for match in matches
id = match.opponents[0].userID
unless id
console.error 'Found bad opponent ID in malformed match:', match, 'from session', session
@ -37,7 +38,8 @@ module.exports = class MyMatchesTabView extends CocoView
success = (nameMap) =>
return if @destroyed
for session in @sessions.models
for match in session.get('matches') or []
matches = @statsFromSession(session).matches or []
for match in matches
opponent = match.opponents[0]
continue if @nameMap[opponent.userID]
opponentUser = nameMap[opponent.userID]
@ -88,15 +90,16 @@ module.exports = class MyMatchesTabView extends CocoView
for team in @teams
team.session = (s for s in @sessions.models when s.get('team') is team.id)[0]
stats = @statsFromSession team.session
team.readyToRank = team.session?.readyToRank()
team.isRanking = team.session?.get('isRanking')
team.matches = (convertMatch(match, team.session.get('submitDate')) for match in team.session?.get('matches') or [])
team.matches = (convertMatch(match, team.session.get('submitDate')) for match in (stats?.matches or []))
team.matches.reverse()
team.score = (team.session?.get('totalScore') or 10).toFixed(2)
team.score = (stats?.totalScore ? 10).toFixed(2)
team.wins = _.filter(team.matches, {state: 'win', stale: false}).length
team.ties = _.filter(team.matches, {state: 'tie', stale: false}).length
team.losses = _.filter(team.matches, {state: 'loss', stale: false}).length
scoreHistory = team.session?.get('scoreHistory')
scoreHistory = stats?.scoreHistory
if scoreHistory?.length > 1
team.scoreHistory = scoreHistory
@ -123,6 +126,12 @@ module.exports = class MyMatchesTabView extends CocoView
@$el.find('tr.fresh').removeClass('fresh', 5000)
statsFromSession: (session) ->
return null unless session
if @options.league
return _.find(session.get('leagues') or [], leagueID: @options.league.id)?.stats ? {}
session.attributes
generateScoreLineChart: (wrapperID, scoreHistory, teamName) =>
margin =
top: 20

View file

@ -98,19 +98,6 @@ module.exports = class SimulateTabView extends CocoView
link = if @simulationSpectateLink then "<a href=#{@simulationSpectateLink}>#{_.string.escapeHTML(@simulationMatchDescription)}</a>" else ''
$('#simulation-status-text').html "<h3>#{@simulationStatus}</h3>#{link}"
resimulateAllSessions: ->
postData =
originalLevelID: @level.get('original')
levelMajorVersion: @level.get('version').major
console.log postData
$.ajax
url: '/queue/scoring/resimulateAllSessions'
method: 'POST'
data: postData
complete: (jqxhr) ->
console.log jqxhr.responseText
destroy: ->
clearTimeout @simulationPageRefreshTimeout
@simulator?.destroy()

View file

@ -267,6 +267,7 @@ module.exports = class CampaignView extends RootView
level.locked = false if @campaign?.get('name') is 'Auditions'
level.locked = false if @campaign?.get('name') is 'Intro'
level.locked = false if me.isInGodMode()
level.locked = false if level.slug is 'robot-ragnarok'
level.disabled = true if level.adminOnly and @levelStatusMap[level.slug] not in ['started', 'complete']
level.disabled = false if me.isInGodMode()
level.color = 'rgb(255, 80, 60)'
@ -323,7 +324,8 @@ module.exports = class CampaignView extends RootView
me.isPremium() or
not nextLevel.requiresSubscription or
(nextLevel.slug is 'boom-and-bust' and not @levelStatusMap['defense-of-plainswood']) or
(nextLevel.slug is 'favorable-odds' and not @levelStatusMap['the-raised-sword'])
(nextLevel.slug is 'favorable-odds' and not @levelStatusMap['the-raised-sword']) or
(nextLevel.slug is 'robot-ragnarok' and @levelStatusMap['the-raised-sword'])
)
nextLevel.next = true
foundNext = true
@ -379,7 +381,7 @@ module.exports = class CampaignView extends RootView
particleKey.push 'premium' if level.requiresSubscription
particleKey.push 'gate' if level.slug in ['kithgard-gates', 'siege-of-stonehold', 'clash-of-clones', 'summits-gate']
particleKey.push 'hero' if level.unlocksHero and not level.unlockedHero
#particleKey.push 'item' if level.slug is 'apocalypse' # TODO: generalize
particleKey.push 'item' if level.slug is 'robot-ragnarok' # TODO: generalize
continue if particleKey.length is 2 # Don't show basic levels
continue unless level.hidden or _.intersection(particleKey, ['item', 'hero-ladder', 'replayable']).length
@particleMan.addEmitter level.position.x / 100, level.position.y / 100, particleKey.join('-')

View file

@ -28,6 +28,7 @@ ControlBarView = require './level/ControlBarView'
PlaybackView = require './level/LevelPlaybackView'
GoalsView = require './level/LevelGoalsView'
GoldView = require './level/LevelGoldView'
DuelStatsView = require './level/DuelStatsView'
VictoryModal = require './level/modal/VictoryModal'
InfiniteLoopModal = require './level/modal/InfiniteLoopModal'
@ -179,8 +180,8 @@ module.exports = class SpectateLevelView extends RootView
@insertSubView new GoldView {}
@insertSubView new HUDView {level: @level}
worldName = utils.i18n @level.attributes, 'name'
@controlBar = @insertSubView new ControlBarView {worldName: worldName, session: @session, level: @level, supermodel: @supermodel, spectateGame: true}
@insertSubView new DuelStatsView level: @level, session: @session, otherSession: @otherSession, supermodel: @supermodel, thangs: @world.thangs if @level.get('type') in ['hero-ladder', 'course-ladder']
@insertSubView @controlBar = new ControlBarView {worldName: utils.i18n(@level.attributes, 'name'), session: @session, level: @level, supermodel: @supermodel, spectateGame: true}
# callbacks

View file

@ -97,7 +97,7 @@ module.exports = class ControlBarView extends CocoView
c.homeLink = @homeLink
c
showGameMenuModal: (tab='guide') ->
showGameMenuModal: (e, tab=null) ->
gameMenuModal = new GameMenuModal level: @level, session: @session, supermodel: @supermodel, showTab: tab
@openModalView gameMenuModal
@listenToOnce gameMenuModal, 'change-hero', ->
@ -111,9 +111,9 @@ module.exports = class ControlBarView extends CocoView
Backbone.Mediator.publish 'router:navigate', route: @homeLink, viewClass: @homeViewClass, viewArgs: @homeViewArgs
onClickMultiplayer: (e) ->
@showGameMenuModal 'multiplayer'
@showGameMenuModal e, 'multiplayer'
onClickSignupButton: ->
onClickSignupButton: (e) ->
window.tracker?.trackEvent 'Started Signup', category: 'Play Level', label: 'Control Bar', level: @levelID
onDisableControls: (e) -> @toggleControls e, false

View file

@ -0,0 +1,120 @@
CocoView = require 'views/core/CocoView'
template = require 'templates/play/level/duel-stats-view'
ThangAvatarView = require 'views/play/level/ThangAvatarView'
utils = require 'core/utils'
# TODO:
# - if a hero is dead, a big indication that they are dead
# - each hero's current action?
# - if one player is you, an indicator that it's you?
# - indication of which team won (not always hero dead--ties and other victory conditions)
# - army composition or power or attack/defense (for certain levels): experiment with something simple, not like the previous unit list thing
module.exports = class DuelStatsView extends CocoView
id: 'duel-stats-view'
template: template
subscriptions:
#'surface:gold-changed': 'onGoldChanged'
'god:new-world-created': 'onNewWorld'
'god:streaming-world-updated': 'onNewWorld'
'surface:frame-changed': 'onFrameChanged'
constructor: (options) ->
super options
options.thangs = _.filter options.thangs, 'inThangList'
unless options.otherSession
options.otherSession = get: (prop) -> {
creatorName: $.i18n.t 'ladder.simple_ai'
team: if options.session.get('team') is 'humans' then 'ogres' else 'humans'
heroConfig: options.session.get('heroConfig')
}[prop]
#@teamGold = {}
#@teamGoldEarned = {}
getRenderData: (c) ->
c = super c
c.players = @players = (@formatPlayer team for team in ['humans', 'ogres'])
c
formatPlayer: (team) ->
p = team: team
session = _.find [@options.session, @options.otherSession], (s) -> s.get('team') is team
p.name = session.get 'creatorName'
p.heroThangType = (session.get('heroConfig') ? {}).thangType or '529ffbf1cf1818f2be000001'
p.heroID = if team is 'ogres' then 'Hero Placeholder 1' else 'Hero Placeholder'
p
afterRender: ->
super()
for player in @players
@buildAvatar player.heroID, player.team
buildAvatar: (heroID, team) ->
@avatars ?= {}
return if @avatars[team]
thang = _.find @options.thangs, id: heroID
@avatars[team] = avatar = new ThangAvatarView thang: thang, includeName: false, supermodel: @supermodel
@$find(team, '.thang-avatar-placeholder').replaceWith avatar.$el
avatar.render()
onNewWorld: (e) ->
@options.thangs = _.filter e.world.thangs, 'inThangList'
onFrameChanged: (e) ->
@update()
update: ->
for player in @players
thang = _.find @options.thangs, id: @avatars[player.team].thang.id
@updateHealth thang
@updatePower()
updateHealth: (thang) ->
$health = @$find thang.team, '.player-health'
$health.find('.health-bar').css 'width', Math.max(0, Math.min(100, 100 * thang.health / thang.maxHealth)) + '%'
utils.replaceText $health.find('.health-value'), Math.round thang.health
updatePower: ->
# Right now we just display the army cost of all living units as opposed to doing something more sophisticate to measure power.
@costTable ?=
soldier: 20
archer: 25
decoy: 25
'griffin-rider': 50
paladin: 80
artillery: 75
'arrow-tower': 75
palisade: 10
peasant: 50
powers = humans: 0, ogres: 0
for thang in @options.thangs when thang.health > 0
powers[thang.team] += @costTable[thang.type] or 0 if powers[thang.team]?
for player in @players
utils.replaceText @$find(player.team, '.power-value'), powers[player.team]
$find: (team, selector) ->
@$el.find(".player-container.team-#{team} " + selector)
destroy: ->
avatar.destroy() for team, avatar of @avatars ? {}
super()
#onGoldChanged: (e) ->
# return if @teamGold[e.team] is e.gold and @teamGoldEarned[e.team] is e.goldEarned
# @teamGold[e.team] = e.gold
# @teamGoldEarned[e.team] = e.goldEarned
# goldEl = @$find e.team, '.gold-amount'
# text = '' + e.gold
# if e.goldEarned and e.goldEarned > e.gold
# text += " (#{e.goldEarned})"
# goldEl.text text
# @updateTitle()
#
#updateTitle: ->
# for team, gold of @teamGold
# if @teamGoldEarned[team]
# title = "Team '#{team}' has #{gold} now of #{@teamGoldEarned[team]} gold earned."
# else
# title = "Team '#{team}' has #{gold} gold."
# @$find(team, '.player-gold').attr 'title', title

View file

@ -32,6 +32,7 @@ LevelPlaybackView = require './LevelPlaybackView'
GoalsView = require './LevelGoalsView'
LevelFlagsView = require './LevelFlagsView'
GoldView = require './LevelGoldView'
DuelStatsView = require './DuelStatsView'
VictoryModal = require './modal/VictoryModal'
HeroVictoryModal = require './modal/HeroVictoryModal'
InfiniteLoopModal = require './modal/InfiniteLoopModal'
@ -246,8 +247,8 @@ module.exports = class PlayLevelView extends RootView
@insertSubView new LevelDialogueView {level: @level, sessionID: @session.id}
@insertSubView new ChatView levelID: @levelID, sessionID: @session.id, session: @session
@insertSubView new ProblemAlertView session: @session, level: @level, supermodel: @supermodel
worldName = utils.i18n @level.attributes, 'name'
@controlBar = @insertSubView new ControlBarView {worldName: worldName, session: @session, level: @level, supermodel: @supermodel}
@insertSubView new DuelStatsView level: @level, session: @session, otherSession: @otherSession, supermodel: @supermodel, thangs: @world.thangs if @level.get('type') in ['hero-ladder', 'course-ladder']
@insertSubView @controlBar = new ControlBarView {worldName: utils.i18n(@level.attributes, 'name'), session: @session, level: @level, supermodel: @supermodel}
#_.delay (=> Backbone.Mediator.publish('level:set-debug', debug: true)), 5000 if @isIPadApp() # if me.displayName() is 'Nick'
initVolume: ->

View file

@ -23,7 +23,6 @@ module.exports = class GameMenuModal extends ModalView
constructor: (options) ->
super options
@options.showTab = options.showTab
@options.levelID = @options.level.get('slug')
@options.startingSessionHeroConfig = $.extend {}, true, (@options.session.get('heroConfig') ? {})
Backbone.Mediator.publish 'music-player:enter-menu', terrain: @options.level.get('terrain', true) ? 'Dungeon'

View file

@ -46,6 +46,15 @@ module.exports = class BuyGemsModal extends ModalView
c.stateMessage = @stateMessage
return c
afterRender: ->
super()
return unless @supermodel.finished()
@playSound 'game-menu-open'
onHidden: ->
super()
@playSound 'game-menu-close'
onIPadProducts: (e) ->
newProducts = []
for iapProduct in e.products

View file

@ -43,6 +43,7 @@ module.exports = class PlayAchievementsModal extends ModalView
@listenTo achievementsFetcher, 'sync', @onAchievementsLoaded
@listenTo earnedAchievementsFetcher, 'sync', @onEarnedAchievementsLoaded
@stopListening @supermodel, 'loaded-all'
@supermodel.loadCollection(achievementsFetcher, 'achievement')
@supermodel.loadCollection(earnedAchievementsFetcher, 'achievement')

View file

@ -78,6 +78,7 @@ module.exports = class PlayHeroesModal extends ModalView
afterRender: ->
super()
return unless @supermodel.finished()
@playSound 'game-menu-open'
@$el.find('.hero-avatar').addClass 'ie' if @isIE()
heroes = @heroes.models
@$el.find('.hero-indicator').each ->
@ -334,7 +335,7 @@ module.exports = class PlayHeroesModal extends ModalView
onHidden: ->
super()
Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'game-menu-close', volume: 1
@playSound 'game-menu-close'
destroy: ->
clearInterval @heroAnimationInterval

View file

@ -80,6 +80,7 @@ module.exports = class PlayItemsModal extends ModalView
itemFetcher.skip = 0
itemFetcher.fetch({data: {skip: 0, limit: PAGE_SIZE}})
@listenTo itemFetcher, 'sync', @onItemsFetched
@stopListening @supermodel, 'loaded-all'
@supermodel.loadCollection(itemFetcher, 'items')
@idToItem = {}
@ -121,7 +122,7 @@ module.exports = class PlayItemsModal extends ModalView
afterRender: ->
super()
return unless @supermodel.finished()
Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'game-menu-open', volume: 1
@playSound 'game-menu-open'
@$el.find('.nano:visible').nanoScroller({alwaysVisible: true})
@itemDetailsView = new ItemDetailsView()
@insertSubView(@itemDetailsView)
@ -133,7 +134,7 @@ module.exports = class PlayItemsModal extends ModalView
onHidden: ->
super()
Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'game-menu-close', volume: 1
@playSound 'game-menu-close'
#- Click events

View file

@ -0,0 +1,37 @@
// Add clan leagues to sessions
// Usage:
// mongo <address>:<port>/<database> <script file> -u <username> -p <password>
//
// It just goes through all users in clans, then adds their clans to all their multiplayer sessions.
migrateClans();
function migrateClans() {
print("Adding clan leagues to sessions...");
var cursor = db.users.find({emailLower: {$exists: true}, clans: {$exists: true}}, {clans: 1, name: 1});
print("Users with clans found: " + cursor.count());
var users = cursor.toArray();
var arenas = [
"5442ba0e1e835500007eb1c7",
"550363b4ec31df9c691ab629",
"5469643c37600b40e0e09c5b",
"54b83c2629843994803c838e",
"544437e0645c0c0000c3291d"
];
users.forEach(function (user, userIndex) {
var leagues = [];
for (var i = 0; i < user.clans.length; ++i) {
leagues.push({leagueID: user.clans[i] + '', stats: {standardDeviation: 25 / 3, numberOfWinsAndTies: 0, numberOfLosses: 0, totalScore: 10, meanStrength: 25}});
};
//var sessions = db.level.sessions.find({creator: user._id + '', 'level.original': {$in: arenas}, submitted: true}, {clans: 1}).toArray();
//print("Found sessions", sessions, "for user", user._id, user.name, 'who has clans', user.clans.join(', '));
//print("Going to set leagues to...")
//printjson(leagues);
var conditions = {creator: user._id + '', 'level.original': {$in: arenas}, submitted: true};
var operations = {$set: {leagues: leagues}};
var result = db.level.sessions.update(conditions, operations, {multi: true});
if (userIndex % 1000 === 0)
print("Done", userIndex, "\tof", users.length, "users.");
});
print("Done.");
}

View file

@ -160,20 +160,27 @@ LevelHandler = class LevelHandler extends Handler
creator: req.user._id+''
query = Session.find(sessionQuery).select('-screenshot -transpiledCode')
# TODO: take out "code" as well, since that can get huge containing the transpiled code for the lat hero, and find another way of having the LadderSubmissionViews in the MyMatchesTab determine rankin readiness
# TODO: take out "code" as well, since that can get huge containing the transpiled code for the lat hero, and find another way of having the LadderSubmissionViews in the MyMatchesTab determine ranking readiness
query.exec (err, results) =>
if err then @sendDatabaseError(res, err) else @sendSuccess res, results
getHistogramData: (req, res, slug) ->
match = levelID: slug, submitted: true, team: req.query.team
match['leagues.leagueID'] = league if league = req.query['leagues.leagueID']
project = totalScore: 1, _id: 0
project['leagues.leagueID'] = project['leagues.stats.totalScore'] = 1 if league
aggregate = Session.aggregate [
{$match: {'levelID': slug, 'submitted': true, 'team': req.query.team}}
{$project: {totalScore: 1, _id: 0}}
{$match: match}
{$project: project}
]
aggregate.cache()
aggregate.cache() unless league
aggregate.exec (err, data) =>
if err? then return @sendDatabaseError res, err
valueArray = _.pluck data, 'totalScore'
if league
valueArray = _.pluck data, (session) -> _.find(session.leagues, leagueID: league)?.stats?.totalScore or 10
else
valueArray = _.pluck data, 'totalScore'
@sendSuccess res, valueArray
checkExistence: (req, res, slugOrID) ->
@ -198,7 +205,7 @@ LevelHandler = class LevelHandler extends Handler
sortParameters =
'totalScore': req.query.order
selectProperties = ['totalScore', 'creatorName', 'creator', 'submittedCodeLanguage', 'heroConfig']
selectProperties = ['totalScore', 'creatorName', 'creator', 'submittedCodeLanguage', 'heroConfig', 'leagues.leagueID', 'leagues.stats.totalScore']
query = Session
.find(sessionsQueryParameters)
@ -232,6 +239,7 @@ LevelHandler = class LevelHandler extends Handler
team: req.query.team
totalScore: scoreQuery
submitted: true
query['leagues.leagueID'] = league if league = req.query['leagues.leagueID']
query
validateLeaderboardRequestParameters: (req) ->

View file

@ -17,6 +17,7 @@ LevelSessionSchema.index({levelID: 1})
LevelSessionSchema.index({'level.majorVersion': 1})
LevelSessionSchema.index({'level.original': 1}, {name: 'Level Original'})
LevelSessionSchema.index({'level.original': 1, 'level.majorVersion': 1, 'creator': 1, 'team': 1})
LevelSessionSchema.index({creator: 1, level: 1}) # Looks like the ones operating on level as two separate fields might not be working, and sometimes this query uses the "level" index instead of the "creator" index.
LevelSessionSchema.index({playtime: 1}, {name: 'Playtime'})
LevelSessionSchema.index({submitDate: 1})
LevelSessionSchema.index({submitted: 1}, {sparse: true})
@ -24,10 +25,13 @@ LevelSessionSchema.index({team: 1}, {sparse: true})
LevelSessionSchema.index({totalScore: 1}, {sparse: true})
LevelSessionSchema.index({user: 1, changed: -1}, {name: 'last played index', sparse: true})
LevelSessionSchema.index({'level.original': 1, 'state.topScores.type': 1, 'state.topScores.date': -1, 'state.topScores.score': -1}, {name: 'top scores index', sparse: true})
LevelSessionSchema.index({submitted: 1, team: 1, level:1, totalScore: -1}, {name: 'rank counting index', sparse: true})
LevelSessionSchema.index({levelID: 1, submitted:1, team: 1}, {name: 'get all scores index', sparse: true})
LevelSessionSchema.index({submitted: 1, team: 1, level: 1, totalScore: -1}, {name: 'rank counting index', sparse: true})
#LevelSessionSchema.index({level: 1, 'leagues.leagueID': 1, submitted: 1, team: 1, totalScore: -1}, {name: 'league rank counting index', sparse: true}) # needed for league leaderboards?
LevelSessionSchema.index({levelID: 1, submitted: 1, team: 1}, {name: 'get all scores index', sparse: true})
#LevelSessionSchema.index({levelID: 1, 'leagues.leagueID': 1, submitted: 1, team: 1}, {name: 'league get all scores index', sparse: true}) # needed for league histograms?
LevelSessionSchema.index({submitted: 1, team: 1, levelID: 1, submitDate: -1}, {name: 'matchmaking index', sparse: true})
LevelSessionSchema.index({submitted: 1, team: 1, levelID: 1, randomSimulationIndex: -1}, {name: 'matchmaking random index', sparse: true})
LevelSessionSchema.index({'leagues.leagueID': 1, submitted: 1, levelID: 1, team: 1, randomSimulationIndex: -1}, {name: 'league-based matchmaking random index', sparse: true}) # Really need MongoDB 3.2 for partial indexes for this and several others: https://jira.mongodb.org/browse/SERVER-785
LevelSessionSchema.plugin(plugins.PermissionsPlugin)
LevelSessionSchema.plugin(AchievablePlugin)

View file

@ -530,7 +530,7 @@ class SubscriptionHandler extends Handler
quantity: getSponsoredSubsAmount(subscriptions.basic.amount, stripeInfo.recipients.length, stripeInfo.subscriptionID?)
stripe.customers.updateSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, options, (err, subscription) =>
if err
logStripeWebhookError(err)
@logSubscriptionError(user, 'Sponsored subscription quantity update error. ' + JSON.stringify(err))
return done({res: 'Database error.', code: 500})
done()

View file

@ -9,25 +9,24 @@ queues = require '../commons/queue'
LevelSession = require '../levels/sessions/LevelSession'
Level = require '../levels/Level'
User = require '../users/User'
TaskLog = require './task/ScoringTask'
bayes = new (require 'bayesian-battle')()
TaskLog = require './scoring/ScoringTask'
scoringUtils = require './scoring/scoringUtils'
getTwoGames = require './scoring/getTwoGames'
recordTwoGames = require './scoring/recordTwoGames'
createNewTask = require './scoring/createNewTask'
dispatchTaskToConsumer = require './scoring/dispatchTaskToConsumer'
processTaskResult = require './scoring/processTaskResult'
scoringTaskQueue = undefined
scoringTaskTimeoutInSeconds = 600
SIMULATOR_VERSION = 3
module.exports.setup = (app) -> connectToScoringQueue()
connectToScoringQueue = ->
module.exports.setup = (app) ->
# Connect to scoring queue
queues.initializeQueueClient ->
queues.queueClient.registerQueue 'scoring', {}, (error, data) ->
if error? then throw new Error "There was an error registering the scoring queue: #{error}"
scoringTaskQueue = data
scoringUtils.scoringTaskQueue = data
#log.info 'Connected to scoring task queue!'
module.exports.messagesInQueueCount = (req, res) ->
scoringTaskQueue.totalMessagesInQueue (err, count) ->
scoringUtils.scoringTaskQueue.totalMessagesInQueue (err, count) ->
if err? then return errors.serverError res, "There was an issue finding the Mongoose count:#{err}"
response = String(count)
res.send(response)
@ -35,673 +34,13 @@ module.exports.messagesInQueueCount = (req, res) ->
module.exports.addPairwiseTaskToQueueFromRequest = (req, res) ->
taskPair = req.body.sessions
addPairwiseTaskToQueue req.body.sessions, (err, success) ->
scoringUtils.addPairwiseTaskToQueue req.body.sessions, (err, success) ->
if err? then return errors.serverError res, "There was an error adding pairwise tasks: #{err}"
sendResponseObject req, res, {message: 'All task pairs were succesfully sent to the queue'}
scoringUtils.sendResponseObject res, {message: 'All task pairs were succesfully sent to the queue'}
addPairwiseTaskToQueue = (taskPair, cb) ->
LevelSession.findOne(_id: taskPair[0]).lean().exec (err, firstSession) =>
if err? then return cb err
LevelSession.find(_id: taskPair[1]).exec (err, secondSession) =>
if err? then return cb err
try
taskPairs = generateTaskPairs(secondSession, firstSession)
catch e
if e then return cb e
sendEachTaskPairToTheQueue taskPairs, (taskPairError) ->
if taskPairError? then return cb taskPairError
cb null
# We should rip these out, probably
module.exports.resimulateAllSessions = (req, res) ->
unless isUserAdmin req then return errors.unauthorized res, 'Unauthorized. Even if you are authorized, you shouldn\'t do this'
originalLevelID = req.body.originalLevelID
levelMajorVersion = parseInt(req.body.levelMajorVersion)
findParameters =
submitted: true
level:
original: originalLevelID
majorVersion: levelMajorVersion
query = LevelSession
.find(findParameters)
.lean()
query.exec (err, result) ->
if err? then return errors.serverError res, err
result = _.sample result, 10
async.each result, resimulateSession.bind(@, originalLevelID, levelMajorVersion), (err) ->
if err? then return errors.serverError res, err
sendResponseObject req, res, {message: 'All task pairs were succesfully sent to the queue'}
resimulateSession = (originalLevelID, levelMajorVersion, session, cb) =>
sessionUpdateObject =
submitted: true
submitDate: new Date()
meanStrength: 25
standardDeviation: 25/3
totalScore: 10
numberOfWinsAndTies: 0
numberOfLosses: 0
isRanking: true
LevelSession.update {_id: session._id}, sessionUpdateObject, (err, updatedSession) ->
if err? then return cb err, null
opposingTeam = calculateOpposingTeam(session.team)
fetchInitialSessionsToRankAgainst levelMajorVersion, originalLevelID, opposingTeam, (err, sessionsToRankAgainst) ->
if err? then return cb err, null
taskPairs = generateTaskPairs(sessionsToRankAgainst, session)
sendEachTaskPairToTheQueue taskPairs, (taskPairError) ->
if taskPairError? then return cb taskPairError, null
cb null
earliestSubmissionCache = {}
findEarliestSubmission = (queryParams, callback) ->
cacheKey = JSON.stringify queryParams
return callback null, cached if cached = earliestSubmissionCache[cacheKey]
LevelSession.findOne(queryParams).sort(submitDate: 1).lean().exec (err, earliest) ->
return callback err if err
result = earliestSubmissionCache[cacheKey] = earliest?.submitDate
callback null, result
findRecentRandomSession = (queryParams, callback) ->
# We pick a random submitDate between the first submit date for the level and now, then do a $lt fetch to find a session to simulate.
# We bias it towards recently submitted sessions.
findEarliestSubmission queryParams, (err, startDate) ->
return callback err, null unless startDate
now = new Date()
interval = now - startDate
cutoff = new Date now - Math.pow(Math.random(), 4) * interval
queryParams.submitDate = $gte: startDate, $lt: cutoff
selection = 'team totalScore transpiledCode submittedCodeLanguage teamSpells levelID creatorName creator submitDate'
LevelSession.findOne(queryParams).sort(submitDate: -1).select(selection).lean().exec (err, session) ->
return callback err if err
callback null, session
findRandomSession = (queryParams, callback) ->
queryParams.submitted = true
favorRecent = queryParams.favorRecent
delete queryParams.favorRecent
if favorRecent
return findRecentRandomSession queryParams, callback
queryParams.randomSimulationIndex = $lte: Math.random()
selection = 'team totalScore transpiledCode submittedCodeLanguage teamSpells levelID creatorName creator submitDate'
sort = randomSimulationIndex: -1
LevelSession.findOne(queryParams).sort(sort).select(selection).lean().exec (err, session) ->
return callback err if err
return callback null, session if session
delete queryParams.randomSimulationIndex # Effectively switch to $gt, if our randomSimulationIndex was lower than the lowest one.
LevelSession.findOne(queryParams).sort(sort).select(selection).lean().exec (err, session) ->
return callback err if err
callback null, session
formatSessionInformation = (session) ->
sessionID: session._id
team: session.team ? 'No team'
transpiledCode: session.transpiledCode
submittedCodeLanguage: session.submittedCodeLanguage
teamSpells: session.teamSpells ? {}
levelID: session.levelID
creatorName: session.creatorName
creator: session.creator
totalScore: session.totalScore
module.exports.getTwoGames = (req, res) ->
#if isUserAnonymous req then return errors.unauthorized(res, 'You need to be logged in to get games.')
humansGameID = req.body.humansGameID
ogresGameID = req.body.ogresGameID
return if simulatorIsTooOld req, res
#ladderGameIDs = ['greed', 'criss-cross', 'brawlwood', 'dungeon-arena', 'gold-rush', 'sky-span'] # Let's not give any extra simulations to old ladders.
ladderGameIDs = ['dueling-grounds', 'cavern-survival', 'multiplayer-treasure-grove', 'harrowland', 'zero-sum']
levelID = _.sample ladderGameIDs
unless ogresGameID and humansGameID
recentHumans = Math.random() < 0.5 # We pick one session favoring recent submissions, then find another one uniformly to play against
async.map [{levelID: levelID, team: 'humans', favorRecent: recentHumans}, {levelID: levelID, team: 'ogres', favorRecent: not recentHumans}], findRandomSession, (err, sessions) ->
if err then return errors.serverError(res, "Couldn't get two games to simulate for #{levelID}.")
unless sessions.length is 2
res.send(204, 'No games to score.')
return res.end()
taskObject = messageGenerated: Date.now(), sessions: (formatSessionInformation session for session in sessions)
#console.log 'Dispatching random game between', taskObject.sessions[0].creatorName, 'and', taskObject.sessions[1].creatorName
sendResponseObject req, res, taskObject
else
#console.log "Directly simulating #{humansGameID} vs. #{ogresGameID}."
selection = 'team totalScore transpiledCode submittedCodeLanguage teamSpells levelID creatorName creator submitDate'
LevelSession.findOne(_id: humansGameID).select(selection).lean().exec (err, humanSession) =>
if err? then return errors.serverError(res, 'Couldn\'t find the human game')
LevelSession.findOne(_id: ogresGameID).select(selection).lean().exec (err, ogreSession) =>
if err? then return errors.serverError(res, 'Couldn\'t find the ogre game')
taskObject = messageGenerated: Date.now(), sessions: (formatSessionInformation session for session in [humanSession, ogreSession])
sendResponseObject req, res, taskObject
module.exports.recordTwoGames = (req, res) ->
sessions = req.body.sessions
#console.log 'Recording non-chained result of', sessions?[0]?.name, sessions[0]?.metrics?.rank, 'and', sessions?[1]?.name, sessions?[1]?.metrics?.rank
return if simulatorIsTooOld req, res
req.body?.simulator?.user = '' + req.user?._id
yetiGuru = clientResponseObject: req.body, isRandomMatch: true
async.waterfall [
calculateSessionScores.bind(yetiGuru) # Fetches a few small properties from both sessions, prepares @levelSessionUpdates with the score part
indexNewScoreArray.bind(yetiGuru) # Creates and returns @newScoresObject, no query
addMatchToSessionsAndUpdate.bind(yetiGuru) # Adds matches to the session updates and does the writes
updateUserSimulationCounts.bind(yetiGuru, req.user?._id)
], (err, successMessageObject) ->
if err? then return errors.serverError res, "There was an error recording the single game: #{err}"
sendResponseObject req, res, {message: 'The single game was submitted successfully!'}
module.exports.createNewTask = (req, res) ->
requestSessionID = req.body.session
originalLevelID = req.body.originalLevelID
currentLevelID = req.body.levelID
transpiledCode = req.body.transpiledCode
requestLevelMajorVersion = parseInt(req.body.levelMajorVersion)
yetiGuru = {}
async.waterfall [
validatePermissions.bind(yetiGuru, req, requestSessionID)
fetchAndVerifyLevelType.bind(yetiGuru, currentLevelID)
fetchSessionObjectToSubmit.bind(yetiGuru, requestSessionID)
updateSessionToSubmit.bind(yetiGuru, transpiledCode)
fetchInitialSessionsToRankAgainst.bind(yetiGuru, requestLevelMajorVersion, originalLevelID)
generateAndSendTaskPairsToTheQueue
], (err, successMessageObject) ->
if err? then return errors.serverError res, "There was an error submitting the game to the queue:#{err}"
sendResponseObject req, res, successMessageObject
validatePermissions = (req, sessionID, callback) ->
if isUserAnonymous req then return callback 'You are unauthorized to submit that game to the simulator'
if isUserAdmin req then return callback null
findParameters =
_id: sessionID
selectString = 'creator submittedCode code'
query = LevelSession
.findOne(findParameters)
.select(selectString)
.lean()
query.exec (err, retrievedSession) ->
if err? then return callback err
userHasPermissionToSubmitCode = retrievedSession.creator is req.user?.id and
not _.isEqual(retrievedSession.code, retrievedSession.submittedCode)
unless userHasPermissionToSubmitCode then return callback 'You are unauthorized to submit that game to the simulator'
callback null
fetchAndVerifyLevelType = (levelID, cb) ->
findParameters =
_id: levelID
selectString = 'type'
query = Level
.findOne(findParameters)
.select(selectString)
.lean()
query.exec (err, levelWithType) ->
if err? then return cb err
if not levelWithType.type or not (levelWithType.type in ['ladder', 'hero-ladder']) then return cb 'Level isn\'t of type "ladder"'
cb null
fetchSessionObjectToSubmit = (sessionID, callback) ->
findParameters =
_id: sessionID
selectString = 'team code'
query = LevelSession
.findOne(findParameters)
.select(selectString)
query.exec (err, session) ->
callback err, session?.toObject()
updateSessionToSubmit = (transpiledCode, sessionToUpdate, callback) ->
sessionUpdateObject =
submitted: true
submittedCode: sessionToUpdate.code
transpiledCode: transpiledCode
submitDate: new Date()
#meanStrength: 25 # Let's try not resetting the score on resubmission
standardDeviation: 25/3
#totalScore: 10 # Let's try not resetting the score on resubmission
numberOfWinsAndTies: 0
numberOfLosses: 0
isRanking: true
randomSimulationIndex: Math.random()
LevelSession.update {_id: sessionToUpdate._id}, sessionUpdateObject, (err, result) ->
callback err, sessionToUpdate
fetchInitialSessionsToRankAgainst = (levelMajorVersion, levelID, submittedSession, callback) ->
opposingTeam = calculateOpposingTeam(submittedSession.team)
findParameters =
'level.original': levelID
'level.majorVersion': levelMajorVersion
submitted: true
team: opposingTeam
sortParameters =
totalScore: 1
limitNumber = 1
query = LevelSession.aggregate [
{$match: findParameters}
{$sort: sortParameters}
{$limit: limitNumber}
]
query.exec (err, sessionToRankAgainst) ->
callback err, sessionToRankAgainst, submittedSession
generateAndSendTaskPairsToTheQueue = (sessionToRankAgainst, submittedSession, callback) ->
taskPairs = generateTaskPairs(sessionToRankAgainst, submittedSession)
sendEachTaskPairToTheQueue taskPairs, (taskPairError) ->
if taskPairError? then return callback taskPairError
#console.log 'Sent task pairs to the queue!'
#console.log taskPairs
callback null, {message: 'All task pairs were succesfully sent to the queue'}
module.exports.dispatchTaskToConsumer = (req, res) ->
yetiGuru = {}
async.waterfall [
checkSimulationPermissions.bind(yetiGuru, req)
receiveMessageFromSimulationQueue
changeMessageVisibilityTimeout
parseTaskQueueMessage
constructTaskObject
constructTaskLogObject.bind(yetiGuru, getUserIDFromRequest(req))
processTaskObject
], (err, taskObjectToSend) ->
if err?
if typeof err is 'string' and err.indexOf 'No more games in the queue' isnt -1
res.send(204, 'No games to score.')
return res.end()
else
return errors.serverError res, "There was an error dispatching the task: #{err}"
sendResponseObject req, res, taskObjectToSend
checkSimulationPermissions = (req, cb) ->
if isUserAnonymous req
cb 'You need to be logged in to simulate games'
else
cb null
receiveMessageFromSimulationQueue = (cb) ->
scoringTaskQueue.receiveMessage (err, message) ->
if err? then return cb "No more games in the queue, error:#{err}"
if messageIsInvalid(message) then return cb 'Message received from queue is invalid'
cb null, message
changeMessageVisibilityTimeout = (message, cb) ->
message.changeMessageVisibilityTimeout scoringTaskTimeoutInSeconds, (err) -> cb err, message
parseTaskQueueMessage = (message, cb) ->
try
if typeof message.getBody() is 'object'
messageBody = message.getBody()
else
messageBody = JSON.parse message.getBody()
cb null, messageBody, message
catch e
cb "There was an error parsing the task.Error: #{e}"
constructTaskObject = (taskMessageBody, message, callback) ->
async.map taskMessageBody.sessions, getSessionInformation, (err, sessions) ->
if err? then return callback err
taskObject = messageGenerated: Date.now(), sessions: (formatSessionInformation session for session in sessions)
callback null, taskObject, message
constructTaskLogObject = (calculatorUserID, taskObject, message, callback) ->
taskLogObject = new TaskLog
'createdAt': new Date()
'calculator': calculatorUserID
'sentDate': Date.now()
'messageIdentifierString': message.getReceiptHandle()
taskLogObject.save (err) -> callback err, taskObject, taskLogObject, message
processTaskObject = (taskObject, taskLogObject, message, cb) ->
taskObject.taskID = taskLogObject._id
taskObject.receiptHandle = message.getReceiptHandle()
cb null, taskObject
getSessionInformation = (sessionIDString, callback) ->
findParameters =
_id: sessionIDString
selectString = 'submitDate team submittedCode teamSpells levelID creator creatorName transpiledCode submittedCodeLanguage totalScore'
query = LevelSession
.findOne(findParameters)
.select(selectString)
.lean()
query.exec (err, session) ->
if err? then return callback err, {'error': 'There was an error retrieving the session.'}
callback null, session
module.exports.processTaskResult = (req, res) ->
return if simulatorIsTooOld req, res
originalSessionID = req.body?.originalSessionID
req.body?.simulator?.user = '' + req.user?._id
yetiGuru = {}
try
async.waterfall [
verifyClientResponse.bind(yetiGuru, req.body)
fetchTaskLog.bind(yetiGuru)
checkTaskLog.bind(yetiGuru)
deleteQueueMessage.bind(yetiGuru)
fetchLevelSession.bind(yetiGuru)
checkSubmissionDate.bind(yetiGuru)
logTaskComputation.bind(yetiGuru)
calculateSessionScores.bind(yetiGuru)
indexNewScoreArray.bind(yetiGuru)
addMatchToSessionsAndUpdate.bind(yetiGuru)
updateUserSimulationCounts.bind(yetiGuru, req.user?._id)
determineIfSessionShouldContinueAndUpdateLog.bind(yetiGuru)
findNearestBetterSessionID.bind(yetiGuru)
addNewSessionsToQueue.bind(yetiGuru)
], (err, results) ->
if err is 'shouldn\'t continue'
markSessionAsDoneRanking originalSessionID, (err) ->
if err? then return sendResponseObject req, res, {'error': 'There was an error marking the session as done ranking'}
sendResponseObject req, res, {message: 'The scores were updated successfully, person lost so no more games are being inserted!'}
else if err is 'no session was found'
markSessionAsDoneRanking originalSessionID, (err) ->
if err? then return sendResponseObject req, res, {'error': 'There was an error marking the session as done ranking'}
sendResponseObject req, res, {message: 'There were no more games to rank (game is at top)!'}
else if err?
errors.serverError res, "There was an error:#{err}"
else
sendResponseObject req, res, {message: 'The scores were updated successfully and more games were sent to the queue!'}
catch e
errors.serverError res, 'There was an error processing the task result!'
verifyClientResponse = (responseObject, callback) ->
#TODO: better verification
if typeof responseObject isnt 'object' or responseObject?.originalSessionID?.length isnt 24
callback 'The response to that query is required to be a JSON object.'
else
@clientResponseObject = responseObject
callback null, responseObject
fetchTaskLog = (responseObject, callback) ->
TaskLog.findOne(_id: responseObject.taskID).lean().exec (err, taskLog) =>
return callback new Error("Couldn't find TaskLog for _id #{responseObject.taskID}!") unless taskLog
@taskLog = taskLog
callback err, taskLog
checkTaskLog = (taskLog, callback) ->
if taskLog.calculationTimeMS then return callback 'That computational task has already been performed'
if hasTaskTimedOut taskLog.sentDate then return callback 'The task has timed out'
callback null
deleteQueueMessage = (callback) ->
scoringTaskQueue.deleteMessage @clientResponseObject.receiptHandle, (err) ->
callback err
fetchLevelSession = (callback) ->
LevelSession.findOne(_id: @clientResponseObject.originalSessionID).select('submitDate creator level standardDeviation meanStrength totalScore submittedCodeLanguage').lean().exec (err, session) =>
@levelSession = session
callback err
checkSubmissionDate = (callback) ->
supposedSubmissionDate = new Date(@clientResponseObject.sessions[0].submitDate)
if Number(supposedSubmissionDate) isnt Number(@levelSession.submitDate)
callback 'The game has been resubmitted. Removing from queue...'
else
callback null
logTaskComputation = (callback) ->
@taskLog.set('calculationTimeMS', @clientResponseObject.calculationTimeMS)
@taskLog.set('sessions') # Huh?
@taskLog.calculationTimeMS = @clientResponseObject.calculationTimeMS
@taskLog.sessions = @clientResponseObject.sessions
@taskLog.save (err, saved) ->
callback err
calculateSessionScores = (callback) ->
sessionIDs = _.pluck @clientResponseObject.sessions, 'sessionID'
async.map sessionIDs, retrieveOldSessionData, (err, oldScores) =>
if err? then callback err, {error: 'There was an error retrieving the old scores'}
try
oldScoreArray = _.toArray putRankingFromMetricsIntoScoreObject @clientResponseObject, oldScores
newScoreArray = bayes.updatePlayerSkills oldScoreArray
createSessionScoreUpdate.call @, scoreObject for scoreObject in newScoreArray
callback err, newScoreArray
catch e
callback e
createSessionScoreUpdate = (scoreObject) ->
newTotalScore = scoreObject.meanStrength - 1.8 * scoreObject.standardDeviation
scoreHistoryAddition = [Date.now(), newTotalScore]
@levelSessionUpdates ?= {}
@levelSessionUpdates[scoreObject.id] =
meanStrength: scoreObject.meanStrength
standardDeviation: scoreObject.standardDeviation
totalScore: newTotalScore
$push: {scoreHistory: {$each: [scoreHistoryAddition], $slice: -1000}}
randomSimulationIndex: Math.random()
indexNewScoreArray = (newScoreArray, callback) ->
newScoresObject = _.indexBy newScoreArray, 'id'
@newScoresObject = newScoresObject
callback null, newScoresObject
addMatchToSessionsAndUpdate = (newScoreObject, callback) ->
matchObject = {}
matchObject.date = new Date()
matchObject.opponents = {}
for session in @clientResponseObject.sessions
sessionID = session.sessionID
matchObject.opponents[sessionID] = match = {}
match.sessionID = sessionID
match.userID = session.creator
match.name = session.name
match.totalScore = session.totalScore
match.metrics = {}
match.metrics.rank = Number(newScoreObject[sessionID]?.gameRanking ? 0)
match.codeLanguage = newScoreObject[sessionID].submittedCodeLanguage
#log.info "Match object computed, result: #{JSON.stringify(matchObject, null, 2)}"
#log.info 'Writing match object to database...'
#use bind with async to do the writes
sessionIDs = _.pluck @clientResponseObject.sessions, 'sessionID'
async.each sessionIDs, updateMatchesInSession.bind(@, matchObject), (err) ->
callback err
updateMatchesInSession = (matchObject, sessionID, callback) ->
currentMatchObject = {}
currentMatchObject.date = matchObject.date
currentMatchObject.metrics = matchObject.opponents[sessionID].metrics
opponentsClone = _.cloneDeep matchObject.opponents
opponentsClone = _.omit opponentsClone, sessionID
opponentsArray = _.toArray opponentsClone
currentMatchObject.opponents = opponentsArray
currentMatchObject.codeLanguage = matchObject.opponents[opponentsArray[0].sessionID].codeLanguage
#currentMatchObject.simulator = @clientResponseObject.simulator # Uncomment when actively debugging simulation mismatches
#currentMatchObject.randomSeed = parseInt(@clientResponseObject.randomSeed or 0, 10) # Uncomment when actively debugging simulation mismatches
sessionUpdateObject = @levelSessionUpdates[sessionID]
sessionUpdateObject.$push.matches = {$each: [currentMatchObject], $slice: -200}
#log.info "Update is #{JSON.stringify(sessionUpdateObject, null, 2)}"
LevelSession.update {_id: sessionID}, sessionUpdateObject, callback
updateUserSimulationCounts = (reqUserID, callback) ->
incrementUserSimulationCount reqUserID, 'simulatedBy', (err) =>
if err? then return callback err
#console.log 'Incremented user simulation count!'
unless @isRandomMatch
incrementUserSimulationCount @levelSession.creator, 'simulatedFor', callback
else
callback null
incrementUserSimulationCount = (userID, type, callback) =>
return callback null unless userID
inc = {}
inc[type] = 1
User.update {_id: userID}, {$inc: inc}, (err, affected) ->
log.error "Error incrementing #{type} for #{userID}: #{err}" if err
callback err
determineIfSessionShouldContinueAndUpdateLog = (cb) ->
sessionID = @clientResponseObject.originalSessionID
sessionRank = parseInt @clientResponseObject.originalSessionRank
queryParameters = _id: sessionID
updateParameters = '$inc': {}
if sessionRank is 0
updateParameters['$inc'] = {numberOfWinsAndTies: 1}
else
updateParameters['$inc'] = {numberOfLosses: 1}
LevelSession.findOneAndUpdate queryParameters, updateParameters, {select: 'numberOfWinsAndTies numberOfLosses', lean: true}, (err, updatedSession) ->
if err? then return cb err, updatedSession
totalNumberOfGamesPlayed = updatedSession.numberOfWinsAndTies + updatedSession.numberOfLosses
if totalNumberOfGamesPlayed < 10
#console.log 'Number of games played is less than 10, continuing...'
cb null
else
ratio = (updatedSession.numberOfLosses) / (totalNumberOfGamesPlayed)
if ratio > 0.33
cb 'shouldn\'t continue'
#console.log "Ratio(#{ratio}) is bad, ending simulation"
else
#console.log "Ratio(#{ratio}) is good, so continuing simulations"
cb null
findNearestBetterSessionID = (cb) ->
try
levelOriginalID = @levelSession.level.original
levelMajorVersion = @levelSession.level.majorVersion
sessionID = @clientResponseObject.originalSessionID
sessionTotalScore = @newScoresObject[sessionID].totalScore
opponentSessionID = _.pull(_.keys(@newScoresObject), sessionID)
opponentSessionTotalScore = @newScoresObject[opponentSessionID].totalScore
opposingTeam = calculateOpposingTeam(@clientResponseObject.originalSessionTeam)
catch e
cb e
retrieveAllOpponentSessionIDs sessionID, (err, opponentSessionIDs) ->
if err? then return cb err, null
queryParameters =
totalScore:
$gt: opponentSessionTotalScore
_id:
$nin: opponentSessionIDs
'level.original': levelOriginalID
'level.majorVersion': levelMajorVersion
submitted: true
team: opposingTeam
if opponentSessionTotalScore < 30
# Don't play a ton of matches at low scores--skip some in proportion to how close to 30 we are.
# TODO: this could be made a lot more flexible.
queryParameters['totalScore']['$gt'] = opponentSessionTotalScore + 2 * (30 - opponentSessionTotalScore) / 20
limitNumber = 1
sortParameters =
totalScore: 1
selectString = '_id totalScore'
query = LevelSession.findOne(queryParameters)
.sort(sortParameters)
.limit(limitNumber)
.select(selectString)
.lean()
#console.log "Finding session with score near #{opponentSessionTotalScore}"
query.exec (err, session) ->
if err? then return cb err, session
unless session then return cb 'no session was found'
#console.log "Found session with score #{session.totalScore}"
cb err, session._id
retrieveAllOpponentSessionIDs = (sessionID, cb) ->
query = LevelSession.findOne({_id: sessionID})
.select('matches.opponents.sessionID matches.date submitDate')
.lean()
query.exec (err, session) ->
if err? then return cb err, null
opponentSessionIDs = (match.opponents[0].sessionID for match in session.matches when match.date > session.submitDate)
cb err, opponentSessionIDs
calculateOpposingTeam = (sessionTeam) ->
teams = ['ogres', 'humans']
opposingTeams = _.pull teams, sessionTeam
return opposingTeams[0]
addNewSessionsToQueue = (sessionID, callback) ->
sessions = [@clientResponseObject.originalSessionID, sessionID]
addPairwiseTaskToQueue sessions, callback
messageIsInvalid = (message) -> (not message?) or message.isEmpty()
sendEachTaskPairToTheQueue = (taskPairs, callback) -> async.each taskPairs, sendTaskPairToQueue, callback
generateTaskPairs = (submittedSessions, sessionToScore) ->
taskPairs = []
for session in submittedSessions
if session.toObject?
session = session.toObject()
teams = ['ogres', 'humans']
opposingTeams = _.pull teams, sessionToScore.team
if String(session._id) isnt String(sessionToScore._id) and session.team in opposingTeams
#console.log 'Adding game to taskPairs!'
taskPairs.push [sessionToScore._id, String session._id]
return taskPairs
sendTaskPairToQueue = (taskPair, callback) ->
scoringTaskQueue.sendMessage {sessions: taskPair}, 5, (err, data) -> callback? err, data
getUserIDFromRequest = (req) -> if req.user? then return req.user._id else return null
isUserAnonymous = (req) -> if req.user? then return req.user.get('anonymous') else return true
isUserAdmin = (req) -> return Boolean(req.user?.isAdmin())
sendResponseObject = (req, res, object) ->
res.setHeader('Content-Type', 'application/json')
res.send(object)
res.end()
hasTaskTimedOut = (taskSentTimestamp) -> taskSentTimestamp + scoringTaskTimeoutInSeconds * 1000 < Date.now()
handleTimedOutTask = (req, res, taskBody) -> errors.clientTimeout res, 'The results weren\'t provided within the timeout'
putRankingFromMetricsIntoScoreObject = (taskObject, scoreObject) ->
scoreObject = _.indexBy scoreObject, 'id'
scoreObject[session.sessionID].gameRanking = session.metrics.rank for session in taskObject.sessions
return scoreObject
retrieveOldSessionData = (sessionID, callback) ->
formatOldScoreObject = (session) ->
standardDeviation: session.standardDeviation ? 25/3
meanStrength: session.meanStrength ? 25
totalScore: session.totalScore ? (25 - 1.8*(25/3))
id: sessionID
submittedCodeLanguage: session.submittedCodeLanguage
return formatOldScoreObject @levelSession if sessionID is @levelSession?._id # No need to fetch again
query = _id: sessionID
selection = 'standardDeviation meanStrength totalScore submittedCodeLanguage'
LevelSession.findOne(query).select(selection).lean().exec (err, session) ->
return callback err, {'error': 'There was an error retrieving the session.'} if err?
callback err, formatOldScoreObject session
markSessionAsDoneRanking = (sessionID, cb) ->
#console.log 'Marking session as done ranking...'
LevelSession.update {_id: sessionID}, {isRanking: false}, cb
simulatorIsTooOld = (req, res) ->
clientSimulator = req.body.simulator
return false if clientSimulator?.version >= SIMULATOR_VERSION
message = "Old simulator version #{clientSimulator?.version}, need to clear cache and get version #{SIMULATOR_VERSION}."
log.debug "400: #{message}"
res.send 400, message
res.end()
true
module.exports.getTwoGames = getTwoGames
module.exports.recordTwoGames = recordTwoGames
module.exports.createNewTask = createNewTask
module.exports.dispatchTaskToConsumer = dispatchTaskToConsumer
module.exports.processTaskResult = processTaskResult

View file

@ -0,0 +1,109 @@
log = require 'winston'
async = require 'async'
errors = require '../../commons/errors'
scoringUtils = require './scoringUtils'
LevelSession = require '../../levels/sessions/LevelSession'
Level = require '../../levels/Level'
module.exports = createNewTask = (req, res) ->
requestSessionID = req.body.session
originalLevelID = req.body.originalLevelID
currentLevelID = req.body.levelID
transpiledCode = req.body.transpiledCode
requestLevelMajorVersion = parseInt(req.body.levelMajorVersion)
yetiGuru = {}
async.waterfall [
validatePermissions.bind(yetiGuru, req, requestSessionID)
fetchAndVerifyLevelType.bind(yetiGuru, currentLevelID)
fetchSessionObjectToSubmit.bind(yetiGuru, requestSessionID)
updateSessionToSubmit.bind(yetiGuru, transpiledCode, req.user)
fetchInitialSessionsToRankAgainst.bind(yetiGuru, requestLevelMajorVersion, originalLevelID)
generateAndSendTaskPairsToTheQueue
], (err, successMessageObject) ->
if err? then return errors.serverError res, "There was an error submitting the game to the queue:#{err}"
scoringUtils.sendResponseObject res, successMessageObject
validatePermissions = (req, sessionID, callback) ->
return callback 'You are unauthorized to submit that game to the simulator' unless req.user?.get('email')
return callback null if req.user?.isAdmin()
findParameters = _id: sessionID
selectString = 'creator submittedCode code'
LevelSession.findOne(findParameters).select(selectString).lean().exec (err, retrievedSession) ->
if err? then return callback err
userHasPermissionToSubmitCode = retrievedSession.creator is req.user?.id and
not _.isEqual(retrievedSession.code, retrievedSession.submittedCode)
unless userHasPermissionToSubmitCode then return callback 'You are unauthorized to submit that game to the simulator'
callback null
fetchAndVerifyLevelType = (levelID, cb) ->
Level.findOne(_id: levelID).select('type').lean().exec (err, levelWithType) ->
if err? then return cb err
if not levelWithType.type or not (levelWithType.type in ['ladder', 'hero-ladder', 'course-ladder']) then return cb 'Level isn\'t of type "ladder"'
cb null
fetchSessionObjectToSubmit = (sessionID, callback) ->
LevelSession.findOne({_id: sessionID}).select('team code leagues').exec (err, session) ->
callback err, session?.toObject()
updateSessionToSubmit = (transpiledCode, user, sessionToUpdate, callback) ->
sessionUpdateObject =
submitted: true
submittedCode: sessionToUpdate.code
transpiledCode: transpiledCode
submitDate: new Date()
#meanStrength: 25 # Let's try not resetting the score on resubmission
standardDeviation: 25 / 3
#totalScore: 10 # Let's try not resetting the score on resubmission
numberOfWinsAndTies: 0
numberOfLosses: 0
isRanking: true
randomSimulationIndex: Math.random()
# Reset all league stats as well, and enter the session into any leagues the user is currently part of (not retroactive when joining new leagues)
leagueIDs = user.get('clans') or []
#leagueIDs = leagueIDs.concat user.get('courseInstances') or []
leagueIDs = (leagueID + '' for leagueID in leagueIDs) # Make sure to save them as strings.
newLeagues = []
for leagueID in leagueIDs
league = _.find(sessionToUpdate.leagues, leagueID: leagueID) ? leagueID: leagueID
league.stats ?= {}
league.stats.standardDeviation = 25 / 3
league.stats.numberOfWinsAndTies = 0
league.stats.numberOfLosses = 0
league.stats.meanStrength ?= 25
league.stats.totalScore ?= 10
newLeagues.push(league)
unless _.isEqual newLeagues, sessionToUpdate.leagues
sessionUpdateObject.leagues = sessionToUpdate.leagues = newLeagues
LevelSession.update {_id: sessionToUpdate._id}, sessionUpdateObject, (err, result) ->
callback err, sessionToUpdate
fetchInitialSessionsToRankAgainst = (levelMajorVersion, levelID, submittedSession, callback) ->
opposingTeam = scoringUtils.calculateOpposingTeam(submittedSession.team)
findParameters =
'level.original': levelID
'level.majorVersion': levelMajorVersion
submitted: true
team: opposingTeam
sortParameters = totalScore: 1
limitNumber = 1
query = LevelSession.aggregate [
{$match: findParameters}
{$sort: sortParameters}
{$limit: limitNumber}
]
query.exec (err, sessionToRankAgainst) ->
callback err, sessionToRankAgainst, submittedSession
generateAndSendTaskPairsToTheQueue = (sessionToRankAgainst, submittedSession, callback) ->
taskPairs = scoringUtils.generateTaskPairs(sessionToRankAgainst, submittedSession)
scoringUtils.sendEachTaskPairToTheQueue taskPairs, (taskPairError) ->
if taskPairError? then return callback taskPairError
#console.log 'Sent task pairs to the queue!'
#console.log taskPairs
callback null, {message: 'All task pairs were succesfully sent to the queue'}

View file

@ -0,0 +1,80 @@
log = require 'winston'
async = require 'async'
errors = require '../../commons/errors'
scoringUtils = require './scoringUtils'
LevelSession = require '../../levels/sessions/LevelSession'
TaskLog = require './ScoringTask'
module.exports = dispatchTaskToConsumer = (req, res) ->
yetiGuru = {}
async.waterfall [
checkSimulationPermissions.bind(yetiGuru, req)
receiveMessageFromSimulationQueue
changeMessageVisibilityTimeout
parseTaskQueueMessage
constructTaskObject
constructTaskLogObject.bind(yetiGuru, getUserIDFromRequest(req))
processTaskObject
], (err, taskObjectToSend) ->
if err?
if typeof err is 'string' and err.indexOf 'No more games in the queue' isnt -1
res.send(204, 'No games to score.')
return res.end()
else
return errors.serverError res, "There was an error dispatching the task: #{err}"
scoringUtils.sendResponseObject res, taskObjectToSend
checkSimulationPermissions = (req, cb) ->
if req.user?.get('email')
cb null
else
cb 'You need to be logged in to simulate games'
receiveMessageFromSimulationQueue = (cb) ->
scoringUtils.scoringTaskQueue.receiveMessage (err, message) ->
if err? then return cb "No more games in the queue, error: #{err}"
if not message? or message.isEmpty() then return cb 'Message received from queue is invalid'
cb null, message
changeMessageVisibilityTimeout = (message, cb) ->
message.changeMessageVisibilityTimeout scoringUtils.scoringTaskTimeoutInSeconds, (err) ->
cb err, message
parseTaskQueueMessage = (message, cb) ->
try
messageBody = message.getBody()
unless typeof messageBody is 'object'
messageBody = JSON.parse messageBody
cb null, messageBody, message
catch e
cb "There was an error parsing the task. Error: #{e}"
constructTaskObject = (taskMessageBody, message, callback) ->
async.map taskMessageBody.sessions, getSessionInformation, (err, sessions) ->
if err? then return callback err
taskObject = messageGenerated: Date.now(), sessions: (scoringUtils.formatSessionInformation session for session in sessions)
callback null, taskObject, message
getSessionInformation = (sessionIDString, callback) ->
selectString = 'submitDate team submittedCode teamSpells levelID creator creatorName transpiledCode submittedCodeLanguage totalScore'
LevelSession.findOne(_id: sessionIDString).select(selectString).lean().exec (err, session) ->
if err? then return callback err, {'error': 'There was an error retrieving the session.'}
callback null, session
constructTaskLogObject = (calculatorUserID, taskObject, message, callback) ->
taskLogObject = new TaskLog
createdAt: new Date()
calculator: calculatorUserID
sentDate: Date.now()
messageIdentifierString: message.getReceiptHandle()
taskLogObject.save (err) ->
callback err, taskObject, taskLogObject, message
getUserIDFromRequest = (req) ->
if req.user? then return req.user._id else return null
processTaskObject = (taskObject, taskLogObject, message, cb) ->
taskObject.taskID = taskLogObject._id
taskObject.receiptHandle = message.getReceiptHandle()
cb null, taskObject

View file

@ -0,0 +1,114 @@
log = require 'winston'
async = require 'async'
errors = require '../../commons/errors'
scoringUtils = require './scoringUtils'
LevelSession = require '../../levels/sessions/LevelSession'
module.exports = getTwoGames = (req, res) ->
#return errors.unauthorized(res, 'You need to be logged in to get games.') unless req.user?.get('email')
return if scoringUtils.simulatorIsTooOld req, res
humansSessionID = req.body.humansGameID
ogresSessionID = req.body.ogresGameID
return getSpecificSessions res, humansSessionID, ogresSessionID if humansSessionID and ogresSessionID
getRandomSessions req.user, sendSessionsResponse(res)
sessionSelectionString = 'team totalScore transpiledCode submittedCodeLanguage teamSpells levelID creatorName creator submitDate leagues'
sendSessionsResponse = (res) ->
(err, sessions) ->
if err then return errors.serverError res, "Couldn't get two games to simulate: #{err}"
unless sessions.length is 2
console.log 'No games to score.', sessions.length
res.send 204, 'No games to score.'
return res.end()
taskObject = messageGenerated: Date.now(), sessions: (scoringUtils.formatSessionInformation session for session in sessions)
#console.log 'Dispatching ladder game simulation between', taskObject.sessions[0].creatorName, 'and', taskObject.sessions[1].creatorName
scoringUtils.sendResponseObject res, taskObject
getSpecificSessions = (res, humansSessionID, ogresSessionID) ->
async.map [humansSessionID, ogresSessionID], getSpecificSession, sendSessionsResponse(res)
getSpecificSession = (sessionID, callback) ->
LevelSession.findOne(_id: sessionID).select(sessionSelectionString).lean().exec (err, session) ->
if err? then return callback "Couldn\'t find target simulation session #{sessionID}"
callback null, session
getRandomSessions = (user, callback) ->
# Determine whether to play a random match, an internal league match, or an external league match.
# Only people in a league will end up simulating internal league matches (for leagues they're in) except by dumb chance.
# If we don't like that, we can rework sampleByLevel to have an opportunity to switch to internal leagues if the first session had a league affiliation.
leagueIDs = user.get('clans') or []
#leagueIDs = leagueIDs.concat user.get('courseInstances') or []
leagueIDs = (leagueID + '' for leagueID in leagueIDs) # Make sure to fetch them as strings.
return sampleByLevel callback unless leagueIDs.length and Math.random() > 1 / leagueIDs.length
leagueID = _.sample leagueIDs
findRandomSession {'leagues.leagueID': leagueID}, (err, session) ->
if err then return callback err
unless session then return sampleByLevel callback
otherTeam = scoringUtils.calculateOpposingTeam session.team
queryParameters = team: otherTeam, levelID: session.levelID
if Math.random() < 0.5
# Try to play a match on the internal league ladder for this level
queryParameters['leagues.leagueID'] = leagueID
findRandomSession queryParameters, (err, otherSession) ->
if err then return callback err
if otherSession then return callback null, [session, otherSession]
# No opposing league session found; try to play an external match
delete queryParameters['leagues.leagueID']
findRandomSession queryParameters, (err, otherSession) ->
if err then return callback err
callback null, [session, otherSession]
else
# Play what will probably end up being an external match
findRandomSession queryParameters, (err, otherSession) ->
if err then return callback err
callback null, [session, otherSession]
# Sampling by level: we pick a level, then find a human and ogre session for that level, one at random, one biased towards recent submissions.
#ladderLevelIDs = ['greed', 'criss-cross', 'brawlwood', 'dungeon-arena', 'gold-rush', 'sky-span'] # Let's not give any extra simulations to old ladders.
ladderLevelIDs = ['dueling-grounds', 'cavern-survival', 'multiplayer-treasure-grove', 'harrowland', 'zero-sum']
sampleByLevel = (callback) ->
levelID = _.sample ladderLevelIDs
favorRecentHumans = Math.random() < 0.5 # We pick one session favoring recent submissions, then find another one uniformly to play against
async.map [{levelID: levelID, team: 'humans', favorRecent: favorRecentHumans}, {levelID: levelID, team: 'ogres', favorRecent: not favorRecentHumans}], findRandomSession, callback
findRandomSession = (queryParams, callback) ->
# In MongoDB 3.2, we will be able to easily get a random document with aggregate $sample: https://jira.mongodb.org/browse/SERVER-533
queryParams.submitted = true
favorRecent = queryParams.favorRecent
delete queryParams.favorRecent
if favorRecent
return findRecentRandomSession queryParams, callback
queryParams.randomSimulationIndex = $lte: Math.random()
sort = randomSimulationIndex: -1
LevelSession.findOne(queryParams).sort(sort).select(sessionSelectionString).lean().exec (err, session) ->
return callback err if err
return callback null, session if session
delete queryParams.randomSimulationIndex # Just find the highest-indexed session, if our randomSimulationIndex was lower than the lowest one.
LevelSession.findOne(queryParams).sort(sort).select(sessionSelectionString).lean().exec (err, session) ->
return callback err if err
callback null, session
findRecentRandomSession = (queryParams, callback) ->
# We pick a random submitDate between the first submit date for the level and now, then do a $lt fetch to find a session to simulate.
# We bias it towards recently submitted sessions.
findEarliestSubmission queryParams, (err, startDate) ->
return callback err, null unless startDate
now = new Date()
interval = now - startDate
cutoff = new Date now - Math.pow(Math.random(), 4) * interval
queryParams.submitDate = $gte: startDate, $lt: cutoff
LevelSession.findOne(queryParams).sort(submitDate: -1).select(sessionSelectionString).lean().exec (err, session) ->
return callback err if err
callback null, session
earliestSubmissionCache = {}
findEarliestSubmission = (queryParams, callback) ->
cacheKey = JSON.stringify queryParams
return callback null, cached if cached = earliestSubmissionCache[cacheKey]
LevelSession.findOne(queryParams).sort(submitDate: 1).lean().exec (err, earliest) ->
return callback err if err
result = earliestSubmissionCache[cacheKey] = earliest?.submitDate
callback null, result

View file

@ -0,0 +1,172 @@
log = require 'winston'
async = require 'async'
errors = require '../../commons/errors'
scoringUtils = require './scoringUtils'
LevelSession = require '../../levels/sessions/LevelSession'
TaskLog = require './ScoringTask'
module.exports = processTaskResult = (req, res) ->
return if scoringUtils.simulatorIsTooOld req, res
originalSessionID = req.body?.originalSessionID
req.body?.simulator?.user = '' + req.user?._id
yetiGuru = {}
try
async.waterfall [
verifyClientResponse.bind(yetiGuru, req.body)
fetchTaskLog.bind(yetiGuru)
checkTaskLog.bind(yetiGuru)
deleteQueueMessage.bind(yetiGuru)
fetchLevelSession.bind(yetiGuru)
checkSubmissionDate.bind(yetiGuru)
logTaskComputation.bind(yetiGuru)
scoringUtils.calculateSessionScores.bind(yetiGuru)
scoringUtils.indexNewScoreArray.bind(yetiGuru)
scoringUtils.addMatchToSessionsAndUpdate.bind(yetiGuru)
scoringUtils.updateUserSimulationCounts.bind(yetiGuru, req.user?._id)
determineIfSessionShouldContinueAndUpdateLog.bind(yetiGuru)
findNearestBetterSessionID.bind(yetiGuru)
addNewSessionsToQueue.bind(yetiGuru)
], (err, results) ->
if err is 'shouldn\'t continue'
markSessionAsDoneRanking originalSessionID, (err) ->
if err? then return scoringUtils.sendResponseObject res, {'error': 'There was an error marking the session as done ranking'}
scoringUtils.sendResponseObject res, {message: 'The scores were updated successfully, person lost so no more games are being inserted!'}
else if err is 'no session was found'
markSessionAsDoneRanking originalSessionID, (err) ->
if err? then return scoringUtils.sendResponseObject res, {'error': 'There was an error marking the session as done ranking'}
scoringUtils.sendResponseObject res, {message: 'There were no more games to rank (game is at top)!'}
else if err?
errors.serverError res, "There was an error:#{err}"
else
scoringUtils.sendResponseObject res, {message: 'The scores were updated successfully and more games were sent to the queue!'}
catch e
errors.serverError res, 'There was an error processing the task result!'
verifyClientResponse = (responseObject, callback) ->
# TODO: better verification
if typeof responseObject isnt 'object' or responseObject?.originalSessionID?.length isnt 24
callback 'The response to that query is required to be a JSON object.'
else
@clientResponseObject = responseObject
callback null, responseObject
fetchTaskLog = (responseObject, callback) ->
TaskLog.findOne(_id: responseObject.taskID).lean().exec (err, taskLog) =>
return callback new Error("Couldn't find TaskLog for _id #{responseObject.taskID}!") unless taskLog
@taskLog = taskLog
callback err, taskLog
checkTaskLog = (taskLog, callback) ->
if taskLog.calculationTimeMS then return callback 'That computational task has already been performed'
if hasTaskTimedOut taskLog.sentDate then return callback 'The task has timed out'
callback null
hasTaskTimedOut = (taskSentTimestamp) ->
taskSentTimestamp + scoringUtils.scoringTaskTimeoutInSeconds * 1000 < Date.now()
deleteQueueMessage = (callback) ->
scoringUtils.scoringTaskQueue.deleteMessage @clientResponseObject.receiptHandle, (err) ->
callback err
fetchLevelSession = (callback) ->
selectString = 'submitDate creator level standardDeviation meanStrength totalScore submittedCodeLanguage leagues'
LevelSession.findOne(_id: @clientResponseObject.originalSessionID).select(selectString).lean().exec (err, session) =>
@levelSession = session
callback err
checkSubmissionDate = (callback) ->
supposedSubmissionDate = new Date(@clientResponseObject.sessions[0].submitDate)
if Number(supposedSubmissionDate) isnt Number(@levelSession.submitDate)
callback 'The game has been resubmitted. Removing from queue...'
else
callback null
logTaskComputation = (callback) ->
@taskLog.set('calculationTimeMS', @clientResponseObject.calculationTimeMS)
@taskLog.set('sessions') # Huh?
@taskLog.calculationTimeMS = @clientResponseObject.calculationTimeMS
@taskLog.sessions = @clientResponseObject.sessions
@taskLog.save (err, saved) ->
callback err
determineIfSessionShouldContinueAndUpdateLog = (cb) ->
sessionID = @clientResponseObject.originalSessionID
sessionRank = parseInt @clientResponseObject.originalSessionRank
update = '$inc': {}
if sessionRank is 0
update['$inc'] = {numberOfWinsAndTies: 1}
else
update['$inc'] = {numberOfLosses: 1}
LevelSession.findOneAndUpdate {_id: sessionID}, update, {select: 'numberOfWinsAndTies numberOfLosses', lean: true}, (err, updatedSession) ->
if err? then return cb err, updatedSession
totalNumberOfGamesPlayed = updatedSession.numberOfWinsAndTies + updatedSession.numberOfLosses
if totalNumberOfGamesPlayed < 10
#console.log 'Number of games played is less than 10, continuing...'
cb null
else
ratio = (updatedSession.numberOfLosses) / (totalNumberOfGamesPlayed)
if ratio > 0.33
cb 'shouldn\'t continue'
#console.log "Ratio(#{ratio}) is bad, ending simulation"
else
#console.log "Ratio(#{ratio}) is good, so continuing simulations"
cb null
findNearestBetterSessionID = (cb) ->
try
levelOriginalID = @levelSession.level.original
levelMajorVersion = @levelSession.level.majorVersion
sessionID = @clientResponseObject.originalSessionID
sessionTotalScore = @newScoresObject[sessionID].totalScore
opponentSessionID = _.pull(_.keys(@newScoresObject), sessionID)
opponentSessionTotalScore = @newScoresObject[opponentSessionID].totalScore
opposingTeam = scoringUtils.calculateOpposingTeam(@clientResponseObject.originalSessionTeam)
catch e
cb e
retrieveAllOpponentSessionIDs sessionID, (err, opponentSessionIDs) ->
if err? then return cb err, null
queryParameters =
totalScore:
$gt: opponentSessionTotalScore
_id:
$nin: opponentSessionIDs
'level.original': levelOriginalID
'level.majorVersion': levelMajorVersion
submitted: true
team: opposingTeam
if opponentSessionTotalScore < 30
# Don't play a ton of matches at low scores--skip some in proportion to how close to 30 we are.
# TODO: this could be made a lot more flexible.
queryParameters['totalScore']['$gt'] = opponentSessionTotalScore + 2 * (30 - opponentSessionTotalScore) / 20
limitNumber = 1
sortParameters = totalScore: 1
selectString = '_id totalScore'
query = LevelSession.findOne(queryParameters)
.sort(sortParameters)
.limit(limitNumber)
.select(selectString)
.lean()
#console.log "Finding session with score near #{opponentSessionTotalScore}"
query.exec (err, session) ->
if err? then return cb err, session
unless session then return cb 'no session was found'
#console.log "Found session with score #{session.totalScore}"
cb err, session._id
retrieveAllOpponentSessionIDs = (sessionID, cb) ->
selectString = 'matches.opponents.sessionID matches.date submitDate'
LevelSession.findOne({_id: sessionID}).select(selectString).lean().exec (err, session) ->
if err? then return cb err, null
opponentSessionIDs = (match.opponents[0].sessionID for match in session.matches when match.date > session.submitDate)
cb err, opponentSessionIDs
addNewSessionsToQueue = (sessionID, callback) ->
sessions = [@clientResponseObject.originalSessionID, sessionID]
scoringUtils.addPairwiseTaskToQueue sessions, callback
markSessionAsDoneRanking = (sessionID, cb) ->
#console.log 'Marking session as done ranking...'
LevelSession.update {_id: sessionID}, {isRanking: false}, cb

View file

@ -0,0 +1,21 @@
log = require 'winston'
async = require 'async'
errors = require '../../commons/errors'
scoringUtils = require './scoringUtils'
LevelSession = require '../../levels/sessions/LevelSession'
module.exports = recordTwoGames = (req, res) ->
sessions = req.body.sessions
#console.log 'Recording non-chained result of', sessions?[0]?.name, sessions[0]?.metrics?.rank, 'and', sessions?[1]?.name, sessions?[1]?.metrics?.rank
return if scoringUtils.simulatorIsTooOld req, res
req.body?.simulator?.user = '' + req.user?._id
yetiGuru = clientResponseObject: req.body, isRandomMatch: true
async.waterfall [
scoringUtils.calculateSessionScores.bind(yetiGuru) # Fetches a few small properties from both sessions, prepares @levelSessionUpdates with the score part
scoringUtils.indexNewScoreArray.bind(yetiGuru) # Creates and returns @newScoresObject, no query
scoringUtils.addMatchToSessionsAndUpdate.bind(yetiGuru) # Adds matches to the session updates and does the writes
scoringUtils.updateUserSimulationCounts.bind(yetiGuru, req.user?._id)
], (err, successMessageObject) ->
if err? then return errors.serverError res, "There was an error recording the single game: #{err}"
scoringUtils.sendResponseObject res, {message: 'The single game was submitted successfully!'}

View file

@ -0,0 +1,236 @@
log = require 'winston'
async = require 'async'
bayes = new (require 'bayesian-battle')()
LevelSession = require '../../levels/sessions/LevelSession'
User = require '../../users/User'
SIMULATOR_VERSION = 3
module.exports.scoringTaskTimeoutInSeconds = 600
module.exports.scoringTaskQueue = null
module.exports.simulatorIsTooOld = (req, res) ->
clientSimulator = req.body.simulator
return false if clientSimulator?.version >= SIMULATOR_VERSION
message = "Old simulator version #{clientSimulator?.version}, need to clear cache and get version #{SIMULATOR_VERSION}."
log.debug "400: #{message}"
res.send 400, message
res.end()
true
module.exports.sendResponseObject = (res, object) ->
res.setHeader('Content-Type', 'application/json')
res.send(object)
res.end()
module.exports.formatSessionInformation = (session) ->
sessionID: session._id
team: session.team ? 'No team'
transpiledCode: session.transpiledCode
submittedCodeLanguage: session.submittedCodeLanguage
teamSpells: session.teamSpells ? {}
levelID: session.levelID
creatorName: session.creatorName
creator: session.creator
totalScore: session.totalScore
module.exports.calculateSessionScores = (callback) ->
sessionIDs = _.pluck @clientResponseObject.sessions, 'sessionID'
async.map sessionIDs, retrieveOldSessionData.bind(@), (err, oldScores) =>
if err? then return callback err, {error: 'There was an error retrieving the old scores'}
try
oldScoreArray = _.toArray putRankingFromMetricsIntoScoreObject @clientResponseObject, oldScores
newScoreArray = updatePlayerSkills oldScoreArray
createSessionScoreUpdate.call @, scoreObject for scoreObject in newScoreArray
callback err, newScoreArray
catch e
callback e
retrieveOldSessionData = (sessionID, callback) ->
formatOldScoreObject = (session) =>
oldScoreObject =
standardDeviation: session.standardDeviation ? 25/3
meanStrength: session.meanStrength ? 25
totalScore: session.totalScore ? (25 - 1.8*(25/3))
id: sessionID
submittedCodeLanguage: session.submittedCodeLanguage
if session.leagues?.length
_.find(@clientResponseObject.sessions, sessionID: sessionID).leagues = session.leagues
oldScoreObject.leagues = []
for league in session.leagues
oldScoreObject.leagues.push
leagueID: league.leagueID
stats:
id: sessionID
standardDeviation: league.stats.standardDeviation ? 25/3
meanStrength: league.stats.meanStrength ? 25
totalScore: league.stats.totalScore ? (25 - 1.8*(25/3))
oldScoreObject
return formatOldScoreObject @levelSession if sessionID is @levelSession?._id # No need to fetch again
query = _id: sessionID
selection = 'standardDeviation meanStrength totalScore submittedCodeLanguage leagues'
LevelSession.findOne(query).select(selection).lean().exec (err, session) ->
return callback err, {'error': 'There was an error retrieving the session.'} if err?
callback err, formatOldScoreObject session
putRankingFromMetricsIntoScoreObject = (taskObject, scoreObject) ->
scoreObject = _.indexBy scoreObject, 'id'
sharedLeagueIDs = (league.leagueID for league in (taskObject.sessions[0].leagues ? []) when _.find(taskObject.sessions[1].leagues, leagueID: league.leagueID))
for session in taskObject.sessions
scoreObject[session.sessionID].gameRanking = session.metrics.rank
for league in (session.leagues ? []) when league.leagueID in sharedLeagueIDs
# We will also score any shared leagues, and we indicate that by assigning a non-null gameRanking to them.
_.find(scoreObject[session.sessionID].leagues, leagueID: league.leagueID).stats.gameRanking = session.metrics.rank
return scoreObject
updatePlayerSkills = (oldScoreArray) ->
newScoreArray = bayes.updatePlayerSkills oldScoreArray
scoreObjectA = newScoreArray[0]
scoreObjectB = newScoreArray[1]
for leagueA in (scoreObjectA.leagues ? []) when leagueA.stats.gameRanking?
leagueB = _.find scoreObjectB.leagues, leagueID: leagueA.leagueID
[leagueA.stats, leagueB.stats] = bayes.updatePlayerSkills [leagueA.stats, leagueB.stats]
leagueA.stats.updated = leagueB.stats.updated = true
newScoreArray
createSessionScoreUpdate = (scoreObject) ->
newTotalScore = scoreObject.meanStrength - 1.8 * scoreObject.standardDeviation
scoreHistoryAddition = [Date.now(), newTotalScore]
@levelSessionUpdates ?= {}
@levelSessionUpdates[scoreObject.id] =
meanStrength: scoreObject.meanStrength
standardDeviation: scoreObject.standardDeviation
totalScore: newTotalScore
$push: {scoreHistory: {$each: [scoreHistoryAddition], $slice: -1000}}
randomSimulationIndex: Math.random()
for league, leagueIndex in (scoreObject.leagues ? [])
continue unless league.stats.updated
newTotalScore = league.stats.meanStrength - 1.8 * league.stats.standardDeviation
scoreHistoryAddition = [scoreHistoryAddition[0], newTotalScore]
leagueSetPrefix = "leagues.#{leagueIndex}.stats."
@levelSessionUpdates[scoreObject.id].$set ?= {}
@levelSessionUpdates[scoreObject.id].$push ?= {}
@levelSessionUpdates[scoreObject.id].$set[leagueSetPrefix + 'meanStrength'] = league.stats.meanStrength
@levelSessionUpdates[scoreObject.id].$set[leagueSetPrefix + 'standardDeviation'] = league.stats.standardDeviation
@levelSessionUpdates[scoreObject.id].$set[leagueSetPrefix + 'totalScore'] = newTotalScore
@levelSessionUpdates[scoreObject.id].$push[leagueSetPrefix + 'scoreHistory'] = {$each: [scoreHistoryAddition], $slice: -1000}
module.exports.indexNewScoreArray = (newScoreArray, callback) ->
newScoresObject = _.indexBy newScoreArray, 'id'
@newScoresObject = newScoresObject
callback null, newScoresObject
module.exports.addMatchToSessionsAndUpdate = (newScoreObject, callback) ->
matchObject = {}
matchObject.date = new Date()
matchObject.opponents = {}
for session in @clientResponseObject.sessions
sessionID = session.sessionID
matchObject.opponents[sessionID] = match = {}
match.sessionID = sessionID
match.userID = session.creator
match.name = session.name
match.totalScore = session.totalScore
match.metrics = {}
match.metrics.rank = Number(newScoreObject[sessionID]?.gameRanking ? 0)
match.codeLanguage = newScoreObject[sessionID].submittedCodeLanguage
#log.info "Match object computed, result: #{JSON.stringify(matchObject, null, 2)}"
#log.info 'Writing match object to database...'
#use bind with async to do the writes
sessionIDs = _.pluck @clientResponseObject.sessions, 'sessionID'
async.each sessionIDs, updateMatchesInSession.bind(@, matchObject), (err) ->
callback err
updateMatchesInSession = (matchObject, sessionID, callback) ->
currentMatchObject = {}
currentMatchObject.date = matchObject.date
currentMatchObject.metrics = matchObject.opponents[sessionID].metrics
opponentsClone = _.cloneDeep matchObject.opponents
opponentsClone = _.omit opponentsClone, sessionID
opponentsArray = _.toArray opponentsClone
currentMatchObject.opponents = opponentsArray
currentMatchObject.codeLanguage = matchObject.opponents[opponentsArray[0].sessionID].codeLanguage # TODO: we have our opponent code language in twice, do we maybe want our own code language instead?
#currentMatchObject.simulator = @clientResponseObject.simulator # Uncomment when actively debugging simulation mismatches
#currentMatchObject.randomSeed = parseInt(@clientResponseObject.randomSeed or 0, 10) # Uncomment when actively debugging simulation mismatches
sessionUpdateObject = @levelSessionUpdates[sessionID]
sessionUpdateObject.$push.matches = {$each: [currentMatchObject], $slice: -200}
myScoreObject = @newScoresObject[sessionID]
opponentSession = _.find @clientResponseObject.sessions, (session) -> session.sessionID isnt sessionID
for league, leagueIndex in myScoreObject.leagues ? []
continue unless league.stats.updated
opponentLeagueTotalScore = _.find(opponentSession.leagues, leagueID: league.leagueID).stats.totalScore ? (25 - 1.8*(25/3))
leagueMatch = _.cloneDeep currentMatchObject
leagueMatch.opponents[0].totalScore = opponentLeagueTotalScore
sessionUpdateObject.$push["leagues.#{leagueIndex}.stats.matches"] = {$each: [leagueMatch], $slice: -200}
#log.info "Update for #{sessionID} is #{JSON.stringify(sessionUpdateObject, null, 2)}"
LevelSession.update {_id: sessionID}, sessionUpdateObject, callback
module.exports.updateUserSimulationCounts = (reqUserID, callback) ->
incrementUserSimulationCount reqUserID, 'simulatedBy', (err) =>
if err? then return callback err
#console.log 'Incremented user simulation count!'
unless @isRandomMatch
incrementUserSimulationCount @levelSession.creator, 'simulatedFor', callback
else
callback null
incrementUserSimulationCount = (userID, type, callback) =>
return callback null unless userID
inc = {}
inc[type] = 1
User.update {_id: userID}, {$inc: inc}, (err, affected) ->
log.error "Error incrementing #{type} for #{userID}: #{err}" if err
callback err
module.exports.calculateOpposingTeam = (sessionTeam) ->
teams = ['ogres', 'humans']
opposingTeams = _.pull teams, sessionTeam
return opposingTeams[0]
module.exports.sendEachTaskPairToTheQueue = (taskPairs, callback) ->
async.each taskPairs, sendTaskPairToQueue, callback
sendTaskPairToQueue = (taskPair, callback) ->
module.exports.scoringTaskQueue.sendMessage {sessions: taskPair}, 5, (err, data) -> callback? err, data
module.exports.generateTaskPairs = (submittedSessions, sessionToScore) ->
taskPairs = []
for session in submittedSessions
if session.toObject?
session = session.toObject()
teams = ['ogres', 'humans']
opposingTeams = _.pull teams, sessionToScore.team
if String(session._id) isnt String(sessionToScore._id) and session.team in opposingTeams
#console.log 'Adding game to taskPairs!'
taskPairs.push [sessionToScore._id, String session._id]
return taskPairs
module.exports.addPairwiseTaskToQueue = (taskPair, cb) ->
LevelSession.findOne(_id: taskPair[0]).lean().exec (err, firstSession) =>
if err? then return cb err
LevelSession.find(_id: taskPair[1]).exec (err, secondSession) =>
if err? then return cb err
try
taskPairs = module.exports.generateTaskPairs(secondSession, firstSession)
catch e
if e then return cb e
module.exports.sendEachTaskPairToTheQueue taskPairs, (taskPairError) ->
if taskPairError? then return cb taskPairError
cb null

View file

@ -13,10 +13,6 @@ module.exports.setup = (app) ->
handler = loadQueueHandler 'scoring'
handler.messagesInQueueCount req, res
app.post '/queue/scoring/resimulateAllSessions', (req, res) ->
handler = loadQueueHandler 'scoring'
handler.resimulateAllSessions req, res
app.post '/queue/scoring/getTwoGames', (req, res) ->
handler = loadQueueHandler 'scoring'
handler.getTwoGames req, res

View file

@ -51,10 +51,8 @@ UserSchema.methods.isAnonymous = ->
@get 'anonymous'
UserSchema.methods.getUserInfo = ->
info =
id : @get('_id')
email : if @get('anonymous') then 'Unregistered User' else @get('email')
return info
id: @get('_id')
email: if @get('anonymous') then 'Unregistered User' else @get('email')
UserSchema.methods.trackActivity = (activityName, increment) ->
now = new Date()