UNPKG

@coorpacademy/progression-engine

Version:

340 lines (335 loc) 13.3 kB
"use strict"; exports.__esModule = true; exports.prepareStateToSwitchChapters = exports.nextSlidePool = exports.computeNextStepForReview = exports.computeNextStepForNewChapter = exports.computeNextStep = void 0; var _getOr2 = _interopRequireDefault(require("lodash/fp/getOr")); var _intersection2 = _interopRequireDefault(require("lodash/fp/intersection")); var _findIndex2 = _interopRequireDefault(require("lodash/fp/findIndex")); var _includes2 = _interopRequireDefault(require("lodash/fp/includes")); var _shuffle2 = _interopRequireDefault(require("lodash/fp/shuffle")); var _isEmpty2 = _interopRequireDefault(require("lodash/fp/isEmpty")); var _isEqual2 = _interopRequireDefault(require("lodash/fp/isEqual")); var _sortBy2 = _interopRequireDefault(require("lodash/fp/sortBy")); var _filter2 = _interopRequireDefault(require("lodash/fp/filter")); var _last2 = _interopRequireDefault(require("lodash/fp/last")); var _slice2 = _interopRequireDefault(require("lodash/fp/slice")); var _size2 = _interopRequireDefault(require("lodash/fp/size")); var _head2 = _interopRequireDefault(require("lodash/fp/head")); var _pipe2 = _interopRequireDefault(require("lodash/fp/pipe")); var _find2 = _interopRequireDefault(require("lodash/fp/find")); var _get2 = _interopRequireDefault(require("lodash/fp/get")); var _map2 = _interopRequireDefault(require("lodash/fp/map")); var _selectRule = _interopRequireDefault(require("../rule-engine/select-rule")); var _applyInstructions = _interopRequireDefault(require("../rule-engine/apply-instructions")); var _updateState = _interopRequireDefault(require("../update-state")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } const hasNoMoreLives = (config, state) => !config.livesDisabled && state.lives <= 0; const getContentRef = (0, _get2.default)('content.ref'); const hasRemainingLifeRequests = state => state.remainingLifeRequests > 0; const stepIsAlreadyExtraLife = state => getContentRef(state) === 'extraLife'; const hasRulesToApply = chapterContent => { return !!(chapterContent && Array.isArray(chapterContent.rules) && !(0, _isEmpty2.default)(chapterContent.rules)); }; const nextSlidePool = (config, availableContent, state) => { if (state.nextContent.type === 'chapter') { const content = (0, _find2.default)({ ref: state.nextContent.ref }, availableContent) || null; return { currentChapterContent: content, nextChapterContent: content, temporaryNextContent: { type: 'slide', ref: '' } }; } const lastSlideRef = (0, _pipe2.default)((0, _get2.default)('slides'), _last2.default)(state); const _currentIndex = (0, _findIndex2.default)(({ slides }) => !!(0, _find2.default)({ _id: lastSlideRef }, slides), availableContent); const currentIndex = _currentIndex !== -1 ? _currentIndex : 0; const currentChapterPool = availableContent[currentIndex] || null; const currentChapterSlideIds = (0, _pipe2.default)((0, _get2.default)('slides'), (0, _map2.default)('_id'))(currentChapterPool || []); const slidesAnsweredForThisChapter = (0, _intersection2.default)(state.slides, currentChapterSlideIds); const isChapterCompleted = (0, _size2.default)(slidesAnsweredForThisChapter) >= Math.min(config.slidesToComplete, (0, _size2.default)(currentChapterSlideIds)); const hasRules = hasRulesToApply(currentChapterPool); const shouldChangeChapter = !hasRules && isChapterCompleted; if (shouldChangeChapter) { const nextChapterContent = (0, _pipe2.default)((0, _slice2.default)(currentIndex + 1, (0, _size2.default)(availableContent)), (0, _filter2.default)(content => !(0, _isEmpty2.default)(content.slides)), _head2.default)(availableContent); return { currentChapterContent: currentChapterPool, nextChapterContent, temporaryNextContent: { type: 'slide', ref: '' } }; } return { currentChapterContent: currentChapterPool, nextChapterContent: currentChapterPool, temporaryNextContent: state.nextContent }; }; exports.nextSlidePool = nextSlidePool; const _getChapterContent = (config, availableContent, state) => { const firstContent = (0, _pipe2.default)((0, _filter2.default)(content => !(0, _isEmpty2.default)(content.slides)), _head2.default)(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 = (0, _sortBy2.default)(slide => typeof slide.position === 'number' ? -slide.position : 0); const pickNextSlide = (0, _pipe2.default)(_shuffle2.default, sortByPosition, _head2.default); 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 = (0, _filter2.default)((0, _pipe2.default)((0, _get2.default)('_id'), slideId => !state || !(0, _includes2.default)(slideId, state.slides)), chapterContent.slides); return { type: 'slide', ref: pickNextSlide(remainingSlides)._id }; }; const prepareStateToSwitchChapters = (chapterRule, state) => { if (!state) { return state; } return (0, _applyInstructions.default)(chapterRule.instructions)({ ...state, nextContent: chapterRule.destination }); }; exports.prepareStateToSwitchChapters = prepareStateToSwitchChapters; 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) }; }; exports.computeNextStepForNewChapter = computeNextStepForNewChapter; 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 : (0, _updateState.default)(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 = (0, _selectRule.default)(nextChapterContent.rules, { ...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: (0, _isEqual2.default)(currentChapterContent, nextChapterContent) ? getIsCorrect(isCorrect, chapterRule) : isCorrect }; } if (nextChapterContent && Array.isArray(nextChapterContent.slides) && nextChapterContent.slides.length > 0) { const stateWithDecrementedLives = state ? { ...state, nextContent: temporaryNextContent } : state; const nextContent = computeNextSlide(config, nextChapterContent, stateWithDecrementedLives); return { nextContent, instructions: null, isCorrect }; } return null; }; exports.computeNextStep = computeNextStep; const getNextSlide = (config, state, availableContent) => { if (!state) return (0, _get2.default)(['0', 'slides', '0'], availableContent); // We filter slides in other to exclude already answered slides. // In case of lag on recallUpdate lambda that would not update the review database // and the getSlide lambda would return as available slide, an already proposed slide const answeredSlides = (0, _getOr2.default)([], 'slides', state); const filteredSlides = (0, _filter2.default)(slide => !answeredSlides.includes(slide._id), availableContent[0].slides); const current = (0, _get2.default)('step.current', state); if (current <= config.slidesToComplete) { return filteredSlides[0]; } return null; }; const getNextPendingSlide = (currentSlide, oldPendingSlides, newPendingSlide) => { if ((0, _isEmpty2.default)(newPendingSlide)) return null; const indexPendingSlide = (0, _findIndex2.default)(s => s === currentSlide, oldPendingSlides) + 1; const index = indexPendingSlide === oldPendingSlides.length ? 0 : indexPendingSlide; return oldPendingSlides[index]; }; const computeNextStepForReview = (config, _state, availableContent, partialAction) => { const action = extendPartialAction(partialAction, _state); const isCorrect = !!action && action.type === 'answer' && !!action.payload.isCorrect; const state = !_state || !action ? _state : (0, _updateState.default)(config, _state, [action]); const correctAnswers = state ? (0, _filter2.default)(a => a.isCorrect, state.allAnswers) : []; if (correctAnswers.length === config.slidesToComplete) { return { nextContent: { type: 'success', ref: 'successExitNode' }, instructions: null, isCorrect }; } const nextSlide = getNextSlide(config, state, availableContent); if (!nextSlide) { // state is null during creation, and we can have the extreme case of creation without slide if (!state) { return null; } else { // if there is no more slides, two scenarios are possible const pendingSlides = !_state ? [] : _state.pendingSlides; const pendingSlide = getNextPendingSlide(state.nextContent.ref, pendingSlides, state.pendingSlides); // all other questions have been already right answered BUT you fail the last question if (!pendingSlide && !isCorrect) { return { nextContent: { type: 'slide', ref: state.nextContent.ref }, instructions: null, isCorrect }; } if (!pendingSlide) { // all other questions have been already right answered, so we close the progression return null; } // or there are wrong question that should be reviewed again return { nextContent: { type: 'slide', ref: pendingSlide }, instructions: null, isCorrect }; } } return { nextContent: { type: 'slide', ref: nextSlide._id }, instructions: null, isCorrect }; }; exports.computeNextStepForReview = computeNextStepForReview; //# sourceMappingURL=compute-next-step.js.map