summaryrefslogtreecommitdiff
path: root/asciifarm/client
diff options
context:
space:
mode:
Diffstat (limited to 'asciifarm/client')
-rw-r--r--asciifarm/client/__init__.py40
-rw-r--r--asciifarm/client/client.py107
-rw-r--r--asciifarm/client/connection.py27
-rw-r--r--asciifarm/client/display/__init__.py59
-rw-r--r--asciifarm/client/display/fieldpad.py44
-rw-r--r--asciifarm/client/display/healthpad.py25
-rw-r--r--asciifarm/client/display/infopad.py25
-rw-r--r--asciifarm/client/display/screen.py44
-rw-r--r--asciifarm/client/fullwidth.json31
-rw-r--r--asciifarm/client/tcommunicate.py32
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)
+