Tag Archives: FOSS

Battleships Server / Client for Education

I’ve been teaching a first year introductory module in Python programming for Engineering at Ulster University for a few years now. As part of the later labs I have let the students build a battleships game using Object Oriented Programming – with “Fleet” objects containing a list of “Ships” and so on where they could play on one computer against themselves. But I had often wanted to see if I could get as far as an on-line server application based on similar principles that the students could use. This would teach them client server fundamentals but also let them have fun playing against each other.

Last year I finally attained that goal. I built a very simple web-server using Python Django to manage players, games, and ships. I wanted to use Python partly because it’s become my language of choice for web applications, but also because I wanted the code to be as accessible and understandable to the students as possible. I have initially placed very little emphasis on the User Interface of the server – this is partly deliberate because I wanted the students to have a strong incentive to develop this in the clients. I did however build a very simple admin view to let me see the games in progress. Additionally Django provides a very easy way to build admin views to manipulate objects. I have enabled this to let admins (well me) tweak data if I need to.

The Server

The server provides a very simple interface – an API – to let the students interact with the game. They can do this initially very directly using a web browser. They can simply type in specific web addresses to take certain actions. For instance

http://battleships.server.net/api/1.0/games/index/

will, for a battleships server installed on battleships.server.net (not a real server), list all the current active games. You’ll note the api/1.0/ part of the URL. I did this so that in future years I could change the API and add new version numbers. That’s partly the focus of a later section of this blog.

The output of all the API calls is actually encoded in JSON. This is still fairly human readable, and indeed some web browsers, like Firefox, will render this in a way that is easy to explore. Again, this makes it possible for the students to play the game with nothing more than a web browser and a certain amount of patience. This is good for exploration, but certainly not the easiest way to play. Here’s some sample output from my real games server with my students (poor Brian).

Sample games index JSON output
Sample games index JSON output – this is designed to be read by machines (a client program) but it’s still clear enough for humans so students can read it directly.

The code for the server and documentation about the API is all publicly available on my GitHub repository. You’ll also find some basic installation instructions in case you’d like to run your own game server.

I did want to build some very basic views into the server, I built a more human version of the games list, and I also built this view which was designed only for admins – obviously in the hands of normal players it would make the game a bit pointless.

Game overview for admins
Game overview for admins only.

This allowed me to see a little of what was actually going on in the games my students were playing, as well as showing the current state of ships and who owns what, it also showed a bit more info – here for another game than that above:

Game history and ship list
Game history and ship list

As you can see this shows some of the history of the game so far – this information is available to clients in the API, and likewise the list of surviving ships are shown. The API call for the ships still surviving only shows the ships for the player making the call, with a secret (password) that was generated when the player was created.

You may notice that the server automatically names ships with names taken from the Culture novels from Iain M. Banks.

The Client(s)

In this way the students are using their web browsers as a highly unsophisticated client to access the server. Actually playing the game this way will be frustrating – requiring careful URLs to be typed every time and noting the output for further commands. This is quite deliberate. It would be easy to build the web server to allow the whole game to be played seamlessly from a web browser – but this isn’t the point – I want the students to experience building a client themselves and developing that.

For my module last year I gave the students a partially complete client, written in Python, using text only. Their aim for the lab was to complete the client. No client would be exactly the same, but they would all be written to work against the same server specification. In theory a better client will give a player an edge against others – a motivation for improvements. One student built the start of a graphical interface, some needed to be given more working pieces to complete their clients.

I’ve placed a more or less complete client on GitHub as well, but it could certainly do with a number of improvements. I may not make those since the idea of the project is to provide a focus for students to make these improvements.

What Went Well

The server worked reasonably well, and once I got going the code for automatic ship generation and placement worked quite nicely. I built a good number of unit tests for this which flushed out some problems, and which again are intended as a useful teaching resource.

I spent a reasonable amount of time building a server API that couldn’t be too easily exploited. For instance, it’s not possible for any player to be more than one move ahead of another to prevent brute force attacks by a client.

The server makes it relatively easy for as many players as one wants in a given game.

What Went Wrong

I made some missteps the first time around the block.

  • the game grid’s default size is too large – I made this configurable, but too large a grid makes the chances of quick hits much more remote – which the students need for motivation. I only really noticed this when I built my admin view shown above.
  • in the initial game, any hit on a ship destroys it, there is no concept of health, and bigger ships are more vulnerable. This is arguably not a mistake, but is a deviation from expected behaviour.

What’s Next

This year I may add another version of the API that adds health to ships so that multiple hits are needed for large ships. By moving all of this to a new version number in the URL as above, e.g.

http://battleships.server.net/api/2.0/games/index/

I hope to allow for improved versions of the game while still supporting (within reason) older clients accessing the original version.

I might also add admin calls to tweak the number of ships per player and size of the ships. There are defaults in the code, but these can easily be overriden.

Shall We Play A Game?

There’s absolutely no reason the clients need to be written in Python. There’s no reason not to write one in C++ or Java, or to build a client that runs on Android or iOS phones. There’s no reason that first year classes with a text mode Python client can’t play against final year students using an Android client.

If you are teaching in any of these languages and feel like writing new clients against the server please do. If you feel like making your clients open source to help more students (and lecturers) learn more, that would be super too.

If you have some improvements in mind for the server, do feel free to fork it, or put in pull requests.

If you or your class do have a go at this, I’d love to hear from you.

Semi Open Book Exams

A few years ago, I switched one of my first year courses to use what I call a semi-open-book approach.

Open-book exams of course allow students to bring whatever materials they wish into them, but they have the disadvantage that students will often bring in materials that they have not studied in detail, or even at all. In such cases, sifting through materials to help them answer a question could be counter productive.

On the other hand, the real world is now an increasingly “open-book” environment, which huge amounts of information available to those in the workplace which is now almost always Internet connected.

So I decided to look at another approach. Students are allowed to bring in a single, personalised, A4 sheet, on which they can write whatever they wish on both sides. There are a few rules:

  • the sheet must be written on “by hand”, that is to say, it cannot be printed to from a computer, or typed;
  • the sheet must be “original”, that is to say, it cannot be a photocopy of another sheet (though students may of course copy their original for reference);
  • the sheet must be the student’s own work, and they must formally declare as much (with a tick box);
  • the sheet must be handed in with the exam paper, although it is not marked.

The purpose of these restrictions are to ensure that each student takes a lead in producing an individual sheet, and to inhibit cottage industries of copied sheets.

In terms of what can go on the sheet? Well anything really. It can be sections from notes, important formulae, sample questions or solutions. The main purpose here is to prompt students to work out what they would individually distill down to an A4 page. So they go through all the module notes, tutorial problems and more, and work out the most valuable material that deserves to go on one A4 page. I believe that this process itself is the greatest value of the sheet, its production rather than its existence in the exam. I’m working on some research to test this.

So I email them each an A4 PDF, which they can print out at home, and on whatever colour paper they may desire. The sheet is individual and has their student number on it with a barcode, for automated processing and analysis afterwards for a project I’m working on, but this is anonymised. The student’s name in particular does not appear, since in Ulster University, it does not appear on the exam booklet.

The top of my sheet looks like this:

The top of a sample guide sheet.

So, if you would like to do the same, I am enclosing the Python script, and LaTeX that I use to achieve this. You could of course use any other technology, or not individualise the sheet at all.

For convenience the most recent code will also be placed on a GitHub repository here, feel free to clone away.

My script has just been rewritten for Python 3.x, and I’ve added a lot of command line parameters to decouple it from me and Ulster University only use. It opens a CSV file from my University which contains student id numbers, student names, and emails in specific columns. These are the default for the script but can be changed. For each student it uses LaTeX to generate the page. It actually creates inserts for each student of the name and student number, you can then edit open-book.tex to allow the page to be as you wish it. You don’t need to know much LaTeX to achieve this, but ping me if you need help. I am also using a LaTeX package to create the barcodes automatically.

I’ve spent a bit of time adding command line parameters to this script, but you can try using

python3 open-book.py --help

for information. The script has been rewritten for Python 3. If you run it without parameters it will enter interactive mode and prompt you.

I’d strongly recommend running with the –test-only option at first to make sure all looks good, and opening open-book.pdf will show you the last generated page so you can see it’s what you want.

Anyway, feel free to do your own thing, or mutilate the code. Enjoy!

#!/usr/bin/env python

#
# Copyright Colin Turner 2014-2017
#
# Free and Open Source Software under GPL v3
#
import argparse

import csv
import re
import subprocess
import smtplib
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText


def process_student(args, row):
    """Takes a line from the CSV file, you will likely need to edit aspects of this."""
    'kicks of the processing of a single student'
    student_number = row[args.student_id_column]
    student_name = row[args.student_name_column]
    student_email = row[args.student_email_column]
    print('  Processing:', student_name , ':', student_email)
    create_latex_inserts(student_number, student_name)
    create_pdf()
    send_email(args, student_name, student_email)


def create_latex_inserts(student_number, student_name):
    """Write LaTeX inserts for the barcode and student name

    For each student this will create two tiny LaTeX files:

     * open-book-insert-barcode.tex which contains the LaTeX code for a barcode representing the student number
     * open-book-insert-name.tex which will contain simply the student's name

    These files can be included/inputted from open-book.tex as desired to personalise that document

    student_number is the ID in the students record system for the student
    student_name is the name of the student"""

    # Open a tiny LaTeX file to put this in
    file = open('open-book-insert-barcode.tex', 'w')

    # All the file contains is LaTeX to code to create the bar code
    string = '\psbarcode{' + student_number + '}{includetext height=0.25}{code39}'
    file.write(string)
    file.close()

    # The same exercise for the second file to contain the student name
    file = open('open-book-insert-name.tex', 'w')
    string = student_name
    file.write(string)
    file.close()


def create_pdf():
    """Calls LaTeX and dvipdf to create the personalised PDF with inserts from create_latex_inserts()"""

    # Suppress stdout, but we leave stderr enabled.
    subprocess.call("latex open-book", stdout=subprocess.DEVNULL, shell=True)
    subprocess.call("dvipdf open-book", stdout=subprocess.DEVNULL, shell=True)


def send_email(args, student_name, student_email):
    """Emails a single student with the generated PDF."""
    #TODO: Might be useful to improve the to address
    #TODO: Allow subject to be tailored.

    subject = args.email_subject
    from_address = args.email_sender
    # to_address = student_name + ' <' + student_email + '>'
    to_address = student_email

    msg = MIMEMultipart()
    msg['Subject'] = subject
    msg['From'] = from_address
    msg['To'] = to_address

    text = 'Dear Student\nPlease find enclosed your guide sheet template for the exam. Read the following email carefully.\n'
    part1 = MIMEText(text, 'plain')
    msg.attach(part1)

    # Open the files in binary mode.  Let the MIMEImage class automatically
    # guess the specific image type.
    fp = open('open-book.pdf', 'rb')
    img = MIMEApplication(fp.read(), 'pdf')
    fp.close()

    msg.attach(img)

    # Send the email via our own SMTP server, if we are not testing.
    if not args.test_only:
        s = smtplib.SMTP(args.smtp_server)
        s.sendmail(from_address, to_address, msg.as_string())
        s.quit()


def override_arguments(args):
    """If necessary, prompt for arguments and override them

    Takes, as input, args from an ArgumentParser and returns the same after processing or overrides.
    """

    # If the user enabled batch mode, we disable interactive mode
    if args.batch_mode:
        args.interactive_mode = False

    if args.interactive_mode:
        override = input("CSV filename? default=[{}] :".format(args.input_file))
        if len(override):
            args.input_file = override

        override = input("Student ID Column? default=[{}] :".format(args.student_id_column))
        if len(override):
            args.student_id_column = int(override)

        override = input("Student Name Column? default=[{}] :".format(args.student_name_column))
        if len(override):
            args.student_name_column = int(override)

        override = input("Student Email Column? default=[{}] :".format(args.student_email_column))
        if len(override):
            args.student_email_column = int(override)

        override = input("Student ID Regular Expression? default=[{}] :".format(args.student_id_regexp))
        if len(override):
            args.student_id_regexp = override

        override = input("SMTP Server? default=[{}] :".format(args.smtp_server))
        if len(override):
            args.smtp_server = override

        override = input("Email subject? default=[{}] :".format(args.email_subject))
        if len(override):
            args.email_subject = override

        override = input("Email sender address? default=[{}] :".format(args.email_sender))
        if len(override):
            args.email_sender = override

    return(args)


def parse_arguments():
    """Get all the command line arguments for the file and return the args from an ArgumentParser"""

    parser = argparse.ArgumentParser(
        description="A script to email students study pages for a semi-open book exam",
        epilog="Note that column count arguments start from zero."

    )

    parser.add_argument('-b', '--batch-mode',
                        action='store_true',
                        dest='batch_mode',
                        default=False,
                        help='run automatically with values given')

    parser.add_argument('--interactive-mode',
                        action='store_true',
                        dest='interactive_mode',
                        default=True,
                        help='prompt the user for details (default)')

    parser.add_argument('-i', '--input-file',
                        dest='input_file',
                        default='students.csv',
                        help='the name of the input CSV file with one row per student')

    parser.add_argument('-sidc', '--student-id-column',
                        dest='student_id_column',
                        default=1,
                        help='the column containing the student id (default 1)')

    parser.add_argument('-snc', '--student-name-column',
                        dest='student_name_column',
                        default=2,
                        help='the column containing the student name (default 2)')

    parser.add_argument('-sec', '--student-email-column',
                        dest='student_email_column',
                        default=9,
                        help='the column containing the student email (default 9)')

    parser.add_argument('-sidregexp', '--student-id-regexp',
                        dest='student_id_regexp',
                        default='B[0-9]+',
                        help='a regular expression for valid student IDs (default B[0-9]+)')

    parser.add_argument('--smtp-server',
                        dest='smtp_server',
                        default='localhost',
                        help='the address of an smtp server')

    parser.add_argument('--email-subject',
                        dest='email_subject',
                        default='IMPORTANT: Your semi-open-book Guide Sheet',
                        help='the subject of emails that are sent')

    parser.add_argument('--email-sender',
                        dest='email_sender',
                        default='noreply@nowhere.org',
                        help='the sender address from which to send emails')

    parser.add_argument('-t', '--test-only',
                        action='store_true',
                        dest='test_only',
                        default=False,
                        help='do not send any emails')

    args = parser.parse_args()

    # Allow for any overrides from program logic or interaction with the user
    args = override_arguments(args)
    return(args)


def main():
    """the main function that kicks everything else off"""

    print("Hello")
    args = parse_arguments()

    print("Starting open-book...")
    print(args)
    csvReader = csv.reader(open(args.input_file, 'r'), dialect='excel')

    student_count = 0
    # Go through each row
    for row in csvReader:
        student_number = row[args.student_id_column]
        # Check if the second cell looks like a student number
        if re.match(args.student_id_regexp, row[args.student_id_column]):
            student_count = student_count + 1
            process_student(args, row)
        else:
            print('  Skipping: non matching row')

    print('Stopping open-book...')


if __name__ == '__main__':
    main()

I use a LaTeX template for the base information, this can be easily edited for taste.

\documentclass[12pt,a4paper]{minimal}
\usepackage[latin1]{inputenc}
\usepackage{pst-barcode}
\usepackage[margin=2cm]{geometry}


%
% Does it all have to be Arial now? <sigh>
%
\renewcommand{\familydefault}{\sfdefault}

\author{Professor Colin Turner}
\begin{document}
\begin{centering}
\textbf{EEE122 Examination Guide Sheet}

This sheet, and its contents that you have added, can be brought into
the examination for EEE122. The contents \textbf{must} be compiled
by yourself, be handwritten, and be original (i.e. \textbf{NOT} 
photocopied or similar). You may use the
reverse side. You may retain a copy you have made before the examination
but the original must be handed in with your examination scripts at the
end of your examination.

%\input{open-book-insert-name.tex}
% D'Oh! Not supposed to put a name on anything going in the exam.
\input{open-book-insert-barcode.tex}
\hfill
\begin{pspicture}(7,1in)
%\psbarcode{
%\input{./open_book_insert.tex}
%B00526636
%}{includetext height=0.25}{code39}
\input{open-book-insert-barcode.tex}
\end{pspicture}
\end{centering}

\vfill
\begin{centering}
Please read the following declaration and tick the box to indicate you agree:

I declare this sheet to have been compiled by myself and not by another, and that the student number above is mine.

%\rule[-1 cm]{10 cm}{1 pt}
\framebox[0.3 cm]{ }

\end{centering}

\end{document}