forked from Open-CT/openct-tasks
996 lines
29 KiB
JavaScript
996 lines
29 KiB
JavaScript
function initTask(subTask) {
|
|
var state = {};
|
|
var level;
|
|
var answer = null;
|
|
var data = {
|
|
easy: {
|
|
grid: [
|
|
['B', '.', '.'],
|
|
['O', '.', '.'],
|
|
['.', 'O', 'O']
|
|
],
|
|
visualGraphStr: '{"vertexVisualInfo":{},"edgeVisualInfo":{},"minGraph":{"vertexInfo":{},"edgeInfo":{},"edgeVertices":{},"directed":false}}', // TODO: factorize with others
|
|
allowDragging: true
|
|
},
|
|
medium: {
|
|
grid: [
|
|
['B', 'O', '.'],
|
|
['O', 'O', '.'],
|
|
['.', 'O', '.']
|
|
],
|
|
visualGraphStr: '{"vertexVisualInfo":{},"edgeVisualInfo":{},"minGraph":{"vertexInfo":{},"edgeInfo":{},"edgeVertices":{},"directed":false}}',
|
|
allowDragging: true
|
|
},
|
|
hard: {
|
|
grid: [
|
|
['B', 'O', '.'],
|
|
['B', '.', '.'],
|
|
['O', '.', '.']
|
|
],
|
|
visualGraphStr: '{"vertexVisualInfo":{},"edgeVisualInfo":{},"minGraph":{"vertexInfo":{},"edgeInfo":{},"edgeVertices":{},"directed":false}}',
|
|
allowDragging: true
|
|
}
|
|
};
|
|
|
|
var rows;
|
|
var cols;
|
|
var grid;
|
|
|
|
var paperConfig;
|
|
var paperGraph;
|
|
var paperGraphBorder;
|
|
var graph;
|
|
|
|
var dragAndDrop;
|
|
var dragContainer;
|
|
|
|
var initialConfig;
|
|
var validPositions;
|
|
var positionToIndex;
|
|
var numBalls;
|
|
var ballShadow;
|
|
var configPlaces;
|
|
var droppingOnBall;
|
|
|
|
var visualGraph;
|
|
var graphDrawer;
|
|
var graphMouse;
|
|
var vertexConnector;
|
|
var fuzzyRemover;
|
|
|
|
var visualConfig;
|
|
|
|
var fuzzyVertexThreshold = 0;
|
|
var fuzzyEdgeThreshold = 20;
|
|
var dragThreshold = 5;
|
|
var dragLimits;
|
|
var vertexRadius;
|
|
var minimalDropDistance = { easy: 95, medium: 80, hard: 80 };
|
|
var targetGraph;
|
|
|
|
var graphParams;
|
|
var radius = { easy: 40, medium: 35, hard: 35 };
|
|
var rectSide = { easy: 80, medium: 67, hard: 67 };
|
|
var graphParamsLevels = {
|
|
circleAttr: {
|
|
r: radius,
|
|
stroke: "black",
|
|
"stroke-width": 1,
|
|
fill: "gray",
|
|
opacity: 0
|
|
},
|
|
squareAttr: {
|
|
fill: "#222222",
|
|
stroke: "white",
|
|
"stroke-width": 2
|
|
},
|
|
lineAttr: {
|
|
"stroke-width": 9
|
|
},
|
|
highlightCircle: {
|
|
r: radius,
|
|
stroke: "red",
|
|
"stroke-width": 3,
|
|
fill: "gray"
|
|
},
|
|
highlightSquare: {
|
|
stroke: "red",
|
|
"stroke-width": 7
|
|
},
|
|
gridOffset: {
|
|
easy: {x: 0, y: 0},
|
|
medium: {x: 0, y: 3},
|
|
hard: {x: 0, y: 0}
|
|
}
|
|
};
|
|
|
|
var paperGraphParams = {
|
|
width: 700,
|
|
height: 450,
|
|
borderAttr: {
|
|
"stroke-width": 3,
|
|
fill: "#9999aa"
|
|
}
|
|
};
|
|
|
|
var paperConfigParams = {
|
|
xPad: 20,
|
|
yPad: 20,
|
|
borderAttr: {
|
|
"stroke-width": 3,
|
|
"fill": "#222222"
|
|
}
|
|
};
|
|
|
|
var cellParams = {
|
|
width: 76,
|
|
height: 72,
|
|
midRatio: 0.5,
|
|
xJump: 76,
|
|
yJump: 54,
|
|
oddXOffset: 38,
|
|
ballRadius: 24,
|
|
attrs: {
|
|
big: {
|
|
scale: 0.8,
|
|
line: {
|
|
"stroke-width": 3,
|
|
"fill": "white"
|
|
},
|
|
ball: {
|
|
"stroke-width": 3,
|
|
"fill": "red"
|
|
}
|
|
},
|
|
small: {
|
|
scale: 0.3,
|
|
line: {
|
|
"stroke-width": 1,
|
|
"fill": "white"
|
|
},
|
|
ball: {
|
|
"stroke-width": 1,
|
|
"fill": "red"
|
|
}
|
|
},
|
|
placeHolder: {
|
|
scale: 0.8,
|
|
"stroke" : "yellow",
|
|
"stroke-width" : "2",
|
|
"stroke-dasharray": "-"
|
|
}
|
|
},
|
|
shadowAttr: {
|
|
opacity: 0.2
|
|
}
|
|
};
|
|
|
|
subTask.loadLevel = function(curLevel) {
|
|
level = curLevel;
|
|
grid = data[level].grid;
|
|
rows = grid.length;
|
|
cols = grid[0].length;
|
|
graphParams = extractLevelSpecific(graphParamsLevels, level);
|
|
initConfig();
|
|
initTargetGraph();
|
|
};
|
|
|
|
subTask.getStateObject = function() {
|
|
return state;
|
|
};
|
|
|
|
subTask.reloadAnswerObject = function(answerObj) {
|
|
answer = answerObj;
|
|
if(!answer) {
|
|
return;
|
|
}
|
|
resetGraph();
|
|
};
|
|
|
|
subTask.resetDisplay = function() {
|
|
$(".hexCount").text(validPositions.length);
|
|
initPaperConfig();
|
|
initPaperGraph();
|
|
initHandlers();
|
|
showMessage(null);
|
|
};
|
|
|
|
subTask.getAnswerObject = function() {
|
|
return answer;
|
|
};
|
|
|
|
subTask.getDefaultAnswerObject = function() {
|
|
return {
|
|
visualGraphStr: data[level].visualGraphStr,
|
|
config: $.extend(true, [], initialConfig)
|
|
};
|
|
};
|
|
|
|
subTask.unloadLevel = function(callback) {
|
|
if(dragAndDrop) {
|
|
dragAndDrop.disable();
|
|
}
|
|
if (visualGraph) {
|
|
vertexConnector.setEnabled(false);
|
|
if(fuzzyRemover) {
|
|
fuzzyRemover.setEnabled(false);
|
|
}
|
|
visualGraph.remove();
|
|
}
|
|
$("#addSituation").unbind("click");
|
|
callback();
|
|
};
|
|
|
|
function initConfig() {
|
|
validPositions = [];
|
|
initialConfig = [];
|
|
for(var row = 0; row < rows; row++) {
|
|
for(var col = 0; col < cols; col++) {
|
|
if(grid[row][col] === 'B') {
|
|
initialConfig.push({row: row, col: col});
|
|
}
|
|
if(grid[row][col] !== '.') {
|
|
validPositions.push({row: row, col: col});
|
|
}
|
|
}
|
|
}
|
|
numBalls = initialConfig.length;
|
|
|
|
positionToIndex = {};
|
|
for(var iPosition = 0; iPosition < validPositions.length; iPosition++) {
|
|
var currentRow = validPositions[iPosition].row;
|
|
var currentCol = validPositions[iPosition].col;
|
|
if(!positionToIndex[currentRow]) {
|
|
positionToIndex[currentRow] = {};
|
|
}
|
|
positionToIndex[currentRow][currentCol] = iPosition;
|
|
}
|
|
}
|
|
|
|
function initTargetGraph() {
|
|
if(data[level].target) {
|
|
return;
|
|
}
|
|
|
|
targetGraph = new Graph(false);
|
|
|
|
// Generate all vertices recursively.
|
|
function recurse(config, index) {
|
|
if(config.length === numBalls) {
|
|
targetGraph.addVertex(configToString(config));
|
|
}
|
|
for(var next = index; next < validPositions.length; next++) {
|
|
config.push(validPositions[next]);
|
|
recurse(config, next + 1);
|
|
config.pop();
|
|
}
|
|
}
|
|
|
|
recurse([], 0);
|
|
|
|
// Find all edges.
|
|
targetGraph.forEachVertex(function(id) {
|
|
var neighbors = getConfigNeighbors(configFromString(id));
|
|
for(var iNeighbor in neighbors) {
|
|
var neighborID = configToString(neighbors[iNeighbor]);
|
|
var edgeID = vertexPairToString(id, neighborID);
|
|
if(!targetGraph.isEdge(edgeID)) {
|
|
targetGraph.addEdge(edgeID, id, neighborID);
|
|
}
|
|
}
|
|
});
|
|
|
|
data[level].target = targetGraph;
|
|
}
|
|
|
|
function resetGraph() {
|
|
if(answer.visualGraphStr) {
|
|
graph = Graph.fromMinimized(JSON.parse(answer.visualGraphStr).minGraph);
|
|
}
|
|
else {
|
|
graph = new Graph(false);
|
|
}
|
|
}
|
|
|
|
function initHandlers() {
|
|
$("#addSituation").unbind("click");
|
|
$("#addSituation").click(clickAdd);
|
|
}
|
|
|
|
function initPaperConfig() {
|
|
visualConfig = new VisualConfig([], cellParams.attrs.big);
|
|
var width = visualConfig.getWidth() + 2 * paperConfigParams.xPad;
|
|
var height = visualConfig.getHeight() + 2 * paperConfigParams.yPad;
|
|
paperConfig = subTask.raphaelFactory.create("anim_config", "anim_config", width, height);
|
|
paperConfig.rect(0, 0, width, height).attr(paperConfigParams.borderAttr);
|
|
visualConfig.draw(paperConfig, width / 2, height / 2);
|
|
visualConfig.onClick(showDragError);
|
|
|
|
initDragAndDrop();
|
|
initDragContainer();
|
|
}
|
|
|
|
function initDragAndDrop() {
|
|
dragAndDrop = DragAndDropSystem({
|
|
paper: paperConfig,
|
|
actionIfDropped: actionIfDropped,
|
|
actionIfEjected: actionIfEjected,
|
|
drop: onDrop
|
|
});
|
|
|
|
configPlaces = visualConfig.getValidPlaces();
|
|
dragContainer = dragAndDrop.addContainer({
|
|
ident: "cells",
|
|
dropMode: "replace",
|
|
cx: 0,
|
|
cy: 0,
|
|
nbPlaces: configPlaces.length,
|
|
places: configPlaces,
|
|
placeBackgroundArray: []
|
|
});
|
|
|
|
/*
|
|
* This is a dirty trick to get a hexagon placeholder in drag and drop library.
|
|
* The library creates a "container.placeHolder" object, which is a "component" object
|
|
* (defined in the library).
|
|
*
|
|
* A component is a custom implementation of a Raphael-element-container (like set),
|
|
* with several auxiliary methods.
|
|
*
|
|
* The only such method actually used for "container.placeHolder" is "placeAt", so we define it here.
|
|
*/
|
|
var placeHolderElement = drawCell(paperConfig, 0, 0, cellParams.attrs.placeHolder.scale * cellParams.width, cellParams.attrs.placeHolder.scale * cellParams.height, cellParams.attrs.placeHolder).hide();
|
|
var placeHolderPath = placeHolderElement.attrs.path;
|
|
placeHolderElement.placeAt = function(centerX, centerY) {
|
|
placeHolderElement.attr({
|
|
path: Raphael.transformPath(placeHolderPath, ["T", centerX, centerY])
|
|
});
|
|
};
|
|
dragContainer.placeHolder = placeHolderElement;
|
|
/* End of trick */
|
|
}
|
|
|
|
function initDragContainer() {
|
|
for(var iBall in answer.config) {
|
|
var ballPosition = answer.config[iBall];
|
|
var positionIndex = positionToIndex[ballPosition.row][ballPosition.col];
|
|
var ball = drawBall(paperConfig, 0, 0, cellParams.attrs.big.scale * cellParams.ballRadius, cellParams.attrs.big.ball);
|
|
ball.mouseup(showDragError);
|
|
dragAndDrop.insertObjects("cells", positionIndex, [
|
|
{
|
|
ident: iBall,
|
|
elements: [ball]
|
|
}
|
|
]);
|
|
}
|
|
}
|
|
|
|
function showDragError() {
|
|
showMessage(taskStrings.dragError);
|
|
}
|
|
|
|
function initPaperGraph() {
|
|
paperGraph = subTask.raphaelFactory.create("anim_graph", "anim_graph", paperGraphParams.width, paperGraphParams.height);
|
|
paperGraphBorder = paperGraph.rect(0, 0, paperGraphParams.width, paperGraphParams.height).attr(paperGraphParams.borderAttr);
|
|
|
|
graphDrawer = new SimpleGraphDrawer(graphParams.circleAttr, graphParams.lineAttr, vertexDrawer, true, null);
|
|
|
|
if(answer.visualGraphStr) {
|
|
visualGraph = VisualGraph.fromJSON(answer.visualGraphStr, "visual_graph", paperGraph, graph, graphDrawer, true);
|
|
}
|
|
else {
|
|
visualGraph = new VisualGraph("visual_graph", paperGraph, graph, graphDrawer, true);
|
|
}
|
|
|
|
graphMouse = new GraphMouse("mouse", graph, visualGraph);
|
|
vertexRadius = graphParams.circleAttr.r;
|
|
dragLimits = {
|
|
minX: vertexRadius,
|
|
maxX: paperGraphParams.width - vertexRadius,
|
|
minY: vertexRadius,
|
|
maxY: paperGraphParams.height - vertexRadius
|
|
};
|
|
|
|
if(data[level].allowDragging) {
|
|
vertexConnector = new VertexDragAndConnect({
|
|
id: "connector",
|
|
paperElementID: "anim_graph",
|
|
paper: paperGraph,
|
|
graph: graph,
|
|
visualGraph: visualGraph,
|
|
graphMouse: graphMouse,
|
|
onDragEnd: onDragEnd,
|
|
onVertexSelect: onVertexSelect,
|
|
onPairSelect: onPairSelect,
|
|
onEdgeSelect: deleteEdge,
|
|
vertexThreshold: fuzzyVertexThreshold,
|
|
edgeThreshold: fuzzyEdgeThreshold,
|
|
dragThreshold: dragThreshold,
|
|
dragLimits: dragLimits,
|
|
snapToLastGoodPosition: true,
|
|
isGoodPosition: isGoodPosition,
|
|
enabled: true
|
|
});
|
|
fuzzyRemover = null;
|
|
}
|
|
else {
|
|
vertexConnector = new EdgeCreator("connector", "anim_graph", paperGraph, graph, visualGraph, graphMouse, onVertexSelect, onPairSelect, true);
|
|
fuzzyRemover = new FuzzyRemover("fuzzyRemover", "anim_graph", paperGraph, graph, visualGraph, null, false, true, fuzzyVertexThreshold, fuzzyEdgeThreshold, true);
|
|
}
|
|
|
|
graph.addPostListener("answerUpdater", answerUpdater, 10000);
|
|
paperGraphBorder.toBack();
|
|
}
|
|
|
|
var answerUpdater = {
|
|
addVertex: onVisualGraphChange,
|
|
addEdge: onVisualGraphChange,
|
|
removeVertex: onVisualGraphChange,
|
|
removeEdge: onVisualGraphChange
|
|
};
|
|
|
|
function VisualConfig(config, attrs) {
|
|
var self = this;
|
|
|
|
this.init = function() {
|
|
this.cellWidth = attrs.scale * cellParams.width;
|
|
this.cellHeight = attrs.scale * cellParams.height;
|
|
this.xJump = attrs.scale * cellParams.xJump;
|
|
this.yJump = attrs.scale * cellParams.yJump;
|
|
this.oddXOffset = attrs.scale * cellParams.oddXOffset;
|
|
this.ballRadius = attrs.scale * cellParams.ballRadius;
|
|
this.height = (rows - 1) * this.yJump + this.cellHeight;
|
|
this.calculateWidth();
|
|
|
|
};
|
|
|
|
this.calculateWidth = function() {
|
|
this.width = 0;
|
|
for(var row = 0; row < rows; row++) {
|
|
this.width = Math.max(this.width, this.calculateRowWidth(row));
|
|
}
|
|
};
|
|
|
|
this.calculateRowWidth = function(row) {
|
|
// The rightmost cell determines the width.
|
|
var result;
|
|
for(var col = cols - 1; col >= 0; col--) {
|
|
if(grid[row][col] !== '.') {
|
|
result = col * this.xJump + this.cellWidth;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// This row has no cells.
|
|
if(!result) {
|
|
return 0;
|
|
}
|
|
|
|
// Odd rows are given an offset.
|
|
if(row % 2 === 1) {
|
|
result += this.oddXOffset;
|
|
}
|
|
return result;
|
|
};
|
|
|
|
this.getWidth = function() {
|
|
return this.width;
|
|
};
|
|
|
|
this.getHeight = function() {
|
|
return this.height;
|
|
};
|
|
|
|
this.draw = function(paper, centerX, centerY) {
|
|
this.paper = paper;
|
|
this.centerX = centerX;
|
|
this.centerY = centerY;
|
|
this.leftX = centerX - this.width / 2;
|
|
this.topY = centerY - this.height / 2;
|
|
this.elements = {
|
|
cells: [],
|
|
balls: []
|
|
};
|
|
|
|
for(var row = 0; row < rows; row++) {
|
|
for(var col = 0; col < cols; col++) {
|
|
if(grid[row][col] === '.') {
|
|
continue;
|
|
}
|
|
var center = this.getCellCenter(row, col);
|
|
var cell = drawCell(paper, center.x, center.y, this.cellWidth, this.cellHeight, attrs.line);
|
|
this.elements.cells.push(cell);
|
|
}
|
|
}
|
|
|
|
for(var iBall = 0; iBall < config.length; iBall++) {
|
|
var ballCenter = this.getCellCenter(config[iBall].row, config[iBall].col);
|
|
var ball = drawBall(paper, ballCenter.x, ballCenter.y, this.ballRadius, attrs.ball);
|
|
this.elements.balls.push(ball);
|
|
}
|
|
};
|
|
|
|
this.getCellCenter = function(row, col) {
|
|
var leftX = this.leftX + (col * this.xJump);
|
|
var topY = this.topY + (row * this.yJump);
|
|
|
|
// Hexagon rows shift alternately.
|
|
if(row % 2 === 1) {
|
|
leftX += this.oddXOffset;
|
|
}
|
|
|
|
return {
|
|
x: leftX + this.cellWidth / 2,
|
|
y: topY + this.cellHeight / 2
|
|
};
|
|
};
|
|
|
|
this.getValidPlaces = function() {
|
|
var result = [];
|
|
for(var iPosition = 0; iPosition < validPositions.length; iPosition++) {
|
|
var position = validPositions[iPosition];
|
|
var center = self.getCellCenter(position.row, position.col);
|
|
result.push([center.x, center.y]);
|
|
}
|
|
return result;
|
|
};
|
|
|
|
this.getRaphaels = function() {
|
|
return this.elements.cells.concat(this.elements.balls);
|
|
};
|
|
|
|
this.onClick = function(handler) {
|
|
for(var iCell in this.elements.cells) {
|
|
this.elements.cells[iCell].click(handler);
|
|
}
|
|
};
|
|
|
|
this.init();
|
|
}
|
|
|
|
function drawCell(paper, centerX, centerY, width, height, attr) {
|
|
var leftX = centerX - width / 2;
|
|
var rightX = centerX + width / 2;
|
|
var topY = centerY - height / 2;
|
|
var bottomY = centerY + height / 2;
|
|
//return paper.rect(leftX, topY, rightX, bottomY).attr(attr);
|
|
var midHigh = centerY - (cellParams.midRatio / 2) * height;
|
|
var midLow = centerY + (cellParams.midRatio / 2) * height;
|
|
return paper.path([
|
|
"M",
|
|
centerX, topY,
|
|
"L",
|
|
rightX, midHigh,
|
|
"V",
|
|
midLow,
|
|
"L",
|
|
centerX, bottomY,
|
|
"L",
|
|
leftX, midLow,
|
|
"V",
|
|
midHigh,
|
|
"Z"
|
|
]).attr(attr);
|
|
}
|
|
|
|
function drawBall(paper, centerX, centerY, radius, attr) {
|
|
return paper.circle(centerX, centerY, radius).attr(attr);
|
|
}
|
|
|
|
function drawShadowBall(index) {
|
|
var place = configPlaces[index];
|
|
ballShadow = drawBall(paperConfig, place[0], place[1], cellParams.attrs.big.scale * cellParams.ballRadius, cellParams.attrs.big.ball);
|
|
ballShadow.attr(cellParams.shadowAttr);
|
|
}
|
|
|
|
function actionIfDropped(srcCont, srcPos, dstCont, dstPos, dropType) {
|
|
// If we are dragging, draw (once) a copy of the ball in the original location.
|
|
if(!ballShadow) {
|
|
drawShadowBall(srcPos);
|
|
}
|
|
|
|
// We will set this to true if overlap is detected.
|
|
droppingOnBall = false;
|
|
|
|
// Allow ejecting the ball. This causes it to be re-inserted, in actionIfEjected below.
|
|
if(dstCont === null) {
|
|
return true;
|
|
}
|
|
|
|
// Allow dropping back in place.
|
|
if(srcPos === dstPos) {
|
|
return true;
|
|
}
|
|
|
|
// Don't allow dropping on non-neighbors.
|
|
if(!isPositionNeighbor(validPositions[srcPos], validPositions[dstPos])) {
|
|
return false;
|
|
}
|
|
|
|
// If a ball exists here, the action is to put the ball back in the original cell.
|
|
// We set droppingOnBall to true, so that onDrop will be able to show the relevant error message.
|
|
var contents = dragAndDrop.getObjects("cells");
|
|
if(contents[dstPos]) {
|
|
droppingOnBall = true;
|
|
return DragAndDropSystem.action("cells", srcPos, "insert");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function actionIfEjected(refElement, srcCont, srcPos) {
|
|
// The ball has been ejected, we show an error and re-insert.
|
|
return DragAndDropSystem.action("cells", srcPos, "insert");
|
|
}
|
|
|
|
function onDrop(srcContainerID, srcPos, dstContainerID, dstPos, dropType) {
|
|
if(ballShadow) {
|
|
ballShadow.remove();
|
|
ballShadow = null;
|
|
}
|
|
updateUserConfig();
|
|
|
|
// When the ball is re-inserted after ejection, the source container is
|
|
// "temporaryContainer" and the destination is "cells".
|
|
if (srcContainerID !== "cells") {
|
|
showMessage(taskStrings.wrongCell);
|
|
} else if(droppingOnBall) {
|
|
showMessage(taskStrings.droppingOnBall);
|
|
} else {
|
|
showMessage(null);
|
|
}
|
|
}
|
|
|
|
function onDragEnd(id, isSnappedToGoodPosition) {
|
|
onVisualGraphChange();
|
|
}
|
|
|
|
// A photo position is good if it's not too close to another.
|
|
// The distance is: max(|dx|,|dy|).
|
|
function isGoodPosition(originalID, position) {
|
|
var result = true;
|
|
graph.forEachVertex(function(id) {
|
|
if(!result || id === originalID) {
|
|
return;
|
|
}
|
|
var info = visualGraph.getVertexVisualInfo(id);
|
|
var distance = Math.max(Math.abs(info.x - position.x), Math.abs(info.y - position.y));
|
|
if(distance < minimalDropDistance[level]) {
|
|
result = false;
|
|
}
|
|
});
|
|
return result;
|
|
}
|
|
|
|
function updateUserConfig() {
|
|
var contents = dragAndDrop.getObjects("cells");
|
|
answer.config = [];
|
|
for(var index = 0; index < contents.length; index++) {
|
|
if(contents[index]) {
|
|
answer.config.push(validPositions[index]);
|
|
}
|
|
}
|
|
}
|
|
|
|
function isValidPosition(position) {
|
|
return position.row < rows && position.row >= 0 &&
|
|
position.col < cols && position.col >= 0 &&
|
|
grid[position.row][position.col] !== '.';
|
|
}
|
|
|
|
function isPositionNeighbor(position1, position2) {
|
|
if(!isValidPosition(position1) || !isValidPosition(position2)) {
|
|
return false;
|
|
}
|
|
|
|
var row1 = position1.row;
|
|
var col1 = position1.col;
|
|
var row2 = position2.row;
|
|
var col2 = position2.col;
|
|
|
|
// Same row - must be adjacent cols.
|
|
if(row1 === row2) {
|
|
return Math.abs(col1 - col2) === 1;
|
|
}
|
|
|
|
// Must be adjacent rows.
|
|
if(Math.abs(row1 - row2) !== 1) {
|
|
return false;
|
|
}
|
|
|
|
// Same col means they are adjacent.
|
|
if(col1 === col2) {
|
|
return true;
|
|
}
|
|
|
|
// Diagonals - on even rows (0-based), hexagon adjacencies go up/down and left.
|
|
if(row1 % 2 === 0) {
|
|
return col2 === col1 - 1;
|
|
}
|
|
|
|
// On odd rows, they go up/down and right.
|
|
return col2 === col1 + 1;
|
|
}
|
|
|
|
function getPositionNeighbors(position) {
|
|
var result = [];
|
|
for(var dRow = -1; dRow <= 1; dRow++) {
|
|
for(var dCol = -1; dCol <= 1; dCol++) {
|
|
var neighbor = {
|
|
row: position.row + dRow,
|
|
col: position.col + dCol
|
|
};
|
|
if(isPositionNeighbor(position, neighbor)) {
|
|
result.push(neighbor);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function clickAdd() {
|
|
var id = configToString(answer.config);
|
|
if(graph.getVerticesCount() === targetGraph.getVerticesCount()) {
|
|
showMessage(taskStrings.needEdges);
|
|
return;
|
|
}
|
|
if(graph.isVertex(id)) {
|
|
showMessage(taskStrings.vertexExists);
|
|
return;
|
|
}
|
|
|
|
var position = findNewVertexPosition();
|
|
graph.addVertex(id);
|
|
graphDrawer.moveVertex(id, position.x, position.y);
|
|
onVisualGraphChange();
|
|
|
|
if(graph.getVerticesCount() === targetGraph.getVerticesCount()) {
|
|
showMessage(taskStrings.needEdges);
|
|
} else {
|
|
showMessage(taskStrings.vertexAdded);
|
|
}
|
|
}
|
|
|
|
function findNewVertexPosition() {
|
|
// Try to find a position that isn't too close to any existing vertex.
|
|
var positions = [];
|
|
graph.forEachVertex(function(id) {
|
|
positions.push(graphDrawer.getVertexPosition(id));
|
|
});
|
|
|
|
var generator = new RandomGenerator(1);
|
|
var bestPosition;
|
|
var bestDistance = 0;
|
|
|
|
// Try 100 times, then settle for best found.
|
|
for(var attempt = 0; attempt < 100; attempt++) {
|
|
position = {
|
|
x: generator.nextInt(dragLimits.minX, dragLimits.maxX),
|
|
y: generator.nextInt(dragLimits.minY, dragLimits.maxY)
|
|
};
|
|
var distance = Infinity;
|
|
|
|
// Find the minimal distance to a vertex. Distance is max(|dx|,|dy|).
|
|
for(var iVertex in positions) {
|
|
distance = Math.min(distance, Math.max(Math.abs(position.x - positions[iVertex].x), Math.abs(position.y - positions[iVertex].y)));
|
|
}
|
|
|
|
// If the minimal distance is big enough, this is a good position.
|
|
if(distance >= minimalDropDistance[level]) {
|
|
bestPosition = position;
|
|
break;
|
|
}
|
|
|
|
// Record this position if it's the best so far.
|
|
if(distance > bestDistance) {
|
|
bestDistance = distance;
|
|
bestPosition = position;
|
|
}
|
|
}
|
|
|
|
return bestPosition;
|
|
}
|
|
|
|
function vertexDrawer(id, info, centerX, centerY) {
|
|
var result = [];
|
|
var side = rectSide[level];
|
|
var rect = paperGraph.rect(centerX - side / 2, centerY - side / 2, side, side).attr(graphParams.squareAttr);
|
|
result.push(rect);
|
|
|
|
var config = configFromString(id);
|
|
var visualConfig = new VisualConfig(config, cellParams.attrs.small);
|
|
visualConfig.draw(paperGraph, centerX + graphParams.gridOffset.x, centerY + graphParams.gridOffset.y);
|
|
result = result.concat(visualConfig.getRaphaels());
|
|
|
|
return result;
|
|
}
|
|
|
|
function onVisualGraphChange() {
|
|
showMessage(null);
|
|
answer.visualGraphStr = visualGraph.toJSON();
|
|
}
|
|
|
|
function onVertexSelect(id, selected, pairCompleted) {
|
|
if(!pairCompleted) {
|
|
/*
|
|
* If pairCompleted is true, this means that onPairSelect
|
|
* has been called, which handled any feedback message necessary
|
|
* (if an edge has been added, then onVisualGraphChange was invoked,
|
|
* and if not, then a suitable feedback message was displayed).
|
|
* Otherwise, the feedback should be reset.
|
|
*/
|
|
showMessage(null);
|
|
}
|
|
var attr;
|
|
if(selected) {
|
|
attr = graphParams.highlightSquare;
|
|
}
|
|
else {
|
|
attr = graphParams.squareAttr;
|
|
}
|
|
visualGraph.getRaphaelsFromID(id)[1].attr(attr);
|
|
}
|
|
|
|
function onPairSelect(id1, id2) {
|
|
var id = vertexPairToString(id1, id2);
|
|
if(graph.isEdge(id)) {
|
|
showMessage(taskStrings.edgeExists);
|
|
return;
|
|
}
|
|
graph.addEdge(id, id1, id2);
|
|
paperGraphBorder.toBack();
|
|
}
|
|
|
|
function deleteEdge(id) {
|
|
showMessage(null);
|
|
graph.removeEdge(id);
|
|
}
|
|
|
|
function getConfigNeighbors(config) {
|
|
// Remember which positions are occupied.
|
|
var configPositionObj = {};
|
|
for(var iPosition in config) {
|
|
configPositionObj[positionToString(config[iPosition])] = true;
|
|
}
|
|
|
|
var result = [];
|
|
|
|
/*
|
|
* Each neighbor of each ball position contributes a neighboring configuration.
|
|
* We ignore neighboring positions that overlap other balls.
|
|
*/
|
|
for(var iBall = 0; iBall < config.length; iBall++) {
|
|
var position = config[iBall];
|
|
var positionNeighbors = getPositionNeighbors(position);
|
|
for(var iNeighbor in positionNeighbors) {
|
|
var neighbor = positionNeighbors[iNeighbor];
|
|
var neighborString = positionToString(neighbor);
|
|
if(configPositionObj[neighborString]) {
|
|
continue;
|
|
}
|
|
var configCopy = $.extend(true, [], config);
|
|
configCopy[iBall] = neighbor;
|
|
result.push(configCopy);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function vertexPairToString(id1, id2) {
|
|
// We sort the array, because the edge direction is not distinguished.
|
|
var ids = [id1, id2];
|
|
ids.sort();
|
|
return ids.join(" ");
|
|
}
|
|
|
|
function configToString(config) {
|
|
// We sort the array, because the balls are not distinguished.
|
|
var result = $.map(config, positionToString);
|
|
result.sort();
|
|
return result.join("_");
|
|
}
|
|
|
|
function positionToString(position) {
|
|
return position.row + "," + position.col;
|
|
}
|
|
|
|
function positionFromString(string) {
|
|
var parts = string.split(",");
|
|
return {
|
|
row: parseInt(parts[0]),
|
|
col: parseInt(parts[1])
|
|
};
|
|
}
|
|
|
|
function configFromString(string) {
|
|
var parts = string.split("_");
|
|
return $.map(parts, positionFromString);
|
|
}
|
|
|
|
function showMessage(string) {
|
|
if(string === null || string === undefined || string === "") {
|
|
string = " ";
|
|
}
|
|
$("#messageConfig").html(string);
|
|
}
|
|
|
|
function checkUser(schoolArray, userArray, missingMessage, wrongMessage, emptyMessage) {
|
|
// Check that the user's array is identical to the school array.
|
|
// To display helpful errors, we also check if an entry is wrong
|
|
// or missing.
|
|
var schoolObj = {};
|
|
var userObj = {};
|
|
|
|
var index;
|
|
for(index in schoolArray) {
|
|
schoolObj[schoolArray[index]] = true;
|
|
}
|
|
for(index in userArray) {
|
|
userObj[userArray[index]] = true;
|
|
}
|
|
|
|
// Special case if no edges
|
|
if(userArray.length == 0) {
|
|
return {
|
|
success: false,
|
|
message: emptyMessage
|
|
};
|
|
}
|
|
|
|
// Wrong entries.
|
|
var key;
|
|
for(key in userObj) {
|
|
if(!schoolObj[key]) {
|
|
return {
|
|
success: false,
|
|
message: wrongMessage
|
|
};
|
|
}
|
|
}
|
|
|
|
for(key in schoolObj) {
|
|
if(!userObj[key]) {
|
|
return {
|
|
success: false,
|
|
message: missingMessage
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: true
|
|
};
|
|
}
|
|
|
|
function getResultAndMessage() {
|
|
/*
|
|
* The vertex and edge IDs are the same in the target graph
|
|
* and in the user's graph. We check that they are identical.
|
|
*/
|
|
var vertexResult = checkUser(data[level].target.getAllVertices(), graph.getAllVertices(), taskStrings.missingVertex);
|
|
if(!vertexResult.success) {
|
|
return {
|
|
successRate: 0,
|
|
message: vertexResult.message
|
|
};
|
|
}
|
|
|
|
var edgeResult = checkUser(data[level].target.getAllEdges(), graph.getAllEdges(), taskStrings.missingEdge, taskStrings.wrongEdge, taskStrings.shouldAddVertices);
|
|
if(!edgeResult.success) {
|
|
return {
|
|
successRate: 0,
|
|
message: edgeResult.message
|
|
};
|
|
}
|
|
|
|
return {
|
|
successRate: 1,
|
|
message: taskStrings.success
|
|
};
|
|
}
|
|
|
|
subTask.getGrade = function(callback) {
|
|
callback(getResultAndMessage());
|
|
};
|
|
}
|
|
initWrapper(initTask, ["easy", "medium", "hard"]);
|