TicTacToe in ROS
Problem Statement
Implement a TicTacToe game using services in ROS with 3 nodes - Node 1 acts as Player 1, Node 2 acts as Player 2, and Node 3 acts as the Computer (provides the board).
Solution
For approaching this problem, I made 4 files for Node 3 (so the code doesn’t get messy) and 1 file for Node 2 and 1 file for Node 1.
Custom Service File
For this problem, I made custom service file for passing request and getting response
The srv file (moe.srv) :-
int64[] gaming_data
---
int64 row
int64 col
In this file,
- The array
gaming_data
present in request part of srv file, will be use to request 3x3 array which contains information of the tictactoe board, with each element of array representing each element of the board. If value of a element in the array is 0, then it means board is empty, if it is 1, then that position in the board is filled with ‘X’ mark, and if it is 2, then that position in the board is filled wth ‘O’ mark. - The variable
row
andcol
in response part of the srv file, will be use for giving response in terms of which row and column has to be filled in the board when a player enters input from one of the nodes ‘Node 1’ and ‘Node 2’.
We will further discuss how we will use each variable in this solution as we move on
Node 3
Node 1 and Node 2 will take input from Player 1 and Player 2 and give to Node 3 using services, which means, Node 1 and Node 2 will act as a server and Node 3 will act as a client, which basically calls each node when their turns come. And as mentioned already, the Node 3 is divided into 4 files - main.py, display.py, draw.py, checker.py
The code for display.py:-
from draw import *
import sys
class Picture_Class:
def __init__(self):
self.game_data = [[0 for i in range(3)] for j in range(3)]
self.picture_data = [[' ' for i in range(41)] for j in range(17)]
self.__setup__()
def __setup__(self):
for i in range(17):
self.picture_data[i][0] = '|'
self.picture_data[i][13] = '|'
self.picture_data[i][26] = '|'
self.picture_data[i][39] = '|'
for i in range(41):
self.picture_data[5][i] = '-'
self.picture_data[11][i] = '-'
def print_result(self):
for i in range(17):
for j in range(41):
sys.stdout.write(str(self.picture_data[i][j]))
sys.stdout.write('\n')
def upload_data(self, some_array):
self.game_data = some_array
self.__refresh__()
def __refresh__(self):
for i in range(3):
for j in range(3):
if (self.game_data[i][j] == 1):
self.picture_data = draw_x(self.picture_data, i, j)
elif (self.game_data[i][j] == 2):
self.picture_data = draw_o(self.picture_data, i, j)
This file contains code for a class which contains functions, arrays and variable which will be useful for displaying the board in terminal In this code,
gaming_data
array will be array which will be storing data of the tictactoe board (same array discussed during discussion of srv file)picture_data
array will contains keyboard symbols in an arranged way such that if we print each row of that array one after the another in the terminal, it will display the tictactoe board For example :-- The
__setup__()
function is initializing thepicture_data
array with filling ‘|’ and ‘-‘ such that it will create border line of tictactoe board, and this function is called during intialization of the object print_result()
function prints thepicture_data
array in the terminalupload_data()
fucntion uploads thegaming_data
array and calls__refresh__()
function__refresh__()
function adds ‘X’ and ‘O’ in thepicture_data
array usingdraw_x()
anddraw_o()
, which is defined in draw.py file
Note :- Here, I used sys
for printing instead of normal print function because when we use normal print function, it automatically starts from new line. There is a way to prevent it by using print(<something>, end='')
, but this only works in Python3 and my ROS was compatable with Python2.
The code for draw.py:-
def draw_x(some_data, a, b):
x = 13*b + 2
y = 6*a
some_data[y][x] = 'x'
some_data[y][x+1] = 'x'
some_data[y][x+8] = 'x'
some_data[y][x+9] = 'x'
some_data[y+1][x+2] = 'x'
some_data[y+1][x+3] = 'x'
some_data[y+1][x+6] = 'x'
some_data[y+1][x+7] = 'x'
some_data[y+2][x+4] = 'x'
some_data[y+2][x+5] = 'x'
some_data[y+3][x+2] = 'x'
some_data[y+3][x+3] = 'x'
some_data[y+3][x+6] = 'x'
some_data[y+3][x+7] = 'x'
some_data[y+4][x] = 'x'
some_data[y+4][x+1] = 'x'
some_data[y+4][x+8] = 'x'
some_data[y+4][x+9] = 'x'
return some_data
def draw_o(some_data, a, b):
x = 13*b + 2
y = 6*a
some_data[y][x+3] = 'o'
some_data[y][x+4] = 'o'
some_data[y][x+5] = 'o'
some_data[y][x+6] = 'o'
some_data[y+1][x+1] = 'o'
some_data[y+1][x+2] = 'o'
some_data[y+1][x+7] = 'o'
some_data[y+1][x+8] = 'o'
some_data[y+2][x] = 'o'
some_data[y+2][x+1] = 'o'
some_data[y+2][x+8] = 'o'
some_data[y+2][x+9] = 'o'
some_data[y+4][x+3] = 'o'
some_data[y+4][x+4] = 'o'
some_data[y+4][x+5] = 'o'
some_data[y+4][x+6] = 'o'
some_data[y+3][x+1] = 'o'
some_data[y+3][x+2] = 'o'
some_data[y+3][x+7] = 'o'
some_data[y+3][x+8] = 'o'
return some_data
This file contains the draw_x()
and draw_o()
functions which basically takes row and column of the tictactoe board and apply a certain equation that relates position of element of the board to the picture_data
array and fills it with symbols in certain manner, such that during printing the array, it will represent ‘X’ and ‘O’.
The code for checker.py:-
def row_check(some_array):
a = 0
b = 0
for i in range(3):
if ((some_array[i][0] == some_array[i][1]) and (some_array[i][1] == some_array[i][2])):
a = 1
b = some_array[i][0]
break
return [a, b]
def column_check(some_array):
a = 0
b = 0
for i in range(3):
if ((some_array[0][i] == some_array[1][i]) and (some_array[1][i] == some_array[2][i])):
a = 2
b = some_array[0][i]
break
return [a, b]
def cross_check(some_array):
if (((some_array[0][0] == some_array[1][1]) and (some_array[1][1] == some_array[2][2])) or ((some_array[0][2] == some_array[1][1]) and (some_array[1][1] == some_array[2][0]))):
return [3, some_array[1][1]]
return [0, 0]
def check(some_array):
a = column_check(some_array)
if (a[0] != 0):
return a[1]
a = row_check(some_array)
if (a[0] != 0):
return a[1]
a = cross_check(some_array)
if (a[0] != 0):
return a[1]
return 0
This file has functions for checking if any player has won or not
row_check
will takegaming_data
array in entry and scan if any entry in every row is same or not and return an array containing 2 elements in which 1st element contain if someone has won or not in ‘1’ and ‘0’ and 2nd element will contain the information of player who won- Same for
column_check
and andcross_check
for check in column and cross direction respectively. - These function will be called by
check()
function, and return which player won. If no one won, then it will return 0
The code for main.py:-
1. #! /usr/bin/python2
2. import rospy
3. from tictactoe.srv import moeRequest, moe
4. from std_msgs.msg import Int64
5. from display import *
6. from draw import *
7. from checker import *
8. import sys
9. requ = moeRequest()
10. gaming_data = [[0 for i in range(3)] for j in range(3)]
11. tictactoe = Picture_Class()
12. func1 = rospy.ServiceProxy('player_one_server', moe)
13. func2 = rospy.ServiceProxy('player_two_server', moe)
14.
15. def input_pos(a): # Defining a function for taking input from each node used by players
16. requ.gaming_data = [Int64(0) for i in range(9)]
17. for i in range(3):
18. for j in range(3):
19. requ.gaming_data[j+(3*i)] = gaming_data[i][j]
20. if (a % 2 == 0):
21. print('1st player turn\n\n')
22. rospy.wait_for_service('player_one_server')
23. response = func1(requ)
24. gaming_data[response.row-1][response.col-1] = 1
25.
26. else:
27. print('2nd player turn')
28. rospy.wait_for_service('player_two_server')
29. response = func2(requ)
30. gaming_data[response.row-1][response.col-1] = 2
31.
32. def main():
33. print('\n\n')
34. for i in range(9):
35. input_pos(i)
36. tictactoe.upload_data(gaming_data)
37. tictactoe.print_result()
38. if (check(gaming_data) != 0):
39. print('\n\nCongrats '),
40. if (check(gaming_data) == 1):
41. sys.stdout.write(str("1st"))
42. if (check(gaming_data) == 2):
43. sys.stdout.write(str('2nd'))
44. print(' player, You won...!!\n\n')
45. break
46. print('\n\n')
47. if (check(gaming_data) == 0):
48. print('Tie...!!')
49. if (raw_input('Do you want to play again (Y/N) : ') == "Y"):
50. main()
51.
52.
53. if __name__ == '__main__':
54. try:
55. rospy.loginfo('Displayer Activated\n\nStarting the game for data...')
56. main()
57. except rospy.ROSInterruptException():
58. pass
This code will be main code, and this code will be only executed for running Node 3.
- The Line 1 code is shebang which basically tells ROS that which language is this code is written
- From Line 2 to 8, I am importing all functions for display.py, checker.py and draw.py, importing service file, and Int64 data type for initializing
gaming_data
array - In Line 15, we are defining a function which basically calls Node 1 and Node 2 using services and takes input from them, and then save the inputs to the
gaming_data
array - Line 32 will be defining the function
main()
which will be calling all functions to run the code. - Line 53 will call the
main()
function in a safely way, such that during some failure due to an error, we could stop the process by interuppting.
Node 1 & Node 2
Code of Node 1 and Node 2 will be similar, so I will be explaining code of Node 1 only
The code :-
import rospy
from tictactoe.srv import moe, moeResponse
respo = moeResponse()
rospy.init_node('player_one')
def command(req):
input_pos(req)
return respo
def input_pos(req):
print('\n\nYour turn\n\n')
b = int(input('Row number : '))
c = int(input('Column number : '))
if (((0 <= b and b <= 3) and (0 <= c and c <= 3)) and (req.gaming_data[((b-1)*3)+c-1] == 0)):
respo.row = b
respo.col = c
else:
print('\nWrong Input!!...Try once again\n')
input_pos(req)
def callback():
print('Player 1 ready.......Waiting for Displayer')
rospy.Service('/player_one_server', moe, command)
rospy.spin()
if __name__ == '__main__':
try:
rospy.loginfo('Player One Activated\n\nWaiting for data...')
callback()
except rospy.ROSInterruptException():
pass
In this code :-
callback()
function will activate the server ‘/player_one_server’, which will be called by Node 3 during player one turninput_pos()
will be called bycommand()
function, which will be called by the server.input_pos()
when called, will take input from the player.- Now, the
gaming_data
array is passed in request is for preventing the player from marking the position which is already being marked.