Here it is...

This commit is contained in:
Thomas FORGIONE 2015-08-04 18:59:24 +02:00
parent 04a85123f5
commit 3a91a33d74
11 changed files with 352 additions and 138 deletions

View File

@ -10,4 +10,4 @@ block content
saw in the tutorial : they are here to help you navigate in the scene. saw in the tutorial : they are here to help you navigate in the scene.
Use them or ignore them at your convenience. Use them or ignore them at your convenience.
p You can start <a href='/prototype/arrows'>when you're ready</a> ! p You can start <a href='/prototype/game'>when you're ready</a> !

View File

@ -2,7 +2,28 @@ var pg = require('pg');
var pgc = require('../../private.js'); var pgc = require('../../private.js');
var Log = require('../../lib/NodeLog.js'); var Log = require('../../lib/NodeLog.js');
// Shuffle array
function shuffle(array) {
var currentIndex = array.length, temporaryValue, randomIndex ;
// While there remain elements to shuffle...
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
/** /**
*
* @namespace * @namespace
*/ */
var DBReq = {}; var DBReq = {};
@ -506,11 +527,10 @@ DBReq.UserCreator.prototype.finish = function() {
* @memberof DBReq * @memberof DBReq
* @constructor * @constructor
*/ */
DBReq.ExpCreator = function(user_id, scene_id, template, finishAction) { DBReq.ExpCreator = function(user_id, finishAction) {
this.finishAction = finishAction; this.finishAction = finishAction;
this.user_id = user_id; this.user_id = user_id;
this.scene_id = scene_id; this.finalResult = {};
this.template = template;
// Connect to db // Connect to db
var self = this; var self = this;
@ -524,16 +544,156 @@ DBReq.ExpCreator = function(user_id, scene_id, template, finishAction) {
/** /**
* Executes the SQL request and calls the callback * Executes the SQL request and calls the callback
*/ */
// Abandon all hope ye who enter here
DBReq.ExpCreator.prototype.execute = function() { DBReq.ExpCreator.prototype.execute = function() {
var self = this; var self = this;
this.client.query( this.client.query(
"INSERT INTO experiment(user_id, scene_id, template) VALUES($1,$2,$3);", "SELECT DISTINCT RecommendationStyle.name, CoinCombination.scene_id, CoinCombination.id, coin_1, coin_2, coin_3, coin_4, coin_5, coin_6, coin_7, coin_8\n" +
[self.user_id, self.scene_id, self.template], "FROM CoinCombination, Experiment, Users U, Users Other, RecommendationStyle, Scene\n" +
"WHERE\n" +
" Scene.id = CoinCombination.scene_id AND\n" +
" Scene.name != 'peachcastle' AND\n" +
" CoinCombination.id = Experiment.coin_combination_id AND\n" +
" Other.id = Experiment.user_id AND\n" +
" Other.rating = U.rating AND\n" +
" Other.id != U.id AND\n" +
" U.id = $1 AND\n" +
" RecommendationStyle.name NOT IN (\n" +
" SELECT DISTINCT Experiment.recommendation_style\n" +
" FROM CoinCombination, Experiment, Users U, Users Other\n" +
" WHERE\n" +
" CoinCombination.id = Experiment.coin_combination_id AND\n" +
" Other.id = Experiment.user_id AND\n" +
" Other.rating = U.rating AND\n" +
" Other.id != U.id AND\n" +
" CoinCombination.scene_id = Scene.id\n" +
" ) AND\n" +
" RecommendationStyle.name NOT IN (\n" +
" SELECT DISTINCT Experiment.recommendation_style\n" +
" FROM Experiment\n" +
" WHERE Experiment.user_id = $1 AND Experiment.recommendation_style != ''\n" +
" ) AND\n" +
" CoinCombination.scene_id NOT IN (\n" +
" SELECT DISTINCT scene_id\n" +
" FROM Experiment, CoinCombination\n" +
" WHERE Experiment.coin_combination_id = CoinCombination.id AND Experiment.user_id = $1\n" +
" )\n" +
"LIMIT 1;",
[self.user_id],
function(err, result) { function(err, result) {
self.client.query("SELECT MAX(id) AS id FROM experiment;", function(err, result) { console.log(result.rows);
self.finalResult = result.rows[0].id; if (result.rows.length > 0) {
// Set the result
self.finalResult.coin_combination_id = result.rows[0].id;
self.finalResult.scene_id = result.rows[0].scene_id;
self.finalResult.recommendation_style = result.rows[0].name;
self.finalResult.coins = [
result.rows[0].coin_1,
result.rows[0].coin_2,
result.rows[0].coin_3,
result.rows[0].coin_4,
result.rows[0].coin_5,
result.rows[0].coin_6,
result.rows[0].coin_7,
result.rows[0].coin_8
];
// There is a suggested experiment : create it
self.client.query(
"INSERT INTO Experiment(user_id, coin_combination_id, recommendation_style)\n" +
"VALUES($1,$2,$3)\n" +
"RETURNING id",
[self.user_id, result.rows[0].id, result.rows[0].name],
function(err, result) {
self.finalResult.exp_id = result.rows[0].id;
self.finish(); self.finish();
}); }
);
} else {
// Find the scene / recommendation style for the new experiment
self.client.query(
"SELECT RecommendationStyle.name, Scene.id as scene_id\n" +
"FROM RecommendationStyle, Scene\n" +
"WHERE\n" +
" RecommendationStyle.name NOT IN(\n" +
" SELECT Experiment.recommendation_style AS name\n" +
" FROM Experiment\n" +
" WHERE Experiment.user_id = $1\n" +
" AND Experiment.recommendation_style != ''\n" +
" ) AND\n" +
" Scene.id NOT IN(\n" +
" SELECT DISTINCT CoinCombination.scene_id AS id\n" +
" FROM Experiment, CoinCombination\n" +
" WHERE\n" +
" Experiment.coin_combination_id = CoinCombination.id AND\n" +
" user_id = $1\n" +
" ) AND\n" +
" Scene.name != 'peachcastle'\n" +
"\n" +
"ORDER BY RANDOM()\n" +
"LIMIT 1;",
[self.user_id],
function(err, result) {
if (result.rows.length > 0) {
self.finalResult.scene_id = result.rows[0].scene_id;
self.finalResult.recommendation_style = result.rows[0].name;
// Generate random coins
self.client.query(
"SELECT generate_series AS id\n" +
"FROM Scene, generate_series(0,Scene.coin_number-1)\n" +
"WHERE Scene.id = $1\n" +
"ORDER BY RANDOM()\n" +
"LIMIT 8;",
[self.finalResult.scene_id],
function(err, result) {
self.finalResult.coins = [];
for (var i = 0; i < 8; i++) {
self.finalResult.coins.push(result.rows[i].id);
}
// And then, create the CoinCombination
self.client.query(
"INSERT INTO CoinCombination(scene_id, coin_1, coin_2, coin_3, coin_4, coin_5, coin_6, coin_7, coin_8)\n" +
"VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9)\n" +
"RETURNING id;",
[
self.finalResult.scene_id,
self.finalResult.coins[0],
self.finalResult.coins[1],
self.finalResult.coins[2],
self.finalResult.coins[3],
self.finalResult.coins[4],
self.finalResult.coins[5],
self.finalResult.coins[6],
self.finalResult.coins[7],
],
function(err, result) {
self.finalResult.coin_combination_id = result.rows[0].id;
// And create the experiment
self.client.query(
"INSERT INTO Experiment(user_id, coin_combination_id, recommendation_style)\n" +
"VALUES($1,$2,$3)\n" +
"RETURNING id;",
[self.user_id, self.finalResult.coin_combination_id, self.finalResult.recommendation_style],
function(err, result) {
self.finalResult.exp_id = result.rows[0].id;
self.finish();
}
);
}
);
}
);
} else {
self.finish();
}
}
);
}
} }
); );
}; };
@ -546,7 +706,13 @@ DBReq.ExpCreator.prototype.finish = function() {
this.client = null; this.client = null;
this.release = null; this.release = null;
this.finishAction(this.finalResult); this.finishAction(
this.finalResult.exp_id,
this.finalResult.coin_combination_id,
this.finalResult.scene_id,
this.finalResult.recommendation_style,
this.finalResult.coins
);
}; };
/** /**
@ -731,6 +897,77 @@ DBReq.ExpGetter.prototype.finish = function() {
this.finishAction(this.finalResult); this.finishAction(this.finalResult);
}; };
DBReq.TutorialCreator = function(id, finishAction) {
this.id = id;
this.finishAction = finishAction;
var self = this;
pg.connect(pgc.url, function(err, client, release) {
self.client = client;
self.release = release;
self.execute();
});
};
DBReq.TutorialCreator.prototype.execute = function() {
var self = this;
this.client.query(
// Generate random coins
"SELECT Scene.id AS scene_id, generate_series AS id\n" +
"FROM Scene, generate_series(1,Scene.coin_number)\n" +
"WHERE Scene.name = 'peachcastle'\n" +
"ORDER BY RANDOM()\n" +
"LIMIT 8;",
[],
function(err, result) {
self.coins = [];
for (var i = 0; i < 8; i++) {
self.coins.push(result.rows[i].id);
}
// Create CoinCombination
self.client.query(
"INSERT INTO CoinCombination(scene_id, coin_1, coin_2, coin_3, coin_4, coin_5, coin_6, coin_7, coin_8)\n" +
"VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9)\n" +
"RETURNING id;",
[
result.rows[0].scene_id,
result.rows[0].id,
result.rows[1].id,
result.rows[2].id,
result.rows[3].id,
result.rows[4].id,
result.rows[5].id,
result.rows[6].id,
result.rows[7].id
],
function(err, result) {
// Create experiment
self.client.query(
"INSERT INTO Experiment(user_id, coin_combination_id)\n" +
"VALUES($1,$2)\n" +
"RETURNING id;",
[self.id, result.rows[0].id],
function(err, result) {
console.log(err);
self.finalResult = result.rows[0].id;
self.finish();
}
);
}
);
}
);
};
DBReq.TutorialCreator.prototype.finish = function() {
this.release();
this.release = null;
this.client = null;
this.finishAction(this.finalResult);
}
/** /**
* Try to get a user by id, and creates it if it doesn't exists * Try to get a user by id, and creates it if it doesn't exists
* @param id {Number} id to test * @param id {Number} id to test
@ -777,8 +1014,12 @@ DBReq.createUser = function(callback) {
* @param scene_id {Number} id of the scene on which the experiment is * @param scene_id {Number} id of the scene on which the experiment is
* @param callback {function} callback called on the new experiment id * @param callback {function} callback called on the new experiment id
*/ */
DBReq.createExp = function(id, scene_id, template, callback) { DBReq.createExp = function(id, callback) {
new DBReq.ExpCreator(id, scene_id, template, callback); new DBReq.ExpCreator(id, callback);
};
DBReq.createTutorial = function(id, callback) {
new DBReq.TutorialCreator(id, callback);
}; };
/** /**

View File

@ -70,7 +70,7 @@ var generateRecommendationStyle = function(req, res) {
} }
return req.session.recos.shift(); return req.session.recos.shift();
} };
var sceneToFunction = function(scene) { var sceneToFunction = function(scene) {
switch (scene) { switch (scene) {
@ -85,40 +85,39 @@ var sceneToFunction = function(scene) {
} }
}; };
var protoHelper = function(template) { module.exports.game = function(req, res) {
return function(req, res) {
template = generateRecommendationStyle(req, res);
if (template === undefined) {
res.redirect('/feedback');
return;
}
db.tryUser(req.session.user_id, function(id) { db.tryUser(req.session.user_id, function(id) {
// Get random scene number
var scene = generateSceneNumber(req, res); var scene = generateSceneNumber(req, res);
res.locals.scene = sceneToFunction(scene); res.locals.scene = sceneToFunction(scene);
res.locals.recommendationStyle = 'L3D.ArrowRecommendation';
req.session.user_id = id; req.session.user_id = id;
db.createExp(id, req.session.scenes[req.session.currentSceneIndex], template, function(id) { db.createExp(id, function(expId, coinCombinationId, sceneId, recommendationStyle, coins) {
req.session.exp_id = id;
if (expId === undefined) {
res.redirect('/feedback');
return;
}
req.session.exp_id = expId;
req.session.save(); req.session.save();
res.locals.scene = sceneToFunction(sceneId);
res.locals.recommendationStyle = recommendationStyle;
res.locals.coins = coins;
res.setHeader('Content-Type','text/html'); res.setHeader('Content-Type','text/html');
res.render(template, res.locals, function(err, result) { res.render('prototype_recommendation.jade', res.locals, function(err, result) {
res.send(result); res.send(result);
}); });
}); });
}); });
};
}; };
module.exports.arrows = protoHelper('prototype_arrows.jade');
module.exports.viewports = protoHelper('prototype_viewports.jade');
module.exports.reverse = protoHelper('prototype_reverse.jade');
module.exports.empty = protoHelper('prototype_empty.jade');
module.exports.sponza = function(req, res) { module.exports.sponza = function(req, res) {
res.setHeader('Content-Type', 'text/html'); res.setHeader('Content-Type', 'text/html');
@ -174,7 +173,7 @@ module.exports.tutorial = function(req, res) {
req.session.user_id = id; req.session.user_id = id;
// 1 is the ID of peach scene // 1 is the ID of peach scene
db.createExp(id, 1, null, function(id) { db.createTutorial(id, function(id) {
req.session.exp_id = id; req.session.exp_id = id;
req.session.save(); req.session.save();

View File

@ -1,9 +1,6 @@
module.exports = { module.exports = {
'/prototype': 'index', '/prototype': 'index',
'/prototype/arrows': 'arrows', '/prototype/game': 'game',
'/prototype/viewports': 'viewports',
'/prototype/empty': 'empty',
'/prototype/reverse': 'reverse',
'/prototype/replay': 'replay_index', '/prototype/replay': 'replay_index',
'/prototype/replay/:id': 'replay', '/prototype/replay/:id': 'replay',
'/prototype/replay_info/:id': 'replay_info', '/prototype/replay_info/:id': 'replay_info',

View File

@ -0,0 +1,25 @@
extends ./prototype_interactive
block title
title #{title} - Prototype - Arrows
block configjs
script Recommendation = #{recommendationStyle}; coinsId = [#{coins}];
script.
Coin.onLastCoin = function() {
$('#next').show();
$('#next').click(function() {
window.location = '/prototype/game';
});
};
block lastbutton
button#next.btn.btn-success.navbar-btn(style={'margin-right':'10px', 'margin-bottom':'10px', 'display':'none'})
span Go to the next step
//-block preciseDescription
p
| Recommended views are displayed with a
| transparent blue arrow. They disappear when you
| come closer to them, and shows the motion between
| your current position and the recommendation.

View File

@ -159,8 +159,8 @@ TutorialSteps.prototype.nextStep = function() {
} }
// Block camera // Block camera
for (var key in camera.motion) { for (var key in this.camera.motion) {
camera.motion[key] = false; this.camera.motion[key] = false;
} }
Coin.domElement.style.display = ""; Coin.domElement.style.display = "";

View File

@ -582,6 +582,8 @@ L3D.PointerCamera.prototype.onKeyEvent = function(event, toSet) {
// Log any change // Log any change
var e = new L3D.DB.Event.KeyboardEvent(); var e = new L3D.DB.Event.KeyboardEvent();
e.camera = this; e.camera = this;
e.keypressed = toSet;
e.keycode = event.keyCode;
e.send(); e.send();
} }
}; };
@ -667,6 +669,8 @@ L3D.PointerCamera.prototype.onMouseUp = function(event) {
if (this.dragging && this.mouseMoved && !this.moving && !this.movingHermite) { if (this.dragging && this.mouseMoved && !this.moving && !this.movingHermite) {
var e = new L3D.DB.Event.KeyboardEvent(); var e = new L3D.DB.Event.KeyboardEvent();
e.camera = this; e.camera = this;
e.keypressed = false;
e.keycode = -1;
e.send(); e.send();
} }

View File

@ -219,9 +219,17 @@ L3D.generateCoins = function(totalCoins, coin_ids) {
var i = 0; var i = 0;
if (coin_ids === undefined) if (coin_ids === undefined) {
L3D.shuffle(totalCoins); var tmp = [];
else if (coin_ids === null) { // Use global variable coinsId
for (i = 0; i < totalCoins.length; i++) {
tmp.push(totalCoins[i]);
}
totalCoins.length = 0;
for (var i = 0; i < 8; i++) {
totalCoins.push(tmp[coinsId[i]]);
}
} else if (coin_ids === null) {
return []; return [];
} else { } else {
@ -247,7 +255,9 @@ L3D.generateCoins = function(totalCoins, coin_ids) {
var bound = (coin_ids instanceof Array && coin_ids.length === 0) ? totalCoins.length : 8; var bound = (coin_ids instanceof Array && coin_ids.length === 0) ? totalCoins.length : 8;
console.log(totalCoins);
for (i = 0; i < bound; i++) { for (i = 0; i < bound; i++) {
console.log(i + '/' + totalCoins.length);
coins.push(totalCoins[i].coin); coins.push(totalCoins[i].coin);
totalCoins[i].coin.id = totalCoins[i].id; totalCoins[i].coin.id = totalCoins[i].id;
indices.push(totalCoins[i].id); indices.push(totalCoins[i].id);

View File

@ -158,7 +158,9 @@ L3D.DB.Event.KeyboardEvent.prototype.send = function() {
var url = "/posts/keyboard-event"; var url = "/posts/keyboard-event";
var data = { var data = {
camera: L3D.DB.Private.compactCamera(this.camera) camera: L3D.DB.Private.compactCamera(this.camera),
keycode: this.keycode, // -1 represents mouse event
keypressed: this.keypressed // mousepressed if keycode === -1
}; };
L3D.DB.Private.sendData(url, data); L3D.DB.Private.sendData(url, data);

View File

@ -6,8 +6,8 @@ module.exports.index = function(req, res) {
pg.connect(secret.url, function(err, client, release) { pg.connect(secret.url, function(err, client, release) {
client.query( client.query(
"INSERT INTO keyboardevent(exp_id, camera, time)" + "INSERT INTO keyboardevent(exp_id, camera, time, keycode, keypressed)" +
"VALUES($1, ROW(ROW($2,$3,$4),ROW($5,$6,$7)), to_timestamp($8));" , "VALUES($1, ROW(ROW($2,$3,$4),ROW($5,$6,$7)), to_timestamp($8), $9, $10);" ,
[ [
req.session.exp_id, req.session.exp_id,
req.body.camera.position.x, req.body.camera.position.x,
@ -18,7 +18,10 @@ module.exports.index = function(req, res) {
req.body.camera.target.y, req.body.camera.target.y,
req.body.camera.target.z, req.body.camera.target.z,
req.body.time req.body.time,
req.body.keycode,
req.body.keypressed
], ],
function(err, result) { function(err, result) {
if (err !== null) if (err !== null)

View File

@ -13,6 +13,7 @@ DROP TABLE IF EXISTS PointerLocked CASCADE;
DROP TABLE IF EXISTS SwitchedLockOption CASCADE; DROP TABLE IF EXISTS SwitchedLockOption CASCADE;
DROP TABLE IF EXISTS Coin CASCADE; DROP TABLE IF EXISTS Coin CASCADE;
DROP TABLE IF EXISTS CoinCombination CASCADE; DROP TABLE IF EXISTS CoinCombination CASCADE;
DROP TABLE IF EXISTS RecommendationStyle CASCADE;
DROP TYPE IF EXISTS VECTOR3 CASCADE; DROP TYPE IF EXISTS VECTOR3 CASCADE;
DROP TYPE IF EXISTS CAMERA CASCADE; DROP TYPE IF EXISTS CAMERA CASCADE;
@ -45,6 +46,12 @@ CREATE TABLE Users(
); );
CREATE TABLE Scene( CREATE TABLE Scene(
id SERIAL PRIMARY KEY,
name CHAR(50),
coin_number INTEGER
);
CREATE TABLE RecommendationStyle(
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
name CHAR(50) name CHAR(50)
); );
@ -66,7 +73,7 @@ CREATE TABLE Experiment(
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
user_id SERIAL REFERENCES Users (id), user_id SERIAL REFERENCES Users (id),
coin_combination_id SERIAL REFERENCES CoinCombination (id), coin_combination_id SERIAL REFERENCES CoinCombination (id),
template VARCHAR(30) recommendation_style VARCHAR(30)
); );
CREATE TABLE Coin( CREATE TABLE Coin(
@ -75,10 +82,14 @@ CREATE TABLE Coin(
); );
-- Init scene table -- Init scene table
INSERT INTO Scene(name) VALUES ('peachcastle'); INSERT INTO Scene(name, coin_number) VALUES ('peachcastle' , 41);
INSERT INTO Scene(name) VALUES ('bobomb'); INSERT INTO Scene(name, coin_number) VALUES ('bobomb' , 44);
INSERT INTO Scene(name) VALUES ('coolcoolmountain'); INSERT INTO Scene(name, coin_number) VALUES ('coolcoolmountain', 69);
INSERT INTO Scene(name) VALUES ('whomp'); INSERT INTO Scene(name, coin_number) VALUES ('whomp' , 50);
INSERT INTO RecommendationStyle(name) VALUES('L3D.EmptyRecommendation');
INSERT INTO RecommendationStyle(name) VALUES('L3D.ArrowRecommendation');
INSERT INTO RecommendationStyle(name) VALUES('L3D.ViewportRecommendation');
-- Events -- Events
CREATE TABLE ArrowClicked( CREATE TABLE ArrowClicked(
@ -99,6 +110,8 @@ CREATE TABLE KeyboardEvent(
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
exp_id SERIAL REFERENCES Experiment (id), exp_id SERIAL REFERENCES Experiment (id),
time TIMESTAMP DEFAULT NOW(), time TIMESTAMP DEFAULT NOW(),
keycode INTEGER,
keypressed BOOLEAN,
camera CAMERA camera CAMERA
); );
@ -144,83 +157,3 @@ CREATE TABLE SwitchedLockOption(
time TIMESTAMP DEFAULT NOW(), time TIMESTAMP DEFAULT NOW(),
locked BOOLEAN locked BOOLEAN
); );
-- Fill with example
INSERT INTO Users(rating) VALUES(3);
INSERT INTO Users(rating) VALUES(3);
INSERT INTO Users(rating) VALUES(3);
INSERT INTO Users(rating) VALUES(3);
INSERT INTO CoinCombination(scene_id) VALUES (2);
INSERT INTO CoinCombination(scene_id) VALUES (3);
INSERT INTO CoinCombination(scene_id) VALUES (4);
INSERT INTO CoinCombination(scene_id) VALUES (4);
INSERT INTO Experiment(user_id, coin_combination_id, template) VALUES(1, 1, '1');
INSERT INTO Experiment(user_id, coin_combination_id, template) VALUES(2, 1, '2');
-- INSERT INTO Experiment(user_id, coin_combination_id, template) VALUES(2, 1, '3');
INSERT INTO Experiment(user_id, coin_combination_id, template) VALUES(1, 2, '2');
INSERT INTO Experiment(user_id, coin_combination_id, template) VALUES(2, 2, '3');
INSERT INTO Experiment(user_id, coin_combination_id, template) VALUES(3, 4, '3');
SELECT * FROM Experiment;
-- PROTO request
SELECT DISTINCT CoinCombination.id AS id, all_template as template, CoinCombination.scene_id as scene_id
FROM Experiment, CoinCombination,
(SELECT * FROM generate_series(1,3) all_template) ali
WHERE
Experiment.coin_combination_id = CoinCombination.id AND
CoinCombination.scene_id IN(
SELECT CoinCombination.scene_id as scene_id
FROM Users, Experiment, CoinCombination
WHERE
Experiment.user_id = Users.id AND
Experiment.coin_combination_id = CoinCombination.id AND
Users.rating = 3 AND
Users.id != 3 AND
all_template NOT IN (
SELECT CAST(template AS INTEGER)
FROM Experiment, Users
WHERE Experiment.user_id = Users.id AND Users.id = 3
) AND
CoinCombination.scene_id NOT IN(
SELECT scene_id FROM Experiment, Users
WHERE Experiment.user_id = Users.id AND Users.id = 3
)
GROUP BY CoinCombination.scene_id
HAVING COUNT(DISTINCT Experiment.template) < 3
)
EXCEPT
SELECT CoinCombination.id AS id, CAST(Experiment.template AS INTEGER) as template, CoinCombination.scene_id as scene_id
FROM Experiment, CoinCombination
WHERE
Experiment.coin_combination_id = CoinCombination.id AND
CoinCombination.scene_id IN(
SELECT CoinCombination.scene_id as scene_id
FROM Users, Experiment, CoinCombination
WHERE
Experiment.user_id = Users.id AND
Experiment.coin_combination_id = CoinCombination.id AND
Users.rating = 3 AND
Users.id != 3
GROUP BY CoinCombination.scene_id
HAVING COUNT(DISTINCT Experiment.template) < 3
);