mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-04-07 18:44:29 -04:00
Merge remote-tracking branch 'codecombat/master'
This commit is contained in:
commit
f6e3800970
57 changed files with 1545 additions and 999 deletions
app
core
lib/surface
locale
schemas/models
styles/play
templates
clans
play
views
scripts/mongodb/migrations
server
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 we’ll 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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 we’ll 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:
|
||||
|
|
|
@ -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."
|
||||
|
||||
|
|
|
@ -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."
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
h1
|
||||
text-align: center
|
||||
|
||||
&.league-header
|
||||
margin: 15px 0 20px 0
|
||||
|
||||
.tournament-blurb
|
||||
margin-top: -10px
|
||||
margin-bottom: 10px
|
||||
|
|
|
@ -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%
|
||||
|
|
154
app/styles/play/level/duel-stats-view.sass
Normal file
154
app/styles/play/level/duel-stats-view.sass
Normal 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
|
|
@ -385,6 +385,9 @@
|
|||
.offer
|
||||
display: none
|
||||
|
||||
img
|
||||
margin: 5px 10px
|
||||
|
||||
p
|
||||
color: white
|
||||
|
||||
|
|
|
@ -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%
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!
|
||||
|
||||
|
|
|
@ -29,6 +29,8 @@
|
|||
|
||||
#multiplayer-status-view
|
||||
|
||||
#duel-stats-view
|
||||
|
||||
#playback-view
|
||||
|
||||
#thang-hud
|
||||
|
|
21
app/templates/play/level/duel-stats-view.jade
Normal file
21
app/templates/play/level/duel-stats-view.jade
Normal 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
|
||||
|
||||
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -9,5 +9,6 @@
|
|||
#canvas-top-gradient.gradient
|
||||
#gold-view.secret.expanded
|
||||
#level-chat-view
|
||||
#duel-stats-view
|
||||
#playback-view
|
||||
#thang-hud
|
||||
|
|
|
@ -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?()
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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('-')
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
120
app/views/play/level/DuelStatsView.coffee
Normal file
120
app/views/play/level/DuelStatsView.coffee
Normal 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
|
|
@ -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: ->
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.");
|
||||
}
|
|
@ -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) ->
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
109
server/queues/scoring/createNewTask.coffee
Normal file
109
server/queues/scoring/createNewTask.coffee
Normal 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'}
|
80
server/queues/scoring/dispatchTaskToConsumer.coffee
Normal file
80
server/queues/scoring/dispatchTaskToConsumer.coffee
Normal 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
|
114
server/queues/scoring/getTwoGames.coffee
Normal file
114
server/queues/scoring/getTwoGames.coffee
Normal 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
|
||||
|
||||
|
172
server/queues/scoring/processTaskResult.coffee
Normal file
172
server/queues/scoring/processTaskResult.coffee
Normal 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
|
21
server/queues/scoring/recordTwoGames.coffee
Normal file
21
server/queues/scoring/recordTwoGames.coffee
Normal 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!'}
|
236
server/queues/scoring/scoringUtils.coffee
Normal file
236
server/queues/scoring/scoringUtils.coffee
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Add table
Reference in a new issue