├── .gitignore ├── bot.png ├── .editorconfig ├── README.md ├── promise.js ├── image-reader.js ├── TASK.md ├── index.html ├── boxbot-map.js ├── boxbot-editor.js ├── boxbot.css ├── boxbot-finder.js ├── boxbot-bot.js ├── app.js └── boxbot.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiuxiang/boxbot/HEAD/bot.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [任务描述](https://github.com/qiuxiang/boxbot/blob/master/TASK.md)   [在线演示](http://qiuxiang.github.io/boxbot) 2 | 3 | ![演示](https://cloud.githubusercontent.com/assets/1709072/14040147/1d75833e-f29f-11e5-957a-59c04909932d.gif) 4 | -------------------------------------------------------------------------------- /promise.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 简易版 Promise,非标准实现,只兼容一小部分用法 3 | * 4 | * @param {function} executor 5 | * @constructor 6 | */ 7 | var Promise = function (executor) { 8 | executor(this.resolve.bind(this), this.reject.bind(this)) 9 | } 10 | 11 | Promise.prototype.resolve = function (value) { 12 | setTimeout((function () { 13 | if (this.onResolve) { 14 | this.onResolve(value) 15 | } 16 | }).bind(this), 0) 17 | } 18 | 19 | Promise.prototype.reject = function (reason) { 20 | setTimeout((function () { 21 | if (this.onReject) { 22 | this.onReject(reason) 23 | } 24 | }).bind(this), 0) 25 | } 26 | 27 | Promise.prototype.then = function (onResolve, onReject) { 28 | this.onResolve = onResolve 29 | if (onReject) { 30 | this.onReject = onReject 31 | } 32 | } 33 | 34 | Promise.prototype.catch = function (onReject) { 35 | this.onReject = onReject 36 | } -------------------------------------------------------------------------------- /image-reader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @constructor 3 | */ 4 | var ImageReader = function () { 5 | this.image = document.createElement('img') 6 | this.canvas = document.createElement('canvas').getContext('2d') 7 | this.reader = new FileReader() 8 | this.reader.addEventListener('load', this.load.bind(this)) 9 | } 10 | 11 | /** 12 | * 读取图片数据 13 | * 14 | * @param {Blob} file 15 | * @param {int} width 16 | * @param {int} height 17 | * @returns {Promise} 18 | */ 19 | ImageReader.prototype.read = function (file, width, height) { 20 | return new Promise((function (resolve) { 21 | this.width = width 22 | this.height = height 23 | this.reader.readAsDataURL(file) 24 | this.resolve = resolve 25 | }).bind(this)) 26 | } 27 | 28 | ImageReader.prototype.load = function () { 29 | this.image.src = this.reader.result 30 | this.canvas.drawImage(this.image, 0, 0, this.width, this.height) 31 | var data = [] 32 | for (var y = 0; y < this.width; y += 1) { 33 | data.push([]) 34 | for (var x = 0; x < this.height; x += 1) { 35 | data[y].push(this.toRGBA(this.canvas.getImageData(x, y, 1, 1).data)) 36 | } 37 | } 38 | this.resolve(data) 39 | } 40 | 41 | /** 42 | * RGBA 数组转十六进制 43 | * 44 | * @param {CanvasPixelArray} pixel 45 | * @returns {string} 46 | */ 47 | ImageReader.prototype.toRGBA = function (pixel) { 48 | return 'rgba(' + pixel[0] + ',' + pixel[1] + ',' + pixel[2] + ',' + pixel[2] + ')' 49 | } 50 | -------------------------------------------------------------------------------- /TASK.md: -------------------------------------------------------------------------------- 1 | # 百度前端技术学院2016春季班任务:听指令的小方块 2 | 3 | ## 阶段一 4 | 5 | ![阶段一](http://7xrp04.com1.z0.glb.clouddn.com/task_2_33_1.jpg) 6 | 7 | * 如图,实现一个类似棋盘的格子空间,每个格子用两个数字可以定位,一个红正方形的DOM在这个空间内,正方形中的蓝色边表示这是他的正面,有* 一个input输入框 8 | * 在输入框中允许输入如下指令,按下按钮后,使得正方形做相应动作 9 | * GO:向蓝色边所面向的方向前进一格(一格等同于正方形的边长) 10 | * TUN LEF:向左转(逆时针旋转90度) 11 | * TUN RIG:向右转(顺时针旋转90度) 12 | * TUN BAC:向右转(旋转180度) 13 | * 移动不能超出格子空间 14 | 15 | ## 阶段二 16 | 17 | * 对于正方形的移动增加相应动画,包括移动和旋转 18 | * 每个指令的执行时间是1s(可以自己调整) 19 | * 增加新的指令如下: 20 | * TRA LEF:向屏幕的左侧移动一格,方向不变 21 | * TRA TOP:向屏幕的上面移动一格,方向不变 22 | * TRA RIG:向屏幕的右侧移动一格,方向不变 23 | * TRA BOT:向屏幕的下面移动一格,方向不变 24 | * MOV LEF:方向转向屏幕左侧,并向屏幕的左侧移动一格 25 | * MOV TOP:方向转向屏幕上面,向屏幕的上面移动一格 26 | * MOV RIG:方向转向屏幕右侧,向屏幕的右侧移动一格 27 | * MOV BOT:方向转向屏幕下面,向屏幕的下面移动一格 28 | 29 | ## 阶段三 30 | 31 | ![阶段三](http://7xrp04.com1.z0.glb.clouddn.com/task_2_35_2.jpg) 32 | 33 | * 如图,命令输入框由input变为textarea,可以允许输入多条指令,每一行一条 34 | * textarea左侧有一列可以显示当前行数的列(代码行数列),列数保持和textarea中一致 35 | * 当textarea发生上下滚动时,代码行数列同步滚动 36 | * 能够判断指令是否合法,不合法的指令给出提示(如图) 37 | * 点击执行时,依次逐条执行所有命令 38 | * 对于GO,TRA以及MOV指令增加可以移动格子数量的参数,例如 39 | * GO 3:向当前方向前进三格 40 | * TRA TOP 2:向屏幕上方平移两格 41 | * MOV RIG 4:方向转向屏幕右侧,向屏幕的右侧移动四格 42 | 43 | ## 阶段四 44 | 45 | ![阶段三](http://7xrp04.com1.z0.glb.clouddn.com/task_2_36_1.jpg) 46 | 47 | * 如图,新增元素“墙”,墙是正方形不可进入、越过的区域 48 | * 新增修墙的指令,BIUD,执行指令时,会在当前方块面对的方向前修建一格墙壁,如果被指定修墙的地方超过边界墙或者已经有墙了,则取消修墙操作,并调用浏览器的console.log方法打印一个错误日志 49 | * 新增粉刷的指令,BRU color,color是一个字符串,保持和css中颜色编码一致。执行指令时,如果当前方块蓝色边面对方向有紧相邻的墙,* 则将这个墙颜色改为参数颜色,如果没有,则通过调用浏览器的console.log方法,打印一个错误日志 50 | * 尝试写一段代码,实现在空间内修建一个长长的五颜六色的墙或者有趣的图形 51 | * 新增一个按钮,可以在空间内随机生成一些墙 52 | * 增加一个指令:MOV TO x, y,会使得方块从当前位置移动到坐标为x,y的地方,移动过程中不能进入墙所在的地方,寻路算法请自行选择并实现,不做具体要求 53 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 听指令的小方块 7 | 8 | 9 | 10 |
11 |
12 |
13 | bot 14 |
15 |
16 |
17 | 21 | 27 | 28 | 29 | 30 | 31 |
32 |
33 |
34 |
35 |
1
36 |
37 |
38 | 50 |
51 |
52 |
53 |

注:选择图片文件后,程序会读取图片并生成绘图命令,接着运行命令,机器人将一步一步地把图形画出来!

54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /boxbot-map.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @constructor 3 | * @param {string} selector 4 | */ 5 | var BoxbotMap = function (selector) { 6 | this.element = document.querySelector(selector) 7 | } 8 | 9 | /** 10 | * 创建地图 11 | * 12 | * @param {int} columns 13 | * @param {int} rows 14 | */ 15 | BoxbotMap.prototype.create = function (columns, rows) { 16 | var html = '' 17 | for (var y = 0; y <= rows; y += 1) { 18 | html += '' 19 | for (var x = 0; x <= columns; x += 1) { 20 | if (x == 0 && y == 0) { 21 | html += '' 22 | } else { 23 | if (y == 0) { 24 | html += '' + x + '' 25 | } else if (x == 0) { 26 | html += '' + y + '' 27 | } else { 28 | html += '' 29 | } 30 | } 31 | } 32 | html += '' 33 | } 34 | this.columns = columns 35 | this.rows = rows 36 | this.element.innerHTML = html 37 | this.boxs = this.element.getElementsByTagName('td') 38 | } 39 | 40 | /** 41 | * 清除地图数据 42 | */ 43 | BoxbotMap.prototype.clear = function () { 44 | for (var y = 1; y <= this.rows; y += 1) { 45 | for (var x = 1; x <= this.columns; x += 1) { 46 | var box = this.get([x, y]) 47 | box.style.backgroundColor = '' 48 | box.dataset.type = 'null' 49 | } 50 | } 51 | } 52 | 53 | /** 54 | * 获取指定位置的方块 55 | * 56 | * @param {[int]} position 57 | * @returns {Element} 58 | */ 59 | BoxbotMap.prototype.get = function (position) { 60 | return this.boxs[position[1] * (this.rows + 1) + position[0]] 61 | } 62 | 63 | /** 64 | * 设置指定位置的方块类型 65 | * 66 | * @param {[int]} position 67 | * @param type 68 | */ 69 | BoxbotMap.prototype.set = function (position, type) { 70 | this.get(position).dataset.type = type 71 | } 72 | 73 | /** 74 | * 设置指定位置的方块颜色 75 | * 76 | * @param {[int]} position 77 | * @param color 78 | */ 79 | BoxbotMap.prototype.setColor = function (position, color) { 80 | this.get(position).style.backgroundColor = color 81 | } 82 | 83 | /** 84 | * 判断指定位置是否为空 85 | * 86 | * @param {[int]} position 87 | * @returns {string} 88 | */ 89 | BoxbotMap.prototype.getType = function (position) { 90 | var box = this.get(position) 91 | return box && box.dataset.type 92 | } 93 | -------------------------------------------------------------------------------- /boxbot-editor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @constructor 3 | * @param {string} selector 4 | */ 5 | var BoxbotEditor = function (selector) { 6 | this.element = document.querySelector(selector) 7 | this.$lines = this.element.querySelector('.commander-lines') 8 | this.$textarea = this.element.querySelector('.commander-editor') 9 | this.$textarea.addEventListener('input', this.update.bind(this)) 10 | this.$textarea.addEventListener('scroll', this.scroll.bind(this)) 11 | this.update() 12 | } 13 | 14 | /** 15 | * 代码行数同步滚动 16 | * 17 | * @param event 18 | */ 19 | BoxbotEditor.prototype.scroll = function (event) { 20 | this.$lines.style.top = -event.target.scrollTop + 'px' 21 | } 22 | 23 | /** 24 | * 滚动到制定行数 25 | * 26 | * @param line 27 | */ 28 | BoxbotEditor.prototype.scrollTo = function (line) { 29 | this.$textarea.scrollTop = line * 20 30 | } 31 | 32 | /** 33 | * 更新代码行数 34 | */ 35 | BoxbotEditor.prototype.update = function () { 36 | var html = '' 37 | var codes = this.$textarea.value 38 | var lines = codes.match(/\n/g) 39 | lines = lines ? lines.length + 1 : 1 40 | 41 | for (var l = 1; l <= lines; l++) { 42 | html += '
' + l + '
' 43 | } 44 | 45 | this.$lines.innerHTML = html 46 | } 47 | 48 | /** 49 | * @returns {NodeList} 50 | */ 51 | BoxbotEditor.prototype.getLines = function () { 52 | return this.element.querySelectorAll('.commander-lines-item') 53 | } 54 | 55 | /** 56 | * 设置标记 57 | * 58 | * @param {int} line 59 | * @param {string} flag 60 | */ 61 | BoxbotEditor.prototype.setFlag = function (line, flag) { 62 | this.getLines()[line].classList.add(flag) 63 | } 64 | 65 | /** 66 | * 清除标记 67 | * 68 | * @param {int} [line] 默认清除所有标记 69 | */ 70 | BoxbotEditor.prototype.clearFlag = function (line) { 71 | var lines = this.getLines() 72 | if (line) { 73 | lines[line].className = 'commander-lines-item' 74 | } else { 75 | for (var i = 0; i < lines.length; i += 1) { 76 | lines[i].className = 'commander-lines-item' 77 | } 78 | } 79 | } 80 | 81 | /** 82 | * 获取代码列表 83 | * 84 | * @returns {[string]} 85 | */ 86 | BoxbotEditor.prototype.getCodes = function () { 87 | var codes = [] 88 | this.$textarea.value.split('\n').forEach(function (code) { 89 | codes.push(code.trim()) 90 | }) 91 | return codes 92 | } 93 | 94 | /** 95 | * 设置编辑器代码,同时触发更新 96 | * 97 | * @param codes 98 | */ 99 | BoxbotEditor.prototype.setCodes = function (codes) { 100 | this.$textarea.value = codes 101 | this.update() 102 | } -------------------------------------------------------------------------------- /boxbot.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | width: 1240px; 4 | margin: 20px auto; 5 | padding-right: 10px; 6 | } 7 | 8 | .clearfix:after { 9 | content: ''; 10 | display: block; 11 | clear: both; 12 | } 13 | 14 | .boxbot { 15 | position: relative; 16 | } 17 | 18 | .boxbot-box { 19 | width: 37px; 20 | height: 37px; 21 | box-sizing: border-box; 22 | transition-property: left, top, transform; 23 | transition-duration: .25s; 24 | } 25 | 26 | .boxbot-bot { 27 | position: absolute; 28 | padding-left: 1px; 29 | padding-top: 1px; 30 | } 31 | 32 | .boxbot-bot img { 33 | width: 36px; 34 | height: 36px; 35 | } 36 | 37 | .boxbot-map { 38 | border-collapse: collapse; 39 | float: left; 40 | } 41 | 42 | td.boxbot-box { 43 | border: 1px solid #bdc3c7; 44 | padding: 5px; 45 | font-size: 12px; 46 | } 47 | 48 | .boxbot-box[data-type="y-axis"], 49 | .boxbot-box[data-type="x-axis"] { 50 | border: 0; 51 | } 52 | 53 | .boxbot-box[data-type="y-axis"] { 54 | text-align: right; 55 | } 56 | 57 | .boxbot-box[data-type="x-axis"] { 58 | text-align: center; 59 | } 60 | 61 | .boxbot-box[data-type="wall"] { 62 | background-color: #bdc3c7; 63 | } 64 | 65 | .boxbot-controls { 66 | float: right; 67 | width: 440px; 68 | overflow: hidden; 69 | } 70 | 71 | .boxbot-buttons { 72 | margin: 7px 0 12px 0; 73 | } 74 | 75 | .boxbot-buttons select, 76 | .boxbot-buttons input, 77 | .boxbot-buttons button { 78 | height: 18px; 79 | margin-right: 5px; 80 | font-size: 12px; 81 | box-sizing: border-box; 82 | float: left; 83 | } 84 | 85 | .boxbot-buttons input { 86 | float: right; 87 | margin-right: 0; 88 | width: 140px; 89 | } 90 | 91 | .boxbot-commander { 92 | height: 741px; 93 | overflow: hidden; 94 | } 95 | 96 | .commander-lines-wrapper, 97 | .commander-editor { 98 | float: left; 99 | font-family: Menlo, Consolas, monospace; 100 | line-height: 20px; 101 | height: 100%; 102 | color: #ecf0f1; 103 | } 104 | 105 | .commander-lines-wrapper { 106 | border-radius: 4px 0 0 4px; 107 | background-color: #2c3e50; 108 | width: 40px; 109 | font-size: 12px; 110 | text-align: right; 111 | padding-top: 5px; 112 | box-sizing: border-box; 113 | cursor: default; 114 | } 115 | 116 | .commander-lines { 117 | position: relative; 118 | } 119 | 120 | .commander-lines-item { 121 | padding-right: 5px; 122 | } 123 | 124 | .commander-lines-item.error { 125 | background-color: #ea6153; 126 | } 127 | 128 | .commander-lines-item.success { 129 | background-color: #27ae60; 130 | } 131 | 132 | .commander-lines-item.warning { 133 | background-color: #f39c12; 134 | } 135 | 136 | .commander-editor { 137 | font-size: 16px; 138 | width: 400px; 139 | box-sizing: border-box; 140 | border: 0; 141 | border-radius: 0 4px 4px 0; 142 | outline: none; 143 | padding: 5px; 144 | margin: 0; 145 | background-color: #34495e; 146 | resize: none; 147 | } 148 | 149 | .note { 150 | margin-top: 20px; 151 | color: #999; 152 | } 153 | 154 | .boxbot-30x30 .boxbot-commander { 155 | height: 751px; 156 | } 157 | 158 | .boxbot-30x30 .boxbot-buttons { 159 | margin: 0 0 7px 0; 160 | } 161 | 162 | .boxbot-30x30 .boxbot-box { 163 | width: 25px; 164 | height: 25px; 165 | } 166 | 167 | .boxbot-30x30 .boxbot-bot img { 168 | width: 24px; 169 | height: 24px; 170 | } 171 | -------------------------------------------------------------------------------- /boxbot-finder.js: -------------------------------------------------------------------------------- 1 | var FinderNode = function (parent, position) { 2 | this.parent = parent 3 | this.position = position 4 | this.children = [] 5 | } 6 | 7 | FinderNode.prototype.isParent = function (position, current) { 8 | current = current || this 9 | if (position[0] == current.position[0] && position[1] == current.position[1]) { 10 | return true 11 | } 12 | if (!current.parent) { 13 | return false 14 | } 15 | return this.isParent(position, current.parent) 16 | } 17 | 18 | FinderNode.prototype.getPath = function (current, path) { 19 | current = current || this 20 | path = path || [] 21 | if (current.parent) { 22 | path.unshift(current.position) 23 | return this.getPath(current.parent, path) 24 | } else { 25 | return path 26 | } 27 | } 28 | 29 | /** 30 | * @constructor 31 | * @param {BoxbotMap} map 32 | */ 33 | var BoxbotFinder = function (map) { 34 | this.map = map 35 | } 36 | 37 | /** 38 | * 判断当前位置是否有效 39 | * @param {[int]} position 40 | * @returns {boolean} 41 | */ 42 | BoxbotFinder.prototype.isAvailable = function (position) { 43 | return this.map.getType(position) == 'null' 44 | } 45 | 46 | /** 47 | * 指定搜索算法并开始搜索 48 | * 49 | * @param {string} algorithm 50 | * @param {[int]} from 51 | * @param {[int]} to 52 | */ 53 | BoxbotFinder.prototype.search = function (algorithm, from, to) { 54 | return this[algorithm.toLowerCase()](from, to) 55 | } 56 | 57 | /** 58 | * @param {[int]} distance 目标距离 59 | * @returns {[{weight: int, x: int, y: int}]} 60 | */ 61 | BoxbotFinder.prototype.createOffsets = function (distance) { 62 | var offsets = [ 63 | {x:0, y:1}, {x: -1, y: 0}, {x: 0, y: -1}, {x: 1, y: 0} 64 | ].map(function (item) { 65 | item.weight = item.x * distance[0] + item.y * distance[1] 66 | return item 67 | }) 68 | 69 | offsets.sort(function (a, b) { 70 | return b.weight - a.weight 71 | }) 72 | 73 | return offsets 74 | } 75 | 76 | /** 77 | * 递归实现的深度优先搜索算法 78 | * 79 | * @param path 80 | * @param target 81 | * @param visited 82 | * @returns {[[int]]} 83 | */ 84 | BoxbotFinder.prototype.dfs = function (path, target, visited) { 85 | if (!visited) { 86 | visited = {} 87 | path = [path] 88 | } 89 | 90 | var current = path[path.length - 1] 91 | if (current[0] == target[0] && current[1] == target[1]) { 92 | path.shift() 93 | return path 94 | } 95 | 96 | var offsets = this.createOffsets([target[0] - current[0], target[1] - current[1]]) 97 | for (var i = 0; i < offsets.length; i += 1) { 98 | var next = [offsets[i].x + current[0], offsets[i].y + current[1]] 99 | var positionKey = next[0] + '-' + next[1] 100 | 101 | if (this.isAvailable(next) && !visited[positionKey]) { 102 | path.push(next) 103 | visited[positionKey] = next 104 | 105 | var result = this.dfs(path, target, visited) 106 | if (result) { 107 | return result 108 | } 109 | 110 | path.pop() 111 | } 112 | } 113 | } 114 | 115 | /** 116 | * 广度优先搜索 117 | * 118 | * @param {[int]} from 119 | * @param {[int]} to 120 | */ 121 | BoxbotFinder.prototype.bfs = function (from, to) { 122 | var offsets = [[0, 1], [-1, 0], [0, -1], [1, 0]]; 123 | var queue = [new FinderNode(null, from)] 124 | 125 | while (true) { 126 | var current = queue.shift() 127 | 128 | if (current.position[0] == to[0] && current.position[1] == to[1]) { 129 | return current.getPath() 130 | } 131 | 132 | for (var i = 0; i < offsets.length; i += 1) { 133 | var position = [current.position[0] + offsets[i][0], current.position[1] + offsets[i][1]] 134 | 135 | if (this.isAvailable(position) && !current.isParent(position)) { 136 | var node = new FinderNode(current, position) 137 | 138 | current.children.push(node) 139 | queue.push(node) 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /boxbot-bot.js: -------------------------------------------------------------------------------- 1 | var BOTTOM = 0 2 | var LEFT = 90 3 | var TOP = 180 4 | var RIGHT = 270 5 | 6 | /** 7 | * @constructor 8 | * @param {string} selector 9 | */ 10 | var BoxbotBot = function (selector) { 11 | this.element = document.querySelector(selector) 12 | this.init() 13 | } 14 | 15 | BoxbotBot.prototype.init = function () { 16 | this.element.style.left = this.element.clientWidth + 'px' 17 | this.element.style.top = this.element.clientHeight + 'px' 18 | this.element.style.transform = 'rotate(0deg)' 19 | } 20 | 21 | /** 22 | * 转换方向 23 | * 24 | * @param {int} direction 25 | */ 26 | BoxbotBot.prototype.turn = function (direction) { 27 | var ROTATE_MAP = { 28 | 0: {0:0, 90: 90, 180: 180, 270: -90}, 29 | 90: {90: 0, 180: 90, 270: 180, 0: -90}, 30 | 180: {180: 0, 270: 90, 0: 180, 90: -90}, 31 | 270: {270: 0, 0: 90, 90: 180, 180: -90} 32 | } 33 | this.element.style.transform = 'rotate(' + 34 | (this.getCurrentAngle() + ROTATE_MAP[this.getCurrentDirection()][direction]) + 'deg)' 35 | } 36 | 37 | /** 38 | * 获取当前旋转角度 39 | * 40 | * @return {int} 41 | */ 42 | BoxbotBot.prototype.getCurrentAngle = function () { 43 | var match = this.element.style.transform.match(/rotate\((.*)deg\)/) 44 | if (match) { 45 | return parseInt(match[1]) 46 | } else { 47 | return 0 48 | } 49 | } 50 | 51 | /** 52 | * 旋转 53 | * 54 | * @param {int} angle 55 | */ 56 | BoxbotBot.prototype.rotate = function (angle) { 57 | this.element.style.transform = 'rotate(' + (this.getCurrentAngle() + angle) + 'deg)' 58 | } 59 | 60 | /** 61 | * 获取当前方向 62 | * 63 | * @return {int} 64 | */ 65 | BoxbotBot.prototype.getCurrentDirection = function () { 66 | var angle = this.getCurrentAngle() % 360 67 | return angle >= 0 ? angle : angle + 360 68 | } 69 | 70 | /** 71 | * 获取指定方向上偏移的位置 72 | * 73 | * @param {int} direction 74 | * @param offset 75 | * @returns {[int]} 76 | */ 77 | BoxbotBot.prototype.getOffsetPosition = function (direction, offset) { 78 | var position = {0: [0, 1], 90: [-1, 0], 180: [0, -1], 270: [1, 0]}[direction] 79 | return [position[0] * offset, position[1] * offset] 80 | } 81 | 82 | /** 83 | * 获取当前位置便宜量 84 | * 85 | * @param {string} direction left|top 86 | * @returns {int} 87 | */ 88 | BoxbotBot.prototype.getCurrentOffset = function (direction) { 89 | var offset = this.element.style[direction] 90 | if (offset) { 91 | return parseInt(offset.replace('px', '')) 92 | } else { 93 | return 0 94 | } 95 | } 96 | 97 | /** 98 | * 获取当前位置 99 | * 100 | * @returns {[int]} 101 | */ 102 | BoxbotBot.prototype.getCurrentPosition = function () { 103 | return [ 104 | Math.round(this.getCurrentOffset('left') / this.element.clientWidth), 105 | Math.round(this.getCurrentOffset('top') / this.element.clientHeight)] 106 | } 107 | 108 | /** 109 | * 以当前位置为基准,获取指定方向上的位置 110 | * 111 | * @param {int|null} [direction] 如果为 null 则取当前方向 112 | * @param {int} [offset=0] 113 | * @returns {[int]} 114 | */ 115 | BoxbotBot.prototype.getPosition = function (direction, offset) { 116 | if (direction == null) { 117 | direction = this.getCurrentDirection() 118 | } 119 | offset = offset || 0 120 | var offsetPosition = this.getOffsetPosition(direction, offset) 121 | var currentPosition = this.getCurrentPosition() 122 | return [currentPosition[0] + offsetPosition[0], currentPosition[1] + offsetPosition[1]] 123 | } 124 | 125 | /** 126 | * 跳到指定位置 127 | * 128 | * @param {[int]} position 129 | * @param {boolean} [turn] 是否旋转方向 130 | */ 131 | BoxbotBot.prototype.goto = function (position, turn) { 132 | if (turn) { 133 | var currentPosition = this.getCurrentPosition() 134 | var distance = [position[0] - currentPosition[0], position[1] - currentPosition[1]] 135 | if (distance[0] < 0) { 136 | this.turn(LEFT) 137 | } else if (distance[0] > 0) { 138 | this.turn(RIGHT) 139 | } else if (distance[1] < 0) { 140 | this.turn(TOP) 141 | } else if (distance[1] > 0) { 142 | this.turn(BOTTOM) 143 | } 144 | } 145 | this.element.style.left = position[0] * this.element.clientWidth + 'px' 146 | this.element.style.top = position[1] * this.element.clientHeight + 'px' 147 | } 148 | 149 | /** 150 | * 朝指定方向移动 151 | * 152 | * @param {int} direction 153 | * @param {int} step 154 | */ 155 | BoxbotBot.prototype.move = function (direction, step) { 156 | this.goto(this.getPosition(direction, step)) 157 | } 158 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var Application = function () { 2 | this.boxbot = new Boxbot() 3 | this.editor = new BoxbotEditor('.boxbot-commander') 4 | this.imageReader = new ImageReader() 5 | 6 | this.$image = document.querySelector('#image') 7 | this.$random = document.querySelector('#random') 8 | this.$run = document.querySelector('#run') 9 | this.$reset = document.querySelector('#reset') 10 | this.$duration = document.querySelector('#duration') 11 | this.$resolution = document.querySelector('#resolution') 12 | 13 | this.init() 14 | this.reset() 15 | } 16 | 17 | Application.prototype.init = function () { 18 | document.addEventListener('keydown', this.hotkey.bind(this)) 19 | this.$run.addEventListener('click', this.run.bind(this)) 20 | this.$reset.addEventListener('click', this.reset.bind(this)) 21 | this.$random.addEventListener('click', this.random.bind(this)) 22 | this.$duration.addEventListener('change', this.setDuration.bind(this)) 23 | this.$resolution.addEventListener('change', this.setResolution.bind(this)) 24 | this.$image.addEventListener('change', this.loadImage.bind(this)) 25 | } 26 | 27 | /** 28 | * 设置运行速度 29 | */ 30 | Application.prototype.setDuration = function () { 31 | this.boxbot.setDuration(parseInt(this.$duration.value)) 32 | } 33 | 34 | /** 35 | * 设置地图尺寸 36 | */ 37 | Application.prototype.setResolution = function () { 38 | this.boxbot.setResolution(parseInt(this.$resolution.value)) 39 | this.reset() 40 | } 41 | 42 | /** 43 | * 读取图片并生成绘图命令 44 | */ 45 | Application.prototype.loadImage = function () { 46 | if (this.$image.files.length > 0) { 47 | var self = this 48 | this.imageReader 49 | .read(this.$image.files[0], this.boxbot.map.columns, this.boxbot.map.rows) 50 | .then(function (data) { 51 | var commands = 'mov to 1,1\nmov bot\nmov top\n' 52 | for (var y = 1; y <= data.length; y += 1) { 53 | // 移动到下一个位置 54 | if (y == data.length) { 55 | commands += 'tun rig\ntra lef\n' 56 | } else { 57 | commands += 'tra bot\n' 58 | } 59 | 60 | var columns = data[y - 1].length 61 | for (var x = 1; x <= columns; x += 1) { 62 | // 最后一个方块无法修墙,结束绘图 63 | if (y == data.length && x == columns) { 64 | break 65 | } 66 | 67 | var _x = columns - x 68 | var direction = 'lef' 69 | 70 | if (y % 2) { 71 | _x = [x - 1] 72 | direction = 'rig' 73 | } 74 | 75 | if (x != 1) { 76 | commands += 'tra ' + direction + '\n' 77 | } 78 | commands += 'biud\nbru ' + data[y - 1][_x] + '\n' 79 | } 80 | } 81 | self.editor.setCodes(commands) 82 | }) 83 | } 84 | } 85 | 86 | /** 87 | * 键盘事件处理 88 | * 89 | * @param {Event} event 90 | */ 91 | Application.prototype.hotkey = function (event) { 92 | if (event.target.tagName == 'BODY') { 93 | var direction = {37: LEFT, 38: TOP, 39: RIGHT, 40: BOTTOM}[event.keyCode] 94 | if (typeof direction != 'undefined') { 95 | event.preventDefault() 96 | if (direction == this.boxbot.bot.getCurrentDirection()) { 97 | this.boxbot.run(this.boxbot.go).catch(function (e) { 98 | console.log(e) 99 | }) 100 | } else { 101 | this.boxbot.run(this.boxbot.turn, [direction]) 102 | } 103 | } else if (event.keyCode == 32) { 104 | event.preventDefault() 105 | this.boxbot.run(this.boxbot.build) 106 | } 107 | } 108 | } 109 | 110 | Application.prototype.run = function () { 111 | this.editor.clearFlag() 112 | var parseError = false 113 | var codes = this.editor.getCodes() 114 | 115 | // 检查命令是否有误 116 | for (var i = 0; i < codes.length; i += 1) { 117 | if (codes[i] && this.boxbot.parse(codes[i]) === false) { 118 | parseError = true 119 | this.editor.setFlag(i, 'error') 120 | } 121 | } 122 | 123 | // 依次运行命令 124 | if (!parseError) { 125 | var self = this 126 | var prev = 0 127 | codes.forEach(function (code, i) { 128 | if (code) { 129 | self.boxbot.exec(code).then( 130 | function () { 131 | if (i % 37 == 0) { 132 | self.editor.scrollTo(i) 133 | } 134 | self.editor.clearFlag(prev) 135 | self.editor.setFlag(i, 'success') 136 | prev = i 137 | }, 138 | function (e) { 139 | console.log(e) 140 | self.editor.clearFlag(prev) 141 | self.editor.setFlag(i, 'warning') 142 | }) 143 | } 144 | }) 145 | } 146 | } 147 | 148 | Application.prototype.random = function () { 149 | // 先找出所有可修墙的位置 150 | var positions = [] 151 | for (var y = 1; y <= this.boxbot.map.rows; y += 1) { 152 | for (var x = 1; x <= this.boxbot.map.columns; x += 1) { 153 | if (this.boxbot.map.getType([x, y]) == 'null') { 154 | positions.push([x, y]) 155 | } 156 | } 157 | } 158 | 159 | if (positions.length) { 160 | this.boxbot.build(positions[Math.floor(Math.random() * positions.length)]) 161 | } 162 | } 163 | 164 | Application.prototype.reset = function () { 165 | this.boxbot.queue = [] 166 | this.boxbot.map.clear() 167 | this.boxbot.bot.init() 168 | this.editor.clearFlag() 169 | this.$image.value = '' 170 | } 171 | 172 | new Application() 173 | -------------------------------------------------------------------------------- /boxbot.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @constructor 3 | */ 4 | var Boxbot = function () { 5 | this.element = document.querySelector('.boxbot') 6 | this.bot = new BoxbotBot('.boxbot-bot') 7 | this.map = new BoxbotMap('.boxbot-map') 8 | this.map.create(20, 20) 9 | this.finder = new BoxbotFinder(this.map) 10 | this.duration = 250 11 | this.queue = [] 12 | this.running = false 13 | } 14 | 15 | Boxbot.prototype.directions = {bot: BOTTOM, lef: LEFT, top: TOP, rig: RIGHT} 16 | Boxbot.prototype.commands = [ 17 | { 18 | pattern: /^go(\s+)?(\d+)?$/i, 19 | handler: function (step) { 20 | return this.run(this.go, [arguments[1]]) 21 | } 22 | }, 23 | { 24 | pattern: /^go\s+to\s+(\d+)[,\s+](\d+)$/i, 25 | handler: function (x, y) { 26 | return this.run(this.goto, [[x, y]]) 27 | } 28 | }, 29 | { 30 | pattern: /^tun\s+(lef|rig|bac)$/i, 31 | handler: function (direction) { 32 | return this.run(this.rotate, [{lef: -90, rig: 90, bac: 180}[direction.toLowerCase()]]) 33 | } 34 | }, 35 | { 36 | pattern: /^mov\s+(bot|lef|top|rig)(\s+)?(\w+)?$/i, 37 | handler: function () { 38 | var direction = this.directions[arguments[0].toLowerCase()] 39 | return this.run(this.move, [direction, arguments[2] || 1]) 40 | } 41 | }, 42 | { 43 | pattern: /^tra\s+(bot|lef|top|rig)(\s+)?(\w+)?$/i, 44 | handler: function () { 45 | var direction = this.directions[arguments[0].toLowerCase()] 46 | return this.run(this.moveDirect, [direction, arguments[2] || 1]) 47 | } 48 | }, 49 | { 50 | pattern: /^biud$/i, 51 | handler: function () { 52 | return this.run(this.build, []) 53 | } 54 | }, 55 | { 56 | pattern: /^bru\s+(.*)$/i, 57 | handler: function (color) { 58 | return this.run(this.setColor, [color]) 59 | } 60 | }, 61 | { 62 | pattern: /^mov\s+to\s+(\d+)[,\s+](\d+)(\s+)?(dfs|bfs)?$/i, 63 | handler: function (x, y, _, algorithm) { 64 | return this.run(this.search, [[parseInt(x), parseInt(y)], algorithm]) 65 | } 66 | } 67 | ] 68 | 69 | /** 70 | * 解析命令,如果成功则返回命令对象,否则返回 false 71 | * 72 | * @param {string} string 73 | * @returns {boolean|{handler: handler, params: []}} 74 | */ 75 | Boxbot.prototype.parse = function (string) { 76 | for (var i = 0; i < this.commands.length; i += 1) { 77 | var command = this.commands[i] 78 | var match = string.match(command.pattern) 79 | if (match) { 80 | match.shift() 81 | return {handler: command.handler, params: match} 82 | } 83 | } 84 | return false 85 | } 86 | 87 | /** 88 | * 运行命令 89 | * 90 | * @param {string} string 91 | * @returns {boolean|Promise} 92 | */ 93 | Boxbot.prototype.exec = function (string) { 94 | var command = this.parse(string) 95 | if (command) { 96 | return command.handler.apply(this, command.params) 97 | } else { 98 | return false 99 | } 100 | } 101 | 102 | /** 103 | * 设置命令运行与动画的延时 104 | * 105 | * @param {int} duration 单位为毫秒 106 | */ 107 | Boxbot.prototype.setDuration = function (duration) { 108 | this.duration = duration 109 | var boxs = document.querySelectorAll('.boxbot-box') 110 | for (var i = 0; i < boxs.length; i += 1) { 111 | boxs[i].style.transitionDuration = duration + 'ms' 112 | } 113 | } 114 | 115 | /** 116 | * 设置地图尺寸 117 | * 118 | * @param {int} size 119 | */ 120 | Boxbot.prototype.setResolution = function (size) { 121 | this.map.create(size, size) 122 | this.element.className = 'clearfix boxbot boxbot-' + size + 'x' + size 123 | } 124 | 125 | /** 126 | * 修墙 127 | * 128 | * @param {[int]} [position] 默认为前方 129 | */ 130 | Boxbot.prototype.build = function (position) { 131 | position = position || this.bot.getPosition(null, 1) 132 | if (this.map.getType(position) == 'null') { 133 | this.map.set(position, 'wall') 134 | } else { 135 | throw '前方不可修墙' 136 | } 137 | } 138 | 139 | /** 140 | * 涂色 141 | * 142 | * @param {string} color 143 | * @param {[int]} [position] 默认为前方 144 | */ 145 | Boxbot.prototype.setColor = function (color, position) { 146 | position = position || this.bot.getPosition(null, 1) 147 | if (this.map.getType(position) == 'wall') { 148 | this.map.setColor(position, color) 149 | } else { 150 | throw '前方没有墙' 151 | } 152 | } 153 | 154 | /** 155 | * 旋转 156 | * 157 | * @param {int} angle 158 | */ 159 | Boxbot.prototype.rotate = function (angle) { 160 | this.bot.rotate(angle) 161 | } 162 | 163 | /** 164 | * 转换方向 165 | * 166 | * @param {int} direction 167 | */ 168 | Boxbot.prototype.turn = function (direction) { 169 | this.bot.turn(direction) 170 | } 171 | 172 | /** 173 | * 朝指定方向移动 174 | * 175 | * @param {int} direction 176 | * @param {int} step 177 | */ 178 | Boxbot.prototype.moveDirect = function (direction, step) { 179 | this.checkPath(direction, step) 180 | this.bot.move(direction, step) 181 | } 182 | 183 | /** 184 | * 朝指定方向旋转并移动 185 | * 186 | * @param {int} direction 187 | * @param {int} step 188 | */ 189 | Boxbot.prototype.move = function (direction, step) { 190 | this.checkPath(direction, step) 191 | this.bot.turn(direction) 192 | this.bot.move(direction, step) 193 | } 194 | 195 | /** 196 | * 跳到指定位置 197 | * 198 | * @param {[int]} position 199 | * @param {boolean} [turn] 是否旋转方向 200 | */ 201 | Boxbot.prototype.goto = function (position, turn) { 202 | this.bot.goto(position, turn) 203 | } 204 | 205 | /** 206 | * 朝当前方向移动 207 | * 208 | * @param {int} [step=1] 209 | */ 210 | Boxbot.prototype.go = function (step) { 211 | step = step || 1 212 | var direction = this.bot.getCurrentDirection() 213 | this.checkPath(direction, step) 214 | this.bot.move(direction, step) 215 | } 216 | 217 | /** 218 | * 检查是否可以到达目的地 219 | * 220 | * @param direction 221 | * @param step 222 | */ 223 | Boxbot.prototype.checkPath = function (direction, step) { 224 | var offsetPosition = this.bot.getOffsetPosition(direction, 1) 225 | var currentPosition = this.bot.getCurrentPosition() 226 | 227 | for (var s = 1; s <= step; s += 1) { 228 | var x = currentPosition[0] + offsetPosition[0] * s 229 | var y = currentPosition[1] + offsetPosition[1] * s 230 | 231 | if (this.map.getType([x, y]) != 'null') { 232 | throw '无法移动到 [' + x + ',' + y + ']' 233 | } 234 | } 235 | } 236 | 237 | /** 238 | * 在任务循环里运行任务 239 | * 240 | * @param {function} func 241 | * @param {[]} [params] 242 | * @return Promise 243 | */ 244 | Boxbot.prototype.run = function (func, params) { 245 | var promise = new Promise((function (resolve, reject) { 246 | this.queue.push({ 247 | func: func, params: params, callback: function (exception) { 248 | if (exception) { 249 | reject(exception) 250 | } else { 251 | resolve() 252 | } 253 | } 254 | }) 255 | }).bind(this)) 256 | 257 | if (!this.running) { 258 | this.taskloop() 259 | } 260 | 261 | return promise 262 | } 263 | 264 | Boxbot.prototype.taskloop = function () { 265 | this.running = true 266 | var task = this.queue.shift() 267 | if (task) { 268 | try { 269 | task.func.apply(this, task.params) 270 | task.callback() 271 | setTimeout(this.taskloop.bind(this), this.duration) 272 | } catch (e) { 273 | this.running = false 274 | this.queue = [] 275 | task.callback(e) 276 | } 277 | } else { 278 | this.running = false 279 | } 280 | } 281 | 282 | /** 283 | * 指定搜索算法并开始搜索 284 | * 285 | * @param {[int]} target 286 | * @param {string} [algorithm=dfs] 287 | */ 288 | Boxbot.prototype.search = function (target, algorithm) { 289 | if (this.map.getType(target) == 'null') { 290 | this.nextPosition = this.nextPosition || this.bot.getCurrentPosition() 291 | var self = this 292 | var path = this.finder.search(algorithm || 'dfs', this.nextPosition, target) 293 | this.nextPosition = target 294 | 295 | if (path.length > 0) { 296 | path.forEach(function (item, i) { 297 | if (i == path.length - 1) { 298 | var then = function () { 299 | self.nextPosition = null 300 | } 301 | } 302 | self.run(self.goto, [item, true]).then(then) 303 | }) 304 | } else { 305 | throw '寻路失败' 306 | } 307 | } else { 308 | throw '无法移动到 [' + target[0] + ',' + target[1] + ']' 309 | } 310 | } 311 | --------------------------------------------------------------------------------