Torxion

This is a tiled puzzle game where you make a path to match the markers at the edge of the board. Click and right click rotate. Torxion is written in Javascript and has touchscreen support, and as such can be played on any modern platform. It's a puzzle game about completing everything at once. Sometimes when you manage to complete something you want to hold onto the solution even if it gets in the way of everything else. This is how Torxion relates to the theme 'What do we do now?' Finishing one loop leads to asking how to complete others, asking if you can spare solutions already made, and general confusion.
Jam Site: 
Jam year: 
2015
Platforms: 
Web standard (HTML5, Java, JavaScript, Flash)
Technology Notes: 
Pure Javascript. Can be run on anything that can support a browser.
Game Stills: 
Embed code: 
<!DOCTYPE html><html><head><title>Torxion</title><meta charset="UTF-8"> <style>*{position:absolute;margin:0px;}</style> <body><script type="text/javascript"> /*+==========================================================================+*\ __________ ___ _ __________ _ __ /_ __/ __ \/ _ \| |/_/ _/ __ \/ |/ / / / / /_/ / , _ > <_/ // /_/ / / /_/ \____/_/|_/_/|_/___/\____/_/|_/ \*+==========================================================================+*/ "use strict" var canvas = document.createElement("canvas"); var gfx = canvas.getContext("2d"); var ww,wh,tick=0,tickOffset=0,elapsed=0; function rfloat(x){return Math.random()*x;}; function rInt(x){return Math.floor(Math.random()*x);}; function shuffle(ls){return ls.sort(function(){return 0.5-Math.random();});} Number.prototype.mod = function(n){return((this%n)+n)%n;}; //== RENDERING =============================================================// function rgb(r,g,b){return gfx.fillStyle=gfx.strokeStyle="rgb("+Math.floor(255*r)+","+Math.floor(255*g)+","+Math.floor(255*b)+")";}; function rgba(r,g,b,a){return gfx.fillStyle=gfx.strokeStyle="rgba("+Math.floor(255*r)+","+Math.floor(255*g)+","+Math.floor(255*b)+","+a+")";}; function hsv(h,s,v){ var r,g,b,i,f,p,q,t; if (h&&s===undefined&&v===undefined)s=h.s,v=h.v,h=h.h; i = Math.floor(h*6); f = h*6-i; p = v*(1-s); q = v*(1-f*s); t = v*(1-(1-f)*s); switch(i%6){ case 0:r=v,g=t,b=p;break; case 1:r=q,g=v,b=p;break; case 2:r=p,g=v,b=t;break; case 3:r=p,g=q,b=v;break; case 4:r=t,g=p,b=v;break; case 5:r=v,g=p,b=q;break; }return gfx.fillStyle=gfx.strokeStyle="rgb("+Math.floor(255*(1-r))+","+Math.floor(255*(1-g))+","+Math.floor(255*(1-b))+")"; } var goldenAngle = 0.381966; function hsva(h,s,v,a){ var s = hsv(h,s,v); s = s.substring(0,3)+"a"+s.substring(3,s.length-1); return gfx.fillStyle=gfx.strokeStyle=s+","+a+")"; } function drawHex(x,y,r){ gfx.save(); gfx.translate(x,y); gfx.beginPath(); gfx.moveTo(0,r); gfx.lineTo(r/2*sq3,r/2); gfx.lineTo(r/2*sq3,-r/2); gfx.lineTo(0,-r); gfx.lineTo(-r/2*sq3,-r/2); gfx.lineTo(-r/2*sq3,r/2); gfx.lineTo(0,r); gfx.fill(); gfx.restore(); }; var drawPath = function(cell,i,offset){ if(offset === undefined)offset = 0; gfx.beginPath(); var p0 = Math.min(cell.path[i],cell.path[i+1]); var p1 = Math.max(cell.path[i],cell.path[i+1]); switch(p0*10+p1){ case 1:gfx.arc(baselineRad/2*sq3,baselineRad/2,baselineRad/2,5/6*Math.PI+offset,3/2*Math.PI-offset);break; case 2:gfx.arc(baselineRad/2*sq3,baselineRad*1.5,baselineRad*1.5,7/6*Math.PI+offset,3/2*Math.PI-offset);break; case 3:gfx.moveTo(baselineRad/2*sq3-offset,0);gfx.lineTo(-baselineRad/2*sq3+offset,0);break; case 4:gfx.arc(baselineRad/2*sq3,-baselineRad*1.5,baselineRad*1.5,1/2*Math.PI+offset,5/6*Math.PI-offset);break; case 5:gfx.arc(baselineRad/2*sq3,-baselineRad/2,baselineRad/2,1/2*Math.PI+offset,7/6*Math.PI-offset);break; case 12:gfx.arc(0,baselineRad,baselineRad/2,7/6*Math.PI+offset,11/6*Math.PI-offset);break; case 13:gfx.arc(-baselineRad/2*sq3,baselineRad*1.5,baselineRad*1.5,3/2*Math.PI+offset,11/6*Math.PI-offset);break; case 14:gfx.moveTo((baselineRad-offset)/4*sq3,(baselineRad-offset)*3/4);gfx.lineTo(-(baselineRad+offset)/4*sq3,-(baselineRad+offset)*3/4);break; case 15:gfx.arc(baselineRad*sq3,0,baselineRad*1.5,5/6*Math.PI+offset,7/6*Math.PI-offset);break; case 23:gfx.arc(-baselineRad/2*sq3,baselineRad/2,baselineRad/2,3/2*Math.PI+offset,1/6*Math.PI-offset);break; case 24:gfx.arc(-baselineRad*sq3,0,baselineRad*1.5,-1/6*Math.PI+offset,1/6*Math.PI-offset);break; case 25:gfx.moveTo(-(baselineRad+offset)/4*sq3,(baselineRad-offset)*3/4);gfx.lineTo((baselineRad-offset)/4*sq3,-(baselineRad+offset)*3/4);break; case 34:gfx.arc(-baselineRad/2*sq3,-baselineRad/2,baselineRad/2,11/6*Math.PI+offset,1/2*Math.PI-offset);break; case 35:gfx.arc(-baselineRad/2*sq3,-baselineRad*1.5,baselineRad*1.5,1/6*Math.PI+offset,1/2*Math.PI-offset);break; case 45:gfx.arc(0,-baselineRad,baselineRad/2,1/6*Math.PI+offset,5/6*Math.PI-offset);break; }gfx.stroke(); } //== GRID GENERATION =======================================================// var sq3 = Math.sqrt(3); var baselineRad = 48; var cellLs; var markerLs; var edgeCellLs; var fillLs = []; var fading = false; var evaluate = false; var lastEvalReqTick = -1; var cellCtr = 0; var width,height,scale; var cell = function(){ this.id = ++cellCtr+"."; this.path = []; // 0-1 | 2-3 | 4-5 this.used = [false,false,false,false,false,false]; this.adjacent = [null,null,null,null,null,null]; // E SE SW W NW NE this.markers = [null,null,null,null,null,null]; // E SE SW W NW NE this.rotation = 0; // cw = + | ccw = - [ when this is a float, treat as its rendered rotation, treat discrete rot as invalid ] this.goalRotation = 0; // when rotating tile, modify this value instead of rotation this.x; this.y; this.shadow = function(){ this.rotation+=(this.goalRotation-this.rotation)*elapsed*0.02; if(this.goalRotation !== this.rotation && Math.abs(this.goalRotation-this.rotation)<0.01)this.rotation = this.goalRotation; gfx.save(); gfx.translate(this.x,this.y); gfx.rotate(this.rotation*Math.PI/3); gfx.shadowBlur = 8; gfx.shadowColor = rgb(0.1,0.1,0.1);drawHex(0,0,baselineRad-2); gfx.restore(); return this.rotation === this.goalRotation; } this.render = function(){ for(var i=0;i<6;++i)if(this.adjacent[i] === null && this.markers[i] === null){ // TODO: experiment with endcaps } gfx.save(); gfx.translate(this.x,this.y); gfx.rotate(this.rotation*Math.PI/3); rgb(0.6,0.6,0.6);drawHex(0,0,baselineRad-8); for(var i=0;i<this.path.length;i+=2){ rgb(0.1,0.1,0.1);gfx.lineWidth = 19;drawPath(this,i); rgb(0.9,0.9,0.9);gfx.lineWidth = 10;drawPath(this,i,-0.02); }gfx.restore(); }; this.highlight = function(){ gfx.save(); gfx.translate(this.x,this.y); drawPath(this,0); gfx.restore(); }; this.rotateCW = function(){++this.goalRotation;}; this.rotateCCW = function(){--this.goalRotation;}; }; var marker = function(attach,side,color){ this.attachedTo = attach; this.side = side; // 0-5, side marker is attached to on cell this.color = color; // what color this.partner = null; // marker this should connect to this.update = function(){}; this.render = function(){ gfx.save(); gfx.translate(this.attachedTo.x,this.attachedTo.y); var r = this.side*1/3*Math.PI; gfx.shadowColor = rgb(0.1,0.1,0.1); gfx.shadowBlur = 8; gfx.lineWidth = 4; hsv(this.color,1,1); gfx.beginPath(); gfx.moveTo(Math.cos(r)*baselineRad*0.92,Math.sin(r)*baselineRad*0.92); gfx.lineTo(Math.cos(r)*baselineRad*1.12,Math.sin(r)*baselineRad*1.12); gfx.stroke(); gfx.restore(); }; }; function generateGrid(minDepth,markerPairs,preGrid){ // initializes cells, grid, and cellLs cellLs = []; edgeCellLs = []; for(var i=0;i<preGrid.length;++i) for(var j=0;j<preGrid[i].length;++j) if(preGrid[i][j] === 1){ var c = new cell(); preGrid[i][j] = c; cellLs.push(c); }else preGrid[i][j] = null; // transpose grid var grid = preGrid[0].map(function(col,i){ return preGrid.map(function(row){ return row[i]; }); }); // initialize pointers and coordinates var minX=Infinity,maxX=-Infinity,minY=Infinity,maxY=-Infinity; for(var i=0;i<grid.length;++i) for(var j=0;j<grid[i].length;++j) if(grid[i][j] !== null){ var x = grid[i][j].x = (-(j-Math.floor(grid.length/2))+2*i-2*Math.floor(grid[0].length/2))*baselineRad/2*sq3; var y = grid[i][j].y = (1.5*baselineRad*j)-1.5*baselineRad*Math.floor(grid.length/2); if(x<minX)minX=x;if(x>maxX)maxX=x;if(y<minY)minY=y;if(y>maxY)maxY=y; if(i<grid.length-1) grid[i][j].adjacent[0]=grid[i+1][j ]; if(i<grid.length-1&&j<grid[i].length-1) grid[i][j].adjacent[1]=grid[i+1][j+1]; if(j<grid[i].length-1) grid[i][j].adjacent[2]=grid[i ][j+1]; if(i>0) grid[i][j].adjacent[3]=grid[i-1][j ]; if(i>0&&j>0) grid[i][j].adjacent[4]=grid[i-1][j-1]; if(j>0) grid[i][j].adjacent[5]=grid[i ][j-1]; for(var k=0;k<6;++k){ if(grid[i][j].adjacent[k] === null){ edgeCellLs.push(grid[i][j]); break; } } } minX -= baselineRad/2*sq3; maxX += baselineRad/2*sq3; minY -= baselineRad; maxY += baselineRad; width = (maxX-minX)/baselineRad; height = (maxY-minY)/baselineRad; // initialize markers markerLs = []; shuffle(edgeCellLs); var markers = 0; fullList: for(var i in edgeCellLs) { var c = edgeCellLs[i]; var dir, m0, m1; for (var k = 0; k < 6; k++) { if ((c.adjacent[k] != null) || (c.markers[k] != null)) { continue; } var dir = k; m0 = new marker(c, dir, markers * goldenAngle + 0.3); c.markers[dir] = m0; var destCell, destDir; var recurse = function (fromCell, fromDir, depth) { if (depth > 0)fromDir = (fromDir + 3) % 6; var adjLs = [0, 1, 2, 3, 4, 5]; adjLs.splice(fromDir, 1); shuffle(adjLs); for (var a = 0; a < 6; ++a) { if (fromCell.used[adjLs[a]])continue; fromCell.used[fromDir] = true; fromCell.used[adjLs[a]] = true; fromCell.path.push(fromDir); fromCell.path.push(adjLs[a]); var clear = function () { fromCell.used[fromDir] = false; fromCell.used[adjLs[a]] = false; fromCell.path.pop(); fromCell.path.pop(); } var d = fromCell.adjacent[adjLs[a]]; if (d !== null && d !== undefined) { if (recurse(d, adjLs[a], depth + 1))return true; else { clear(); continue; } } else if (fromCell.markers[adjLs[a]]) { clear(); continue; } else if (depth < minDepth) { clear(); continue; } else { destCell = fromCell; destDir = adjLs[a]; m1 = new marker(destCell, destDir, markers * goldenAngle + 0.3); destCell.markers[adjLs[a]] = m1; return true; } } return false; }; if (!recurse(c, dir, 0)) { c.markers[dir] = null; continue; } m0.partner = m1; m1.partner = m0; markerLs.push(m0); markerLs.push(m1); markers++; if (markers === markerPairs) break fullList; } } // add rest of paths for(var i in cellLs){ var c = cellLs[i]; if(c.path.length === 6)continue; var indicies = c.used.map(function(b,i){if(!b)return i;}).filter(function(e){return e !== undefined;}); shuffle(indicies); c.path = c.path.concat(indicies); } // randomize rotations for(var i in cellLs)cellLs[i].goalRotation+=rInt(6)-3; requestEval(); }; function requestEval(){ if(lastEvalReqTick === tick)return; lastEvalReqTick = tick; evaluate = true; requestAnimationFrame(render); } function evalPaths(){ for(var i=0;i<fillLs.length;++i)fillLs[i][3] = 0; // fade everything out, gets set back to true if its detected for(var i=0;i<markerLs.length;i+=2){ // returns true=found pair,save path | false=found null,ignore path var tracedPath = []; var hash = ""; var recurse = function(fromCell,fromSide,originMkr){ fromSide = (fromSide-fromCell.goalRotation).mod(6); var toSide; for(var i=0;i<6;++i)if(fromCell.path[i] === fromSide){ if(i%2 === 0)toSide = fromCell.path[i+1]; else toSide = fromCell.path[i-1]; break; }toSide = (toSide+fromCell.goalRotation).mod(6); var saveCell = function(){ var c = new cell(); c.path.push((fromSide+fromCell.goalRotation).mod(6)); c.path.push(toSide); c.x = fromCell.x; c.y = fromCell.y; tracedPath.push(c); hash += fromCell.id; } if(fromCell.adjacent[toSide] === null){ if(fromCell.markers[toSide] === null)return false; else if(fromCell.markers[toSide].partner === originMkr){ saveCell(); return true; }else return false; }else{ if(recurse(fromCell.adjacent[toSide],(toSide+3)%6,originMkr)){ saveCell(); return true; }else return false; } }; var m = markerLs[i]; recurse(m.attachedTo,m.side,m); if(tracedPath.length > 0){ var hashFound = false; for(var q in fillLs){ var f = fillLs[q]; if(f[4] !== hash)continue; hashFound = true; f[3] = 1; break; }if(!hashFound)fillLs.push([m.color,tracedPath,0,1,hash]); } } }; //== MAIN LOOP =============================================================// function render(){ var currentTick = new Date().getTime()-tickOffset; elapsed = Math.min(currentTick-tick,5); tick = currentTick; if(evaluate){ evaluate = false; evalPaths(); } rgb(0.9,0.9,0.9); gfx.fillRect(0,0,ww,wh); gfx.save(); gfx.translate(ww/2,wh/2); gfx.scale(scale/baselineRad,scale/baselineRad); var animating = false; for(var i in cellLs)if(!cellLs[i].shadow())animating = true; for(var i in cellLs)cellLs[i].render(); for(var i in markerLs)markerLs[i].render(); for(var i in fillLs){ var f = fillLs[i]; f[2] += (f[3]-f[2])*elapsed*0.02; if(f[2] !== f[3] && Math.abs(f[2]-f[3]) < 0.01)f[2] = f[3]; if(f[2] !== f[3])animating = true; gfx.shadowColor = hsva(f[0],1,1,f[2]); gfx.shadowBlur = 4; gfx.lineWidth = 4; for(var j in f[1])f[1][j].highlight(); }if(animating)requestAnimationFrame(render); gfx.restore(); }; //== INPUT HANDLING ========================================================// var tchStart,tchMove,tchCell; function getMousePos(evt){ var rect = canvas.getBoundingClientRect(); return{x:evt.clientX-rect.left,y:evt.clientY-rect.top}; }; canvas.addEventListener("mouseup",function(e){ var mouse = getMousePos(e); var minDist = Infinity; var minCell = null; for(var i in cellLs){ var c = cellLs[i]; var x = c.x*scale/baselineRad+ww/2-mouse.x; var y = c.y*scale/baselineRad+wh/2-mouse.y; var sqDist = x*x+y*y; if(sqDist<minDist){ minDist = sqDist; minCell = c; } }if(minDist>scale*scale)return; switch(e.which){ case 1:minCell.rotateCCW();break; case 3:minCell.rotateCW();break; }requestEval(); }); document.addEventListener("keyup",function(e){ switch(e.keyCode){ case 83:for(var i in cellLs)cellLs[i].goalRotation=rInt(6)-3;requestEval();break; // S } },false); canvas.addEventListener("touchstart",function(e){ // e.preventDefault(); // turns off clicking var tch = e.changedTouches[0]; tchCell = null; tchStart = [tch.pageX,tch.pageY]; tchMove = tchStart; var minDist = Infinity; var minCell = null; for(var i in cellLs){ var c = cellLs[i]; var x = c.x*scale/baselineRad+ww/2-tchStart[0]; var y = c.y*scale/baselineRad+wh/2-tchStart[1]; var sqDist = x*x+y*y; if(sqDist<minDist){ minDist = sqDist; minCell = c; } }if(minDist>scale*scale)return; tchCell = minCell; },false); canvas.addEventListener("touchmove",function(e){ e.preventDefault(); if(tchCell === null)return; var tch = e.changedTouches[0]; var move = tch.pageX-tchMove[0]; if(Math.abs(move)>scale){ if(move<0)tchCell.rotateCCW(); else if(move>0)tchCell.rotateCW(); tchMove = [tch.pageX,tch.pageY]; }requestEval(); },false); //== PROGRAM ENTRY + MISC ==================================================// canvas.oncontextmenu = function(){return false;}; // prevent right click menu window.onresize = function(){ // auto-resize canvas to window ww = canvas.width = window.innerWidth; wh = canvas.height = window.innerHeight; var scaleX = ww/(width*1.2); var scaleY = wh/(height*1.2); scale = Math.min(scaleX,scaleY); requestAnimationFrame(render); }; (function main(){ document.body.appendChild(canvas); var minDepth = 6; var markerPairs = 7; window.onresize(); // generateGrid(2,3,[[1,1,0],[1,1,1],[0,1,1]]); generateGrid(4,5,[[1,1,1,0,0],[1,1,1,1,0],[1,1,0,1,1],[0,1,1,1,1],[0,0,1,1,1]]); // generateGrid(6,7,[[1,1,1,1,0,0,0],[1,1,1,1,1,0,0],[1,1,1,1,1,1,0],[1,1,1,0,1,1,1],[0,1,1,1,1,1,1],[0,0,1,1,1,1,1],[0,0,0,1,1,1,1]]); window.onresize(); tickOffset=new Date().getTime();requestAnimationFrame(render);} )(); </script></body></html>
Source files: 

Team