The OSU Freshman Engineering program concludes with a Software Design Project where students compete to build the best MATLAB games they can. Since there was no way I’d win for UI I took a stab at building a multiplayer version of battleship.

Background

While MATLAB certainly isn’t known for its easy game creation this hasn’t stopped people from creating some really impressive projects. TCP/IP support exists, but provides the bare minimum for writing servers. You only get one socket per port, and every function call blocks, so sometimes you have to get creative.

Since this was created for a class the goal was to create the minimal viable product with the least effort, and it definitely shows in the code quality.

Networking

To avoid having to synchronize state across multiple programs I decided to only have one single server and have all clients connect through TCP. This is a common technique in cybersecurity exploits, where you open up a remote root shell, so it is doable. Since we can only open one socket per port, so we’ll have use two:

t1_socket = tcpip('0.0.0.0', 30000, 'NetworkRole', 'server');
t2_socket = tcpip('0.0.0.0', 30002, 'NetworkRole', 'server');

fopen(t1_socket);
fprintf(t1_socket, 'Welcome to Battleship Player 1!\nWaiting for Player 2...\n');
fopen(t2_socket);
fprintf(t2_socket, 'Hello Player 2! Welcome to Battleship!\n');

Great! Now we can send data to the clients that connect with nc <ip> 3000{0,2}! But then we’ll have to find a way to clear the screen so it at least looks presentable. This should be as easy as faking a terminal clear command:

$ clear | xxd
00000000: 1b5b 334a 1b5b 481b 5b32 4a              .[3J.[H.[2J

Now we can clear a client screen by sending those bytes:

function clearSock(socket)
    data = char([hex2dec('1b'), hex2dec('5b'), hex2dec('48'), hex2dec('1b'), hex2dec('5b'), hex2dec('4a')]);
    fprintf(socket, '%s', data);
end

Now we need to get user input. This gets a bit more complex since reading from multiple sockets at once isn’t a supported feature. Luckily, we do have a BytesAvailable property, so we can avoid blocking on a read. Here we have a function that takes an array of sockets and a bitmask to show which ones still need data:

function [p_id,data] = getSomeInput(socks, sockmask)
    p_id = 0;
    data = '';
    while 1
        for pid = 1:length(socks)
            p_id = pid;
            if ~sockmask(p_id)
                continue
            end
            if socks(p_id).BytesAvailable > 0
                data = fread(socks(p_id), socks(p_id).BytesAvailable);
                data = data(1:length(data)-1); % Remove newline
                fprintf('Got data from %d: [%s]\n', p_id, data);
                return
            end
        end
    end
end

Game Logic

Now that all the boilerplate networking functions are out of the way we can get down into the game code. There isn’t too much special about our implementation, so I’ll skip most of this. Despite not being built for the task the language made most of it short and simple if you can represent it as an element-wise operation. For example, here is how I tested for collisions when placing ships:

[err,sBoard] = drawShip(coord, vert, SHIP_SIZES(ship_places(p_id)));

if err
    fprintf(socks(p_id), 'Collides with wall, try again: ');
    continue
end

if sum(sum(board & sBoard))
    fprintf(socks(p_id), 'Collides with another ship, try again: ');
    continue
end

To create a proper user experience they should be able to see an ASCII art version of the board. Something like this:

------------
|      HHH |A
|  H       |B
|  H       |C
|  H  H    |D
|  H  H    |E
|  H  H    |F
|     H    |G
|          |H
|  HHH     |I
|        HH|J
------------
 0123456789

This is where element-wise and matrix operations save the day. Surprisingly, this doesn’t take very many lines at all. All of this, and more, can be drawn with this monstrosity:

function printBoard(socket, board)
    width = size(board,2);
    line = char(zeros(1,width + 2) + '-');
    asciiBoard = char((board == 1) * 'H' + (board == 0) * ' ' + (board == 2) * 'X' + '.' * (board == 3));
    fprintf(socket,'%s\n', line);
    for i=1:width
        fprintf(socket,'|%s|', convertCharsToStrings(asciiBoard(i,:)),'sync');
        fprintf(socket,'%s\n', convertCharsToStrings(char('A' + i - 1)), 'sync');
    end
    fprintf(socket,'%s\n', convertCharsToStrings(line));
    fprintf(socket,' %s\n',convertCharsToStrings(char((1:width) + '0' - 1)));
end

This type of problem is one of the few where MATLAB comes only second to Python in lines of code. ASCII matrices are by no means good solutions to problems, and neither is embedding branching through element-wise logic, but they do have their uses.

Takeaways

This code is bad and wrong, with a UI only a sysadmin could love. But it does work. If I had to do it again I’d use Python, or maybe Go, but it really wasn’t as bad as I was expecting it to be. At the end it was even playable, albeit it’s pretty slow game to play.

Here is a link to the full code.