rename learn concepts: (lesson/level)->stage, stage->level

pull/2069/head
Thibault Duplessis 2016-06-30 13:45:17 +02:00
parent c7fc5c8ab2
commit c64dba92a7
35 changed files with 358 additions and 359 deletions

View File

@ -20,15 +20,15 @@ object Learn extends LilaController {
}
}
private val levelForm = Form(mapping(
"level" -> nonEmptyText,
private val stageForm = Form(mapping(
"stage" -> nonEmptyText,
"score" -> number
)(Tuple2.apply)(Tuple2.unapply))
def level = AuthBody { implicit ctx =>
def stage = AuthBody { implicit ctx =>
me =>
implicit val body = ctx.body
levelForm.bindFromRequest.fold(
stageForm.bindFromRequest.fold(
err => BadRequest.fuccess,
data => env.api.setScore(me, data._1, data._2) >>
env.api.get(me).map { progress =>

View File

@ -137,7 +137,7 @@ GET /study/$id<\w{8}>/$chapterId<\w{8}> controllers.Study.chapter(id: S
# Learn
GET /learn controllers.Learn.index
POST /learn/level controllers.Learn.level
POST /learn/stage controllers.Learn.stage
# Round
GET /$gameId<\w{8}> controllers.Round.watcher(gameId: String, color: String = "white")

View File

@ -6,10 +6,10 @@ import reactivemongo.bson._
object BSONHandlers {
private implicit val LevelProgressScoreHandler = intAnyValHandler[LevelProgress.Score](_.value, LevelProgress.Score.apply)
private implicit val LevelProgressTriesHandler = intAnyValHandler[LevelProgress.Tries](_.value, LevelProgress.Tries.apply)
private implicit val LevelProgressBSONHandler = Macros.handler[LevelProgress]
private implicit val StageProgressScoreHandler = intAnyValHandler[StageProgress.Score](_.value, StageProgress.Score.apply)
private implicit val StageProgressTriesHandler = intAnyValHandler[StageProgress.Tries](_.value, StageProgress.Tries.apply)
private implicit val StageProgressBSONHandler = Macros.handler[StageProgress]
private implicit val LearnProgressLevelsHandler = BSON.MapDocument.MapHandler[LevelProgress]
private implicit val LearnProgressStagesHandler = BSON.MapDocument.MapHandler[StageProgress]
implicit val LearnProgressHandler = Macros.handler[LearnProgress]
}

View File

@ -5,9 +5,9 @@ import lila.common.PimpedJson._
object JSONHandlers {
private implicit val LevelProgressScoreWriter = intAnyValWriter[LevelProgress.Score](_.value)
private implicit val LevelProgressTriesWriter = intAnyValWriter[LevelProgress.Tries](_.value)
implicit val LevelProgressWriter = Json.writes[LevelProgress]
private implicit val StageProgressScoreWriter = intAnyValWriter[StageProgress.Score](_.value)
private implicit val StageProgressTriesWriter = intAnyValWriter[StageProgress.Tries](_.value)
implicit val StageProgressWriter = Json.writes[StageProgress]
implicit val LearnProgressWriter = Json.writes[LearnProgress]
}

View File

@ -13,7 +13,7 @@ final class LearnApi(coll: Coll) {
private def save(p: LearnProgress): Funit =
coll.update($id(p.id), p, upsert = true).void
def setScore(user: User, level: String, score: Int) = get(user) flatMap { prog =>
save(prog.withScore(level, LevelProgress.Score(score)))
def setScore(user: User, stage: String, score: Int) = get(user) flatMap { prog =>
save(prog.withScore(stage, StageProgress.Score(score)))
}
}

View File

@ -6,15 +6,15 @@ import lila.user.User
case class LearnProgress(
_id: User.ID,
levels: Map[String, LevelProgress],
stages: Map[String, StageProgress],
createdAt: DateTime,
updatedAt: DateTime) {
def id = _id
def withScore(level: String, s: LevelProgress.Score) = copy(
levels = levels + (
level -> levels.getOrElse(level, LevelProgress empty level).withScore(s)
def withScore(stage: String, s: StageProgress.Score) = copy(
stages = stages + (
stage -> stages.getOrElse(stage, StageProgress empty stage).withScore(s)
),
updatedAt = DateTime.now)
}
@ -23,7 +23,7 @@ object LearnProgress {
def empty(userId: User.ID) = LearnProgress(
_id = userId,
levels = Map.empty,
stages = Map.empty,
createdAt = DateTime.now,
updatedAt = DateTime.now)
}

View File

@ -2,13 +2,13 @@ package lila.learn
import org.joda.time.DateTime
case class LevelProgress(
level: String,
score: LevelProgress.Score,
tries: LevelProgress.Tries,
case class StageProgress(
stage: String,
score: StageProgress.Score,
tries: StageProgress.Tries,
updatedAt: DateTime) {
import LevelProgress._
import StageProgress._
def withScore(s: Score) = copy(
score = Score(score.value max s.value),
@ -16,10 +16,10 @@ case class LevelProgress(
updatedAt = DateTime.now)
}
object LevelProgress {
object StageProgress {
def empty(level: String) = LevelProgress(
level = level,
def empty(stage: String) = StageProgress(
stage = stage,
score = Score(0),
tries = Tries(0),
updatedAt = DateTime.now)

View File

@ -407,10 +407,10 @@
#learn_side {
border: none;
}
#learn_side .lessons {
#learn_side .stages {
margin: 10px;
}
#learn_side .lesson {
#learn_side .stage {
display: flex;
align-items: center;
transition: 0.13s;
@ -419,46 +419,46 @@
font-weight: 300;
opacity: 0.9;
}
#learn_side .lesson img,
#learn_side .lesson i {
#learn_side .stage img,
#learn_side .stage i {
width: 30px;
height: 30px;
opacity: 0.9;
margin-right: 5px;
padding: 10px;
}
#learn_side .lesson i::before {
#learn_side .stage i::before {
font-size: 30px;
}
#learn_side .lesson.home {
#learn_side .stage.home {
font-weight: bold;
}
#learn_side .lesson.next img {
#learn_side .stage.next img {
background: #3893E8;
-webkit-filter: brightness(0.9);
}
#learn_side .lesson.next img:hover {
#learn_side .stage.next img:hover {
-webkit-filter: brightness(1.2);
}
#learn_side .lesson.done img {
#learn_side .stage.done img {
background: #4caf50;
}
#learn_side .lesson.future img {
#learn_side .stage.future img {
opacity: 0.7;
background: #f57c00;
cursor: default;
}
#learn_side .lesson:hover {
#learn_side .stage:hover {
-webkit-filter: brightness(1.2);
}
#learn_app .map .lessons {
#learn_app .map .stages {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
margin: 10px;
}
#learn_app .map .lesson {
#learn_app .map .stage {
position: relative;
display: flex;
align-items: center;
@ -472,13 +472,13 @@
box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.3), 0 2px 2px 0 rgba(0, 0, 0, 0.2);
white-space: nowrap;
}
#learn_app .map .lesson img {
#learn_app .map .stage img {
width: 75px;
height: 75px;
margin: 5px;
opacity: 0.9;
}
#learn_app .map .lesson h2 {
#learn_app .map .stage h2 {
font-size: 2em;
margin: 0 0 5px -2px;
}
@ -487,36 +487,36 @@
-webkit-filter: brightness(1.1);
}
}
#learn_app .map .lesson.next {
#learn_app .map .stage.next {
background: #3893E8;
-webkit-filter: brightness(0.9);
}
#learn_app .map .lesson.next {
#learn_app .map .stage.next {
animation: 1.7s soft-bright ease-in-out infinite;
}
#learn_app .map .lesson.next:hover {
#learn_app .map .stage.next:hover {
-webkit-filter: brightness(1.2);
}
#learn_app .map .lesson.done {
#learn_app .map .stage.done {
background: #4caf50;
}
#learn_app .map .lesson.future {
#learn_app .map .stage.future {
opacity: 0.7;
background: #f57c00;
cursor: default;
}
#learn_app .map .lesson.future img {
#learn_app .map .stage.future img {
opacity: 0.7;
-webkit-filter: blur(2px);
filter: blur(2px);
}
#learn_app .map a.lesson.current,
#learn_app .map a.lesson:hover {
#learn_app .map a.stage.current,
#learn_app .map a.stage:hover {
-webkit-filter: brightness(1.08);
transform: scale(1.05);
opacity: 1;
}
#learn_app .map .lesson .ribbon-wrapper {
#learn_app .map .stage .ribbon-wrapper {
width: 85px;
height: 88px;
overflow: hidden;
@ -524,7 +524,7 @@
top: -3px;
right: -3px;
}
#learn_app .map .lesson .ribbon {
#learn_app .map .stage .ribbon {
font-size: 15px;
font-weight: bold;
color: #333;
@ -539,37 +539,37 @@
color: #6a6340;
box-shadow: 0px 0px 3px rgba(0,0,0,0.3);
}
#learn_app .map .lesson .ribbon i {
#learn_app .map .stage .ribbon i {
color: #d59120;
animation: 0.7s soft-hue ease-in-out infinite;
}
#learn_app .map .lesson .ribbon:before,
#learn_app .map .lesson .ribbon:after {
#learn_app .map .stage .ribbon:before,
#learn_app .map .stage .ribbon:after {
content: "";
border-left: 3px solid transparent;
border-right: 3px solid transparent;
position:absolute;
bottom: -3px;
}
#learn_app .map .lesson .ribbon:before {
#learn_app .map .stage .ribbon:before {
left: 0;
}
#learn_app .map .lesson .ribbon:after {
#learn_app .map .stage .ribbon:after {
right: 0;
}
#learn_app .map .lesson .ribbon.done {
#learn_app .map .stage .ribbon.done {
background-color: #BFDC7A;
}
#learn_app .map .lesson .ribbon.done:before,
#learn_app .map .lesson .ribbon.done:after {
#learn_app .map .stage .ribbon.done:before,
#learn_app .map .stage .ribbon.done:after {
border-top: 3px solid #6e8900;
}
#learn_app .map .lesson .ribbon.next {
#learn_app .map .stage .ribbon.next {
background-color: #B3E5FC;
}
#learn_app .map .lesson .ribbon.next:before,
#learn_app .map .lesson .ribbon.next:after {
#learn_app .map .stage .ribbon.next:before,
#learn_app .map .stage .ribbon.next:after {
border-top: 3px solid #536DFE;
}

View File

@ -1,66 +0,0 @@
var m = require('mithril');
var stageBuilder = require('./stage');
var makeProgress = require('./progress').ctrl;
var sound = require('./sound');
module.exports = function(blueprint, opts) {
var onStageComplete = function() {
progress.inc();
var s = makeStage(stage.blueprint.id + 1);
if (s) {
stage = s;
}
else {
vm.completed = true;
sound.lessonEnd();
opts.setScore(blueprint, vm.score);
}
m.redraw();
};
var makeStage = function(id) {
var stageBlueprint = blueprint.stages[id - 1];
if (stageBlueprint) return stageBuilder(stageBlueprint, {
onScore: function(score) {
vm.score += score;
m.redraw();
},
onComplete: onStageComplete,
restart: restartStage
})
}
var restartStage = function() {
vm.score = vm.score - stage.vm.score;
stage = makeStage(stage.blueprint.id);
m.redraw();
};
var stage = makeStage(opts.stage || 1);
var progress = makeProgress({
steps: blueprint.stages.length,
step: stage.blueprint.id
});
var vm = {
score: 0,
starting: stage.blueprint.id === 1,
completed: false
};
sound.lessonStart();
return {
blueprint: blueprint,
progress: progress,
vm: vm,
stage: function() {
return stage;
},
start: function() {
vm.starting = false;
},
restartStage: restartStage
}
};

View File

@ -0,0 +1,111 @@
var m = require('mithril');
var makeItems = require('./item').ctrl;
var itemView = require('./item').view;
var makeChess = require('./chess');
var ground = require('./ground');
var scoring = require('./score');
var sound = require('./sound');
var promotion = require('./promotion');
module.exports = function(blueprint, opts) {
var items = makeItems({
apples: blueprint.apples
});
var vm = {
initialized: false,
lastStep: false,
completed: false,
failed: false,
score: 0,
nbMoves: 0
};
setTimeout(function() {
vm.initialized = true;
m.redraw();
}, 100);
var addScore = function(v) {
vm.score += v;
opts.onScore(v);
};
var complete = function() {
setTimeout(function() {
if (vm.failed) return opts.restart();
vm.lastStep = false;
vm.completed = true;
sound.levelEnd();
var rank = scoring.getLevelRank(blueprint, vm.nbMoves);
var bonus = scoring.getLevelBonus(rank);
addScore(bonus);
ground.stop();
m.redraw();
setTimeout(opts.onComplete, 1200);
}, ground.data().stats.dragged ? 0 : 250);
};
// cheat
Mousetrap.bind(['shift+enter'], complete);
var detectFailure = function() {
var failed = false;
(blueprint.failure || []).forEach(function(f) {
failed = failed || f(chess);
});
if (failed) sound.failure();
return failed;
};
var sendMove = function(orig, dest, prom) {
vm.nbMoves++;
var move = chess.move(orig, dest, prom);
if (!move) throw 'Invalid move!';
var appleTaken = false;
items.withItem(move.to, function(item) {
if (item === 'apple') {
addScore(scoring.apple);
items.remove(move.to);
appleTaken = true;
}
});
if (!items.hasItem('apple')) complete();
else if (appleTaken) sound.take();
else sound.move();
if (vm.completed) return;
vm.failed = vm.failed || detectFailure();
chess.color(blueprint.color);
ground.color(blueprint.color, chess.dests());
m.redraw();
};
var onMove = function(orig, dest) {
if (!promotion.start(orig, dest, sendMove)) sendMove(orig, dest);
};
var chess = makeChess(
blueprint.fen,
blueprint.emptyApples ? [] : items.appleKeys());
ground.set({
chess: chess,
orientation: blueprint.color,
onMove: onMove,
items: {
render: function(pos, key) {
return items.withItem(key, itemView);
}
},
shapes: blueprint.shapes
});
if (blueprint.id !== 1) sound.levelStart();
return {
blueprint: blueprint,
items: items,
vm: vm,
restart: opts.restart
};
};

View File

@ -2,7 +2,6 @@ var m = require('mithril');
var map = require('./map/mapMain');
var mapSide = require('./map/mapSide');
var run = require('./run/runMain');
var makeLesson = require('./lesson');
module.exports = function(element, opts) {
@ -10,7 +9,7 @@ module.exports = function(element, opts) {
m.route(element, '/', {
'/': map(opts),
'/:id/:stage': run(opts),
'/:id/:level': run(opts),
'/:id': run(opts)
});

View File

@ -4,7 +4,7 @@ var mapView = require('./mapView');
module.exports = function(opts) {
return {
controller: function() {
opts.lessonId = null;
opts.stageId = null;
opts.route = 'map';
return {
data: opts.data

View File

@ -1,13 +1,13 @@
var m = require('mithril');
var util = require('../util');
var lessons = require('../lesson/list');
var stages = require('../stage/list');
module.exports = function(opts) {
return {
controller: function() {
return {
data: opts.data,
current: opts.lessonId,
current: opts.stageId,
enabled: function() {
return opts.route === 'run';
}
@ -15,28 +15,28 @@ module.exports = function(opts) {
},
view: function(ctrl) {
if (ctrl.enabled()) return m('div.learn.map', [
m('div.lessons', [
m('a.lesson.home', {
m('div.stages', [
m('a.stage.home', {
href: '/',
config: m.route
}, [
m('i[data-icon=I]'),
'Learn chess'
]),
lessons.list.map(function(l) {
var result = ctrl.data.levels[l.key];
var previousDone = l.id === 1 ? true : !!ctrl.data.levels[lessons.get(l.id - 1).key];
stages.list.map(function(s) {
var result = ctrl.data.stages[s.key];
var previousDone = s.id === 1 ? true : !!ctrl.data.stages[stages.get(s.id - 1).key];
var status = result ? 'done' : (previousDone ? 'next' : 'future')
var current = l.id === ctrl.current;
var current = s.id === ctrl.current;
return m(status === 'future' ? 'span' : 'a', {
class: 'lesson ' + status + (current ? ' current' : ''),
href: '/' + l.id,
class: 'stage ' + status + (current ? ' current' : ''),
href: '/' + s.id,
config: status === 'future' ? null : m.route
}, [
m('img', {
src: status === 'future' ? util.assetUrl + 'images/learn/help.svg' : l.image
src: status === 'future' ? util.assetUrl + 'images/learn/help.svg' : s.image
}),
m('h2', l.title)
m('h2', s.title)
]);
})
])

View File

@ -1,7 +1,7 @@
var m = require('mithril');
var util = require('../util');
var scoring = require('../score');
var lessons = require('../lesson/list');
var stages = require('../stage/list');
function makeStars(nb) {
var stars = [];
@ -12,9 +12,9 @@ function makeStars(nb) {
return stars;
}
function ribbon(l, status, result) {
function ribbon(s, status, result) {
if (status === 'future') return;
var rank = result ? scoring.getLevelRank(l, result.score) : null;
var rank = result ? scoring.getStageRank(s, result.score) : null;
var content = rank ? makeStars(rank) : 'play!';
return m('div.ribbon-wrapper',
m('div.ribbon', {
@ -25,22 +25,22 @@ function ribbon(l, status, result) {
module.exports = function(ctrl) {
return m('div.learn.map', [
m('div.lessons', lessons.list.map(function(l) {
var result = ctrl.data.levels[l.key];
var previousDone = l.id === 1 ? true : !!ctrl.data.levels[lessons.get(l.id - 1).key];
m('div.stages', stages.list.map(function(s) {
var result = ctrl.data.stages[s.key];
var previousDone = s.id === 1 ? true : !!ctrl.data.stages[stages.get(s.id - 1).key];
var status = result ? 'done' : (previousDone ? 'next' : 'future')
return m(status === 'future' ? 'span' : 'a', {
class: 'lesson ' + status,
href: '/' + l.id,
class: 'stage ' + status,
href: '/' + s.id,
config: status === 'future' ? null : m.route
}, [
ribbon(l, status, result),
ribbon(s, status, result),
m('img', {
src: status === 'future' ? util.assetUrl + 'images/learn/help.svg' : l.image
src: status === 'future' ? util.assetUrl + 'images/learn/help.svg' : s.image
}),
m('div.text', [
m('h2', l.title),
m('p.subtitle', l.subtitle)
m('h2', s.title),
m('p.subtitle', s.subtitle)
])
]);
}))

View File

@ -1,18 +0,0 @@
var m = require('mithril');
module.exports = function(lesson, next) {
return m('div.screen-overlay', {
onclick: lesson.start
},
m('div.screen', [
m('h1', 'Level ' + lesson.blueprint.id + ': ' + lesson.blueprint.title),
lesson.blueprint.illustration,
m('p', m.trust(lesson.blueprint.intro)),
m('div.buttons',
m('a.next', {
onclick: lesson.start
}, "Let's go!")
)
])
);
};

View File

@ -1,31 +1,31 @@
var m = require('mithril');
var lessons = require('../lesson/list');
var makeLesson = require('../lesson');
var stages = require('../stage/list');
var makeStage = require('../stage');
var xhr = require('../xhr');
module.exports = function(opts) {
var setScore = function(level, score) {
xhr.setScore(level.key, score).then(function(data) {
var setScore = function(stage, score) {
xhr.setScore(stage.key, score).then(function(data) {
opts.data = data;
});
};
var lesson = makeLesson(lessons.get(m.route.param("id")), {
stage: m.route.param('stage') || 1,
var stage = makeStage(stages.get(m.route.param("id")), {
level: m.route.param('level') || 1,
setScore: setScore
});
opts.route = 'run';
opts.lessonId = lesson.blueprint.id;
opts.stageId = stage.blueprint.id;
var getNext = function() {
return lessons.get(lesson.blueprint.id + 1);
return stages.get(stage.blueprint.id + 1);
};
return {
lesson: function() {
return lesson;
stage: function() {
return stage;
},
getNext: getNext
};

View File

@ -3,55 +3,55 @@ var chessground = require('chessground');
var ground = require('../ground');
var classSet = chessground.util.classSet;
var congrats = require('../congrats');
var lessonStarting = require('./lessonStarting');
var lessonComplete = require('./lessonComplete');
var stageStarting = require('./stageStarting');
var stageComplete = require('./stageComplete');
var renderPromotion = require('../promotion').view;
var renderScore = require('./scoreView');
var renderProgress = require('../progress').view;
function renderFailed(stage) {
function renderFailed(level) {
return m('div.failed', [
m('h2', 'Puzzle failed!'),
m('button', {
onclick: stage.restart
onclick: level.restart
}, 'Retry')
]);
}
module.exports = function(ctrl) {
var lesson = ctrl.lesson();
var stage = lesson.stage();
var stage = ctrl.stage();
var level = stage.level();
return m('div', {
class: classSet({
'lichess_game': true,
'initialized': stage.vm.initialized,
'starting': stage.vm.starting,
'completed': stage.vm.completed,
'last-step': stage.vm.lastStep
}) + ' ' + stage.blueprint.cssClass
'initialized': level.vm.initialized,
'starting': level.vm.starting,
'completed': level.vm.completed,
'last-step': level.vm.lastStep
}) + ' ' + level.blueprint.cssClass
}, [
lesson.vm.starting ? lessonStarting(lesson) : null,
lesson.vm.completed ? lessonComplete(lesson, ctrl.getNext()) : null,
stage.vm.starting ? stageStarting(stage) : null,
stage.vm.completed ? stageComplete(stage, ctrl.getNext()) : null,
m('div.lichess_board_wrap', [
m('div.lichess_board', chessground.view(ground.instance)),
renderPromotion(stage),
renderPromotion(level),
]),
m('div.lichess_ground', [
m('div.title', [
m('img', {
src: lesson.blueprint.image
src: stage.blueprint.image
}),
m('div.text', [
m('h2', lesson.blueprint.title),
m('p.subtitle', lesson.blueprint.subtitle)
m('h2', stage.blueprint.title),
m('p.subtitle', stage.blueprint.subtitle)
])
]),
stage.vm.failed ? renderFailed(stage) : m('div.goal',
stage.vm.completed ? congrats() : m.trust(stage.blueprint.goal)
level.vm.failed ? renderFailed(level) : m('div.goal',
level.vm.completed ? congrats() : m.trust(level.blueprint.goal)
),
renderProgress(lesson.progress),
renderScore(lesson)
renderProgress(stage.progress),
renderScore(stage)
])
]);
};

View File

@ -1,10 +1,10 @@
var m = require('mithril');
module.exports = function(lesson) {
module.exports = function(stage) {
return m('div.score', [
m('span.plus', {
config: function(el, isUpdate, ctx) {
var score = lesson.vm.score;
var score = stage.vm.score;
if (isUpdate) {
var diff = score - (ctx.prev || 0);
if (diff > 0) {
@ -26,11 +26,11 @@ module.exports = function(lesson) {
m('span.legend', 'SCORE'),
m('span.value', {
config: function(el, isUpdate, ctx) {
var score = lesson.vm.score;
var score = stage.vm.score;
if (!ctx.spread) {
el.textContent = lichess.numberFormat(score);
ctx.spread = $.spreadNumber(el, 50, function() {
var diff = lesson.vm.score - ctx.prev;
var diff = stage.vm.score - ctx.prev;
return Math.min(1000, 5 * diff);
}, score);
} else if (score !== ctx.prev) ctx.spread(score, (score - ctx.prev) / 5);

View File

@ -8,21 +8,21 @@ function makeStars(rank) {
return stars;
}
module.exports = function(lesson, next) {
module.exports = function(stage, next) {
return m('div.screen-overlay', {
onclick: function(e) {
if (e.target.classList.contains('screen-overlay')) m.route('/');
}
},
m('div.screen', [
m('div.stars', makeStars(scoring.getLevelRank(lesson.blueprint, lesson.vm.score))),
m('h1', 'Level ' + lesson.blueprint.id + ' complete'),
m('div.stars', makeStars(scoring.getStageRank(stage.blueprint, stage.vm.score))),
m('h1', 'Stage ' + stage.blueprint.id + ' complete'),
m('span.score', [
'Your score: ',
m('span', {
config: function(el, isUpdate) {
if (!isUpdate) setTimeout(function() {
var score = lesson.vm.score;
var score = stage.vm.score;
$.spreadNumber(el, 50, function() {
return 3000;
}, 0)(score);
@ -31,7 +31,7 @@ module.exports = function(lesson, next) {
}, 0)
]),
m('p', [
m.trust(lesson.blueprint.complete)
m.trust(stage.blueprint.complete)
]),
m('div.buttons', [
next ? m('a.next', {

View File

@ -0,0 +1,18 @@
var m = require('mithril');
module.exports = function(stage, next) {
return m('div.screen-overlay', {
onclick: stage.start
},
m('div.screen', [
m('h1', 'Stage ' + stage.blueprint.id + ': ' + stage.blueprint.title),
stage.blueprint.illustration,
m('p', m.trust(stage.blueprint.intro)),
m('div.buttons',
m('a.next', {
onclick: stage.start
}, "Let's go!")
)
])
);
};

View File

@ -2,43 +2,43 @@ var util = require('./util');
var apple = 50;
function getStageRank(s, nbMoves) {
function getLevelRank(s, nbMoves) {
var late = nbMoves - s.nbMoves;
if (late <= 0) return 1;
else if (late <= Math.max(1, s.nbMoves / 8)) return 2;
return 3;
}
var stageBonus = {
var levelBonus = {
1: 500,
2: 300,
3: 100
};
function getStageBonus(rank) {
return stageBonus[Math.min(rank, 3)];
}
function getStageMaxScore(l) {
return util.readKeys(l.apples).length * apple + stageBonus[1];
function getLevelBonus(rank) {
return levelBonus[Math.min(rank, 3)];
}
function getLevelMaxScore(l) {
return l.stages.reduce(function(sum, s) {
return sum + getStageMaxScore(s);
return util.readKeys(l.apples).length * apple + levelBonus[1];
}
function getStageMaxScore(l) {
return l.levels.reduce(function(sum, s) {
return sum + getLevelMaxScore(s);
}, 0);
}
function getLevelRank(l, score) {
var max = getLevelMaxScore(l);
if (score >= max - Math.max(200, l.stages.length * 50)) return 1;
if (score >= max - Math.max(200, l.stages.length * 300)) return 2;
function getStageRank(l, score) {
var max = getStageMaxScore(l);
if (score >= max - Math.max(200, l.levels.length * 50)) return 1;
if (score >= max - Math.max(200, l.levels.length * 300)) return 2;
return 3;
}
module.exports = {
apple: apple,
getStageRank: getStageRank,
getStageBonus: getStageBonus,
getLevelRank: getLevelRank
getLevelRank: getLevelRank,
getLevelBonus: getLevelBonus,
getStageRank: getStageRank
};

View File

@ -18,10 +18,10 @@ var make = function(file, volume) {
module.exports = {
move: make('standard/Move'),
take: make('sfx/Tournament3rd', 0.7),
stageStart: make('other/ping'),
stageEnd: make('other/energy3'),
lessonStart: make('other/guitar'),
// lessonEnd: make('sfx/Tournament1st'),
lessonEnd: make('other/gewonnen'),
levelStart: make('other/ping'),
levelEnd: make('other/energy3'),
stageStart: make('other/guitar'),
// stageEnd: make('sfx/Tournament1st'),
stageEnd: make('other/gewonnen'),
failure: make('other/failure')
};

View File

@ -1,111 +1,66 @@
var m = require('mithril');
var makeItems = require('./item').ctrl;
var itemView = require('./item').view;
var makeChess = require('./chess');
var ground = require('./ground');
var scoring = require('./score');
var levelBuilder = require('./level');
var makeProgress = require('./progress').ctrl;
var sound = require('./sound');
var promotion = require('./promotion');
module.exports = function(blueprint, opts) {
var items = makeItems({
apples: blueprint.apples
var onLevelComplete = function() {
progress.inc();
var s = makeLevel(level.blueprint.id + 1);
if (s) {
level = s;
}
else {
vm.completed = true;
sound.stageEnd();
opts.setScore(blueprint, vm.score);
}
m.redraw();
};
var makeLevel = function(id) {
var levelBlueprint = blueprint.levels[id - 1];
if (levelBlueprint) return levelBuilder(levelBlueprint, {
onScore: function(score) {
vm.score += score;
m.redraw();
},
onComplete: onLevelComplete,
restart: restartLevel
})
}
var restartLevel = function() {
vm.score = vm.score - level.vm.score;
level = makeLevel(level.blueprint.id);
m.redraw();
};
var level = makeLevel(opts.level || 1);
var progress = makeProgress({
steps: blueprint.levels.length,
step: level.blueprint.id
});
var vm = {
initialized: false,
lastStep: false,
completed: false,
failed: false,
score: 0,
nbMoves: 0
starting: level.blueprint.id === 1,
completed: false
};
setTimeout(function() {
vm.initialized = true;
m.redraw();
}, 100);
var addScore = function(v) {
vm.score += v;
opts.onScore(v);
};
var complete = function() {
setTimeout(function() {
if (vm.failed) return opts.restart();
vm.lastStep = false;
vm.completed = true;
sound.stageEnd();
var rank = scoring.getStageRank(blueprint, vm.nbMoves);
var bonus = scoring.getStageBonus(rank);
addScore(bonus);
ground.stop();
m.redraw();
setTimeout(opts.onComplete, 1200);
}, ground.data().stats.dragged ? 0 : 250);
};
// cheat
Mousetrap.bind(['shift+enter'], complete);
var detectFailure = function() {
var failed = false;
(blueprint.failure || []).forEach(function(f) {
failed = failed || f(chess);
});
if (failed) sound.failure();
return failed;
};
var sendMove = function(orig, dest, prom) {
vm.nbMoves++;
var move = chess.move(orig, dest, prom);
if (!move) throw 'Invalid move!';
var appleTaken = false;
items.withItem(move.to, function(item) {
if (item === 'apple') {
addScore(scoring.apple);
items.remove(move.to);
appleTaken = true;
}
});
if (!items.hasItem('apple')) complete();
else if (appleTaken) sound.take();
else sound.move();
if (vm.completed) return;
vm.failed = vm.failed || detectFailure();
chess.color(blueprint.color);
ground.color(blueprint.color, chess.dests());
m.redraw();
};
var onMove = function(orig, dest) {
if (!promotion.start(orig, dest, sendMove)) sendMove(orig, dest);
};
var chess = makeChess(
blueprint.fen,
blueprint.emptyApples ? [] : items.appleKeys());
ground.set({
chess: chess,
orientation: blueprint.color,
onMove: onMove,
items: {
render: function(pos, key) {
return items.withItem(key, itemView);
}
},
shapes: blueprint.shapes
});
if (blueprint.id !== 1) sound.stageStart();
sound.stageStart();
return {
blueprint: blueprint,
items: items,
progress: progress,
vm: vm,
restart: opts.restart
};
level: function() {
return level;
},
start: function() {
vm.starting = false;
},
restartLevel: restartLevel
}
};

View File

@ -11,7 +11,7 @@ module.exports = {
illustration: m('div.is2d.no-square',
m('piece.bishop.white')
),
stages: [{
levels: [{
goal: 'Grab all the stars!',
fen: '8/8/8/8/8/5B2/8/8 w - - 0 1',
apples: 'd5 g8',
@ -42,6 +42,6 @@ module.exports = {
fen: '8/3B4/8/8/8/2B5/8/8 w - - 0 1',
apples: 'a5 b4 c2 c4 c7 e7 f5 f6 g8 h4 h7',
nbMoves: 11
}].map(util.toStage),
}].map(util.toLevel),
complete: 'Congratulations! You can command a bishop.'
};

View File

@ -11,7 +11,7 @@ module.exports = {
image: imgUrl,
intro: 'You are ready for combat! In this level, we will be capturing enemy pieces.',
illustration: m('img', {src: imgUrl}),
stages: [{
levels: [{
goal: 'Grab all the stars!',
fen: '8/8/8/8/8/5B2/8/8 w - - 0 1',
apples: 'd5 g8',
@ -42,7 +42,7 @@ module.exports = {
fen: '8/3B4/8/8/8/2B5/8/8 w - - 0 1',
apples: 'a5 b4 c2 c4 c7 e7 f5 f6 g8 h4 h7',
nbMoves: 11
}].map(util.toStage),
}].map(util.toLevel),
complete: 'Congratulations! You can command a bishop.'
};

View File

@ -11,7 +11,7 @@ module.exports = {
image: imgUrl,
intro: 'Bring your king to safety, and deploy your rook for attack!',
illustration: m('img', {src: imgUrl}),
stages: [{
levels: [{
goal: 'Grab all the stars!',
fen: '8/8/8/8/8/5B2/8/8 w - - 0 1',
apples: 'd5 g8',
@ -42,7 +42,7 @@ module.exports = {
fen: '8/3B4/8/8/8/2B5/8/8 w - - 0 1',
apples: 'a5 b4 c2 c4 c7 e7 f5 f6 g8 h4 h7',
nbMoves: 11
}].map(util.toStage),
}].map(util.toLevel),
complete: 'Congratulations! You can command a bishop.'
};

View File

@ -11,7 +11,7 @@ module.exports = {
image: imgUrl,
intro: 'When the opponent pawn moved by two squares, you can take it like if it moved by one square.',
illustration: m('img', {src: imgUrl}),
stages: [{
levels: [{
goal: 'Grab all the stars!',
fen: '8/8/8/8/8/5B2/8/8 w - - 0 1',
apples: 'd5 g8',
@ -42,7 +42,7 @@ module.exports = {
fen: '8/3B4/8/8/8/2B5/8/8 w - - 0 1',
apples: 'a5 b4 c2 c4 c7 e7 f5 f6 g8 h4 h7',
nbMoves: 11
}].map(util.toStage),
}].map(util.toLevel),
complete: 'Congratulations! You can command a bishop.'
};

View File

@ -11,7 +11,7 @@ module.exports = {
illustration: m('div.is2d.no-square',
m('piece.king.white')
),
stages: [{
levels: [{
goal: 'The king is slow.',
fen: '8/8/8/8/3K4/8/8/8 w - - 0 1',
apples: 'e6',
@ -28,7 +28,7 @@ module.exports = {
apples: 'b5 c5 d6 e3 f3 g4',
nbMoves: 8
}].map(function(s, i) {
s = util.toStage(s, i);
s = util.toLevel(s, i);
s.emptyApples = true;
return s;
}),

View File

@ -11,7 +11,7 @@ module.exports = {
illustration: m('div.is2d.no-square',
m('piece.knight.white')
),
stages: [{
levels: [{
goal: 'Knights have a fancy way<br>of jumping around!',
fen: '8/8/8/8/4N3/8/8/8 w - - 0 1',
apples: 'c5 d7',
@ -42,6 +42,6 @@ module.exports = {
fen: '8/2n5/8/8/8/8/8/8 b - - 0 1',
apples: 'b4 b5 c6 c8 d4 d5 e3 e7 f5',
nbMoves: 9
}].map(util.toStage),
}].map(util.toLevel),
complete: 'Congratulations! You have mastered the knight.'
};

View File

@ -1,6 +1,6 @@
var util = require('../util');
var lessons = [
var stages = [
require('./rook'),
require('./bishop'),
@ -13,12 +13,12 @@ var lessons = [
require('./castling'),
require('./enpassant')
].map(util.toLesson);
].map(util.toStage);
module.exports = {
list: lessons,
list: stages,
get: function(id) {
return lessons.filter(function(l) {
return stages.filter(function(l) {
return l.id == id;
})[0];
}

View File

@ -12,7 +12,7 @@ module.exports = {
illustration: m('div.is2d.no-square',
m('piece.pawn.white')
),
stages: [{
levels: [{
goal: 'Pawns move one square only.<br>But when they reach the other side of the board, they become a stronger piece!',
fen: '8/8/8/P7/8/8/8/8 w - - 0 1',
apples: 'f3',
@ -60,6 +60,6 @@ module.exports = {
fen: '8/8/8/8/8/8/2PPPP2/8 w - - 0 1',
apples: 'c5 d5 e5 f5 d3 e4',
nbMoves: 9
}].map(util.toStage),
}].map(util.toLevel),
complete: 'Congratulations! Pawns have no secrets for you.'
};

View File

@ -11,7 +11,7 @@ module.exports = {
illustration: m('div.is2d.no-square',
m('piece.queen.white')
),
stages: [{
levels: [{
goal: 'Grab all the stars!',
fen: '8/8/8/8/8/8/4Q3/8 w - - 0 1',
apples: 'e5 b8',
@ -37,6 +37,6 @@ module.exports = {
fen: '8/8/8/8/8/8/8/4q3 b - - 0 1',
apples: 'a6 d1 f2 f6 g6 g8 h1 h4',
nbMoves: 9
}].map(util.toStage),
}].map(util.toLevel),
complete: 'Congratulations! Queens have no secrets for you.'
};

View File

@ -11,7 +11,7 @@ module.exports = {
illustration: m('div.is2d.no-square',
m('piece.rook.white')
),
stages: [{
levels: [{
goal: 'Click on the rook<br>to bring it to the star!',
fen: '8/8/8/8/8/8/4R3/8 w - - 0 1',
apples: 'e7',
@ -43,6 +43,6 @@ module.exports = {
fen: '8/8/8/8/8/5R2/8/R7 w - - 0 1',
apples: 'a8 b7 d5 f2 f7 g4 g7 h5 h8',
nbMoves: 11
}].map(util.toStage),
}].map(util.toLevel),
complete: 'Congratulations! You have successfully mastered the rook.'
};

View File

@ -1,9 +1,9 @@
module.exports = {
toLesson: function(l, it) {
toStage: function(l, it) {
l.id = it + 1;
return l;
},
toStage: function(s, it) {
toLevel: function(s, it) {
s.id = it + 1;
s.color = / w /.test(s.fen) ? 'white' : 'black';
return s;

View File

@ -1,11 +1,11 @@
var m = require('mithril');
function setScore(levelKey, score) {
function setScore(stageKey, score) {
return m.request({
method: 'POST',
url: '/learn/level',
url: '/learn/stage',
data: {
level: levelKey,
stage: stageKey,
score: score
}
});