├── .gitignore ├── package.json ├── ABBSlaveino └── ABBSlaveino.ino ├── TestGame.js ├── README.md ├── components ├── SerialConnection.js ├── SerialSensor.js ├── ABBRobot.js ├── TicTacToe.js └── GameController.js ├── ABBNator.txt └── RealGame.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "abbnator", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "RealGame.js", 6 | "dependencies": { 7 | "async": "^1.5.2", 8 | "jsftp": "^1.5.3", 9 | "lodash": "^4.6.1", 10 | "robotjs": "^0.3.7", 11 | "serialport": "^2.0.6" 12 | }, 13 | "devDependencies": {}, 14 | "scripts": { 15 | "start": "node RealGame", 16 | "test": "echo \"Error: no test specified\" && exit 1" 17 | }, 18 | "author": "", 19 | "license": "ISC" 20 | } 21 | -------------------------------------------------------------------------------- /ABBSlaveino/ABBSlaveino.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define PIN_SERVO 9 4 | #define PIN_SENSOR A6 5 | 6 | Servo myservo; 7 | 8 | void setup(){ 9 | pinMode(PIN_SENSOR, INPUT); 10 | pinMode(PIN_SERVO, OUTPUT); 11 | 12 | Serial.begin(115200); 13 | // Serial.println("Im alive!"); 14 | 15 | myservo.attach(9); 16 | } 17 | 18 | char inc = 0; 19 | void loop(){ 20 | 21 | if(!Serial.available()) 22 | return; 23 | 24 | inc = Serial.read(); 25 | 26 | if(inc == 'A'){ 27 | // Goes to active 28 | myservo.write(100); 29 | }else if(inc == 'I'){ 30 | // Goes to inactive 31 | myservo.write(180); 32 | }else if(inc == 'S'){ 33 | long sensorVal = analogRead(PIN_SENSOR); 34 | 35 | Serial.println(sensorVal); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /TestGame.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // 4 | // Plays a game with itself, just to check if algorithm works 5 | // 6 | 7 | var _ = require('lodash'); 8 | 9 | var TicTacToe = require('./components/TicTacToe'); 10 | 11 | var game = new TicTacToe(); 12 | game.state = [ 13 | ['X', null, null], 14 | [null, null, null], 15 | [null, null, null] 16 | ]; 17 | game.turn = 'O'; 18 | 19 | console.log(); 20 | console.log('== Next games:'); 21 | console.log(); 22 | 23 | game.print(); 24 | while(!game.winner()){ 25 | 26 | if(game.turn == 'X'){ 27 | game.minimax(); 28 | game = game.choice; 29 | game.turn = 'O'; 30 | }else{ 31 | game = _.sample(game.nextGames()); 32 | game.turn = 'X'; 33 | } 34 | 35 | // if(game.turn == 'O') 36 | // else 37 | 38 | game.print(); 39 | // console.log(game.choice.print()); 40 | // break; 41 | } 42 | 43 | // var nexts = game.nextGames(); 44 | 45 | // nexts.map((game) => { 46 | // game.print(); 47 | // }); 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ABBNator 2 | A Tic Tac Toe robot using ABB ARM Robot controlled by Node.JS 3 | 4 | *Watch the video:* 5 | 6 | [![ABBNator video](http://img.youtube.com/vi/V9dOoicowb0/0.jpg)](http://www.youtube.com/watch?v=V9dOoicowb0) 7 | 8 | # What was used in the project? 9 | 10 | 1. An ABB Robot 11 | 2. An Arduino board with Bluetooth and a Light Sensor 12 | 3. An Computer, connected to ABB Robot through Ethernet cable 13 | 14 | # How does Node.JS communicate with the ABB Robot? 15 | Files: `components/ABBRobot.js` and `ABBNator.txt`; 16 | 17 | ABB Robots provide us with `RAPID` programming language. That language, allows 18 | us to use `Sockets` and also `FTP`. 19 | 20 | For a matter of simplicity, and also facility, we opted to use `FTP` as the 21 | data exchange format. 22 | 23 | The code running in the ABB is the `ABBNator.txt`. It basically waits for 24 | a given file (In this case, `/hd0a/abbnator/target.txt`), and reads it. 25 | 26 | The file contains the speed, x, y and z params for the `MoveL` instruction. 27 | Here is the format: 28 | 29 | ``` 30 | // Protocol: 31 | :||; 32 | 33 | // Example (Go to [50, 120, 340] with speed 200): 34 | 200:50|120|340; 35 | ``` 36 | 37 | After executing the action, `ABBNator` will remove the file in order to notify 38 | any system using the FTP to know that "the action has been completed". 39 | 40 | # How does Node.JS communicate with the Sensor? 41 | 42 | Files: `components/SerialSensor.js` and `components/SerialConnection.js`; 43 | 44 | The sensor used was a simple infrared light sensor, connected to a servo motor 45 | to raise and lower the sensor. 46 | 47 | The connection with the Arduino used a (really) simple protocol: 48 | 49 | * Send character `A`: Activates the motor (lower) 50 | * Send character `I`: Deactivate the motor (raise) 51 | * Send character `S`: Reads the analog sensor value and sends back 52 | 53 | In Node.JS, the library `serialport` was used to have access to the `stream` of 54 | data from and to the bluetooth port. 55 | 56 | 57 | # How does Node.JS plays Tic Tac Toe? 58 | 59 | File: `components/TicTacToe.js`; 60 | 61 | Using `Minimax` algorithm. 62 | 63 | It will never loose, but it can end in a draw. You can look at the file 64 | `components/TicTacToe.js` for that. 65 | 66 | # How does everything work together? 67 | 68 | Files: `RealGame.js` and `components/GameController.js`; 69 | 70 | Dig in the code, you will get the idea ;) 71 | -------------------------------------------------------------------------------- /components/SerialConnection.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var serialport = require('serialport'); 3 | 4 | function SerialConnection(opts) { 5 | var self = this; 6 | 7 | this.opts = _.defaultsDeep(opts, { 8 | checkInterval: 100, 9 | 10 | devicePattern: {}, 11 | 12 | serialOptions: { 13 | baudRate: 57600, 14 | }, 15 | }); 16 | 17 | // Open Connection will be saved here. 18 | this.connection = null; 19 | 20 | // 21 | // Called every time the connection closes 22 | // 23 | this.connectionClosed = function (data) { 24 | console.log('Closed connection'); 25 | 26 | this.connection = null; 27 | 28 | self._isConnecting = false; 29 | 30 | // Notify callback 31 | this.opts.onConnect && this.opts.onConnect(null); 32 | 33 | // Check for new connection 34 | this.checkConnection(); 35 | } 36 | 37 | 38 | // 39 | // Called every time the connection closes 40 | // 41 | this.connectionOpened = function (connection) { 42 | console.log('Opened connection'); 43 | 44 | self._isConnecting = false; 45 | 46 | // Notify callback 47 | this.opts.onConnect && this.opts.onConnect(this.connection); 48 | } 49 | 50 | 51 | // 52 | // Checks the connection, and connects if not connected 53 | // 54 | this.checkConnection = function () { 55 | // console.log('Checking connection...'); 56 | 57 | if(this.connection && this.connection.isOpen()) 58 | return; 59 | 60 | serialport.list(function (err, ports) { 61 | // console.log(ports); 62 | if(err) return console.log('Error: ' + err); 63 | 64 | // Find matching device 65 | var port = _.find(ports, self.opts.devicePattern); 66 | 67 | if(!port) 68 | return; 69 | 70 | 71 | self.connectTo(port.comName); 72 | }) 73 | 74 | }; 75 | 76 | 77 | // 78 | // Tryies to connect to the defined Path, and close any 79 | // connection before doing it. 80 | // 81 | this.connectTo = function (path) { 82 | if(this._isConnecting) 83 | return; 84 | 85 | this._isConnecting = true; 86 | 87 | var newlyConn; 88 | 89 | console.log('Connecting to '+path); 90 | 91 | if(this.connection && this.connection.isOpen()) 92 | this.connection.close(thenConnect); 93 | else 94 | thenConnect(); 95 | 96 | 97 | function thenConnect (err){ 98 | if(err) { 99 | self._isConnecting = false; 100 | return console.error('Error', err); 101 | } 102 | 103 | self.connection = new serialport.SerialPort(path, self.opts.serialOptions); 104 | 105 | self.connection.on('open', self.connectionOpened.bind(self)); 106 | self.connection.on('close', self.connectionClosed.bind(self)); 107 | }; 108 | }; 109 | 110 | 111 | // 112 | // Pings the Connection 113 | // 114 | setInterval(this.checkConnection.bind(this), this.opts.checkInterval); 115 | } 116 | 117 | module.exports = SerialConnection; 118 | -------------------------------------------------------------------------------- /components/SerialSensor.js: -------------------------------------------------------------------------------- 1 | var serialport = require('serialport'); 2 | var SerialConnection = require('./SerialConnection'); 3 | 4 | var SerialSensor = module.exports = function (serialPath) { 5 | var self = this; 6 | 7 | this.serialConnection = new SerialConnection({ 8 | debug: false, 9 | 10 | // Device pattern to connect to 11 | devicePattern: { 12 | // serialNumber: '9533335393635131E0F1', 13 | // serialNumber: serialPath, 14 | comName: serialPath, 15 | }, 16 | 17 | serialOptions: { 18 | parser: serialport.parsers.readline('\n'), 19 | delimiter: '\n', 20 | }, 21 | 22 | onConnect: self.didConnect.bind(self), 23 | 24 | checkInterval: 10000, 25 | 26 | timeout: 100, 27 | }); 28 | 29 | this.serialConnection.checkConnection(); 30 | } 31 | 32 | 33 | SerialSensor.prototype.waitConnect = function (next) { 34 | if(this.conn && this.conn.isOpen()) 35 | return next(); 36 | 37 | this.onConnected = next; 38 | } 39 | 40 | 41 | SerialSensor.prototype.didConnect = function (connection) { 42 | // Save Connection internally 43 | this.conn = connection; 44 | 45 | if(!connection) 46 | return console.log('onConnect: false'); 47 | 48 | console.log('onConnect: ok'); 49 | 50 | this.onConnected && this.onConnected(); 51 | 52 | connection.on('data', this.gotReading.bind(this)); 53 | } 54 | 55 | SerialSensor.prototype.gotReading = function (reading) { 56 | // console.log('Got reading!', reading); 57 | 58 | if(this.onReceive){ 59 | // Save callback and clear it 60 | var cb = this.onReceive; 61 | this.onReceive = null; 62 | 63 | // Call callback 64 | cb && cb(reading); 65 | } 66 | } 67 | 68 | 69 | 70 | // Send raw data 71 | SerialSensor.prototype.sendData = function (data) { 72 | if(!this.conn) 73 | return false; 74 | 75 | try{ 76 | this.conn.write(data); 77 | this.conn.drain(); 78 | }catch(e){ 79 | return false; 80 | } 81 | 82 | return true; 83 | } 84 | 85 | // Lower arm of sensor 86 | SerialSensor.prototype.activate = function (next) { 87 | if(!this.sendData('A')) 88 | return next('Failed to send data!'); 89 | 90 | // Wait to act 91 | setTimeout(next, 2000); 92 | } 93 | 94 | // Bring back arm 95 | SerialSensor.prototype.deactivate = function (next) { 96 | if(!this.sendData('I')) 97 | return next('Failed to send data!'); 98 | 99 | // Wait to act 100 | setTimeout(next, 100); 101 | } 102 | 103 | // Read sensor value 104 | SerialSensor.prototype.readSensor = function (next) { 105 | this.sendData('S', next); 106 | 107 | var consumed = false; 108 | this.onReceive = (data) => { 109 | // Skip if already timed out 110 | if(consumed) 111 | return; 112 | 113 | consumed = true; 114 | 115 | // console.log('Received! ' + data); 116 | next(null, data); 117 | } 118 | 119 | // Timeout 120 | setTimeout(() => { 121 | if(consumed) 122 | return; 123 | 124 | consumed = true; 125 | return next('Could not read sensor. Timeout.'); 126 | }, 200); 127 | } 128 | -------------------------------------------------------------------------------- /ABBNator.txt: -------------------------------------------------------------------------------- 1 | MODULE MainModule 2 | CONST robtarget p0:=[[667.13,-6.83,794.90],[0.0407328,-0.569198,0.821177,-0.0047701],[-1,0,-1,0],[9E+09,9E+09,9E+09,9E+09,9E+09,9E+09]]; 3 | CONST robtarget p10:=[[795.52,-527.96,1051.40],[0.613573,0.221685,0.734935,-0.185077],[-1,0,0,0],[9E+09,9E+09,9E+09,9E+09,9E+09,9E+09]]; 4 | PROC main() 5 | VAR iodev file; 6 | VAR num number; 7 | VAR string text; 8 | 9 | VAR bool initializing := TRUE; 10 | VAR bool fileExists := FALSE; 11 | 12 | VAR string filePath := "/hd0a/abbnator/target.txt"; 13 | 14 | VAR num speed; 15 | VAR num posX; 16 | VAR num posY; 17 | VAR num posZ; 18 | 19 | 20 | TPErase; 21 | TPWrite "=============================="; 22 | TPWrite "ABBnator is starting..."; 23 | TPWrite "Credits to: Ivan Seidel Gomes"; 24 | TPWrite " Joao Pedro V. Boas"; 25 | TPWrite " Eber Lima Silva"; 26 | TPWrite ""; 27 | WaitTime 1.0; 28 | 29 | IF IsFile(filePath \RegFile) THEN 30 | TPWrite ": Deleting "+filePath; 31 | RemoveFile(filePath); 32 | ENDIF 33 | 34 | TPWrite "ABBnator running..."; 35 | initializing := FALSE; 36 | 37 | loop: 38 | fileExists := IsFile(filePath \RegFile); 39 | 40 | IF NOT fileExists THEN 41 | TPWrite "No file found. Waiting..."; 42 | WaitTime 1.0; 43 | GOTO loop; 44 | ENDIF 45 | 46 | ! File format: 47 | !! :x|y|z; 48 | Open filePath, file \Read; 49 | ! TPWrite ReadStr(file); 50 | 51 | ! Goes back to the start of the file 52 | Rewind file; 53 | 54 | ! Reads the desired speed 55 | speed := ReadNum(file\Delim:=":"); 56 | 57 | ! Reads X, Y and Z 58 | posX := ReadNum(file\Delim:="|"); 59 | posY := ReadNum(file\Delim:="|"); 60 | posZ := ReadNum(file\Delim:=";"); 61 | 62 | ! Validate speed 63 | IF speed < 10 THEN 64 | speed := 10; 65 | ENDIF 66 | IF speed > 400 THEN 67 | speed := 400; 68 | ENDIF 69 | 70 | ! Close file 71 | Close file; 72 | 73 | ! Debug 74 | TPWrite 75 | "SPEED: " + NumToStr(speed, 1) + 76 | " X: " + NumToStr(posX, 1) + 77 | " Y: "+ NumToStr(posY, 1) + 78 | " Z: "+NumToStr(posZ, 1); 79 | 80 | ! Goto Position 81 | MoveL Offs(p0, posX, posY, posZ), [speed, 500, 5000, 1000], z0, tool0; 82 | 83 | ! Delete file 84 | RemoveFile(filePath); 85 | TPWrite "Completed!"; 86 | 87 | ! Wait 10ms 88 | WaitTime 0.010; 89 | GOTO loop; 90 | 91 | 92 | 93 | !MoveJ p0, [speed, 500, 5000, 1000], z0, tool0; 94 | 95 | !MoveL Offs(p0,300,0,0), v10, z0, tool0; 96 | !MoveL Offs(p0,300,300,0), v10, z0, tool0; 97 | !MoveL Offs(p0,0,300,0), v10, z0, tool0; 98 | ERROR 99 | IF ERRNO = ERR_FILEACC THEN 100 | !TPWrite "File does not exists!"; 101 | !WaitTime 0.01; 102 | 103 | fileExists := FALSE; 104 | TRYNEXT; 105 | ENDIF 106 | 107 | ENDPROC 108 | 109 | 110 | ENDMODULE 111 | -------------------------------------------------------------------------------- /RealGame.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // 4 | // Plays a game with a human, with the ABB Robot ARM 5 | // 6 | 7 | var async = require('async'); 8 | 9 | var ABBRobot = require('./components/ABBRobot'); 10 | var TicTacToe = require('./components/TicTacToe'); 11 | var GameController = require('./components/GameController'); 12 | var SerialSensor = require('./components/SerialSensor'); 13 | 14 | // 15 | // Instantiate a robot 16 | // (Configure your ABB Robot IP, PASSWORD and USER here) 17 | // Also, setup the PATH for the target file 18 | // (Separate folder and file) 19 | // 20 | var myRobot = new ABBRobot({ 21 | host: '192.168.125.1', 22 | user: 'ROB', 23 | pass: 'PWD' 24 | }, '/hd0a/abbnator/', 'target.txt'); 25 | 26 | console.log('Waiting for robot...'); 27 | 28 | // Set robot offset 29 | myRobot.offset.x = -300; 30 | myRobot.offset.y = 500; 31 | myRobot.offset.z = -5; 32 | 33 | // 34 | // Setup sensor 35 | // (Configure your bluetooth path) 36 | // 37 | var sensor = new SerialSensor('/dev/cu.MonsterBT-DevB'); 38 | 39 | // 40 | // Setup TicTacToe game controller 41 | // 42 | var gameSizeMM = 300; 43 | var game = new GameController(myRobot, sensor, gameSizeMM); 44 | 45 | 46 | // 47 | // Initialize both systems asyncronously 48 | // (ABB connection and Bluetooth) 49 | // 50 | async.parallel([ 51 | myRobot.onIddle.bind(myRobot), 52 | sensor.waitConnect.bind(sensor), 53 | ], prestart); 54 | 55 | 56 | function prestart() { 57 | // Raise arm 58 | sensor.deactivate(); 59 | 60 | // Wait 5 secs to begin, just for safety 61 | console.log('Starting in 5secs...'); 62 | setTimeout(init, 5000); 63 | } 64 | 65 | 66 | // 67 | // Here the magic happens. 68 | // 69 | function init(){ 70 | 71 | console.log('Started!'); 72 | 73 | // Draw game, and then play it's move 74 | game.drawGame(playMove); 75 | 76 | // 77 | // Compute best move, and draw X on that position 78 | // After that, wait for movement 79 | // (Wait user vary sensor value) 80 | // 81 | function playMove(){ 82 | 83 | console.log('# playMove'); 84 | 85 | game.game.turn = 'X'; 86 | game.game.minimax(); 87 | var nextMove = game.game.choiceMove; 88 | console.log('# playMove.nextMove:', nextMove); 89 | 90 | game.game.play(nextMove.x, nextMove.y, 'X'); 91 | game.game.print(); 92 | 93 | game.drawX(nextMove, waitMove); 94 | } 95 | 96 | 97 | // 98 | // Verify if there was a winner or a DRAW. 99 | // -> Then, show that winner 100 | // Or, 101 | // -> Goes to the WAIT POSITION, and wait for sensor to vary 102 | // 103 | function waitMove(val){ 104 | console.log('# waitMove'); 105 | 106 | if(game.game.winner()) 107 | return showWinner(); 108 | 109 | game.goToWaitPosition(waitSensor) 110 | } 111 | 112 | 113 | // 114 | // Keeps pinging the sensor value, and readMove when detected 115 | // 116 | function waitSensor(){ 117 | // console.log('# waitSensor'); 118 | sensor.activate(); 119 | 120 | sensor.readSensor((err, val) => { 121 | 122 | if(err) 123 | return setTimeout(waitSensor, 200); 124 | 125 | // console.log('# waitSensor.callback ', val); 126 | 127 | if(val > 500) 128 | return setTimeout(waitSensor, 200); 129 | 130 | sensor.deactivate(); 131 | return setTimeout(readMove, 2000); 132 | }); 133 | } 134 | 135 | // 136 | // Goes through all possible positions, 137 | // and check if there is something there (A dark circle?) 138 | // If found something, return that position, and play the 139 | // move in the TicTacToe game (in memory) 140 | // After that, play it's move (and close the cycle); 141 | // 142 | function readMove(){ 143 | console.log('# readMove'); 144 | 145 | game.readMove( (move) => { 146 | 147 | console.log('# readMove.finished'); 148 | 149 | if(!move){ 150 | console.log('No move detected! waiting'); 151 | // process.exit(); 152 | return waitMove(); 153 | } 154 | 155 | // Make move 156 | game.game.play(move.x, move.y, 'O'); 157 | 158 | playMove(); 159 | 160 | }); 161 | } 162 | 163 | // 164 | // Shows the winner, by drawing a LINE on the winner line, 165 | // Or, drawing a 'V' (From portuguese: VELHA). 166 | // 167 | function showWinner(){ 168 | console.log('# showWinner'); 169 | 170 | game.drawWinner(() => { 171 | console.log('END OF GAME!'); 172 | process.exit(); 173 | }); 174 | 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /components/ABBRobot.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var async = require('async'); 3 | var JSFtp = require('jsftp'); 4 | 5 | // 6 | // Robot Class 7 | // 8 | var Robot = module.exports = function (ftpOpts, targetFolder, targetFile) { 9 | this.interval = 100; 10 | this.ftp = new JSFtp(ftpOpts); 11 | 12 | this.folder = targetFolder; 13 | this.file = targetFile; 14 | 15 | this.callback = null; 16 | 17 | // State 18 | this.completed = true; 19 | this.state = { 20 | speed: 0, 21 | x: 0, 22 | y: 0, 23 | z: 0 24 | }; 25 | 26 | // Offset used in targets 27 | this.offset = { 28 | x: 0, 29 | y: 0, 30 | z: 0, 31 | }; 32 | 33 | // Check the target file 34 | // (When deleted, means that action is completed) 35 | this.verifyCompletion(); 36 | } 37 | 38 | Robot.prototype.verifyCompletion = function (){ 39 | var self = this; 40 | 41 | self.ftp.ls(self.folder, function (err, files) { 42 | // Register next check 43 | self.checker = setTimeout( 44 | self.verifyCompletion.bind(self), 45 | self.interval); 46 | 47 | if(err) 48 | return console.log(err); 49 | 50 | var completed = true; 51 | for(var k in files){ 52 | if (files[k].name != self.file) 53 | continue; 54 | 55 | completed = false; 56 | break; 57 | } 58 | 59 | self.completed = completed; 60 | 61 | if(completed && self.callback){ 62 | // Save callback before executing 63 | var cb = self.callback; 64 | 65 | // Clear callback 66 | self.callback = null; 67 | 68 | // Execute callback 69 | cb(); 70 | } 71 | 72 | }) 73 | 74 | } 75 | 76 | Robot.prototype.saveFile = function (content, next){ 77 | var data = new Buffer(content, "binary"); 78 | 79 | // console.log('putting: '+this.folder + this.file); 80 | 81 | this.ftp.put(data, this.folder + this.file, function (err, res) { 82 | // console.log('Sent! Took: ', err); 83 | next && next(err); 84 | }) 85 | } 86 | 87 | Robot.prototype.onIddle = function (next) { 88 | this.callback = next; 89 | } 90 | 91 | Robot.prototype.goTo = function (state, next){ 92 | // Set target callback 93 | this.onIddle(next); 94 | 95 | // Validate file content 96 | if( !_.isObject(state) || 97 | !_.isNumber(state.speed) || 98 | !_.isNumber(state.x) || 99 | !_.isNumber(state.y) || 100 | !_.isNumber(state.z)){ 101 | 102 | return console.error('! Invalid target: ', state); 103 | } 104 | 105 | this.state = state; 106 | 107 | var pack = 108 | +Math.round(state.speed) + ':' + 109 | +Math.round(state.y + this.offset.y) + '|' + 110 | -Math.round(state.x + this.offset.x) + '|' + 111 | +Math.round(state.z + this.offset.z) + ';'; 112 | 113 | console.log(pack); 114 | 115 | this.saveFile(pack); 116 | } 117 | 118 | 119 | Robot.prototype.line = function(path, next){ 120 | console.log('% line', path.speed); 121 | var ABBRobot = this; 122 | 123 | // Speed to move when not drawing 124 | var speedCleared = path.speedCleared || path.speed; 125 | 126 | // Clearence in mm from the z 127 | var clearence = path.clearence ? path.clearence : null; 128 | var speed = path.speed; 129 | var from = path.from; 130 | var to = path.to; 131 | 132 | var paths = []; 133 | // Path consists of: 134 | // 1. Go up pen relative, by `clearence` in Z (if clearence) 135 | if(clearence) 136 | paths.push({ 137 | speed: speedCleared, 138 | x: ABBRobot.state.x, 139 | y: ABBRobot.state.y, 140 | z: ABBRobot.state.z + clearence, 141 | }); 142 | 143 | // 2. Go to X/Y pos with `clearence` summed Z (if clearence) 144 | if(clearence) 145 | paths.push({ 146 | speed: speedCleared, 147 | x: from.x, 148 | y: from.y, 149 | z: from.z + clearence, 150 | }); 151 | 152 | // 3. Go to X/Y/Z 153 | paths.push({ 154 | speed: speed, 155 | x: from.x, 156 | y: from.y, 157 | z: from.z, 158 | }); 159 | 160 | // 4. Go to TO point (If set) 161 | if(path.to){ 162 | paths.push({ 163 | speed: speed, 164 | x: to.x, 165 | y: to.y, 166 | z: to.z, 167 | }); 168 | } 169 | 170 | this.execute(paths, next); 171 | }; 172 | 173 | 174 | // 175 | // Execute actions in the paths array and callback when done 176 | // 177 | Robot.prototype.execute = function (paths, next) { 178 | var ABBRobot = this; 179 | 180 | async.eachSeries(paths, ABBRobot.goTo.bind(ABBRobot), function (err) { 181 | next && next(err); 182 | }); 183 | } 184 | -------------------------------------------------------------------------------- /components/TicTacToe.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('lodash'); 3 | 4 | var _initialGame = [ 5 | [null, null, null], 6 | [null, null, null], 7 | [null, null, null] 8 | ]; 9 | 10 | var TicTacGame = module.exports = function (game){ 11 | var state = game && game.state ? game.state : _initialGame; 12 | 13 | this.state = _.cloneDeep(state); 14 | 15 | this.turn = 'X'; 16 | 17 | if(game){ 18 | this.turn = TicTacGame.nextTurnName(game.turn); 19 | } 20 | } 21 | 22 | TicTacGame.prototype.winner = function () { 23 | var state = this.state; 24 | var draw = true; 25 | for(var k = 0; k < 3; k++){ 26 | if(state[k][0] && 27 | state[k][0] == state[k][1] && 28 | state[k][1] == state[k][2]) 29 | return state[k][0]; 30 | 31 | if(state[0][k] && 32 | state[0][k] == state[1][k] && 33 | state[1][k] == state[2][k]) 34 | return state[0][k]; 35 | 36 | draw = 37 | draw && 38 | (!!state[k][0]) && 39 | (!!state[k][1]) && 40 | (!!state[k][2]); 41 | } 42 | 43 | if(state[0][0] && 44 | state[0][0] == state[1][1] && 45 | state[1][1] == state[2][2]) 46 | return state[0][0]; 47 | 48 | if(state[2][0] && 49 | state[2][0] == state[1][1] && 50 | state[1][1] == state[0][2]) 51 | return state[2][0]; 52 | 53 | return draw ? 'DRAW' : false; 54 | } 55 | 56 | TicTacGame.prototype.possibleMoves = function (){ 57 | var moves = []; 58 | for(var x in this.state){ 59 | for(var y in this.state[x]){ 60 | if(this.state[x][y] !== null) 61 | continue; 62 | moves.push({x: x, y: y}); 63 | } 64 | } 65 | return moves; 66 | } 67 | 68 | TicTacGame.prototype.nextGames = function (moves){ 69 | var moves = moves ? moves : this.possibleMoves(); 70 | var games = []; 71 | for(var k in moves){ 72 | var move = moves[k]; 73 | 74 | var game = new TicTacGame(this); 75 | game.play(move.x, move.y, this.turn); 76 | games.push(game); 77 | } 78 | return games; 79 | } 80 | 81 | TicTacGame.prototype.print = function (){ 82 | console.log('============'); 83 | console.log(`GAME TURN: ${this.turn}`); 84 | console.log(`IS OVER: ${this.winner()}`); 85 | 86 | var none = ' '; 87 | var game = ''; 88 | 89 | game += ` ${this.state[0][0] || none} |`; 90 | game += ` ${this.state[1][0] || none} |`; 91 | game += ` ${this.state[2][0] || none} \n`; 92 | 93 | game += `-----------\n`; 94 | 95 | game += ` ${this.state[0][1] || none} |`; 96 | game += ` ${this.state[1][1] || none} |`; 97 | game += ` ${this.state[2][1] || none} \n`; 98 | 99 | game += `-----------\n`; 100 | 101 | game += ` ${this.state[0][2] || none} |`; 102 | game += ` ${this.state[1][2] || none} |`; 103 | game += ` ${this.state[2][2] || none} \n`; 104 | 105 | console.log(game); 106 | } 107 | 108 | TicTacGame.prototype.play = function (x, y, who){ 109 | who = who ? who : this.turn; 110 | 111 | if(this.state[x][y] !== null) 112 | return console.error('FAILED TO PLAY! IMPOSSIBLE MOVE:', x,y,who); 113 | 114 | // Play 115 | this.state[x][y] = who; 116 | } 117 | 118 | var n = 0; 119 | TicTacGame.prototype.minimax = function (depth){ 120 | 121 | depth = depth ? depth : 0; 122 | 123 | // console.log('minimax #', n++); 124 | var winner = this.winner(); 125 | 126 | if(winner == 'X') 127 | return 10 - depth; 128 | 129 | if(winner == 'O') 130 | return depth - 10; 131 | 132 | if(winner == 'DRAW') 133 | return 0; 134 | 135 | depth += 1; 136 | 137 | // Compute next possible states 138 | var nextMoves = this.possibleMoves(); 139 | var nextStates = this.nextGames(nextMoves); 140 | var nextStatesScore = []; 141 | 142 | nextStates.map((state) => { 143 | nextStatesScore.push(state.minimax(depth)); 144 | }); 145 | 146 | // console.log(depth); 147 | if(depth == 1){ 148 | console.log(nextMoves); 149 | console.log(nextStatesScore); 150 | } 151 | 152 | var score = 0; 153 | if(this.turn == 'X') 154 | score = _.max(nextStatesScore); 155 | else 156 | score = _.min(nextStatesScore); 157 | 158 | var stateIndex = nextStatesScore.indexOf(score); 159 | var nextState = nextStates[stateIndex]; 160 | var nextMove = nextMoves[stateIndex]; 161 | 162 | this.choice = nextState; 163 | this.choiceMove = nextMove; 164 | 165 | // if(log){ 166 | // console.log('Best state: ', stateIndex); 167 | // console.log(this.choice.print); 168 | // console.log(this.choiceMove); 169 | // } 170 | 171 | return score; 172 | } 173 | 174 | TicTacGame.nextTurnName = function (thisTurn) { 175 | if (thisTurn == 'X') 176 | return 'O'; 177 | return 'X'; 178 | } 179 | -------------------------------------------------------------------------------- /components/GameController.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var async = require('async'); 4 | var TicTacToe = require('./TicTacToe'); 5 | 6 | // 7 | // ^ y 8 | // 9 | // . (0,0) > x 10 | // __|__|__ 11 | // __|__|__ 12 | // | | 13 | 14 | var GameController = module.exports = function(robot, sensor, size){ 15 | 16 | this.sensor = sensor; 17 | this.robot = robot; 18 | this.size = size; 19 | 20 | this.game = new TicTacToe(); 21 | } 22 | 23 | 24 | GameController.prototype._callLine = function (line, next){ 25 | console.log('$ _callLine'); 26 | var opts = { 27 | from: { 28 | x: line.x, 29 | y: line.y, 30 | z: 0, 31 | }, 32 | to: { 33 | x: line.x + line.dX, 34 | y: line.y + line.dY, 35 | z: 0, 36 | }, 37 | speedCleared: 400, 38 | clearence: 40, 39 | speed: 75, 40 | } 41 | 42 | this.robot.line(opts, next); 43 | } 44 | 45 | 46 | GameController.prototype.drawGame = function (next) { 47 | console.log('$ drawGame'); 48 | var self = this; 49 | 50 | var size = self.size / 3; 51 | 52 | var lines = [ 53 | { 54 | x: 0, y: -(size * 1), 55 | dX: size * 3, dY: 0 56 | }, 57 | { 58 | x: (size * 3), y: -(size * 2), 59 | dX: -size * 3, dY: 0, 60 | }, 61 | { 62 | x: (size * 1), y: -(size * 3), 63 | dX: 0, dY: size * 3, 64 | }, 65 | { 66 | x: (size * 2), y: 0, 67 | dX: 0, dY: -size * 3, 68 | } 69 | ]; 70 | 71 | async.eachSeries(lines, this._callLine.bind(this), next); 72 | } 73 | 74 | 75 | GameController.prototype.drawX = function (pos, next){ 76 | console.log('$ drawX', pos); 77 | 78 | var self = this; 79 | 80 | var x = pos.x * 1; 81 | var y = pos.y * 1; 82 | 83 | // Play move 84 | // this.game.play(pos.x, pos.y, 'X'); 85 | 86 | // Prepare lines 87 | var size = self.size / 3; 88 | var space = size / 6; 89 | 90 | var lines = [ 91 | { 92 | x: x * size + space, y: -(y + 0) * size - space, 93 | dX: size - 2 * space, dY: -size + 2 * space 94 | }, 95 | { 96 | x: x * size + space, y: -(y + 1) * size + space, 97 | dX: size - 2 * space, dY: size - 2 * space 98 | }, 99 | ]; 100 | 101 | async.eachSeries(lines, this._callLine.bind(this), next); 102 | } 103 | 104 | GameController.prototype.goToWaitPosition = function (next) { 105 | console.log('$ goToWaitPosition'); 106 | 107 | this.robot.goTo({ 108 | speed: 400, 109 | x: 0, 110 | y: -400, 111 | z: 300 112 | }, () =>{ 113 | console.log('!!! IN HOLD POSITION!'); 114 | next(); 115 | }); 116 | } 117 | 118 | GameController.prototype.readMove = function (next) { 119 | console.log('$ readMove'); 120 | 121 | var self = this; 122 | 123 | // Find out possible moves 124 | var moves = this.game.possibleMoves(); 125 | 126 | if(moves.length <= 0) 127 | return next(); 128 | 129 | var currentIndex = 0; 130 | function callNext() { 131 | if(currentIndex >= moves.length) 132 | return next(); 133 | 134 | var move = moves[currentIndex]; 135 | currentIndex++; 136 | 137 | self.readPosition(move, (err, val) => { 138 | 139 | console.log('readMove on', move.x, move.y, 'got', val); 140 | // If found, return this move 141 | if(val > 850) 142 | return next(move); 143 | 144 | // Or, call next check 145 | callNext(); 146 | }); 147 | } 148 | 149 | callNext(); 150 | } 151 | 152 | GameController.prototype.drawWinner = function (next) { 153 | console.log('$ drawGame'); 154 | var self = this; 155 | 156 | var size = self.size / 3; 157 | 158 | var lines = null; 159 | 160 | // Find out who win 161 | var winner = this.game.winner(); 162 | 163 | if(winner == 'DRAW'){ 164 | // Draws a "V" 165 | lines = [ 166 | { 167 | x: size / 2, y: -(size / 2), 168 | dX: size, dY: -size * 2 169 | }, 170 | { 171 | x: size / 2 + size, y: -(size / 2) - size * 2, 172 | dX: size, dY: size * 2 173 | }, 174 | ]; 175 | 176 | }else if(winner = 'X'){ 177 | var start = null; 178 | var end = null; 179 | 180 | // Find winning position 181 | var state = this.game.state; 182 | 183 | for(var k = 0; k < 3; k++){ 184 | if(state[k][0] && 185 | state[k][0] == state[k][1] && 186 | state[k][1] == state[k][2]){ 187 | start = {x: k, y: 0}; 188 | end = {x: k, y: 2}; 189 | break; 190 | } 191 | 192 | if(state[0][k] && 193 | state[0][k] == state[1][k] && 194 | state[1][k] == state[2][k]){ 195 | start = {x: 0, y: k}; 196 | end = {x: 2, y: k}; 197 | break; 198 | } 199 | } 200 | 201 | if(state[0][0] && 202 | state[0][0] == state[1][1] && 203 | state[1][1] == state[2][2]){ 204 | start = {x: 0, y: 0}; 205 | end = {x: 2, y: 2}; 206 | } 207 | 208 | if(state[2][0] && 209 | state[2][0] == state[1][1] && 210 | state[1][1] == state[0][2]){ 211 | start = {x: 2, y: 0}; 212 | end = {x: 0, y: 2}; 213 | } 214 | 215 | var dX = end.x - start.x; 216 | var dY = end.y - start.y; 217 | 218 | lines = [ 219 | { 220 | x: start.x * size + size / 2, y: -start.y * size - size / 2, 221 | dX: (dX * size), dY: -(dY * size) 222 | }, 223 | ]; 224 | 225 | }else{ 226 | console.error('\n\nIMPOSSIBLE TO LOOOOSSSEEE!\n\n'); 227 | process.exit(); 228 | } 229 | 230 | async.eachSeries(lines, this._callLine.bind(this), next); 231 | 232 | } 233 | 234 | 235 | 236 | GameController.prototype.readPosition = function (pos, next) { 237 | console.log('$ readPosition', pos); 238 | 239 | var self = this; 240 | 241 | var size = self.size / 3; 242 | 243 | var center = { 244 | x: +pos.x * size + size / 2, 245 | y: -pos.y * size - size / 2 - 30 246 | }; 247 | 248 | var opts = { 249 | from: { 250 | x: center.x, 251 | y: center.y, 252 | z: 33, 253 | }, 254 | // speedCleared: 400, 255 | // clearence: 20, 256 | speed: 400, 257 | } 258 | 259 | var sensorVal = null; 260 | var steps = [ 261 | (next) => { 262 | self.robot.line(opts, next); 263 | }, 264 | 265 | (next) => { 266 | self.sensor.activate(next); 267 | }, 268 | 269 | (next) => { 270 | self.sensor.readSensor((err, val) => { 271 | sensorVal = val; 272 | next(err); 273 | }) 274 | }, 275 | 276 | (next) => { 277 | this.sensor.deactivate(next); 278 | }, 279 | ] 280 | 281 | async.series(steps, (err, results) => { 282 | // console.log('Executed steps. Err: ', err); 283 | // console.log(results); 284 | next(err, sensorVal); 285 | }); 286 | 287 | } 288 | --------------------------------------------------------------------------------