Commit c23c38be by Иван Кубота

Multiselect questions generators for 3 types without photo.

Text filter fixes
parent cb53a874
Pipeline #513 canceled with stage
......@@ -38,8 +38,10 @@
<script src="js/controller/exportLogic.js"></script>
<script src="js/controller/quizBits/main.js"></script>
<script src="js/controller/quizGenerator.js"></script>
<script src="js/controller/quizGenerator.js"></script>
<script src="js/controller/quizBits/checkbox.js"></script>
<script src="js/controller/quizBits/radio.js"></script>
<script src="main.js"></script>
......
let ComponentsOfProduct;
quizTypes.checkbox = [ ComponentsOfProduct = {
probability: 10,
correct: {min: 2, max: 4},
wrong: {min: 1, max: 4},
answers: {min: 3, max: 6},
doNotLogUnique: true,
questionCmpAmount: 2,
type: 'Select components of product',
products: {from: 3, to: 7},
minSimilarTags: 1,
from: qB.prebuild.similarTaggedProducts.fn,
question(income, log){
return `Какие ингредиенты входят в продукт ${lapk(income.correct.title)}`
},
answer(income, log){
const correct = shuffle(income.allUniq)
.slice(0, rand(this.correct.min, this.correct.max))
.map(i=>new Answer.Correct(textFormat(i.name))),
componentsNotInCorrect = qB.getComponentsNotInCorrect(income),
maxWrongs = Math.min(this.wrong.max, this.answers.max-correct.length),
wrongCount = rand(this.wrong.min, maxWrongs),
wrongs = componentsNotInCorrect
.slice(0, wrongCount)
.map(i=>new Answer.Wrong(textFormat(i.name)));
log.push('');
log.push(`Took ${correct.length} correct:`);
correct.forEach(a=>log.push(` > ${a.text}`))
log.push('');
log.push(`And ${wrongs.length} wrong:`);
wrongs.forEach(a=>log.push(` > ${a.text}`))
return shuffle(correct.concat(wrongs));
}
},
Object.assign({}, ComponentsOfProduct, {
probability: 4,
correct: {min: 2, max: 4},
wrong: {min: 2, max: 4},
answers: {min: 4, max: 8},
realMaxAnswers: 6,
question(income, log){
return `Какие ингредиенты НЕ входят в продукт ${lapk(income.correct.title)}`
},
answer(income, log){
log.push('Generate Answers by `Select components of product` subroutine');
const result = ComponentsOfProduct.answer.call(this, income, log);
log.push('Reversing');
const reversed = result
.map(a=>a instanceof Answer.Correct ? new Answer.Wrong(a.text) : new Answer.Correct(a.text));
const right = reversed.filter(a=>a instanceof Answer.Correct),
wrong = reversed.filter(a=>a instanceof Answer.Wrong);
return shuffle(right.concat(wrong.slice(0, Math.min(this.wrong.max, this.realMaxAnswers - right.length))));
}
}),
{
probability: 10,
correct: {min: 2, max: 4},
wrong: {min: 1, max: 3},
answers: {min: 4, max: 6},
doNotLogUnique: true,
questionCmpAmount: 2,
type: 'Select components of product',
products: {from: 2, to: 4},
minSimilarTags: 1,
from(log){
const products = qB.randomProduct({
minComponents: 1,
amount: this.products,
connectedByTags: true,
minSimilarTags: this.minSimilarTags,
doNotTrim: true
}, log),
componentsWithProducts = qB.getNotUniqComponentsWithProducts(products, 2);
log.push('Shared products count: '+componentsWithProducts.length);
if(componentsWithProducts.length === 0)
return false;
const bounded = componentsWithProducts.filter(cp=>
cp.products.length >= this.correct.min &&
products.length - cp.products.length >= this.wrong.min
);
log.push('After bounding by correct/wrong/full count '+bounded.length+' left');
if(bounded.length === 0){
log.push('Does not meet count criteria');
return false;
}
const chosen = rand(bounded);
log.push('Chosen component: '+chosen.component.name);
return {
component: chosen.component,
correct: chosen.products,
wrong: products.filter(p=>chosen.products.indexOf(p)===-1)
};
},
question(income, log){
return `В какие продукты входит следующий ингредиент ${lapk(textFormat(income.component.name))}`
},
answer(income, log){
const correct = income.correct
.slice(0, rand(this.correct))
.map(p=>new Answer.Correct(p.title)),
wrong = income.wrong
.slice(0, Math.min(rand(this.wrong), this.answers.max - correct.length))
.map(p=>new Answer.Wrong(p.title));
log.push('');
log.push(`Get ${correct.length} correct products:`);
correct.map(c=>log.push(` > ${c.text}`));
log.push('');
log.push(`Get ${correct.length} wrong products:`);
wrong.map(c=>log.push(` > ${c.text}`));
return shuffle(correct.concat(wrong));
}
}
];
\ No newline at end of file
......@@ -4,7 +4,8 @@ const qB = {
amount,
single,
connectedByTags,
minSimilarTags
minSimilarTags,
doNotTrim
}, log) {
let filtered = Object.values(dP.products);
......@@ -93,7 +94,7 @@ const qB = {
return false;
}
result = shuffle(filtered).slice(0, rand(minAmount, maxAmount));
result = shuffle(filtered).slice(0, doNotTrim?filtered.length:rand(minAmount, maxAmount));
log.push('Base products:');
result.forEach(p => log.push(` > ${p.title}`))
log.push('');
......@@ -124,6 +125,33 @@ const qB = {
return Object.values(hash);
},
getNotUniqComponentsWithProducts: function(products, minSharedProductCount) {
const hash = {};
products.forEach(
p=>
p
.getComponents()
.forEach(c=> {
const name = normalizeText( c.name );
(hash[ name ] || (hash[name] = [])).push({c, p});
})
);
return Object
.values(hash)
.filter(i => i.length >= minSharedProductCount)
.map(i=>({component: i[0].c, products: i.map(({p})=>p)}));
},
getComponentsNotInCorrect: function(income) {
return shuffle(
qB.subdivideComponents(
qB.getUniqComponents(income.wrong),
income.correct.getComponents()
)
);
},
prebuild: {
similarTaggedProducts: {
// minimal components in product
......@@ -151,10 +179,16 @@ const qB = {
if(cmps.length>=this.questionCmpAmount){
log.push(`Product ${lapk(product.title)} have >= ${this.questionCmpAmount} uniq components (${cmps.length}):`);
if(this.doNotLogUnique){
log.push( `Product ${lapk( product.title )} have ${cmps.length} uniq components:` );
}else{
log.push( `Product ${lapk( product.title )} have >= ${this.questionCmpAmount} uniq components (${cmps.length}):` );
}
const shuffled = shuffle(cmps.slice()),
used = shuffled.slice(0, this.questionCmpAmount);
const shuffled = shuffle(cmps.slice()).slice(0, this.questionCmpAmount);
cmps.forEach(c=>log.push(`${shuffled.indexOf(c)>-1?'+':' '} > ${c.name}`));
cmps.forEach(c=>log.push(`${!this.doNotLogUnique && used.indexOf(c)>-1?'+':' '} > ${c.name}`));
let wrong = products.slice();
wrong.splice(i,1);
......@@ -162,7 +196,8 @@ const qB = {
return {
correct: product,
wrong: wrong,
uniq: shuffled
uniq: used,
allUniq: shuffled
}
}
}
......
quizTypes.radio = [
{
questionCmpAmount: 4,
probability: 10,
type: 'Product contains 4 components',
products: {from: 4, to: 8},
minSimilarTags: 1,
from: qB.prebuild.similarTaggedProducts.fn,
question(income, log){
return 'В какой из продуктов входят данные ингредиенты: '+income.uniq.map(a=> textFormat(a.name)).join(', ');//products.map(p=>p.title)
},
answer( income, log ){
return shuffle(
[new Answer.Correct(textFormat(income.correct.title))]
.concat(income.wrong.map(p=>new Answer.Wrong(textFormat(p.title)))))
}
},
{
questionCmpAmount: 2,
probability: 8,
type: 'Component in product',
products: {from: 4, to: 6},
minSimilarTags: 1,
from: qB.prebuild.similarTaggedProducts.fn,
question(income, log){
return `Выберите один ингредиент, который входит в продукт ${lapk(income.correct.title)}`
},
answer( income, log ){
return shuffle(
[new Answer.Correct(textFormat(shuffle(income.uniq)[0].name))]
.concat(
shuffle(
qB.subdivideComponents( qB.getUniqComponents(income.wrong), income.correct.getComponents() )
)
.slice(0, rand(this.products.from-1, this.products.to-1))
.map(c=>new Answer.Wrong(textFormat(c.name)))
)
)
}
},
{
questionCmpAmount: 0,
probability: 8,
type: 'Select product description',
products: {from: 3, to: 5},
minSimilarTags: 1,
from: qB.prebuild.similarTaggedProducts.fn,
question(income, log){
return `Какое из этих описаний относится к продукту ${lapk(income.correct.title)}`
},
answer( income, log ){
return shuffle(
[new Answer.Correct(textFormat(income.correct.description))]
.concat(
income.wrong.map(
p => new Answer.Wrong(textFormat(p.description))
)
))
}
},
{
questionCmpAmount: 0,
probability: 8,
type: 'Select product that matches description',
products: {from: 4, to: 6},
minSimilarTags: 1,
from: qB.prebuild.similarTaggedProducts.fn,
question(income, log){
return `К какому из продуктов относится это описание:\n${textFormat(income.correct.description)}`
},
answer( income, log ){
return shuffle(
[new Answer.Correct(textFormat(income.correct.title))]
.concat(
income.wrong.map(
p => new Answer.Wrong(textFormat(p.title))
)
))
}
},
{
probability: 10,
answers: {from: 3, to: 6},
question: (product)=> `Выберите лишний ингредиент, НЕ входящий в продукт "${product.title}"`,
type: 'Do not contain',
from: (log)=>qB.randomProduct({ minComponents: 2, single: true }, log),
answer( product, log ){
const out =
shuffle(
dP.Product.getComponents(product)
.map(i=>({correct: false, text: i.name}))
)
.slice(0, rand(this.answers.from - 1, this.answers.to - 1)),
// get tags
tags = product.tags,
donors = [];
log.push(`Have ${dP.Product.getComponents(product).length} components. Took ${out.length}:`);
out.forEach(function(a) {
log.push(' > '+ a.text)
});
log.push('\nTags: '+dP.Product.getTags(product).map(t=>t.name).join(', '));
Object.keys(dP.products)
.map(k=>dP.products[k])
.forEach(item => {
if(item.id === product.id)
return;
const matchedCount = tags.filter(tid=>item.tags.indexOf(tid)>-1).length;
if(matchedCount>0){
donors.push({
count: matchedCount,
item,
title: item.title,
component: dP.Product.getComponents(item).map(a=>a.name)
});
}
});
log.push('Matched '+donors.length+' component donors.');
donors.sort((a,b)=>b.count-a.count);
if(!donors.length){
log.push('Not enough');
return false;
}
const maxCount = donors[0].count;
const minCount = Math.round(maxCount - maxCount/4);
const alreadyExist = out.reduce((store, i)=>{
store[normalizeText(i.text)] = true;
return store;
}, {'соль': true,'сольпощово': true, 'сохор': true});
log.push('Nearest candidate have got '+ maxCount +' similar tag.');
const may = ([].concat.apply(
[],
donors
.filter(i=>i.count > 0 && i.count >= minCount)
.map(i=>
i.component
.filter(c=>{
if(!c.trim())
return false;
const normalized = normalizeText(c);
if(normalized in alreadyExist)
return false;
alreadyExist[normalized] = true;
return true;
})
.map(x => ({count: i.count, item: i.item, text: x}))
)
));
log.push('Filtered candidates with ['+ (minCount===maxCount? minCount : minCount+' - '+ maxCount) +'] similar tags.');
if(may.length<2)
return false;
//console.clear()
//const addAnswersCount = Math.min(rand(this.answers.from, this.answers.to)-out.length, may.length);
const answer = rand(may);
log.push('Use '+ answer.item.title +' as donor.');
log.push('Because it has '+ answer.count +' similar tags: '+ dP.Product.getTags(answer.item).map(t=>t.name).join(', '));
out.push({correct: true, text: answer.text});
log.push('\n << '+ answer.text);
/*
for(let i = 0; i < addAnswersCount; i++){
if(may.length-i<0)
break;
const idx = Math.random()*(may.length-i)|0;
if(idx>=may.length || idx < 0)debugger
console.log(i, may[idx], may)
out.push({correct: true, text: may[idx]});
may[idx] = may[may.length - 1 - i]
}*/
console.log(dP.Product.getTags(product), donors)
return out;
}
}];
\ No newline at end of file
......@@ -3,7 +3,10 @@ const rand = function(a, b){
if(Array.isArray(a)){
return a[Math.random()*a.length|0];
}
if(typeof a === 'object' && 'max' in a){
b = a.max;
a = a.min || 0;
}
a = Math.ceil(a);
b = Math.floor(b);
return Math.floor(Math.random() * (b - a + 1)) + a;
......@@ -36,183 +39,7 @@ const quizTypes = {
checkboxPhoto: {
},
radio: [
{
questionCmpAmount: 4,
probability: 10,
type: 'Product contains 4 components',
products: {from: 4, to: 8},
minSimilarTags: 1,
from: qB.prebuild.similarTaggedProducts.fn,
question(income, log){
return 'В какой из продуктов входят данные ингредиенты: '+income.uniq.map(a=> textFormat(a.name)).join(', ');//products.map(p=>p.title)
},
answer( income, log ){
return shuffle(
[new Answer.Correct(textFormat(income.correct.title))]
.concat(income.wrong.map(p=>new Answer.Wrong(textFormat(p.title)))))
}
},
{
questionCmpAmount: 2,
probability: 10,
type: 'Component in product',
products: {from: 4, to: 6},
minSimilarTags: 1,
from: qB.prebuild.similarTaggedProducts.fn,
question(income, log){
return `Выберите один ингредиент, который входит в продукт ${lapk(income.correct.title)}`
},
answer( income, log ){
return shuffle(
[new Answer.Correct(textFormat(shuffle(income.uniq)[0].name))]
.concat(
shuffle(
qB.subdivideComponents( qB.getUniqComponents(income.wrong), income.correct.getComponents() )
)
.slice(0, rand(this.products.from-1, this.products.to-1))
.map(c=>new Answer.Wrong(textFormat(c.name)))
)
)
}
},
{
questionCmpAmount: 2,
probability: 10,
type: 'Select product description',
products: {from: 3, to: 5},
minSimilarTags: 1,
from: qB.prebuild.similarTaggedProducts.fn,
question(income, log){
return `Какое из этих описаний относится к продукту ${lapk(income.correct.title)}`
},
answer( income, log ){
return shuffle(
[new Answer.Correct(textFormat(income.correct.description))]
.concat(
income.wrong.map(
p => new Answer.Wrong(textFormat(p.description))
)
))
}
},
{
probability: 10,
answers: {from: 3, to: 6},
question: (product)=> `Выберите лишний ингредиент, НЕ входящий в продукт "${product.title}"`,
type: 'Do not contain',
from: (log)=>qB.randomProduct({ minComponents: 2, single: true }, log),
answer( product, log ){
const out =
shuffle(
dP.Product.getComponents(product)
.map(i=>({correct: false, text: i.name}))
)
.slice(0, rand(this.answers.from - 1, this.answers.to - 1)),
// get tags
tags = product.tags,
donors = [];
log.push(`Have ${dP.Product.getComponents(product).length} components. Took ${out.length}:`);
out.forEach(function(a) {
log.push(' > '+ a.text)
});
log.push('\nTags: '+dP.Product.getTags(product).map(t=>t.name).join(', '));
Object.keys(dP.products)
.map(k=>dP.products[k])
.forEach(item => {
if(item.id === product.id)
return;
const matchedCount = tags.filter(tid=>item.tags.indexOf(tid)>-1).length;
if(matchedCount>0){
donors.push({
count: matchedCount,
item,
title: item.title,
component: dP.Product.getComponents(item).map(a=>a.name)
});
}
});
log.push('Matched '+donors.length+' component donors.');
donors.sort((a,b)=>b.count-a.count);
if(!donors.length){
log.push('Not enough');
return false;
}
const maxCount = donors[0].count;
const minCount = Math.round(maxCount - maxCount/4);
const alreadyExist = out.reduce((store, i)=>{
store[normalizeText(i.text)] = true;
return store;
}, {'соль': true,'сольпощово': true, 'сохор': true});
log.push('Nearest candidate have got '+ maxCount +' similar tag.');
const may = ([].concat.apply(
[],
donors
.filter(i=>i.count > 0 && i.count >= minCount)
.map(i=>
i.component
.filter(c=>{
if(!c.trim())
return false;
const normalized = normalizeText(c);
if(normalized in alreadyExist)
return false;
alreadyExist[normalized] = true;
return true;
})
.map(x => ({count: i.count, item: i.item, text: x}))
)
));
log.push('Filtered candidates with ['+ (minCount===maxCount? minCount : minCount+' - '+ maxCount) +'] similar tags.');
if(may.length<2)
return false;
//console.clear()
//const addAnswersCount = Math.min(rand(this.answers.from, this.answers.to)-out.length, may.length);
const answer = rand(may);
log.push('Use '+ answer.item.title +' as donor.');
log.push('Because it has '+ answer.count +' similar tags: '+ dP.Product.getTags(answer.item).map(t=>t.name).join(', '));
out.push({correct: true, text: answer.text});
log.push('\n << '+ answer.text);
/*
for(let i = 0; i < addAnswersCount; i++){
if(may.length-i<0)
break;
const idx = Math.random()*(may.length-i)|0;
if(idx>=may.length || idx < 0)debugger
console.log(i, may[idx], may)
out.push({correct: true, text: may[idx]});
may[idx] = may[may.length - 1 - i]
}*/
console.log(dP.Product.getTags(product), donors)
return out;
}
}],
radio: [],
radioPhoto: {
}
......@@ -225,26 +52,46 @@ const shuffle = function (a) {
return a;
}
const quizGenerator = function(type, photo, subType) {
return _quizGenerator(type, photo, subType);
},
_quizGenerator = function(type, photo, subType, attempt) {
attempt = attempt || 0;
let log;
if(attempt === 0){
const types = quizTypes[type+(photo?'Photo':'')];
subType = probabilityRand(types);
log = quizGenerator.log = ['Quiz generate '+type+(photo ? ' with photo':'') +' '+(subType?`${lapk(subType.type)}`:'. FAIL')];
}else if(attempt<30){
log = quizGenerator.log;
log.push('\nATTEMPT FAILED!\n\n'+
'------------------------\n' +
' ATTEMPT '+ (attempt+1)+'\n'+
'------------------------\n\n'
)
}else{
throw new Error('Too much errors')
}
const cfg = subType;
const cfg = probabilityRand(types);
const log = ['Quiz generate '+type+(photo ? ' with photo':'') +' '+(cfg?`${lapk(cfg.type)}`:'. FAIL')];
if(!cfg)
return {answers: [], log};
const source = cfg.from.call(cfg, log);
if(source === false){
// давай по новой
return false;
return _quizGenerator( type, photo, subType, attempt + 1 );
}
let answers = shuffle(cfg.answer.call(cfg, source, log));
if(answers === false){
// давай по новой
return quizGenerator( type, photo );
return _quizGenerator( type, photo, subType, attempt + 1 );
}
return {question: cfg.question.call(cfg, source, log), answers, log}
};
};
/*
......
......@@ -9,11 +9,12 @@ const textFilterMatched = function(where, what) {
var yep = true;
for( var i = 0, _i = prepared.length; i < _i; i++ ){
var preparedElement = prepared[ i ];
if(preparedElement.text === '')
continue;
var matched = where.indexOf(preparedElement.text)>-1;
if(preparedElement.has === false && matched){
return false
}
if(!matched)
}else if(preparedElement.has === true && !matched)
yep = false;
}
......@@ -157,7 +158,11 @@ const textFormat = function(text, html) {
out = out.replace(/_!@#(\d+)#@!_/g, (f, num)=>{
return valids[f]
});
out = out.trim();
if(out.length>70)
out = out[0].toUpperCase()+out.substr(1);
console.log(income);
console.log(out);
return out;
......
......@@ -16,11 +16,12 @@ view.page.Generate = function() {
try{
const result = quizGenerator( type, photo );
title.innerHTML = textFormat( result.question );
title.innerHTML = textFormat( result.question, true );
debug.value = result.log.join( '\n' )
D.removeChildren( answers );
D.appendChild( answers, result.answers.map( ( a ) => view.cmp.Answer( a, type ) ) );
}catch( e ){
debug.value = quizGenerator.log.join( '\n' )
console.error(e)
}
};
......
......@@ -198,7 +198,7 @@ body {margin: 0; padding: 0}
textarea.generate-debug {
width: calc(100% - 32px);
margin: 16px 0 5px 14px;
height: 300px;
height: 500px;
}
.tag-manipulations-add-tag__comment {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment