// 解决 IE 和非 IE 浏览器在事件绑定方面的兼容性 function addEventHandler(elem, event, handler) { if (elem.addEventListener) { elem.addEventListener(event, handler, false); } else if (elem.attachEvent) { elem.attachEvent("on" + event, handler); } else { elem["on" + event] = handler; } } // 以下关于 css 前缀的处理方法借鉴自 jQuery :jquery-3.1.0.js function getProperName(cssPropertyName) { var name = cssPropertyName; var cssPrefixes = ["Webkit", "Moz", "ms"]; var emptyStyle = document.createElement("div").style; if (name in emptyStyle) { return name; } var capName = name[0].toUpperCase + name.slice(1); var i = cssPrefixes.length; while (i--) { name = cssPrefixes[i] + capName; if (name in emptyStyle) { return name; } } } function generateInlineStyle(degree) { var deg = degree || 0; return 'rotate(' + deg + 'deg)'; } // 用于偷懒的 $ 方法 function $(selector) { var elem = null; if (selector.indexOf('#') === 0) { elem = document.getElementById(selector.slice(1)); } else if (selector.indexOf('.') === 0) { elem = document.getElementsByClassName(selector.slice(1)); } return elem; } // 用于添加类名 function addClassName(elem, clsname) { var names = elem.className.split(/\s/); var index = names.indexOf(clsname); if (index === -1) { names.push(clsname); elem.className = names.join(' '); return true; } return false; } // 用于删除类名 function removeClassName(elem, clsname) { var names = elem.className.split(/\s/); var index = names.indexOf(clsname); if (index !== -1) { names.splice(index, 1); elem.className = names.join(' '); return true; } return false; } // DOM // var squareDOM = document.getElementById("square"); var boardDOM = $("#board"); var directivesDOM = $("#directives"); var runBtnDOM = $(".run")[0]; var refreshBtnDOM = $(".refresh")[0]; // 区域 var board = (function() { var ROW = 10; var COLUMN = 10; var bd = []; var generateBoard = function() { var cell = null; for (var i = 0; i < ROW; i++) { var rowTmp = []; for (var j = 0; j < COLUMN; j++) { cell = document.createElement("div"); cell.className = "grid"; if (j === 0) { cell.className += " clearfix"; } else if (j === 9) { cell.className += " rightmost"; } if (i === 9) { cell.className += " downmost"; } boardDOM.appendChild(cell); rowTmp.push(0); } bd.push(rowTmp); } }; return { generate: function() { generateBoard(); }, getRowNum: function() { return ROW; }, getColNum: function() { return COLUMN; }, getBoardState: function() { return bd; } }; })(); // 小方块 var square = (function() { var sqPos = { x: 1, y: 1 }; // default position // 1~10 var sqDir = 0; // default direction // ...,-3,-2,-1,0,1,2,3,... var squareDOM = null; var PROPERTY = getProperName("transform"); console.log("当前浏览器支持的是 " + PROPERTY); // 真正的取模,而不是取余 var directionMod4 = function(direction) { return (direction % 4 + 4) % 4; }; // 对 squareDOM 进行移动(定位) var moveSqDOM = function() { squareDOM.style.top = (sqPos.x - 1) * 40 - 1 + "px"; squareDOM.style.left = (sqPos.y - 1) * 40 - 1 + "px"; }; // 返回小方块将抵达的位置 var traDir = function(direction, pace) { var furPos = sqPos; if (direction === 0 && furPos.x >= pace + 1) { furPos.x -= pace; } else if (direction === 1 && furPos.y <= board.getColNum() - pace) { furPos.y += pace; } else if (direction === 2 && furPos.x <= board.getRowNum() - pace) { furPos.x += pace; } else if (direction === 3 && furPos.y >= pace + 1) { furPos.y -= pace; } else { throw new Error("将抵达边界,无法继续前进 XO"); } return furPos; }; // 尝试移动小方块,当“抵达边界”或“存在障碍物”时报错,否则返回 true var tryMove = function(direction, pace) { var pac = pace || 1; var dir = (typeof direction === "number") ? direction : directionMod4(sqDir); // direction can be 0 var furPos = traDir(dir, pac); if (board.getBoardState()[furPos.x - 1][furPos.y - 1] === 0) { sqPos = furPos; } else { throw new Error("此方向存在障碍物,无法继续前进 XO"); } return true; }; // 返回转到某方向的最小角度 var turnTo = function(direction) { var curDir = directionMod4(sqDir); // 小方块的当前朝向 // if(curDir === direction) return 0; /* 就近转向 */ // Any better way ??? var cwStep = 0; // 顺时针(clockwise)所需转向次数 while (true) { if ((curDir + cwStep) % 4 === direction) { break; } cwStep++; } var deg = 0; if (cwStep > 2) { deg = (cwStep - 4) * 90; } else { deg = cwStep * 90; } return deg; }; // 转向 var takeTurn = function(degree) { var deg = degree; sqDir += deg / 90; /* 小方块的旋转 */ squareDOM.style[PROPERTY] = generateInlineStyle(90 * sqDir); }; // 移动小方块 var move = function(direction, pace, isMOV) { if (tryMove(direction, pace)) { if (isMOV) { // 如果是 MOV 指令,在移动前,先转向 takeTurn(turnTo(direction)); } moveSqDOM(); } }; return { generate: function() { // get square random position and direction sqPos.x = Math.ceil(Math.random() * 10); sqPos.y = Math.ceil(Math.random() * 10); sqDir = Math.floor(Math.random() * 4); // generate dom squareDOM = document.createElement("div"); squareDOM.id = "square"; moveSqDOM(); squareDOM.style[PROPERTY] = generateInlineStyle(90 * sqDir); boardDOM.appendChild(squareDOM); }, go: function(step) { move(undefined, step); }, tunlef: function() { takeTurn(-90); }, tunrig: function() { takeTurn(90); }, tunbac: function() { takeTurn(180); }, tratop: function(step) { move(0, step); }, trarig: function(step) { move(1, step); }, trabot: function(step) { move(2, step); }, tralef: function(step) { move(3, step); }, movtop: function(step) { move(0, step, true); }, movrig: function(step) { move(1, step, true); }, movbot: function(step) { move(2, step, true); }, movlef: function(step) { move(3, step, true); }, }; })(); // 代码编辑 textarea var codeArea = (function() { // DOM: directivesDOM var lineNumDOM = $("#lineNum"); var trmdVals = []; var rows = 1; // var executable = 0; // nothing to execute // var VALIDKEYCODE = [] function resetHighlightLine(clsnames) { function removeClassNames(elem) { for (var i = 0; i < clsnames.length; i++) { removeClassName(elem, clsnames[i]); } } for (var i = 0; i < lineNumDOM.children.length; i++) { removeClassNames(lineNumDOM.children[i]); // removeClassName(lineNumDOM.children[i], "current"); // removeClassName(lineNumDOM.children[i], "stop"); } } function highlightCurLine(index) { resetHighlightLine(["current"]); addClassName(lineNumDOM.children[index], "current"); } function highlightRunTimeErrorLine(index) { var curElem = lineNumDOM.children[index]; removeClassName(curElem, "current"); addClassName(curElem, "stop"); } function getDirectives() { var values = directivesDOM.value.split("\n"); // 去掉空白符 & 小写 var trimmedValues = values.map(function(elem, index) { return elem.trim().replace(/\s/g, '').toLowerCase(); }); return trimmedValues; } function getNonEmptyDirectives(directives) { var values = directives || getDirectives(); console.log("Before NonEmp, values: ") console.log(values) return values.filter(function(elem) { // return /^\S+$/.test(elem); return elem; // [!!NOTE] this VS elem }); } function checkOneDirective(directive) { var res = []; var name = ""; var step = 0; if (!/^[a-z]+\d*$/.test(directive)) { return false; } else { name = directive.match(/[a-z]+/)[0]; if (!square[name]) { return false; } // [!!NOTE] 注意这里用 + 而不能是 * :星号总能匹配到空字符串,而加号能匹配到 null 或者数字字符串 // [!!NOTE] 注意下两行中的 && 和 || var temp = directive.match(/\d+/) && directive.match(/\d+/)[0]; // e.g. null && ~ -> null | ["1"] && "1" -> "1" step = parseInt(temp) || 0; // 当值为 null 时,得到 NaN 。NaN || 0 -> 0 /* step = parseInt(temp); if (isNaN(step)) { step = 0; } */ res = [name, step]; return res; } } function findError(directives) { // 默认不带参数到时候,会检查和输入等量的指令 // 如果带了参数,可以减少重复计算; // 或者检查非空指令集 var errorLine = []; var values = directives || getDirectives(); for (var i = 0; i < values.length; i++) { if (values[i] && !checkOneDirective(values[i])) { // 非空行,且未通过 check 的 errorLine.push(i); } } return errorLine; } function highlightError(errors) { for (var i = 0; i < lineNumDOM.children.length; i++) { if (errors.indexOf(i) !== -1) { lineNumDOM.children[i].className = "errorLine"; } else { lineNumDOM.children[i].className = ""; } } } function setLineNum(num) { var i = 0; while (lineNumDOM.children.length < num) { lineNumDOM.appendChild(document.createElement("li")); } while (lineNumDOM.children.length > num) { lineNumDOM.lastElementChild.remove(); } } function autoSize() { var lineH = (directivesDOM.value.match(/\n/g) && (directivesDOM.value.match(/\n/g).length + 1)) || 1; setLineNum(lineH); } function autoSizeAndCheck(e) { // if (e.keyCode === 13) { autoSize(); highlightError(findError()); // checkError(); // } } return { initialize: function() { addEventHandler(directivesDOM, "change", autoSizeAndCheck); addEventHandler(directivesDOM, "keydown", autoSizeAndCheck); addEventHandler(directivesDOM, "keyup", autoSizeAndCheck); autoSize(); }, runDirectives: function() { var values = getNonEmptyDirectives(); console.log("After NonEmp, values: ") console.log(values) var time = 0; var TIMESPACE = 800; var self = this; var stIDs = []; var tempID = null; if (findError(values).length) { throw new Error("You still have some errors in your directives. Please correct them first!\n(Valid directives are presented in Cheat-Sheet top right.)") } // else for (var i = 0; i < values.length; i++) { // window.setTimeout(function() {}, ); console.log("NonEmpQueue-" + i + ": " + checkOneDirective(values[i])); // 闭包 Way 1 - 无法达到 setTimeout 的效果 // [!!NOTE] :( 问题出在,对传给 setTimeout 函数的回调函数的立即调用上 Orz /* tempID = window.setTimeout(function(index) { // Do sth... }(i), time); stIDs.push(tempID); */ // 闭包 Way 2(ver1) - 无法取消 setTimeout 队列(?) /*(function(index) { window.setTimeout(function() { var arr = checkOneDirective(values[index]); var name = arr[0]; var step = arr[1]; try { square[name](step); } catch (e) { alert(e.message); } }, time); })(i);*/ // 闭包 Way 2(ver2) - 尝试达到 setTimeout tempID = (function(index) { return window.setTimeout(function() { var arr = checkOneDirective(values[index]); var name = arr[0]; var step = arr[1]; try { highlightCurLine(index); square[name](step); } catch (e) { highlightRunTimeErrorLine(index); // console.log("whoooops..."); // console.log(stIDs); stIDs.slice(index + 1).forEach(function(elem) { window.clearTimeout(elem); console.log("clear----clear----clear") }); // console.log(e.message); alert(e.message); } }, time); })(i); stIDs.push(tempID); time += TIMESPACE; } }, refresh: function() { resetHighlightLine(["current", "stop", "errorLine"]); directivesDOM.value = ""; directivesDOM.focus(); autoSize(); } }; })(); /*var getValidDirective = function(whatever) { // 数组去重 var valueList = {}; var uniqueTrmdValues = []; for (var i = 0; i < trimmedValues.length; i++) { if (valueList[trimmedValues[i]] === undefined) { valueList[trimmedValues[i]] = 1; uniqueTrmdValues.push(trimmedValues[i]); } else { valueList[trimmedValues[i]]++; } } };*/ addEventHandler(runBtnDOM, "click", function() { // 原本的 try-catch 是写在这里的,但是由于 setTimeout 方法对线程的破坏(阻塞?无视??) // 将 try-catch 移到每个单独的 setTimeout 方法内部 /*try {*/ // console.log("Which one is the first?") codeArea.runDirectives(); // console.log("try{}'s end.") /*}*/ /*catch (e) { alert(e.message); }*/ // console.log("event handler's end") // console.log("-------------") }); addEventHandler(refreshBtnDOM, "click", function() { codeArea.refresh(); }); /*directivesDOM.onkeydown = function(event) { var e = event || window.event; switch (e.keyCode) { case 13: dirInputBtnDOM.click(); // 关于模拟点击事件触发的其他方法(better way???) break; } };*/ board.generate(); square.generate(); codeArea.initialize(); directivesDOM.focus();
<div class="panel"> <div id="board"> <!-- div.grid*100 --> <!-- <div id="square"></div> --> </div> <div class="controller"> <div class="btnWrapper"> <input type="button" value="Run" class="run" /> <input type="button" value="Refresh" class="refresh" /> </div> <div class="dirWrapper"> <label for="directives"> <ol id="lineNum"> <li></li> </ol> <textarea name="directives" id="directives" cols="30" rows="10"></textarea> </label> <!-- <input type="text" id="directives" /> --> </div> </div> </div> <sidebar id="cheatSheet"> <h4>Directive Cheat Sheet</h4> <ul> <li>GO - 前进</li> <li>TUN LEF - 向左转</li> <li>TUN RIG - 向右转</li> <li>TUN BAC - 向后转</li> <li>TRA TOP - 向上转并前进</li> <li>TRA RIG - 向右转并前进</li> <li>TRA BOT - 向下转并前进</li> <li>TRA LEF - 向左转并前进</li> <li>MOV TOP - 向上移动</li> <li>MOV RIG - 向右移动</li> <li>MOV BOT - 向下移动</li> <li>MOV LEF - 向左移动</li> </ul> <p>其中,GO 命令、TRA 命令和 MOV 命令可以添加一个数字做为参数,用来制定移动的步数。</p> </sidebar>
@charset "UTF-8"; /* debug 'Invalid US-ASCII character' */ body { margin: 0; } .panel { counter-reset: rowIndex; } /*-------------------- 区域 & 小方块 --------------------*/ #board { position: relative; margin: 100px auto 20px; border: 2px solid; width: 398px; height: 398px; counter-reset: colIndex; } .grid { position: relative; float: left; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; border-right: 2px solid #ddd; border-bottom: 2px solid #ddd; width: 40px; height: 40px; background-color: mistyrose; /* [!!note] 注意 nth-child 里的 n 必须写在最前,否则不会生效 */ /* 添加数字 */ } .grid.clearfix { clear: both; } .grid:nth-child(-n + 10):before { position: absolute; top: -24px; left: 8px; display: block; /* counter-reset by #board */ counter-increment: colIndex; content: counter(colIndex); width: 24px; height: 24px; line-height: 24px; text-align: center; font-family: Helvetica; } .grid:nth-child(10n + 1):after { position: absolute; top: 8px; left: -24px; display: block; /* counter-reset by .panel */ counter-increment: rowIndex; content: counter(rowIndex); width: 24px; height: 24px; line-height: 24px; text-align: center; font-family: Helvetica; } .grid.downmost { border-bottom: none; height: 38px; } .grid.rightmost { border-right: none; width: 38px; } #square { position: absolute; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; border-top: 8px solid salmon; width: 40px; height: 40px; background-color: lightpink; transition: all .5s; } #square:before, #square:after { position: absolute; top: -6px; display: block; content: '\a0'; border-radius: 5px / 15px; width: 10px; height: 30px; background-color: salmon; } #square:before { left: 6px; transform: rotate(5deg); } #square:after { right: 6px; transform: rotate(-5deg); } /*-------------------- 按钮 & 代码编辑 --------------------*/ .controller { overflow: hidden; margin: 0 auto; /*width: 420px;*/ text-align: center; } .controller .btnWrapper { overflow: hidden; margin: 0 auto; margin-bottom: 10px; width: 500px; } .controller .btnWrapper input { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; display: block; margin: 0; border: 1px solid; border-radius: 3px; padding: 6px 0; width: 45%; background-color: #3eb7d1; color: rgba(255, 255, 255, 0.8); } .controller .btnWrapper input.run { float: left; } .controller .btnWrapper input.refresh { float: right; border-color: #3eb7d1; background-color: #FFF; color: #3eb7d1; } .controller .dirWrapper { clear: left; overflow-y: auto; margin: 0 auto; border-radius: 4px; width: 500px; height: 200px; } .controller .dirWrapper label { position: relative; display: block; margin: 0 auto; } .controller .dirWrapper label ol { margin: 0; padding: 0; width: 10%; min-height: 200px; background-color: #1d1e21; text-align: right; counter-reset: lineNum; } .controller .dirWrapper label ol li { position: relative; padding: 0 5px; list-style: none; } .controller .dirWrapper label ol li:before { display: block; counter-increment: lineNum; content: counter(lineNum); font-family: Consolas; line-height: 20px; color: #8f9293; } .controller .dirWrapper label ol li.current { background-color: #3eb7d1; } .controller .dirWrapper label ol li.current:before { color: #fff; } .controller .dirWrapper label ol li.stop { background-color: #f56680; } .controller .dirWrapper label ol li.stop:before { color: #fff; } .controller .dirWrapper label ol li.errorLine:after { position: absolute; top: 50%; left: 4px; display: block; content: '\a0'; border-radius: 6px; width: 6px; height: 6px; background-color: #f56680; transform: translate(0, -3px); -webkit-animation: twinkling 800ms ease-out infinite alternate; -moz-animation: twinkling 800ms ease-out infinite alternate; -ms-animation: twinkling 800ms ease-out infinite alternate; -o-animation: twinkling 800ms ease-out infinite alternate; animation: twinkling 800ms ease-out infinite alternate; /*这里使用了呼吸动画*/ } .controller .dirWrapper label textarea { overflow: hidden; position: absolute; top: 0; left: 10%; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; border: none; padding: 0 10px; width: 90%; height: 100%; background-color: #2a2b2c; color: #fff; line-height: 20px; font-family: Consolas; resize: none; outline: none; } /*以下定义动画帧*/ @-webkit-keyframes twinkling { 0% { opacity: 0; } 100% { opacity: 1; } } @-moz-keyframes twinkling { 0% { opacity: 0; } 100% { opacity: 1; } } @-ms-keyframes twinkling { 0% { opacity: 0; } 100% { opacity: 1; } } @-o-keyframes twinkling { 0% { opacity: 0; } 100% { opacity: 1; } } @keyframes twinkling { 0% { opacity: 0; } 100% { opacity: 1; } } /*-------------------- 合法指令表 --------------------*/ #cheatSheet { overflow: hidden; position: fixed; top: 0; right: 10px; border: 4px solid #2a9ab2; border-bottom-left-radius: 10px; border-bottom-right-radius: 10px; max-width: 240px; height: 60px; background-color: rgba(255, 255, 255, 0.7); font-family: Consolas; } #cheatSheet:hover { height: auto; } #cheatSheet h4 { position: relative; margin: 21px 0; color: #2a9ab2; text-align: center; font-family: Helvetica; } #cheatSheet h4:after { position: absolute; top: 4px; right: 20px; display: inline-block; content: '\a0'; width: 0; height: 0; border-left: 6px solid transparent; border-top: 8px solid #2a9ab2; border-right: 6px solid transparent; } #cheatSheet ul { padding: 0 20px 0 30px; font-size: 14px; } #cheatSheet ul li { position: relative; padding: 3px 0 3px 10px; list-style: none; } #cheatSheet ul li:before { position: absolute; top: 6px; left: 0; display: block; content: '\a0'; width: 0; height: 0; border-top: 4px solid transparent; border-left: 6px solid #2a9ab2; border-bottom: 4px solid transparent; } #cheatSheet p { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; margin: 0; padding: 0 20px 20px 20px; font-size: 13px; } /*# sourceMappingURL=style.css.map */