diff options
author | Matt Strapp <strap012@umn.edu> | 2021-04-26 10:53:43 -0500 |
---|---|---|
committer | Matt Strapp <strap012@umn.edu> | 2021-04-26 15:03:12 -0500 |
commit | d311af01feb32550aaae8638d4cc167948f5464c (patch) | |
tree | 3c0b8606a7a5267e3e890a63b8565c5c27f10438 /python/dotsandboxes | |
parent | actually add files (diff) | |
download | csci4511w-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.md | 134 | ||||
-rwxr-xr-x | python/dotsandboxes/dotsandboxesagent | 5 | ||||
-rw-r--r-- | python/dotsandboxes/dotsandboxesagent.py | 213 | ||||
-rw-r--r-- | python/dotsandboxes/dotsandboxescompete.py | 212 | ||||
-rw-r--r-- | python/dotsandboxes/dotsandboxesserver.py | 60 | ||||
-rw-r--r-- | python/dotsandboxes/requirements.txt | 1 | ||||
-rw-r--r-- | python/dotsandboxes/static/dotsandboxes.css | 10 | ||||
-rw-r--r-- | python/dotsandboxes/static/dotsandboxes.html | 56 | ||||
-rw-r--r-- | python/dotsandboxes/static/dotsandboxes.js | 454 |
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>© <a href="https://dtai.cs.kuleuven.be">DTAI Research Group</a>, KU Leuven — <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(); |