Skip to content

Instantly share code, notes, and snippets.

@blueset
Created April 15, 2019 04:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save blueset/e7204df05461abdde7532de0888301db to your computer and use it in GitHub Desktop.
Save blueset/e7204df05461abdde7532de0888301db to your computer and use it in GitHub Desktop.
COMP30024 2019 S1 Project B: GUI Player (β)
  • A GUI player for COMP30024 2019 S1 Project B in UniMelb.
  • Written in Python.
  • Adapted from texgen.py by Matt Farrugia (matt.farrugia@unimelb.edu.au).
  • Written with Tkinter, require Tk support to run.

Structure

The same server serves as both a server and a client. Its behaviour depends on how it is started.

The server and client communicates via Python's built-in Simple XML RPC protocol.

Usage

  1. Start a server for each player: python3 tkinter_player.py (red|green|blue)
  2. Start the game using tkinter_player.py as the player: e.g. python3 -m referee your_team_name tkinter_player.py tkinter_player.py
  3. Play.

Things you might want to adjust

# IP address that the server is listening to
SERVER_LISTEN = "0.0.0.0"
# IP address that the client is connecting to
CLIENT_ACCESS = "127.0.0.1"
# Ports for each player
PORTS = {
    "red": 8901,
    "green": 8902,
    "blue": 8903
}

Attention.

Python's Simple XML RQC server is subjected to XML vulnerabilities. Please only run this script in a trusted environment.

Known issues

  • The game only refreshes when it has the focus, and GUI updates are blocking. If multiple GUI players are running on the same machine, each player should be focused at least once in the order of R, G, then B after each round for the game to continue.
  • Servers doesn't kill themselves immediately after the game ends. You might need to ^c manually to quit the server.

Demonstration

Demo video

import json
import tkinter
import threading
import atexit
import time
import sys
from xmlrpc.server import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler
from xmlrpc.client import ServerProxy
from typing import Tuple, Dict
"""
GUI player for COMP30024 AI 2019 Sem 1 Part B.
Adapted from `texgen.py` by Matt Farrugia <matt.farrugia@unimelb.edu.au>.
Modified by Jin Han <jinh4@student.unimelb.edu.au>.
"""
# window size (fixed, but you can adjust here)
H, W = 360, 640
SERVER_LISTEN = "0.0.0.0"
CLIENT_ACCESS = "127.0.0.1"
PORTS = {
"red": 8901,
"green": 8902,
"blue": 8903
}
# Restrict to a particular path.
class RequestHandler(SimpleXMLRPCRequestHandler):
rpc_paths = ('/RPC2',)
def rpc_server():
color = sys.argv[1]
root = tkinter.Tk()
root.title('Tkinter Player: ' + color)
root.wm_resizable(0, 0)
canvas = BoardViewCanvas(root, color)
root.after(500, root.focus_force())
port = PORTS.get(color, 8900)
svr = SimpleXMLRPCServer((SERVER_LISTEN, port), requestHandler=RequestHandler, allow_none=True)
svr.register_introspection_functions()
svr.register_function(canvas.activate, 'activate')
svr.register_function(canvas.update_status, 'update_status')
@svr.register_function
def get_last_move():
return json.dumps(canvas.last_move)
@svr.register_function
def render_board(board):
data = json.loads(board)
root.focus_force()
canvas.render_board(data)
canvas.update()
return 0
@svr.register_function
def destroy():
print("Killing the server as the game is ended.")
root.destroy()
svr.server_close()
svr.shutdown()
exit(1)
return 0
print(f"Running RPC server for player {color} on port {port}")
svr_t = threading.Thread(target=svr.serve_forever)
svr_t.start()
root.mainloop()
class Player:
def __init__(self, colour):
"""
This method is called once at the beginning of the game to initialise
your player. You should use this opportunity to set up your own internal
representation of the game state, and any other information about the
game state you would like to maintain for the duration of the game.
The parameter colour will be a string representing the player your
program will play as (Red, Green or Blue). The value will be one of the
strings "red", "green", or "blue" correspondingly.
"""
self.status = {
'red': {(-3, 3), (-3, 2), (-3, 1), (-3, 0)},
'green': {(0, -3), (1, -3), (2, -3), (3, -3)},
'blue': {(3, 0), (2, 1), (1, 2), (0, 3)},
}
self.rpc = ServerProxy(f'http://{CLIENT_ACCESS}:{PORTS.get(colour, 8900)}', allow_none=True)
# begin!
atexit.register(self.rpc.destroy)
self.render_board(self.status)
def tup(self, val):
if isinstance(val, list):
return tuple(map(self.tup, val))
return val
def action(self):
"""
This method is called at the beginning of each of your turns to request
a choice of action from your program.
Based on the current state of the game, your player should select and
return an allowed action to play on this turn. If there are no allowed
actions, your player must return a pass instead. The action (or pass)
must be represented based on the above instructions for representing
actions.
"""
self.rpc.activate()
move = json.loads(self.rpc.get_last_move())
while not move:
time.sleep(0.1)
move = json.loads(self.rpc.get_last_move())
return self.tup(move)
def update(self, color, action):
"""
This method is called at the end of every turn (including your player’s
turns) to inform your player about the most recent action. You should
use this opportunity to maintain your internal representation of the
game state and any other information about the game you are storing.
The parameter color will be a string representing the player whose turn
it is (Red, Green or Blue). The value will be one of the strings "red",
"green", or "blue" correspondingly.
The parameter action is a representation of the most recent action (or
pass) conforming to the above instructions for representing actions.
You may assume that action will always correspond to an allowed action
(or pass) for the player color (your method does not need to validate
the action/pass against the game rules).
"""
verb, args = action
if verb == "PASS":
desc = f"{color}: PASS."
elif verb == "EXIT":
self.status[color].remove(args)
desc = f"{color}: EXIT from {args}."
self.render_board(self.status)
elif verb in ("JUMP", "MOVE"):
orig, dest = args
self.status[color].remove(orig)
self.status[color].add(dest)
if verb == "JUMP":
mid = ((orig[0] + dest[0]) // 2, (orig[1]+dest[1]) // 2)
for ic in ("red", "green", "blue"):
if mid in self.status[ic]:
self.status[ic].remove(mid)
self.status[color].add(mid)
break
desc = f"{color}: {verb} from {orig} to {dest}."
self.render_board(self.status)
else:
desc = f"{color}: Unknown move: {action}."
self.rpc.update_status(desc)
def render_board(self, board):
json_str = json.dumps({i: list(board[i]) for i in board})
self.rpc.render_board(json_str)
class Color:
BG = "#663300"
FG = "#ffd9b3"
HL = "#ffe6cc"
LINES = "#663300"
STONE = "#fff2e6"
RED = "#cc3300"
RED_S = "#E57373"
GREEN = "#339966"
GREEN_S = "#9CCC65"
BLUE = "#003399"
BLUE_S = "#64B5F6"
BLACK = "#000000"
WHITE = "#ffffff"
NONE = ""
# hex shape flat
FH, FW = 0.866, 1
HEX_F = [(-FW / 2, 0), (-FW / 4, -FH / 2), (FW / 4, -FH / 2),
(FW / 2, 0), (FW / 4, FH / 2), (-FW / 4, FH / 2)]
# hex shape pointy
PH, PW = 1, 0.866
HEX_P = [(-PW / 2, -PH / 4), (0, -PH / 2), (PW / 2, -PH / 4),
(PW / 2, PH / 4), (0, PH / 2), (-PW / 2, PH / 4)]
class BoardViewCanvas(tkinter.Canvas):
def __init__(self, root, color):
bg = Color.BG
self.dests = {}
if color == "red":
self.dests = {(3, -3), (3, -2), (3, -1), (3, 0)}
bg = Color.RED
elif color == "green":
self.dests = {(-3, 3), (-2, 3), (-1, 3), (0, 3)}
bg = Color.GREEN
elif color == "blue":
self.dests = {(-3, 0), (-2, -1), (-1, -2), (0, -3)}
bg = Color.BLUE
super().__init__(root, width=W, height=H, background=bg,
highlightthickness=0)
self.root = root
self.color = color
self.status = ""
self.in_turn = False
self.last_move = None
self.evt = threading.Event()
self.active_piece = None
# set up state
self.initialise()
# # bind keypresses
self.bind("<Button-1>", self.clicked)
# pack into interface
self.pack()
# called in the beginning
def initialise(self):
# build game board
self.board = Board(self)
self.board.set_dests(self.color, self.dests)
self.button = PassButton(self, self.board, self.root)
self.label0 = self.create_text(
(5, 5), anchor="nw",
fill=Color.WHITE,
text=f"Starting game as player {self.color}")
self.label1 = self.create_text(
(W - 5, H - 5), anchor="se",
fill=Color.WHITE,
text="Stand-by."
)
def render_board(self, status):
"""Re-render the board"""
self.board.update_board(status)
def update_status(self, status):
self.status = (self.status + " " + status)[-100:]
self.itemconfig(self.label1, text=self.status)
def update_message(self, message):
self.itemconfig(self.label0, text=message)
return 0
def activate(self):
self.in_turn = True
self.last_move = None
self.active_piece = None
self.evt.clear()
self.update_message(f"{self.color}: Your turn now.")
return 0
def deactivate(self, move):
self.in_turn = False
if self.active_piece:
self.board.pieces[self.active_piece].deactivate()
self.active_piece = None
self.last_move = move
self.update_message(f"{self.color}: Stand-by.")
self.evt.set()
def clicked(self, event):
click = self.find_withtag(tkinter.CURRENT)
if len(click):
if not self.in_turn:
self.update_message("Not your turn now.")
return
(c_id,) = click
if c_id in self.board.hexid:
c_hex = self.board.hexid[c_id]
c_coord = (c_hex.q, c_hex.r)
print(f'clicked hex (q={c_hex.q}, r={c_hex.r}).')
if self.active_piece is None:
# Activate the current piece
c_piece = self.board.pieces[c_coord]
if c_piece.col != self.color[0].upper():
self.update_message(f"No. You are {self.color}, and this is {c_piece.col}.")
return
c_piece.activate()
if c_coord in self.dests:
self.button.toggle_exit(True)
else:
self.button.toggle_exit(False)
self.active_piece = c_coord
else:
c_piece = self.board.pieces[c_coord]
if c_coord == self.active_piece:
c_piece = self.board.pieces[c_coord]
c_piece.deactivate()
self.button.toggle_exit(False)
self.active_piece = None
elif c_coord in self.move_coords(self.active_piece):
if c_piece.col:
self.update_message(f"{c_coord} is occupied by {c_piece.col}.")
return
self.deactivate(("MOVE", (self.active_piece, c_coord)))
elif c_coord in self.jump_coords(self.active_piece):
if c_piece.col:
self.update_message(f"{c_coord} is occupied by {c_piece.col}.")
return
self.deactivate(("JUMP", (self.active_piece, c_coord)))
else:
self.update_message(f"No. You're at {self.active_piece}. {c_coord} is too far away.")
return
elif c_id == self.button.id:
print(f'clicked the {self.button.name} button!')
if self.button.is_exit:
self.deactivate(("EXIT", self.active_piece))
else:
self.deactivate(("SKIP", None))
return
print('clicked nothing.')
@staticmethod
def move_coords(c):
return {(c[0] + i[0], c[1] + i[1]) for i in ((0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1))}
@staticmethod
def jump_coords(c):
return {(c[0] + 2 * i[0], c[1] + 2 * i[1]) for i in ((0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1))}
class PassButton:
def __init__(self, canvas, board, root):
self.name = 'Pass'
self.root = root
self.board = board
self.is_exit = False
self.canvas = canvas
# render the button (a hexagon):
D = min(H, W)
coordinates = transform(HEX_F, (0.8 * W, 0.8 * H), (0.2 * D, 0.2 * D))
self.id = canvas.create_polygon(coordinates, fill=Color.FG)
self.labelid = canvas.create_text((0.8 * W, 0.8 * H), text="Pass",
fill=Color.BG, state=tkinter.DISABLED)
def toggle_exit(self, value):
self.is_exit = value
if value:
self.canvas.itemconfig(self.labelid, text="Exit")
else:
self.canvas.itemconfig(self.labelid, text="Pass")
def click(self):
if self.is_exit:
"Do exit code"
else:
self.canvas.deactivate(("PASS", None))
class Board:
def __init__(self, canvas):
# create board outline:
D = min(H, W)
board_coordinates = transform(HEX_F, (W / 2, H / 2), (0.9 * D, 0.9 * D))
self.id = canvas.create_polygon(board_coordinates, fill=Color.FG)
# create hexagons and piece shadows on board:
self.hexes = {} # type: Dict[Tuple[int, int], Hex]
self.hexid = {}
self.pieces = {} # type: Dict[Tuple[int, int], Piece]
size = 0.12 * D
for q in range(-3, 4):
for r in range(-3, 4):
s = -q - r
if s in range(-3, 4):
new_hex = Hex(q, r, size, canvas)
self.hexes[q, r] = new_hex
self.hexid[new_hex.id] = new_hex
new_piece = Piece(q, r, size, canvas)
self.pieces[q, r] = new_piece
def cycle(self, q, r):
view_piece = self.pieces[q, r]
view_piece.cycle()
def set_dests(self, color, dests):
c = Color.NONE
if color == 'red':
c = Color.RED_S
elif color == 'blue':
c = Color.BLUE_S
elif color == 'green':
c = Color.GREEN_S
for i in dests:
self.hexes[tuple(i)].set_color(c)
def update_board(self, board):
rev_board = {}
for i in board['red']:
rev_board[tuple(i)] = 'R'
for i in board['green']:
rev_board[tuple(i)] = 'G'
for i in board['blue']:
rev_board[tuple(i)] = 'B'
for q in range(-3, 4):
for r in range(-3, 4):
s = -q - r
if s in range(-3, 4):
col = rev_board.get((q, r), None)
self.pieces[q, r].paint(col=col)
def transform(coordinates, translate_xy=(0, 0), dilate_xy=(1, 1)):
a, b = translate_xy
A, B = dilate_xy
return [(a + x * A, b + y * B) for (x, y) in coordinates]
class Hex:
def __init__(self, q, r, d, canvas):
# set me up:
self.q = q
self.r = r
self.s = -q - r
self.canvas = canvas # type: BoardViewCanvas
# and draw me on the canvas:
# place in the center of the board
coords = transform(HEX_P, (W / 2, H / 2), (d, d))
# offset into position based on coordinates
coords = transform(coords, (r * PW / 2 * d + q * PW * d, r * 3 * PH / 4 * d))
# create a hexagon there!
self.id = canvas.create_polygon(coords, tag="hex",
outline=Color.BG, fill=Color.NONE, activefill=Color.HL)
# remember my coordinates too!?
self.coords = (W / 2 + r * PW / 2 * d + q * PW * d, H / 2 + r * 3 * PH / 4 * d)
def set_color(self, color):
self.canvas.itemconfig(self.id, fill=color)
class Piece:
def __init__(self, q, r, d, canvas):
self.canvas = canvas
self.col = None
# create generic stone, invisible, in position
x, y = (W / 2 + r * PW / 2 * d + q * PW * d, H / 2 + r * 3 * PH / 4 * d)
rad1 = 0.35 * d # outer circle (stone)
rad2 = 0.25 * d # inner circle (paint)
self.active = False
self.stone_id = canvas.create_oval(x - rad1, y - rad1, x + rad1, y + rad1,
tag="stone", outline=Color.BLACK, fill=Color.STONE,
state=tkinter.HIDDEN)
self.paint_id = canvas.create_oval(x - rad2, y - rad2, x + rad2, y + rad2,
tag="paint", outline=Color.NONE, fill=Color.BLACK,
state=tkinter.HIDDEN)
self.letter_id = canvas.create_text(x, y,
tag="letter", text="?", fill=Color.STONE,
state=tkinter.HIDDEN)
def paint(self, col=None):
self.col = col
if col is None:
self.canvas.itemconfig(self.letter_id, text="?")
self.canvas.itemconfig(self.paint_id, fill=Color.BLACK)
self.hide()
else:
letter = None
colour = None
if col == 'R':
colour = Color.RED
letter = 'R'
elif col == 'G':
colour = Color.GREEN
letter = 'G'
elif col == 'B':
colour = Color.BLUE
letter = 'B'
elif col == 'BLOCK':
colour = Color.BLACK
letter = ''
self.canvas.itemconfig(self.letter_id, text=letter)
self.canvas.itemconfig(self.paint_id, fill=colour)
self.show()
def activate(self):
self.active = True
if self.col == 'R':
colour = Color.BLACK
elif self.col == 'G':
colour = Color.BLACK
elif self.col == 'B':
colour = Color.BLACK
else:
colour = None
self.canvas.itemconfig(self.paint_id, fill=colour)
def deactivate(self):
self.active = False
if self.col == 'R':
colour = Color.RED
elif self.col == 'G':
colour = Color.GREEN
elif self.col == 'B':
colour = Color.BLUE
else:
colour = None
self.canvas.itemconfig(self.paint_id, fill=colour)
def cycle(self):
if self.col == 'R':
self.paint('G')
elif self.col == 'G':
self.paint('B')
elif self.col == 'B':
self.paint('BLOCK')
elif self.col == 'BLOCK':
self.paint(None)
elif self.col is None:
self.paint('R')
def hide(self):
self.canvas.itemconfig(self.letter_id, state=tkinter.HIDDEN)
self.canvas.itemconfig(self.paint_id, state=tkinter.HIDDEN)
self.canvas.itemconfig(self.stone_id, state=tkinter.HIDDEN)
def show(self):
self.canvas.itemconfig(self.letter_id, state=tkinter.DISABLED)
self.canvas.itemconfig(self.paint_id, state=tkinter.DISABLED)
self.canvas.itemconfig(self.stone_id, state=tkinter.DISABLED)
if __name__ == '__main__':
rpc_server()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment