diff options
Diffstat (limited to 'asciifarm/client')
| -rw-r--r-- | asciifarm/client/__init__.py | 40 | ||||
| -rw-r--r-- | asciifarm/client/client.py | 107 | ||||
| -rw-r--r-- | asciifarm/client/connection.py | 27 | ||||
| -rw-r--r-- | asciifarm/client/display/__init__.py | 59 | ||||
| -rw-r--r-- | asciifarm/client/display/fieldpad.py | 44 | ||||
| -rw-r--r-- | asciifarm/client/display/healthpad.py | 25 | ||||
| -rw-r--r-- | asciifarm/client/display/infopad.py | 25 | ||||
| -rw-r--r-- | asciifarm/client/display/screen.py | 44 | ||||
| -rw-r--r-- | asciifarm/client/fullwidth.json | 31 | ||||
| -rw-r--r-- | asciifarm/client/tcommunicate.py | 32 |
10 files changed, 434 insertions, 0 deletions
diff --git a/asciifarm/client/__init__.py b/asciifarm/client/__init__.py new file mode 100644 index 0000000..5047151 --- /dev/null +++ b/asciifarm/client/__init__.py @@ -0,0 +1,40 @@ + +import curses +import json +import os +import getpass +import sys +from .connection import Connection +from .client import Client +from .display import Display + +defaultAdresses = { + "abstract": "asciifarm", + "unix": "asciifarm.socket", + "inet": "localhost:9021", + } + +def main(name, socketType, address, keybindings, characters, colours=False): + + connection = Connection(socketType) + try: + connection.connect(address) + except ConnectionRefusedError: + print("ERROR: Could not connect to server.\nAre you sure that the server is running and that you're connecting to the right address?", file=sys.stderr) + return + + caught_ctrl_c = False + + def start(stdscr): + display = Display(stdscr, characters, colours) + client = Client(stdscr, display, name, connection, keybindings) + nonlocal caught_ctrl_c + try: + client.start() + except KeyboardInterrupt: + caught_ctrl_c = True + + curses.wrapper(start) + + if caught_ctrl_c: + print('^C caught, goodbye!') diff --git a/asciifarm/client/client.py b/asciifarm/client/client.py new file mode 100644 index 0000000..feb7057 --- /dev/null +++ b/asciifarm/client/client.py @@ -0,0 +1,107 @@ +#! /usr/bin/python3 + +import os +import sys + +import curses +import threading +#import logging +import json +import getpass +import argparse +from .display.screen import Screen +import string +from .display import Display + + +#logging.basicConfig(filename="client.log", filemode='w', level=logging.DEBUG) + + +class Client: + + def __init__(self, stdscr, display, name, connection, keybindings): + self.stdscr = stdscr + self.display = display + self.name = name + self.keepalive = True + self.connection = connection + + self.commands = {ord(key): command for key, command in keybindings['input'].items()} + + self.controlsString = "Controls:\n"+'\n'.join( + chr(key) + ": " + ' '.join(action) + for key, action in self.commands.items() + if chr(key) in string.printable) + + self.info = {} + + + def start(self): + threading.Thread(target=self.listen, daemon=True).start() + self.connection.send(json.dumps(["name", self.name])) + self.command_loop() + + + def listen(self): + self.connection.listen(self.update, self.close) + + def close(self, err=None): + self.keepalive = False + sys.exit() + + + def update(self, databytes): + if not self.keepalive: + sys.exit() + datastr = databytes.decode('utf-8') + data = json.loads(datastr) + if len(data) and isinstance(data[0], str): + data = [data] + for msg in data: + msgType = msg[0] + if msgType == 'error': + error = msg[1] + if error == "nametaken": + print("error: name is already taken", file=sys.stderr) + self.close() + if msgType == 'field': + field = msg[1] + fieldWidth = field['width'] + fieldHeight = field['height'] + fieldCells = field['field'] + mapping = field['mapping'] + self.display.drawFieldCells( + (tuple(reversed(divmod(i, fieldWidth))), + mapping[spr]) + for i, spr in enumerate(fieldCells)) + + if msgType == 'changecells'and len(msg[1]): + self.display.drawFieldCells(msg[1]) + + if msgType == "playerpos": + self.display.setFieldCenter(msg[1]) + + if msgType == "health": + self.display.showHealth(*msg[1]) + if msgType == "inventory": + self.info["inventory"] = msg[1] + if msgType == "ground": + self.info["ground"] = msg[1] + + infostring = json.dumps(self.info, indent=2) + infostring += "\n\n" + self.controlsString + self.display.showInfo(infostring) + + self.display.update() + + def command_loop(self): + while self.keepalive: + key = self.stdscr.getch() + if key == 27: + self.keepalive = False + if key in self.commands: + self.connection.send(json.dumps(["input", self.commands[key]])) + + + + diff --git a/asciifarm/client/connection.py b/asciifarm/client/connection.py new file mode 100644 index 0000000..9ca2ccc --- /dev/null +++ b/asciifarm/client/connection.py @@ -0,0 +1,27 @@ + +import socket + +from .tcommunicate import send, receive + +class Connection: + + def __init__(self, socketType): + if socketType == "abstract" or socketType == "unix": + sockType = socket.AF_UNIX + elif socketType == "inet": + sockType = socket.AF_INET + self.sock = socket.socket(sockType, socket.SOCK_STREAM) + + def connect(self, address): + self.sock.connect(address) + + def listen(self, callback, onError): + while True: + try: + data = receive(self.sock) + except Exception as err: + onError(err) + callback(data) + + def send(self, message): + send(self.sock, bytes(message, 'utf-8')) diff --git a/asciifarm/client/display/__init__.py b/asciifarm/client/display/__init__.py new file mode 100644 index 0000000..41383d6 --- /dev/null +++ b/asciifarm/client/display/__init__.py @@ -0,0 +1,59 @@ + + +import curses +from .fieldpad import FieldPad +from .infopad import InfoPad +from .healthpad import HealthPad +from .screen import Screen + +class Display: + + def __init__(self, stdscr, charMap, colours=False): + + self.screen = Screen(stdscr) + self.fieldPad = FieldPad((64, 32), charMap.get("charwidth", 1), colours) + self.characters = charMap["mapping"] + self.defaultChar = charMap.get("default", "?") + self.infoPad = InfoPad((100, 100)) + self.healthPad = HealthPad((20, 1)) + self.lastinfostring = None + self.colours = colours + if colours: + curses.use_default_colors() + for i in range(0, min(256, curses.COLORS, curses.COLOR_PAIRS)): + curses.init_pair(i, i%16, i//16) + + + def resizeField(self, size): + self.fieldPad.resize(*size) + + def drawFieldCells(self, cells): + for cell in cells: + (x, y), spriteName = cell + sprite = self.getChar(spriteName) + self.fieldPad.changeCell(x, y, *sprite) + self.screen.change() + + def setFieldCenter(self, pos): + self.fieldPad.setCenter(pos) + + def showHealth(self, health, maxHealth): + self.healthPad.setHealth(health, maxHealth) + self.screen.change() + + def showInfo(self, infostring): + if infostring != self.lastinfostring: + self.infoPad.showString(infostring) + self.screen.change() + self.lastinfostring = infostring + + def getChar(self, sprite): + char = self.characters.get(sprite, self.defaultChar) + if isinstance(char, str): + return [char] + return char + + def update(self): + self.screen.update(self.fieldPad, self.infoPad, self.healthPad) + + diff --git a/asciifarm/client/display/fieldpad.py b/asciifarm/client/display/fieldpad.py new file mode 100644 index 0000000..ac69678 --- /dev/null +++ b/asciifarm/client/display/fieldpad.py @@ -0,0 +1,44 @@ + +import curses + + +class FieldPad: + + + + def __init__(self, size=(1,1), charSize=1, colours=False): + self.pad = curses.newpad(size[1]+1, (size[0]+1)*charSize) + self.size = size + self.charSize = charSize + self.center = (0, 0) + self.colours = colours + + def resize(self, width, height): + self.size = (width, height) + self.pad.resize(height+1, width*self.charSize1) + + def changeCell(self, x, y, char, colour=None): + if colour != None and self.colours: + self.pad.addstr(y, x*self.charSize, char, curses.color_pair(colour)) + else: + self.pad.addstr(y, x*self.charSize, char) + + def setCenter(self, pos): + self.center = pos + + def getWidth(self): + return self.size[0]*self.charSize + + def getHeight(self): + return self.size[1] + + def update(self, screen, x, y, xmax, ymax): + width = xmax-x + height = ymax-y + self.pad.noutrefresh( + max(0, min(self.getHeight()-height, self.center[1] - int(height/2))), + max(0, min(self.getWidth()-width, self.center[0]*self.charSize - int(width/2))), + y, + x, + ymax-1, + xmax-1) diff --git a/asciifarm/client/display/healthpad.py b/asciifarm/client/display/healthpad.py new file mode 100644 index 0000000..17baf35 --- /dev/null +++ b/asciifarm/client/display/healthpad.py @@ -0,0 +1,25 @@ + +import curses + + + +class HealthPad: + + + + def __init__(self, size=(1,1), *args): + self.pad = curses.newpad(size[1], size[0]) + self.size = size + + def setHealth(self, health, maxHealth): + self.pad.erase() + self.pad.addstr(0,0,"Health: {}/{}".format(health, maxHealth)) + + def update(self, screen, x, y, xmax, ymax): + self.pad.noutrefresh( + 0, + 0, + y, + x, + ymax-1, + xmax-1) diff --git a/asciifarm/client/display/infopad.py b/asciifarm/client/display/infopad.py new file mode 100644 index 0000000..6fd8655 --- /dev/null +++ b/asciifarm/client/display/infopad.py @@ -0,0 +1,25 @@ + +import curses + + + +class InfoPad: + + + + def __init__(self, size=(1,1), *args): + self.pad = curses.newpad(size[1], size[0]) + self.size = size + + def showString(self, string): + self.pad.clear() + self.pad.addstr(0,0,string) + + def update(self, screen, x, y, xmax, ymax): + self.pad.noutrefresh( + 0, + 0, + y, + x, + ymax-1, + xmax-1) diff --git a/asciifarm/client/display/screen.py b/asciifarm/client/display/screen.py new file mode 100644 index 0000000..ec49d34 --- /dev/null +++ b/asciifarm/client/display/screen.py @@ -0,0 +1,44 @@ + +import curses +from .fieldpad import FieldPad + +import signal + +SIDEWIDTH = 20 +HEALTHHEIGHT = 1 + +class Screen: + + + def __init__(self, stdscr, maxSize=(float("inf"),float("inf")), charSize=1): + curses.curs_set(0) + self.stdscr = stdscr + self.height, self.width = self.stdscr.getmaxyx() + self.changed = False + signal.signal(signal.SIGWINCH, self.updateSize) + + def updateSize(self, *args): + curses.endwin() + curses.initscr() + self.height, self.width = self.stdscr.getmaxyx() + self.stdscr.clear() + self.change() + + def getWidth(self): + return self.width + + def getHeight(self): + return self.height + + def change(self): + self.changed = True + + def update(self, fieldPad, infoPad, healthPad): + if self.changed: + fieldEnd = min(fieldPad.getWidth(), self.getWidth()-SIDEWIDTH-1) + fieldPad.update(self, 0,0,fieldEnd, min(fieldPad.getHeight(), self.getHeight())) + healthPad.update(self, fieldEnd+1,0, self.getWidth(), HEALTHHEIGHT) + infoPad.update(self, fieldEnd+1,HEALTHHEIGHT, self.getWidth(), self.getHeight()) + curses.doupdate() + self.changed = False + diff --git a/asciifarm/client/fullwidth.json b/asciifarm/client/fullwidth.json new file mode 100644 index 0000000..dc1cec8 --- /dev/null +++ b/asciifarm/client/fullwidth.json @@ -0,0 +1,31 @@ +{ + "mapping":{ + "tree": "T", + "wall": "#", + "rock": "X", + "stone": "o", + "pebble": "*", + "player": "@", + "ground": ".", + "grass1": ",", + "grass2": "'", + "grass3": "`", + "rabbit": "r", + "water": "~", + "floor": "+", + "portal": "$", + "stairdown": ">", + "stairup": "<", + "dummy": "d", + "spikes": "^", + "goblin": "g", + "seed": ":", + "plant": "Y", + "youngplant": "v", + "food": "8", + "troll": "T", + " ": " " + }, + "default": "?", + "charwidth": 2 +} diff --git a/asciifarm/client/tcommunicate.py b/asciifarm/client/tcommunicate.py new file mode 100644 index 0000000..b1fc1b0 --- /dev/null +++ b/asciifarm/client/tcommunicate.py @@ -0,0 +1,32 @@ + +HEADER_SIZE = 4 + + +# this module is for sending discree messages over TCP +# this is achieved by prefixing all messages with their length +# calls to send and recv will also keep attempting to send all data unless this proves impossible + + +def send(sock, msg): + length = len(msg) + header = length.to_bytes(4, byteorder="big") + totalmsg = header + msg + sock.sendall(totalmsg) + +def receive(sock): + header = recvall(sock, 4) #sock.recv(4) + length = int.from_bytes(header, byteorder="big") + return recvall(sock, length) + +def recvall(sock, length): + chunks = [] + bytes_recd = 0 + while bytes_recd < length: + chunk = sock.recv(min(length - bytes_recd, 4096)) + if chunk == b'': + break + #raise RuntimeError("socket connection broken") + chunks.append(chunk) + bytes_recd = bytes_recd + len(chunk) + return b''.join(chunks) + |
