@coorpacademy/progression-engine
Version:
245 lines (211 loc) • 8.36 kB
JavaScript
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
import map from 'lodash/fp/map';
import get from 'lodash/fp/get';
import find from 'lodash/fp/find';
import pipe from 'lodash/fp/pipe';
import head from 'lodash/fp/head';
import last from 'lodash/fp/last';
import filter from 'lodash/fp/filter';
import sortBy from 'lodash/fp/sortBy';
import isEqual from 'lodash/fp/isEqual';
import isEmpty from 'lodash/fp/isEmpty';
import shuffle from 'lodash/fp/shuffle';
import includes from 'lodash/fp/includes';
import findIndex from 'lodash/fp/findIndex';
import intersection from 'lodash/fp/intersection';
import selectRule from '../rule-engine/select-rule';
import updateVariables from '../rule-engine/apply-instructions';
import updateState from '../update-state';
const hasNoMoreLives = (config, state) => !config.livesDisabled && state.lives <= 0;
const getContentRef = get('content.ref');
const hasRemainingLifeRequests = state => state.remainingLifeRequests > 0;
const stepIsAlreadyExtraLife = state => getContentRef(state) === 'extraLife';
const hasRulesToApply = chapterContent => {
return !!(chapterContent && Array.isArray(chapterContent.rules) && !isEmpty(chapterContent.rules));
};
export const nextSlidePool = (config, availableContent, state) => {
if (state.nextContent.type === 'chapter') {
const content = find({ ref: state.nextContent.ref }, availableContent) || null;
return {
currentChapterContent: content,
nextChapterContent: content,
temporaryNextContent: { type: 'slide', ref: '' }
};
}
const lastSlideRef = pipe(get('slides'), last)(state);
const _currentIndex = findIndex(({ slides }) => !!find({ _id: lastSlideRef }, slides), availableContent);
const currentIndex = _currentIndex !== -1 ? _currentIndex : 0;
const currentChapterPool = availableContent[currentIndex] || null;
const slidesAnsweredForThisChapter = intersection(state.slides, currentChapterPool && map('_id', currentChapterPool.slides) || []);
const isChapterCompleted = slidesAnsweredForThisChapter.length >= config.slidesToComplete;
const hasRules = hasRulesToApply(currentChapterPool);
const shouldChangeChapter = !hasRules && isChapterCompleted;
if (shouldChangeChapter) {
return {
currentChapterContent: currentChapterPool,
nextChapterContent: availableContent[currentIndex + 1] || null,
temporaryNextContent: { type: 'slide', ref: '' }
};
}
return {
currentChapterContent: currentChapterPool,
nextChapterContent: currentChapterPool,
temporaryNextContent: state.nextContent
};
};
const _getChapterContent = (config, availableContent, state) => {
const firstContent = head(availableContent);
if (!state) {
return {
currentChapterContent: firstContent,
nextChapterContent: firstContent,
temporaryNextContent: { type: 'slide', ref: '' }
};
}
return nextSlidePool(config, availableContent, state);
};
const getChapterContent = (config, availableContent, state) => {
const res = _getChapterContent(config, availableContent, state);
if (!res.currentChapterContent) {
return null;
}
return res;
};
const sortByPosition = sortBy(slide => typeof slide.position === 'number' ? -slide.position : 0);
const pickNextSlide = pipe(shuffle, sortByPosition, head);
const isTargetingIsCorrect = condition => condition.target.scope === 'slide' && condition.target.field === 'isCorrect';
const getIsCorrect = (isCorrect, chapterRule) => {
if (chapterRule.conditions.some(isTargetingIsCorrect)) return isCorrect;
return null;
};
const computeNextSlide = (config, chapterContent, state) => {
const remainingSlides = filter(pipe(get('_id'), slideId => !state || !includes(slideId, state.slides)), chapterContent.slides);
return {
type: 'slide',
ref: pickNextSlide(remainingSlides)._id
};
};
export const prepareStateToSwitchChapters = (chapterRule, state) => {
if (!state) {
return state;
}
return updateVariables(chapterRule.instructions)(_extends({}, state, {
nextContent: chapterRule.destination
}));
};
export const computeNextStepForNewChapter = (config, state, chapterRule, isCorrect, availableContent) => {
// eslint-disable-next-line no-use-before-define
const nextStep = computeNextStep(config, prepareStateToSwitchChapters(chapterRule, state), availableContent, null);
if (!nextStep) {
return null;
}
return {
nextContent: nextStep.nextContent,
instructions: chapterRule.instructions.concat(nextStep.instructions || []),
isCorrect: getIsCorrect(isCorrect, chapterRule)
};
};
const extendPartialAction = (action, state) => {
if (!action) {
return null;
}
switch (action.type) {
case 'answer':
{
const nextContent = action.payload.content || (state ? state.nextContent : { ref: '', type: 'node' });
return {
type: 'answer',
payload: {
answer: action.payload.answer,
godMode: action.payload.godMode,
isCorrect: action.payload.isCorrect,
content: nextContent,
nextContent,
instructions: null
}
};
}
case 'extraLifeAccepted':
{
const nextContent = state ? state.nextContent : { ref: '', type: 'node' };
return {
type: 'extraLifeAccepted',
payload: {
content: nextContent,
nextContent,
instructions: null
}
};
}
default:
return null;
}
};
const computeNextStep = (config, _state, availableContent, partialAction) => {
const action = extendPartialAction(partialAction, _state);
const isCorrect = !!action && action.type === 'answer' && !!action.payload.isCorrect;
const answer = !!action && action.type === 'answer' && action.payload.answer || [];
const state = !_state || !action ? _state : updateState(config, _state, [action]);
const chapterContent = getChapterContent(config, availableContent, state);
if (!chapterContent) {
return null;
}
const { currentChapterContent, nextChapterContent, temporaryNextContent } = chapterContent;
const hasRules = hasRulesToApply(nextChapterContent);
if (!hasRules) {
if (state && hasNoMoreLives(config, state)) {
return {
nextContent: !stepIsAlreadyExtraLife(state) && hasRemainingLifeRequests(state) ? { type: 'node', ref: 'extraLife' } : { type: 'failure', ref: 'failExitNode' },
instructions: null,
isCorrect
};
} else if (!nextChapterContent) {
// If user has answered all questions, return success endpoint
return {
nextContent: {
type: 'success',
ref: 'successExitNode'
},
instructions: null,
isCorrect
};
}
}
if (hasRules) {
const allAnswers = !!state && state.allAnswers || [];
// $FlowFixMe nextChapterContent.rules = array not emtpy -> checked by hasRulesToApply
const chapterRule = selectRule(nextChapterContent.rules, _extends({}, state, {
nextContent: temporaryNextContent,
allAnswers: [...allAnswers, {
slideRef: temporaryNextContent.ref,
answer,
isCorrect
}]
}));
if (!chapterRule) {
return null;
}
if (chapterRule.destination.type === 'chapter') {
return computeNextStepForNewChapter(config, state, chapterRule, isCorrect, availableContent);
}
return {
nextContent: chapterRule.destination,
instructions: chapterRule.instructions,
isCorrect: isEqual(currentChapterContent, nextChapterContent) ? getIsCorrect(isCorrect, chapterRule) : isCorrect
};
}
if (nextChapterContent && Array.isArray(nextChapterContent.slides) && nextChapterContent.slides.length > 0) {
const stateWithDecrementedLives = state ? _extends({}, state, {
nextContent: temporaryNextContent
}) : state;
const nextContent = computeNextSlide(config, nextChapterContent, stateWithDecrementedLives);
return {
nextContent,
instructions: null,
isCorrect
};
}
return null;
};
export default computeNextStep;
//# sourceMappingURL=compute-next-step.js.map