├── .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 | 
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 | 
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 | 
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 | 
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 |
14 |
15 |
16 |
17 |
21 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
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 |
--------------------------------------------------------------------------------