aboutsummaryrefslogtreecommitdiffstats
path: root/python/dotsandboxes
diff options
context:
space:
mode:
authorMatt Strapp <strap012@umn.edu>2021-04-26 10:53:43 -0500
committerMatt Strapp <strap012@umn.edu>2021-04-26 15:03:12 -0500
commitd311af01feb32550aaae8638d4cc167948f5464c (patch)
tree3c0b8606a7a5267e3e890a63b8565c5c27f10438 /python/dotsandboxes
parentactually add files (diff)
downloadcsci4511w-d311af01feb32550aaae8638d4cc167948f5464c.tar
csci4511w-d311af01feb32550aaae8638d4cc167948f5464c.tar.gz
csci4511w-d311af01feb32550aaae8638d4cc167948f5464c.tar.bz2
csci4511w-d311af01feb32550aaae8638d4cc167948f5464c.tar.lz
csci4511w-d311af01feb32550aaae8638d4cc167948f5464c.tar.xz
csci4511w-d311af01feb32550aaae8638d4cc167948f5464c.tar.zst
csci4511w-d311af01feb32550aaae8638d4cc167948f5464c.zip
Rebase newer branch
Diffstat (limited to 'python/dotsandboxes')
-rw-r--r--python/dotsandboxes/README.md134
-rwxr-xr-xpython/dotsandboxes/dotsandboxesagent5
-rw-r--r--python/dotsandboxes/dotsandboxesagent.py213
-rw-r--r--python/dotsandboxes/dotsandboxescompete.py212
-rw-r--r--python/dotsandboxes/dotsandboxesserver.py60
-rw-r--r--python/dotsandboxes/requirements.txt1
-rw-r--r--python/dotsandboxes/static/dotsandboxes.css10
-rw-r--r--python/dotsandboxes/static/dotsandboxes.html56
-rw-r--r--python/dotsandboxes/static/dotsandboxes.js454
9 files changed, 1145 insertions, 0 deletions
diff --git a/python/dotsandboxes/README.md b/python/dotsandboxes/README.md
new file mode 100644
index 0000000..e3f844c
--- /dev/null
+++ b/python/dotsandboxes/README.md
@@ -0,0 +1,134 @@
+Dots and Boxes application
+==========================
+
+Live demo: https://people.cs.kuleuven.be/wannes.meert/dotsandboxes/play
+
+![Screenshot of Dots and Boxes](https://people.cs.kuleuven.be/wannes.meert/dotsandboxes/screenshot.png?v=2)
+
+This setup is part of the course "Machine Learning: Project" (KU Leuven,
+Faculty of engineering, Department of Computer Science,
+[DTAI research group](https://dtai.cs.kuleuven.be)).
+
+
+Installation
+------------
+
+The example agent is designed for Python 3.6 and requires the
+[websockets](https://websockets.readthedocs.io) package. Dependencies can be
+installed using pip:
+
+ $ pip install -r requirements.txt
+
+
+Start the game GUI
+------------------
+
+This program shows a web-based GUI to play the Dots and Boxes
+game. This supports human-human, agent-human and agent-agent combinations.
+It is a simple Javascript based application that runs entirely in the browser.
+You can start it by opening the file `static/dotsandboxes.html` in a browser.
+Or alternatively, you can start the app using the included simple server:
+
+ $ ./dotsandboxesserver.py 8080
+
+The game can then be played by directing your browser to http://127.0.0.1:8080.
+
+
+Start the agent client
+----------------------
+
+This is the program that runs a game-playing agent. This application listens
+to [websocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)
+requests that communicate game information and sends back the next action it
+wants to play.
+
+Starting the agent client is done using the following command:
+
+ $ ./dotsandboxesagent <port>
+
+This starts a websocket on the given port that can receveive JSON messages.
+
+The JSON messages given below should be handled by your agent.
+Take into account the maximal time allowed to reply.
+
+### Initiate the game
+
+Both players get a message that a new game has started:
+
+ {
+ "type": "start",
+ "player": 1,
+ "timelimit", 0.5,
+ "grid": [5, 5],
+ "game": "123456"
+ }
+
+where `player` is the number assigned to this agent, `timelimit` is the
+time in seconds in which you need to send your action back to the server,
+and `grid` is the grid size in rows and columns.
+
+If you are player 1, reply with the first action you want to perform:
+
+ {
+ "type": "action",
+ "location": [1, 1],
+ "orientation": "v"
+ }
+
+The field `location` is expressed as row and column (zero-based numbering) and
+`orientation` is either "v" (vertical) or "h" (horizontal).
+
+
+### Action in the game
+
+When an action is played, the message sent to both players is:
+
+ {
+ "type": "action",
+ "game": "123456",
+ "player": 1,
+ "nextplayer": 2,
+ "score": [0, 0],
+ "location": [1, 1],
+ "orientation": "v"
+ }
+
+
+If it is your turn you should answer with a message that states your next
+move:
+
+ {
+ "type": "action",
+ "location": [1, 1],
+ "orientation": "v"
+ }
+
+
+### Game end
+
+When the game ends after an action, the message is slightly altered:
+
+ {
+ "type": "end",
+ "game": "123456",
+ "player": 1,
+ "nextplayer": 0,
+ "score": [3, 1],
+ "location": [1, 1],
+ "orientation": "v",
+ "winner": 1
+ }
+
+The `type` field becomes `end` and a new field `winner` is set to the player
+that has won the game.
+
+
+Contact information
+-------------------
+
+- Wannes Meert, https://people.cs.kuleuven.be/wannes.meert
+- Hendrik Blockeel, https://people.cs.kuleuven.be/hendrik.blockeel
+- Arne De Brabandere, https://people.cs.kuleuven.be/arne.debrabandere
+- Sebastijan Dumančić, https://people.cs.kuleuven.be/sebastijan.dumancic
+- Pieter Robberechts, https://people.cs.kuleuven.be/pieter.robberechts
+
diff --git a/python/dotsandboxes/dotsandboxesagent b/python/dotsandboxes/dotsandboxesagent
new file mode 100755
index 0000000..eecf719
--- /dev/null
+++ b/python/dotsandboxes/dotsandboxesagent
@@ -0,0 +1,5 @@
+#!/bin/bash
+# It is not necessary to use a shell script for this. Dropping the .py
+# extension and including the correct shebang is also correct.
+python3 $(dirname "$0")/dotsandboxesagent.py $@
+
diff --git a/python/dotsandboxes/dotsandboxesagent.py b/python/dotsandboxes/dotsandboxesagent.py
new file mode 100644
index 0000000..c8bc05e
--- /dev/null
+++ b/python/dotsandboxes/dotsandboxesagent.py
@@ -0,0 +1,213 @@
+#!/usr/bin/env python3
+# encoding: utf-8
+"""
+dotsandboxesagent.py
+
+Template for the Machine Learning Project course at KU Leuven (2017-2018)
+of Hendrik Blockeel and Wannes Meert.
+
+Copyright (c) 2018 KU Leuven. All rights reserved.
+"""
+import sys
+import argparse
+import logging
+import asyncio
+import websockets
+import json
+from collections import defaultdict
+import random
+
+
+logger = logging.getLogger(__name__)
+games = {}
+agentclass = None
+
+
+class DotsAndBoxesAgent:
+ """Example Dots and Boxes agent implementation base class.
+ It returns a random next move.
+
+ A DotsAndBoxesAgent object should implement the following methods:
+ - __init__
+ - add_player
+ - register_action
+ - next_action
+ - end_game
+
+ This class does not necessarily use the best data structures for the
+ approach you want to use.
+ """
+ def __init__(self, player, nb_rows, nb_cols, timelimit):
+ """Create Dots and Boxes agent.
+
+ :param player: Player number, 1 or 2
+ :param nb_rows: Rows in grid
+ :param nb_cols: Columns in grid
+ :param timelimit: Maximum time allowed to send a next action.
+ """
+ self.player = {player}
+ self.timelimit = timelimit
+ self.ended = False
+ self.nb_rows = nb_rows
+ self.nb_cols = nb_cols
+ rows = []
+ for ri in range(nb_rows + 1):
+ columns = []
+ for ci in range(nb_cols + 1):
+ columns.append({"v": 0, "h": 0})
+ rows.append(columns)
+ self.cells = rows
+
+ def add_player(self, player):
+ """Use the same agent for multiple players."""
+ self.player.add(player)
+
+ def register_action(self, row, column, orientation, player):
+ """Register action played in game.
+
+ :param row:
+ :param columns:
+ :param orientation: "v" or "h"
+ :param player: 1 or 2
+ """
+ self.cells[row][column][orientation] = player
+
+ def next_action(self):
+ """Return the next action this agent wants to perform.
+
+ In this example, the function implements a random move. Replace this
+ function with your own approach.
+
+ :return: (row, column, orientation)
+ """
+ logger.info("Computing next move (grid={}x{}, player={})"\
+ .format(self.nb_rows, self.nb_cols, self.player))
+ # Random move
+ free_lines = []
+ for ri in range(len(self.cells)):
+ row = self.cells[ri]
+ for ci in range(len(row)):
+ cell = row[ci]
+ if ri < (len(self.cells) - 1) and cell["v"] == 0:
+ free_lines.append((ri, ci, "v"))
+ if ci < (len(row) - 1) and cell["h"] == 0:
+ free_lines.append((ri, ci, "h"))
+ if len(free_lines) == 0:
+ # Board full
+ return None
+ movei = random.randint(0, len(free_lines) - 1)
+ r, c, o = free_lines[movei]
+ return r, c, o
+
+ def end_game(self):
+ self.ended = True
+
+
+## MAIN EVENT LOOP
+
+async def handler(websocket, path):
+ logger.info("Start listening")
+ game = None
+ # msg = await websocket.recv()
+ try:
+ async for msg in websocket:
+ logger.info("< {}".format(msg))
+ try:
+ msg = json.loads(msg)
+ except json.decoder.JSONDecodeError as err:
+ logger.error(err)
+ return False
+ game = msg["game"]
+ answer = None
+ if msg["type"] == "start":
+ # Initialize game
+ if msg["game"] in games:
+ games[msg["game"]].add_player(msg["player"])
+ else:
+ nb_rows, nb_cols = msg["grid"]
+ games[msg["game"]] = agentclass(msg["player"],
+ nb_rows,
+ nb_cols,
+ msg["timelimit"])
+ if msg["player"] == 1:
+ # Start the game
+ nm = games[game].next_action()
+ print('nm = {}'.format(nm))
+ if nm is None:
+ # Game over
+ logger.info("Game over")
+ continue
+ r, c, o = nm
+ answer = {
+ 'type': 'action',
+ 'location': [r, c],
+ 'orientation': o
+ }
+ else:
+ # Wait for the opponent
+ answer = None
+
+ elif msg["type"] == "action":
+ # An action has been played
+ r, c = msg["location"]
+ o = msg["orientation"]
+ games[game].register_action(r, c, o, msg["player"])
+ if msg["nextplayer"] in games[game].player:
+ # Compute your move
+ nm = games[game].next_action()
+ if nm is None:
+ # Game over
+ logger.info("Game over")
+ continue
+ nr, nc, no = nm
+ answer = {
+ 'type': 'action',
+ 'location': [nr, nc],
+ 'orientation': no
+ }
+ else:
+ answer = None
+
+ elif msg["type"] == "end":
+ # End the game
+ games[msg["game"]].end_game()
+ answer = None
+ else:
+ logger.error("Unknown message type:\n{}".format(msg))
+
+ if answer is not None:
+ print(answer)
+ await websocket.send(json.dumps(answer))
+ logger.info("> {}".format(answer))
+ except websockets.exceptions.ConnectionClosed as err:
+ logger.info("Connection closed")
+ logger.info("Exit handler")
+
+
+def start_server(port):
+ server = websockets.serve(handler, 'localhost', port)
+ print("Running on ws://127.0.0.1:{}".format(port))
+ asyncio.get_event_loop().run_until_complete(server)
+ asyncio.get_event_loop().run_forever()
+
+
+## COMMAND LINE INTERFACE
+
+def main(argv=None):
+ global agentclass
+ parser = argparse.ArgumentParser(description='Start agent to play Dots and Boxes')
+ parser.add_argument('--verbose', '-v', action='count', default=0, help='Verbose output')
+ parser.add_argument('--quiet', '-q', action='count', default=0, help='Quiet output')
+ parser.add_argument('port', metavar='PORT', type=int, help='Port to use for server')
+ args = parser.parse_args(argv)
+
+ logger.setLevel(max(logging.INFO - 10 * (args.verbose - args.quiet), logging.DEBUG))
+ logger.addHandler(logging.StreamHandler(sys.stdout))
+
+ agentclass = DotsAndBoxesAgent
+ start_server(args.port)
+
+
+if __name__ == "__main__":
+ sys.exit(main())
+
diff --git a/python/dotsandboxes/dotsandboxescompete.py b/python/dotsandboxes/dotsandboxescompete.py
new file mode 100644
index 0000000..ee2aee8
--- /dev/null
+++ b/python/dotsandboxes/dotsandboxescompete.py
@@ -0,0 +1,212 @@
+#!/usr/bin/env python3
+# encoding: utf-8
+"""
+dotsandboxescompete.py
+
+Template for the Machine Learning Project course at KU Leuven (2017-2018)
+of Hendrik Blockeel and Wannes Meert.
+
+Copyright (c) 2018 KU Leuven. All rights reserved.
+"""
+
+import sys
+import argparse
+import logging
+import asyncio
+import websockets
+import json
+from collections import defaultdict
+import random
+import uuid
+import time
+
+logger = logging.getLogger(__name__)
+
+
+def start_competition(address1, address2, nb_rows, nb_cols, timelimit):
+ asyncio.get_event_loop().run_until_complete(connect_agent(address1, address2, nb_rows, nb_cols, timelimit))
+
+
+async def connect_agent(uri1, uri2, nb_rows, nb_cols, timelimit):
+ cur_game = str(uuid.uuid4())
+ winner = None
+ cells = []
+ cur_player = 1
+ points = [0, 0, 0]
+ timings = [None, [], []]
+
+ for ri in range(nb_rows + 1):
+ columns = []
+ for ci in range(nb_cols + 1):
+ columns.append({"v":0, "h":0, "p":0})
+ cells.append(columns)
+
+ logger.info("Connecting to {}".format(uri1))
+ async with websockets.connect(uri1) as websocket1:
+ logger.info("Connecting to {}".format(uri2))
+ async with websockets.connect(uri2) as websocket2:
+ logger.info("Connected")
+
+ # Start game
+ msg = {
+ "type": "start",
+ "player": 1,
+ "timelimit": timelimit,
+ "game": cur_game,
+ "grid": [nb_rows, nb_cols]
+ }
+ await websocket1.send(json.dumps(msg))
+ msg["player"] = 2
+ await websocket2.send(json.dumps(msg))
+
+ # Run game
+ while winner is None:
+ ask_time = time.time()
+ logger.info("Waiting for player {}".format(cur_player))
+ if cur_player == 1:
+ msg = await websocket1.recv()
+ else:
+ msg = await websocket2.recv()
+ recv_time = time.time()
+ diff_time = recv_time - ask_time
+ timings[cur_player].append(diff_time)
+ logger.info("Message received after (s): {}".format(diff_time))
+ try:
+ msg = json.loads(msg)
+ except json.decoder.JSONDecodeError as err:
+ logger.debug(err)
+ continue
+ if msg["type"] != "action":
+ logger.error("Unknown message: {}".format(msg))
+ continue
+ r, c = msg["location"]
+ o = msg["orientation"]
+ next_player = user_action(r, c, o, cur_player,
+ cells, points,
+ nb_rows, nb_cols)
+ if points[1] + points[2] == nb_cols * nb_rows:
+ # Game over
+ winner = 1
+ if points[2] == points[1]:
+ winner = 0
+ if points[2] > points[1]:
+ winner = 2
+ else:
+ msg = {
+ "type": "action",
+ "game": cur_game,
+ "player": cur_player,
+ "nextplayer": next_player,
+ "score": [points[1], points[2]],
+ "location": [r, c],
+ "orientation": o
+ }
+ await websocket1.send(json.dumps(msg))
+ await websocket2.send(json.dumps(msg))
+
+ cur_player = next_player
+
+ # End game
+ logger.info("Game ended: points1={} - points2={} - winner={}".format(points[1], points[2], winner))
+ msg = {
+ "type": "end",
+ "game": cur_game,
+ "player": cur_player,
+ "nextplayer": 0,
+ "score": [points[1], points[2]],
+ "location": [r, c],
+ "orientation": o,
+ "winner": winner
+ }
+ await websocket1.send(json.dumps(msg))
+ await websocket2.send(json.dumps(msg))
+
+ # Timings
+ for i in [1, 2]:
+ logger.info("Timings: player={} - avg={} - min={} - max={}"\
+ .format(i,
+ sum(timings[i])/len(timings[i]),
+ min(timings[i]),
+ max(timings[i])))
+
+ logger.info("Closed connections")
+
+
+def user_action(r, c, o, cur_player, cells, points, nb_rows, nb_cols):
+ logger.info("User action: player={} - r={} - c={} - o={}".format(cur_player, r, c, o))
+ next_player = cur_player
+ won_cell = False
+ cell = cells[r][c]
+ if o == "h":
+ if cell["h"] != 0:
+ return cur_player
+ cell["h"] = cur_player
+ # Above
+ if r > 0:
+ if cells[r - 1][c]["v"] != 0 \
+ and cells[r - 1][c + 1]["v"] != 0 \
+ and cells[r - 1][c]["h"] != 0 \
+ and cells[r][c]["h"] != 0:
+ won_cell = True
+ points[cur_player] += 1
+ cells[r - 1][c]["p"] = cur_player
+ # Below
+ if r < nb_rows:
+ if cells[r][c]["v"] != 0 \
+ and cells[r][c + 1]["v"] != 0 \
+ and cells[r][c]["h"] != 0 \
+ and cells[r + 1][c]["h"] != 0:
+ won_cell = True
+ points[cur_player] += 1
+ cells[r][c]["p"] = cur_player
+
+ if o == "v":
+ if cell["v"] != 0:
+ return cur_player
+ cell["v"] = cur_player;
+ # Left
+ if c > 0:
+ if cells[r][c - 1]["v"] != 0 \
+ and cells[r][c]["v"] != 0 \
+ and cells[r][c - 1]["h"] != 0 \
+ and cells[r + 1][c - 1]["h"] != 0:
+ won_cell = True
+ points[cur_player] += 1
+ cells[r][c - 1]["p"] = cur_player
+ # Right
+ if c < nb_cols:
+ if cells[r][c]["v"] != 0 \
+ and cells[r][c + 1]["v"] != 0 \
+ and cells[r][c]["h"] != 0 \
+ and cells[r + 1][c]["h"] != 0:
+ won_cell = True
+ points[cur_player] += 1
+ cells[r][c]["p"] = cur_player
+
+ if not won_cell:
+ next_player = 3 - cur_player
+ else:
+ next_player = cur_player
+ print("Update points: player1={} - player2={}".format(points[1], points[2]))
+ return next_player
+
+
+def main(argv=None):
+ parser = argparse.ArgumentParser(description='Start agent to play Dots and Boxes')
+ parser.add_argument('--verbose', '-v', action='count', default=0, help='Verbose output')
+ parser.add_argument('--quiet', '-q', action='count', default=0, help='Quiet output')
+ parser.add_argument('--cols', '-c', type=int, default=2, help='Number of columns')
+ parser.add_argument('--rows', '-r', type=int, default=2, help='Number of rows')
+ parser.add_argument('--timelimit', '-t', type=float, default=0.5, help='Time limit per request in seconds')
+ parser.add_argument('agents', nargs=2, metavar='AGENT', help='Websockets addresses for agents')
+ args = parser.parse_args(argv)
+
+ logger.setLevel(max(logging.INFO - 10 * (args.verbose - args.quiet), logging.DEBUG))
+ logger.addHandler(logging.StreamHandler(sys.stdout))
+
+ start_competition(args.agents[0], args.agents[1], args.rows, args.cols, args.timelimit)
+
+
+if __name__ == "__main__":
+ sys.exit(main())
+
diff --git a/python/dotsandboxes/dotsandboxesserver.py b/python/dotsandboxes/dotsandboxesserver.py
new file mode 100644
index 0000000..1b66372
--- /dev/null
+++ b/python/dotsandboxes/dotsandboxesserver.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python3
+# encoding: utf-8
+"""
+dotsandboxesserver.py
+
+Template for the Machine Learning Project course at KU Leuven (2017-2018)
+of Hendrik Blockeel and Wannes Meert.
+
+Copyright (c) 2018 KU Leuven. All rights reserved.
+"""
+
+import sys
+import argparse
+import logging
+import http.server
+import socketserver
+import json
+
+logger = logging.getLogger(__name__)
+
+
+class RequestHandler(http.server.SimpleHTTPRequestHandler):
+ def do_GET(self):
+ if self.path == "/":
+ self.send_response(302)
+ self.send_header("Location", "static/dotsandboxes.html")
+ self.end_headers()
+ return super().do_GET()
+
+ def do_PUT(self):
+ response = {
+ 'result': 'ok'
+ }
+ self.send_response(200)
+ self.send_header('Content-type', 'application/json')
+ self.end_headers()
+ self.wfile.write(json.dumps(response).encode())
+
+
+def start_server(port):
+ with socketserver.TCPServer(("", port), RequestHandler) as httpd:
+ print("Running on http://127.0.0.1:{}".format(port))
+ httpd.serve_forever()
+
+
+def main(argv=None):
+ parser = argparse.ArgumentParser(description='Start server to play Dots and Boxes')
+ parser.add_argument('--verbose', '-v', action='count', default=0, help='Verbose output')
+ parser.add_argument('--quiet', '-q', action='count', default=0, help='Quiet output')
+ parser.add_argument('port', metavar='PORT', type=int, help='Port to use for server')
+ args = parser.parse_args(argv)
+
+ logger.setLevel(max(logging.INFO - 10 * (args.verbose - args.quiet), logging.DEBUG))
+ logger.addHandler(logging.StreamHandler(sys.stdout))
+
+ start_server(args.port)
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/python/dotsandboxes/requirements.txt b/python/dotsandboxes/requirements.txt
new file mode 100644
index 0000000..14774b4
--- /dev/null
+++ b/python/dotsandboxes/requirements.txt
@@ -0,0 +1 @@
+websockets
diff --git a/python/dotsandboxes/static/dotsandboxes.css b/python/dotsandboxes/static/dotsandboxes.css
new file mode 100644
index 0000000..71b1d3b
--- /dev/null
+++ b/python/dotsandboxes/static/dotsandboxes.css
@@ -0,0 +1,10 @@
+
+.footer {
+ color: #B3B3B3;
+ margin-bottom: 1ex;
+}
+
+.footer a {
+ color: #87A0B3;
+}
+
diff --git a/python/dotsandboxes/static/dotsandboxes.html b/python/dotsandboxes/static/dotsandboxes.html
new file mode 100644
index 0000000..ecbcbb4
--- /dev/null
+++ b/python/dotsandboxes/static/dotsandboxes.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html>
+<html lang="en">
+<meta charset="utf-8">
+<meta name="author" content="Wannes Meert">
+<meta name="description" content="Dots-and-Boxes game. Part of the Machine Learning: Project course at KU Leuven (Hendrik Blockeel, Wannes Meert).">
+<meta name="keywords" content="artificial intelligence,AI,machine learning,dots and boxes,KU Leuven">
+<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+<title>Dots and Boxes</title>
+<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
+<link rel="stylesheet" href="dotsandboxes.css">
+</head>
+<body>
+ <div class="container">
+ <h1>Dots and Boxes</h1>
+ <div class="row">
+ <div class="col-md">
+ <div id="playing-area"></div>
+ </div>
+ <div class="col-md">
+ <div class="form-group">
+ <p>Size of game:</p>
+ <div class="input-group">
+ <div class="input-group-prepend">
+ <span class="input-group-text">Rows and Columns</span>
+ </div>
+ <input type="number" class="form-control" id="nb-rows" value=6>
+ <input type="number" class="form-control" id="nb-cols" value=6>
+ </div>
+ </div>
+ <div class="form-group">
+ <p>Players:</p>
+ <div class="input-group mb-3">
+ <div class="input-group-prepend"><span class="input-group-text" id="basic-addon3">Agent 1</span></div>
+ <input type="text" class="form-control" id="agent1" aria-describedby="basic-addon3">
+ </div>
+ <div class="input-group mb-3">
+ <div class="input-group-prepend"><span class="input-group-text" id="basic-addon3">Agent 2</span></div>
+ <input type="text" class="form-control" id="agent2" aria-describedby="basic-addon3">
+ </div>
+ <p>Fill in the address where an agent can be reached using WebSockets (e.g. ws://127.0.0.1:8089).
+ If a field is empty a human player is assumed.
+ </p>
+ <button type="button" class="btn btn-secondary" id="restart-btn">Restart game</button>
+ </div>
+ </div>
+ </div>
+ <div class="footer">
+ <small>&copy; <a href="https://dtai.cs.kuleuven.be">DTAI Research Group</a>, KU Leuven &mdash; <a href="https://github.com/wannesm/dotsandboxes">Source</a></small>
+ </div>
+ </div>
+ <script src="https://d3js.org/d3.v4.min.js"></script>
+ <script src="dotsandboxes.js"></script>
+</body>
+</html>
+
diff --git a/python/dotsandboxes/static/dotsandboxes.js b/python/dotsandboxes/static/dotsandboxes.js
new file mode 100644
index 0000000..11e9447
--- /dev/null
+++ b/python/dotsandboxes/static/dotsandboxes.js
@@ -0,0 +1,454 @@
+/**
+ * dotsandboxes.js
+ *
+ * Template for the Machine Learning Project course at KU Leuven (2017-2018)
+ * of Hendrik Blockeel and Wannes Meert.
+ *
+ * Copyright (c) 2018 KU Leuven. All rights reserved.
+ **/
+
+function generateGuid() {
+ var result, i, j;
+ result = '';
+ for(j=0; j<32; j++) {
+ if( j == 8 || j == 12|| j == 16|| j == 20)
+ result = result + '-';
+ i = Math.floor(Math.random()*16).toString(16).toUpperCase();
+ result = result + i;
+ }
+ return result;
+}
+
+// GAME LOGIC
+
+var cur_game = generateGuid();
+var cur_player = 1;
+var cur_ended = false;
+var points = [0, 0, 0];
+var timelimit = 0.5;
+var nb_cols = 6;
+var nb_rows = 6;
+var data = new Array(0);
+
+function restart_game() {
+ //console.log("Restarting game");
+ cur_game = generateGuid();
+ nb_cols = parseInt(document.getElementById('nb-cols').value);
+ if (nb_cols == "" || isNaN(nb_cols)) {
+ nb_cols = 6;
+ }
+ nb_rows = parseInt(document.getElementById('nb-rows').value);
+ if (nb_rows == "" || isNaN(nb_rows)) {
+ nb_rows = 6;
+ }
+ cur_ended = false;
+ console.log("Starting game", cur_game);
+ points = [0, 0, 0];
+ cur_player = 1;
+ var old_length = 0;
+ for (var ri=0; ri<nb_rows + 1; ri++) {
+ if (ri >= data.length) {
+ data.push(new Array(0));
+ }
+ var row = data[ri];
+ for (var ci=0; ci<nb_cols + 1; ci++) {
+ if (ci >= row.length) {
+ row.push({l:0, t:0, p:0, r:0, c:0});
+ }
+ var l = 0;
+ var t = 0;
+ var p = 0;
+ if (ri == nb_rows) {
+ l = undefined;
+ p = undefined;
+ }
+ if (ci == nb_cols) {
+ t = undefined;
+ p = undefined
+ }
+ var cell = row[ci];
+ cell.l = l;
+ cell.t = t;
+ cell.p = p;
+ cell.r = ri;
+ cell.c = ci;
+ }
+ old_length = row.length;
+ for (var ci=nb_cols + 1; ci<old_length; ci++) {
+ row.pop();
+ }
+ }
+ old_length = data.length;
+ for (var ri=nb_rows + 1; ri<old_length; ri++) {
+ data.pop();
+ }
+}
+
+function user_click(cell, o) {
+ if (cur_ended) {
+ //console.log('Game ended, ignoring click');
+ return;
+ }
+ console.log('User click', cell, o);
+ var won_cell = false;
+ var c = cell.c;
+ var r = cell.r;
+ var msg = {
+ type: "action",
+ game: cur_game,
+ player: cur_player,
+ nextplayer: cur_player,
+ score: [points[1], points[2]],
+ location: [r, c],
+ orientation: o
+ };
+ if (o == "h") {
+ if (cell.t != 0) {
+ return;
+ }
+ cell.t = cur_player;
+ // Above
+ if (r > 0) {
+ if (data[r - 1][c].l != 0
+ && data[r - 1][c + 1].l != 0
+ && data[r - 1][c].t != 0
+ && data[r][c].t != 0) {
+ won_cell = true;
+ points[cur_player] += 1;
+ data[r - 1][c].p = cur_player;
+ }
+ }
+ // Below
+ if (r < nb_rows) {
+ if (data[r][c].l != 0
+ && data[r][c + 1].l != 0
+ && data[r][c].t != 0
+ && data[r + 1][c].t != 0) {
+ won_cell = true;
+ points[cur_player] += 1;
+ data[r][c].p = cur_player;
+ }
+ }
+ }
+
+ if (o == "v") {
+ if (cell.l != 0) {
+ return;
+ }
+ cell.l = cur_player;
+ // Left
+ if (c > 0) {
+ if (data[r][c - 1].l != 0
+ && data[r][c].l != 0
+ && data[r][c - 1].t != 0
+ && data[r + 1][c - 1].t != 0) {
+ won_cell = true;
+ points[cur_player] += 1;
+ data[r][c - 1].p = cur_player;
+ }
+ }
+ // Right
+ if (c < nb_cols) {
+ if (data[r][c].l != 0
+ && data[r][c + 1].l != 0
+ && data[r][c].t != 0
+ && data[r + 1][c].t != 0) {
+ won_cell = true;
+ points[cur_player] += 1;
+ data[r][c].p = cur_player;
+ }
+ }
+ }
+
+ msg["score"] = [points[1], points[2]];
+
+ if (!won_cell) {
+ cur_player = 3 - cur_player;
+ msg.nextplayer = cur_player;
+ }
+ update_board();
+ if (points[1] + points[2] == nb_cols * nb_rows) {
+ // Game over
+ var winner = 1
+ if (points[2] == points[1]) {
+ winner = 0;
+ }
+ if (points[2] > points[1]) {
+ winner = 2;
+ }
+ cur_ended = true;
+ msg.type = "end";
+ msg.nextplayer = 0;
+ msg.winner = winner;
+ }
+ send_to_agents(msg);
+}
+
+var field_margin = 10;
+var cell_width = 40;
+var cell_margin = 4;
+var player_height = 40;
+var width = 400;
+var height = 600;
+var line_width = 5;
+
+var player_color = [
+ "#E6E6E6",
+ "#FC6666",
+ "#0F80FF"
+];
+
+var svg = d3.select("#playing-area").append("svg")
+ .attr("width", width)
+ .attr("height", height)
+ .append("g")
+ .attr("transform", "translate("+field_margin+","+field_margin+")");
+
+var player = svg.append("g")
+ .attr("class", "player")
+ .attr("transform", "translate(0,10)");
+
+var field = svg.append("g")
+ .attr("class", "field")
+ .attr("transform", "translate(0,"+player_height+")");
+
+
+function update_board() {
+ // PLAYERS - enter & update
+ var player_text = player.selectAll("text")
+ .data([cur_player, cur_player]);
+
+ player_text = player_text.enter().append("text")
+ .attr("x", function(c, i) { return i * 100;})
+ .merge(player_text)
+ .text(function(c, i) {return "Player " + (i + 1) + ": "+points[i + 1];})
+ .attr("fill", function(c, i) {
+ if (c == i + 1) {
+ return player_color[c];
+ } else {
+ return player_color[0];
+ }
+ });
+
+ // ROWS - enter & update
+ var rows = field.selectAll(".row")
+ .data(data)
+ .attr("fill", function() {return null;});
+
+ rows.exit().remove();
+
+ rows = rows.enter().append("g")
+ .attr("class", "row")
+ .attr("transform", function(row, i) {return "translate(0," + cell_width * i + ")";})
+ .merge(rows);
+
+ // COLS - enter & update
+ var cols = rows.selectAll(".col")
+ .data(function(col) {return col;});
+
+ cols.exit().remove();
+
+ var cols_enter = cols.enter().append("g")
+ .attr("class", "col")
+ .attr("transform", function(col, ri) {return "translate("+cell_width * ri+",0)";});
+
+ // CELL - enter
+ cols_enter.append("rect")
+ .attr("class", "cell")
+ .attr("rx", cell_margin)
+ .attr("ry", cell_margin)
+ .attr("opacity", 0.25)
+ .attr("x", cell_margin)
+ .attr("y", cell_margin)
+ .attr("width", cell_width - 2*cell_margin)
+ .attr("height", cell_width - 2*cell_margin);
+
+ // HLINE - enter
+ cols_enter.append("line")
+ .attr("class", "hline")
+ .attr("x1", function(cell, ci) {return cell_margin;})
+ .attr("x2", function(cell, ci) {return cell_width - cell_margin;})
+ .attr("y1", 0)
+ .attr("y2", 0)
+ .attr("stroke-linecap", "round")
+ .attr("stroke", function(cell) {return player_color[cell.t];});
+
+ cols_enter.append("path")
+ .attr("d", "M"+cell_margin+",0"+
+ "L"+(cell_width/2)+",-"+(cell_width/3)+
+ "L"+(cell_width-cell_margin)+",0"+
+ "L"+(cell_width/2)+","+(cell_width/3)+"Z")
+ .attr("stroke", "black")
+ .attr("stroke-width", 2)
+ .attr("opacity", "0")
+ .on("click", function(cell) {
+ if (agents[cur_player].active == true) {
+ console.log("Ignoring click, automated agent")
+ } else {
+ user_click(cell, "h");
+ }
+ });
+
+ // VLINE - enter
+ cols_enter.append("line")
+ .attr("class", "vline")
+ .attr("y1", function(cell, ci) {return cell_margin;})
+ .attr("y2", function(cell, ci) {return cell_width - cell_margin;})
+ .attr("x1", 0)
+ .attr("x2", 0)
+ .attr("stroke-linecap", "round")
+ .attr("stroke", function(cell) {return player_color[cell.l];});
+
+ cols_enter.append("path")
+ .attr("d", "M0,"+cell_margin+
+ "L-"+(cell_width/3)+","+(cell_width/2)+
+ "L0,"+(cell_width-cell_margin)+
+ "L"+(cell_width/3)+","+(cell_width/2)+"Z")
+ .attr("stroke", "black")
+ .attr("stroke-width", 2)
+ .attr("opacity", "0")
+ .on("click", function(cell) {
+ if (agents[cur_player].active == true) {
+ console.log("Ignoring click, automated agent");
+ } else {
+ user_click(cell, "v");
+ }
+ });
+
+ cols = cols_enter
+ .merge(cols);
+
+ // HLINE - update
+ cols.selectAll(".hline")
+ .attr("stroke-width", function(cell) {
+ if (typeof(cell.t) == "undefined") {
+ return 0;
+ }
+ return line_width;
+ })
+ .attr("stroke", function(cell) {return player_color[cell.t];});
+
+ // VLINE - update
+ cols.selectAll(".vline")
+ .attr("stroke-width", function(cell, ci) {
+ if (typeof(cell.l) == "undefined") {
+ return 0;
+ }
+ return line_width;
+ })
+ .attr("stroke", function(cell) {return player_color[cell.l];});
+
+ // CELL - update
+ cols.selectAll(".cell")
+ .attr("fill", function(cell) {
+ if (cell.p == undefined) {
+ return "white";
+ }
+ return player_color[cell.p];
+ });
+}
+
+
+// AGENT CONNECTIONS
+
+var agents = [
+ {},
+ {address: undefined, active: false, socket: undefined},
+ {address: undefined, active: false, socket: undefined}
+];
+
+var msg_queue = [];
+
+
+function start_connections() {
+ for (var i=1; i<3; i++) {
+ agents[i] = {address:undefined, active: false, socket: undefined};
+ var address = document.getElementById('agent'+i).value;
+ if (address != "") {
+ //console.log("Starting websocket for agent "+i+" on address "+address);
+ var agent = agents[i];
+ agent.address = address;
+ agent.socket = new WebSocket(address);
+ agent.socket.onopen = (function (ii, iagent) { return function(event) {
+ console.log("Agent "+ii+" connected")
+ iagent.active = true;
+ iagent.socket.onmessage = function(event) {
+ var msg = JSON.parse(event.data);
+ //console.log("Get msg from agent "+ii, msg);
+ if (msg.type == "action") {
+ if (cur_player == ii) {
+ console.log("Received action from ACTIVE player "+ii, msg);
+ user_click(data[msg.location[0]][msg.location[1]], msg.orientation);
+ } else {
+ console.log("Received action from NON-ACTIVE player "+ii, msg);
+ }
+ }
+ return false;
+ };
+ iagent.socket.onclose = function(event) {
+ console.log("Closing connection to agent "+ii);
+ };
+ iagent.socket.onerror = function(event) {
+ console.log("Error on connection to agent "+ii, event);
+ };
+ msg = {
+ "type": "start",
+ "player": ii,
+ "timelimit": timelimit,
+ "game": cur_game,
+ "grid": [nb_rows, nb_cols]
+ };
+ iagent.socket.send(JSON.stringify(msg));
+ };}(i, agent));
+ }
+ }
+}
+
+
+function send_to_agents(msg) {
+ msg_queue.push(JSON.stringify(msg));
+ try_sending_to_agents();
+}
+
+
+function try_sending_to_agents() {
+ var all_connected = true;
+ for (var i=1; i<3; i++) {
+ if (agents[i].address !== undefined && agents[i].active == false) {
+ all_connected = false;
+ break;
+ }
+ }
+ if (!all_connected) {
+ // Wait until all are connected
+ setTimeout(try_sending_to_agents, 100);
+ } else {
+ if (msg_queue.length == 0 ) {
+ return;
+ }
+ var msg = msg_queue.shift();
+ console.log("Send msg to agents", msg);
+ for (var i=1; i<3; i++) {
+ if (agents[i].active == true) {
+ agents[i].socket.send(msg);
+ }
+ }
+ }
+}
+
+
+// STARTUP
+
+function restart() {
+ restart_game();
+ update_board();
+ start_connections();
+}
+
+var restartbtn = document.getElementById("restart-btn");
+restartbtn.onclick = function() {
+ console.log("Restart game");
+ restart();
+};
+
+restart();