Merge pull request #21 from jmdejong/ratuil

Use ratuil instead of my old hardcoded ncurses setup
This commit is contained in:
jmdejong 2019-09-20 10:25:47 +02:00 committed by GitHub
commit 10bdb33255
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 343 additions and 805 deletions

6
.editorconfig Normal file
View File

@ -0,0 +1,6 @@
[*.{py,json,hy,yaml}]
charset = utf-8
indent_style = spaces
insert_final_newline = true
indent_size = 4

3
.kateconfig Normal file
View File

@ -0,0 +1,3 @@
kate: indent-pasted-text false; indent-width 4; space-indent true;

View File

@ -95,40 +95,40 @@ class CommandHandler:
self.client.inputHandler.startTyping(startText) self.client.inputHandler.startTyping(startText)
def selectWidget(self, value, relative=False, modular=False): def selectWidget(self, value, relative=False, modular=False):
self.client.display.getWidget("switch").select(value, relative, modular) self.client.display.selectMenu(value, relative, modular)
def selectItem(self, value, relative=False, modular=False): def selectItem(self, value, relative=False, modular=False):
self.client.display.getWidget("switch").getSelectedItem().getImpl().select(value, relative, modular) self.client.display.selectItem(None, value, relative, modular)
def actWithSelected(self, action, widget): def actWithSelected(self, action, menu):
self.input([action, self.client.display.getWidget(widget).getSelected()]) self.input([action, self.client.display.getSelectedItem(menu).getSelected()])
def useSelected(self): def useSelected(self):
widget = self.client.display.getWidget("switch").getSelectedItem() menu = self.client.display.getSelectedMenu()
selected = widget.getImpl().getSelected() selected = self.client.display.getSelectedItem(menu)
if widget.name in ("inventory", "equipment"): if menu in ("inventory", "equipment"):
action = "use" action = "use"
elif widget.name == "ground": elif menu == "ground":
action = "interact", action = "interact",
else: else:
return return
self.input([action, selected]) self.input([action, selected])
def unUseSelected(self): def unUseSelected(self):
widget = self.client.display.getWidget("switch").getSelectedItem() menu = self.client.display.getSelectedMenu()
selected = widget.getImpl().getSelected() selected = self.client.display.getSelectedItem(menu)
if widget.name == "inventory": if menu == "inventory":
action = "drop" action = "drop"
elif widget.name == "equipment": elif menu == "equipment":
action = "unequip" action = "unequip"
else: else:
return return
self.input([action, selected]) self.input([action, selected])
def takeSelected(self): def takeSelected(self):
widget = self.client.display.getWidget("switch").getSelectedItem() menu = self.client.display.getSelectedMenu()
selected = widget.getImpl().getSelected() selected = self.client.display.getSelectedItem(menu)
if widget.name == "ground": if menu == "ground":
action = "take" action = "take"
else: else:
return return

174
asciifarm/client/display.py Normal file
View File

@ -0,0 +1,174 @@
import os
from ratuil.layout import Layout
from ratuil.bufferedscreen import BufferedScreen as Screen
#from ratuil.screen import Screen
from ratuil.textstyle import TextStyle
from asciifarm.common.utils import get
from .listselector import ListSelector
SIDEWIDTH = 20
ALPHABET = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
class Display:
def __init__(self, charMap):
self.characters = {}
def parseSprite(sprite):
if isinstance(sprite, str):
return (sprite, None, None)
char = get(sprite, 0, " ")
fg = get(sprite, 1)
bg = get(sprite, 2)
return (char, fg, bg)
for name, sprite in charMap["mapping"].items():
vals = parseSprite(sprite)
if vals:
self.characters[name] = vals
for name, colours in charMap.get("writable", {}).items():
fg = get(colours, 0)
bg = get(colours, 1)
for i in range(min(len(ALPHABET), len(charMap.get("alphabet", [])))):
self.characters[name + '-' + ALPHABET[i]] = (charMap["alphabet"][i], fg, bg)
self.defaultChar = parseSprite(charMap.get("default", "?"))
self.messageColours = charMap.get("msgcolours", {})
fname = os.path.join(os.path.dirname(__file__), "layout.xml")
self.layout = Layout.from_xml_file(fname)
self.layout.get("field").set_char_size(charMap.get("charwidth", 1))
self.screen = Screen()
self.screen.clear()
self.layout.set_target(self.screen)
self.layout.update()
# temporary, until these have a better place
self.inventory = ListSelector(self.getWidget("inventory"))
self.inventory._debug_name = "inventory"
self.equipment = ListSelector(self.getWidget("equipment"))
self.equipment._debug_name = "equipment"
self.ground = ListSelector(self.getWidget("ground"))
self.ground._debug_name = "ground"
self.switch = ListSelector(self.getWidget("switchtitles"))
self.switch._debug_name = "switch"
self.switch.setItems(["inventory", "equipment", "ground"])
self.menus = {
"inventory": self.inventory,
"equipment": self.equipment,
"ground": self.ground
}
self.layout.get("switch").select(0)
def getWidget(self, name):
return self.layout.get(name)
def resizeField(self, size):
self.getWidget("field").set_size(*size)
def drawFieldCells(self, cells):
field = self.getWidget("field")
for cell in cells:
(x, y), spriteNames = cell
if not len(spriteNames):
char, fg, bg = self.getChar(' ')
else:
char, fg, bg = self.getChar(spriteNames[0])
for spriteName in spriteNames[1:]:
if bg is not None:
break
_char, _fg, bg = self.getChar(spriteName)
field.change_cell(x, y, char, TextStyle(fg, bg))
def setFieldCenter(self, pos):
self.getWidget("field").set_center(*pos)
def setHealth(self, health, maxHealth):
if health is None:
health = 0
if maxHealth is None:
maxHealth = 0
self.getWidget("health").set_total(maxHealth)
self.getWidget("health").set_filled(health)
def showInfo(self, infostring):
self.getWidget("info").set_text(infostring)
def selectMenu(self, *args, **kwargs):
self.switch.select(*args, **kwargs)
self.layout.get("switch").select(self.getSelectedMenu())
def getSelectedMenu(self):
return self.switch.getSelectedItem()
def getSelectedItem(self, menu=None):
return self._getMenu(menu).getSelected()
def selectItem(self, menu=None, *args, **kwargs):
self._getMenu(menu).select(*args, **kwargs)
def _getMenu(self, name=None):
if name is None:
name = self.getSelectedMenu()
name = name.casefold()
return self.menus[name]
def setInventory(self, items):
self.inventory.setItems(items)
def setEquipment(self, slots):
self.equipment.setItems(
sorted([
slot + ": " + (item if item else "")
for slot, item in slots.items()
])
)
def setGround(self, items):
self.ground.setItems(items)
def addMessage(self, message, msgtype=None):
if msgtype is not None:
style = TextStyle(*self.messageColours.get(msgtype, (7,0)))
else:
style = None
self.getWidget("msg").add_message(message, style)
def log(self, message):
self.addMessage(str(message))
def scrollBack(self, amount, relative=True):
self.getWidget("msg").scroll(amount, relative)
def setInputString(self, string, cursor):
self.getWidget("textinput").set_text(string, cursor)
def update(self):
self.layout.update()
self.screen.update()
def getChar(self, sprite):
"""This returns the character belonging to some spritename. This does not read a character"""
return self.characters.get(sprite, self.defaultChar)
def update_size(self):
self.screen.reset()

View File

@ -1,32 +0,0 @@
import curses
class Colours:
def __init__(self):
self.colours = min(curses.COLORS, 16)
self.pairs = self.colours*self.colours
curses.use_default_colors()
for i in range(0, self.pairs):
curses.init_pair(i, i%self.colours, i//self.colours)
def get(self, fg=0, bg=0):
if self.colours == 16:
return curses.color_pair(fg + bg*self.colours)
elif self.colours == 8:
dfg = fg % 8
dbg = bg % 8
if bg == 8:
dbg = 7
if fg == 8:
dfg = 7
colour = curses.color_pair(dfg + dbg*self.colours)
if fg >= 8 and bg < 8:
colour |= curses.A_BOLD
elif fg < 8 and bg >= 8:
colour |= curses.A_DIM
return colour
else:
return curses.color_pair(0)

View File

@ -1,154 +0,0 @@
import curses
from .field import Field
from .info import Info
from .health import Health
from .inventory import Inventory
from .screen import Screen
from .colours import Colours
from .messages import Messages
from .switcher import Switcher
from .textinput import TextInput
from .widget import Widget
from asciifarm.common.utils import get
SIDEWIDTH = 20
ALPHABET = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
class Display:
def __init__(self, stdscr, charMap, colours=False):
if colours and curses.has_colors and curses.COLORS > 1:
self.colours = Colours()
else:
self.colours = None
self.characters = {}
def parseSprite(sprite):
if isinstance(sprite, str):
return (sprite, None, None)
char = get(sprite, 0, " ")
fg = get(sprite, 1)
bg = get(sprite, 2)
return (char, fg, bg)
for name, sprite in charMap["mapping"].items():
vals = parseSprite(sprite)
if vals:
self.characters[name] = vals
for name, colours in charMap.get("writable", {}).items():
fg = get(colours, 0)
bg = get(colours, 1)
for i in range(min(len(ALPHABET), len(charMap.get("alphabet", [])))):
self.characters[name + '-' + ALPHABET[i]] = (charMap["alphabet"][i], fg, bg)
self.defaultChar = parseSprite(charMap.get("default", "?"))
screen = Screen(self, stdscr, self.colours)
self.screen = screen
self.widgets = {}
self.addWidget(Field((1, 1), charMap.get("charwidth", 1), self.colours), "field")
self.addWidget(Info(), "info")
self.addWidget(Health(
charMap.get("healthfull", ("@",7, 2)),
charMap.get("healthempty", ("-",7, 1))
),
"health")
self.addWidget(Inventory("Inventory"), "inventory")
self.addWidget(Inventory("Ground"), "ground")
self.addWidget(Inventory("Equipment"), "equipment")
switcher = Switcher([self.widgets["ground"], self.widgets["inventory"], self.widgets["equipment"]], 1)
self.addWidget(switcher, "switch")
self.addWidget(Messages(charMap.get("msgcolours", {})), "msg")
self.addWidget(TextInput(), "textinput")
self.forced = False
def addWidget(self, w, name, winname=None):
if not winname:
winname = name
widget = Widget(w, name)
self.widgets[name] = widget
widget.setWin(winname, self.screen)
def getWidget(self, name):
if name in self.widgets:
return self.widgets[name].getImpl()
else:
return None
def resizeField(self, size):
self.getWidget("field").resize(*size)
self.forced = True
def drawFieldCells(self, cells):
field = self.getWidget("field")
for cell in cells:
(x, y), spriteNames = cell
sprites = [self.getChar(spriteName) for spriteName in spriteNames]
if not len(sprites):
sprites = [self.getChar(" ")]
field.changeCell(x, y, sprites)
def setFieldCenter(self, pos):
self.getWidget("field").setCenter(pos)
def setHealth(self, health, maxHealth):
self.getWidget("health").setHealth(health, maxHealth)
def showInfo(self, infostring):
self.getWidget("info").showString(infostring)
def setInventory(self, items):
self.getWidget("inventory").setInventory(items)
def setEquipment(self, slots):
self.getWidget("equipment").setInventory(
sorted([
slot + ": " + (item if item else "")
for slot, item in slots.items()
])
)
def setGround(self, items):
self.getWidget("ground").setInventory(items)
def addMessage(self, message, type):
self.getWidget("msg").addMessage(message, type)
def scrollBack(self, amount, relative=True):
self.getWidget("msg").scroll(amount, relative)
def getChar(self, sprite):
"""This returns the character belonging to some spritename. This does not read a character"""
return self.characters.get(sprite, self.defaultChar)
def setInputString(self, string, cursor):
self.getWidget("textinput").setText(string, cursor)
def update(self):
changed = False
for widget in self.widgets.values():
if self.forced or widget.isChanged():
widget.update()
changed = True
if changed:
self.screen.update()
self.forced = False
def forceUpdate(self):
self.forced = True

View File

@ -1,70 +0,0 @@
import curses
from .widimp import WidImp
class Field(WidImp):
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
self.changed = False
self.redraw = False
def resize(self, width, height):
self.size = (width, height)
self.pad.resize(height+1, width*self.charSize)
self.redraw = True
self.change()
def changeCell(self, x, y, sprites):
""" sprites must always have at least one element """
char, colour, bgcolour = sprites[0]
if bgcolour is None:
for (ch, co, bg) in sprites:
if bg is not None:
bgcolour = bg
break
else:
bgcolour = 0
if colour is not None and self.colours:
self.pad.addstr(y, x*self.charSize, " "*self.charSize, self.colours.get(7, 0))
self.pad.addstr(y, x*self.charSize, char, self.colours.get(colour, bgcolour))
else:
self.pad.addstr(y, x*self.charSize, char)
self.change()
def setCenter(self, pos):
self.center = pos
self.change()
def getWidth(self):
return self.size[0]*self.charSize
def getHeight(self):
return self.size[1]
def _roundWidth(self, x):
return x // self.charSize * self.charSize
def update(self, win):
if self.redraw:
win.erase()
win.noutrefresh()
self.redraw = False
width, height = win.getSize()
x, y = win.getPos()
xmax = x + width
ymax = y + height
self.pad.noutrefresh(
max(0, min(self.getHeight()-height, self.center[1] - int(height/2))),
max(0, min(
self._roundWidth(self.getWidth()-width),
self._roundWidth(self.center[0]*self.charSize - int(width/2)))),
y,
x + max(0, (width - self.getWidth()) // 2),
ymax,
xmax)

View File

@ -1,27 +0,0 @@
from .widimp import WidImp
class Health(WidImp):
def __init__(self, char=None, emptyChar=None):
self.char = char or ('@',7,0)
self.emptyChar = emptyChar or ('-',7,0)
self.changed = False
self.health = 0
self.maxHealth = 0
def setHealth(self, health, maxHealth):
self.health = health or 0
self.maxHealth = maxHealth or 0
self.change()
def update(self, win):
width, height = win.getSize()
width -= 1
barEnd = round(self.health/self.maxHealth * width) if self.maxHealth > 0 else 0
win.erase()
win.addLine((0,0),"Health: {}/{}".format(self.health, self.maxHealth)[:width])
win.addLine((0, 1), self.char[0]*barEnd, self.char[1:])
win.addLine((barEnd, 1), self.emptyChar[0]*(width-barEnd), self.emptyChar[1:])
win.noutrefresh()

View File

@ -1,24 +0,0 @@
from .widimp import WidImp
class Info(WidImp):
def __init__(self):
self.changed = False
self.lines = []
self.lastString = None
def showString(self, string):
if string == self.lastString:
return
self.lines = string.split('\n')
self.change()
self.lastString = string
def update(self, win):
width, height = win.getSize()
lines = [line[:width-1] for line in self.lines][:height]
win.erase()
for i, line in enumerate(lines):
win.addLine((0, i), line)
win.noutrefresh()

View File

@ -1,73 +0,0 @@
from asciifarm.common import utils
from .widimp import WidImp
class Inventory(WidImp):
def __init__(self, title, titlebar="{}:", selectorChar="*"):
self.title = title
self.titlebar = titlebar
self.selectorChar = selectorChar
self.items = []
self.selector = 0
def getSelected(self):
return self.selector
def select(self, value, relative=False, modular=False):
invLen = len(self.items)
if relative:
value += self.selector
if modular and invLen:
value %= invLen
if value < 0:
value = 0
if value >= invLen:
value = invLen-1
if value in range(invLen):
self.doSelect(value)
def doSelect(self, value):
self.selector = value
self.change()
def setInventory(self, items):
self.items = items
self.selector = utils.clamp(self.selector, 0, len(items)-1)
self.change()
def getItem(self, num):
return self.items[num]
def getSelectedItem(self):
return self.getItem(self.getSelected())
def setTitle(self, title):
self.title = title
def getNumItems(self):
return len(self.items)
def itemName(self, item):
return item
def update(self, win):
width, height = win.getSize()
height -= 1
selected = self.selector
start = min(selected - height//2, len(self.items)-height)
start = max(start, 0)
end = start + height
win.erase()
win.addLine((0,0), (self.titlebar.format(self.title))[:width])
for i, item in enumerate(self.items[start:end]):
if i + start == selected:
win.addLine((0, i+1), self.selectorChar)
win.addLine((1, i+1), self.itemName(item))
if end < len(self.items):
win.addLine((width-1, height), "+")
if start > 0:
win.addLine((width-1, 1), "-")
win.noutrefresh()

View File

@ -1,56 +0,0 @@
import textwrap
from .widimp import WidImp
class Messages(WidImp):
def __init__(self, colours):
self.changed = False
self.messages = []
self.scrolledBack = 0
self.colours = colours
def addMessage(self, message, type=None):
self.messages.append([message, type])
if self.scrolledBack:
self.scrolledBack += 1
self.change()
def scroll(self, amount, relative=True):
if relative:
self.scrolledBack += amount
else:
self.scrolledBack = amount
self.scrolledBack = max(self.scrolledBack, 0)
self.change()
def update(self, win):
width, height = win.getSize()
if height < 1:
return
lines = []
messages = self.messages
for message, type in messages:
colour = self.colours.get(type, (7,0))
for line in textwrap.wrap(message, width):
lines.append((line, colour))
self.scrolledBack = max(min(self.scrolledBack, len(lines)-height), 0)
moreDown = False
if self.scrolledBack > 0:
lines = lines[:-self.scrolledBack]
moreDown = True
moreUp = False
if len(lines) > height:
moreUp = True
lines = lines[len(lines)-height:]
elif len(lines) < height:
lines = (height-len(lines)) * [("",)] + lines
win.erase()
for i, line in enumerate(lines):
win.addLine((0,i), *line)
if moreUp:
win.addLine((width-1, 0), '-')
if moreDown:
win.addLine((width-1, height-1), '+')
win.noutrefresh()

View File

@ -1,95 +0,0 @@
import curses
from asciifarm.common.utils import clamp
from .window import Window
import signal
class Screen:
def __init__(self, display, stdscr, colours):
self.display = display
try:
curses.curs_set(0)
self.cursorSet = False
except curses.error:
# Not all terminals support this functionality.
# When the error is ignored the screen will look a little uglier,
# A cursor will move around, but that's not terrible
# So in order to keep the game as accesible as possible to everyone, it should be safe to ignore the error.
self.cursorSet = True
# It is probably possible to make sure the cursor is only in a corner of the screen
# but I can't figure out how.
# it seems to ignore all my move commands unless I press a key
# I give up
self.stdscr = stdscr
self.colours = colours
self.setWins()
signal.signal(signal.SIGWINCH, self.updateSize)
def _limitHeight(self, h, y):
return min(h + y, self.height) - y
def setWins(self):
height, width = self.height, self.width = self.stdscr.getmaxyx()
sideW = 20
sideX = width-sideW
msgH = clamp(height // 5, 3, 5)
msgY = height - msgH-1
inputH = 1
inputY = msgY + msgH
healthY = 0
healthH = self._limitHeight(2, healthY)
indexY = healthY + healthH
indexH = self._limitHeight(4, indexY)
listY = indexY + indexH + 1
listH = self._limitHeight(12, listY)
infoY = listY + listH
infoH = self._limitHeight(20, infoY)
lists = self.makeWin(sideX, listY, sideW, listH)
self.windows = {
"field": self.makeWin(0, 0, sideX - 1, msgY),
"msg": self.makeWin(0, msgY, sideX - 1, msgH),
"textinput": self.makeWin(0, inputY, sideX - 1, inputH),
"health": self.makeWin(sideX, healthY, sideW, healthH),
"switch": self.makeWin(sideX, indexY, sideW, indexH),
"ground": lists,
"inventory": lists,
"equipment": lists,
"info": self.makeWin(sideX, infoY, sideW, infoH)
}
def makeWin(self, x, y, width, height):
if width < 1 or height < 1:
win = None
else:
win = curses.newwin(height, width, y, x)
return Window(win, self.colours)
def getWin(self, name):
return self.windows.get(name, None)
def updateSize(self, *args):
curses.endwin()
curses.initscr()
self.setWins()
self.stdscr.clear()
self.display.forceUpdate()
def update(self):
curses.doupdate()
def getWidth(self):
return self.width
def getHeight(self):
return self.height

View File

@ -1,29 +0,0 @@
from .inventory import Inventory
class Switcher(Inventory):
"""An area that can contain multiple widgets but only shows one at a time.
There is a function to switch between the displayed widgets.
"""
def __init__(self, widgets, initial=0):
Inventory.__init__(self, "", "", "=")
self.setInventory(widgets)
for wid in widgets:
wid.hidden = True
self.select(initial)
def doSelect(self, value):
self.getSelectedItem().hidden = True
self.selector = value
self.change()
newWid = self.getSelectedItem()
newWid.hidden = False
newWid.change()
def itemName(self, item):
return item.getImpl().title

View File

@ -1,22 +0,0 @@
import curses
from .widimp import WidImp
class TextInput(WidImp):
def __init__(self):
self.text = ""
self.cursor = -1
def setText(self, text, cursor):
self.text = text
self.cursor = cursor
self.change()
def update(self, win):
width, height = win.getSize()
win.erase()
win.addLine((0, 0), self.text[:width])
if self.cursor >= 0 and self.cursor <= len(self.text):
win.setAttr((min(self.cursor, width-1), 0), curses.A_REVERSE)
win.noutrefresh()

View File

@ -1,35 +0,0 @@
class Widget:
def __init__(self, impl, name=None):
self.impl = impl
self.win = None
self.screen = None
self.changed = False
self.hidden = False
self.name = name
self.impl.setWidget(self)
def setWin(self, win, screen):
self.win = win
self.screen = screen
def getWin(self):
return self.win and self.screen and self.screen.getWin(self.win)
def getImpl(self):
return self.impl
def change(self):
self.changed = True
def isChanged(self):
return self.changed
def update(self):
if not self.getWin() or self.hidden:
return
self.impl.update(self.getWin())
self.changed = False

View File

@ -1,18 +0,0 @@
class WidImp:
"""widget implementation"""
_widget = None
def setWidget(self, widget):
self._widget = widget
self.change()
def change(self):
if self._widget is not None:
self._widget.change()
def update(self, win):
pass

View File

@ -1,74 +0,0 @@
import curses
class Window:
""" Small wrapper around curses windows """
def __init__(self, win, colours=None):
self.win = win
self.colours = colours
def getSize(self):
if not self.win:
return (0, 0)
height, width = self.win.getmaxyx()
return (width, height)
def getPos(self):
if not self.win:
return (0, 0)
y, x = self.win.getparyx()
return (x, y)
def addLine(self, pos, string, colour=(0,0)):
"""Draw a string that does not contain newlines or characters with larger width
long lines are cropped to fit in the window"""
if not self.win:
return
x, y = pos
width, height = self.getSize()
string = string[:width-x]
drawLast = None
if self.colours:
self._addstr(y, x, string, self.colours.get(*colour))
else:
self._addstr(y, x, string)
def _addstr(self, y, x, string, *args):
if not self.win:
return
width, height = self.getSize()
if y == height-1 and x+len(string) == width:
if len(string) > 1:
self.win.addstr(y, x, string[:-1], *args)
try:
self.win.addstr(height-1, width-1, string[-1], *args)
except curses.error:
# ncurses has a weird problem:
# it always raises an error when drawing to the last character in the window
# it draws first and then raises the error
# therefore to draw in the last place of the window the last character needs to be ingored
# other solutions might be possible, but are more hacky
pass
else:
self.win.addstr(y, x, string, *args)
def erase(self):
if self.win:
self.win.erase()
def noutrefresh(self):
if self.win:
self.win.noutrefresh()
def setAttr(self, pos, attr, num=1):
if self.win:
x, y = pos
self.win.chgat(y, x, num, attr)

View File

@ -1,4 +1,4 @@
#! /usr/bin/python3
import os import os
import sys import sys
@ -10,13 +10,14 @@ import argparse
import string import string
from queue import Queue from queue import Queue
import ratuil.inputs
from .inputhandler import InputHandler from .inputhandler import InputHandler
class Client: class Client:
def __init__(self, stdscr, display, name, connection, keybindings, logFile=None): def __init__(self, display, name, connection, keybindings, logFile=None):
self.stdscr = stdscr
self.display = display self.display = display
self.name = name self.name = name
self.keepalive = True self.keepalive = True
@ -54,7 +55,7 @@ class Client:
def getInput(self): def getInput(self):
while True: while True:
key = self.stdscr.getch() key = ratuil.inputs.get_key()
self.queue.put(("input", key)) self.queue.put(("input", key))
def close(self, msg=None): def close(self, msg=None):
@ -138,12 +139,19 @@ class Client:
if action[0] == "message": if action[0] == "message":
self.update(action[1]) self.update(action[1])
elif action[0] == "input": elif action[0] == "input":
if action[1] == "^C":
raise KeyboardInterrupt
self.inputHandler.onInput(action[1]) self.inputHandler.onInput(action[1])
elif action[0] == "error": elif action[0] == "error":
raise action[1] raise action[1]
elif action[0] == "sigwinch":
self.display.update_size()
else: else:
raise Exception("invalid action in queue") raise Exception("invalid action in queue")
def onSigwinch(self, signum, frame):
self.queue.put(("sigwinch", (signum, frame)))

View File

@ -1,9 +1,9 @@
import curses import string
import curses.ascii
from .commandhandler import CommandHandler, InvalidCommandException from .commandhandler import CommandHandler, InvalidCommandException
from .keynames import nameFromKey
import ratuil.inputs as inp
class InputHandler: class InputHandler:
@ -20,7 +20,7 @@ class InputHandler:
def onInput(self, key): def onInput(self, key):
if not self.typing: if not self.typing:
keyName = nameFromKey(key) keyName = key
if keyName in self.keybindings: if keyName in self.keybindings:
self.commandHandler.execute(self.keybindings[keyName]) self.commandHandler.execute(self.keybindings[keyName])
else: else:
@ -58,36 +58,36 @@ class InputHandler:
self.client.display.setInputString(self.string, self.cursor if self.typing else -1) self.client.display.setInputString(self.string, self.cursor if self.typing else -1)
def addKey(self, key): def addKey(self, key):
if curses.ascii.isprint(key): if key in string.printable:
self.string = self.string[:self.cursor] + chr(key) + self.string[self.cursor:] self.string = self.string[:self.cursor] + key + self.string[self.cursor:]
self.cursor += 1 self.cursor += 1
elif key == curses.KEY_BACKSPACE or key == curses.ascii.BS or key == curses.ascii.DEL: elif key == inp.BACKSPACE:
self.string = self.string[:self.cursor-1] + self.string[self.cursor:] self.string = self.string[:self.cursor-1] + self.string[self.cursor:]
self.cursor = max(self.cursor - 1, 0) self.cursor = max(self.cursor - 1, 0)
elif key == curses.KEY_RIGHT: elif key == inp.RIGHT:
self.cursor = min(self.cursor + 1, len(self.string)) self.cursor = min(self.cursor + 1, len(self.string))
elif key == curses.KEY_LEFT: elif key == inp.LEFT:
self.cursor = max(self.cursor - 1, 0) self.cursor = max(self.cursor - 1, 0)
elif key == curses.KEY_DC: elif key == inp.DELETE:
self.string = self.string[:self.cursor] + self.string[self.cursor+1:] self.string = self.string[:self.cursor] + self.string[self.cursor+1:]
elif key == curses.KEY_HOME: elif key == inp.HOME:
self.cursor = 0 self.cursor = 0
elif key == curses.KEY_END: elif key == inp.END:
self.cursor = len(self.string) self.cursor = len(self.string)
elif key == curses.ascii.ESC or key == curses.KEY_DL: elif key == inp.ESCAPE:
# throw away entered string and go back to game # throw away entered string and go back to game
self.typing = False self.typing = False
self.string = "" self.string = ""
self.cursor = 0 self.cursor = 0
elif key == curses.ascii.LF or key == curses.ascii.CR: elif key == inp.ENTER:
# process entered string and reset it # process entered string and reset it
message = self.string message = self.string
self.string = "" self.string = ""
self.cursor = 0 self.cursor = 0
self.typing = False self.typing = False
self.processString(message) self.processString(message)
elif key == curses.ascii.TAB: elif key == "^I": # tab
# return to game but keep entered string # return to game but keep entered string
self.typing = False self.typing = False

View File

@ -1,15 +0,0 @@
import curses
prenamed = {
10: "NEWLINE"
}
def nameFromKey(key):
if key in prenamed:
return prenamed[key]
try:
keyname = curses.keyname(key)
except ValueError:
return None
return str(keyname, "utf-8")

View File

@ -0,0 +1,40 @@
<?xml version="1.0"?>
<hbox>
<vbox width="20" align="right">
<bar id="health" height="2" full-char="#" empty-char="_" full-style="fg:2; bg:2" empty-style="fg:1; bg: 1;"></bar>
<listing id="switchtitles" height="0"></listing>
<switchbox id="switch" height="50%">
<vbox key="inventory">
<textbox height="1">Inventory:</textbox>
<listing id="inventory">
milk
eggs
bread
</listing>
</vbox>
<vbox key="equipment">
<textbox height="1">Equipment:</textbox>
<listing id="equipment">
cotton underwear
cotton shirt
jeans
friendship bracelet
</listing>
</vbox>
<vbox key="ground">
<textbox height="1">Ground:</textbox>
<listing id="ground">
concrete
</listing>
</vbox>
</switchbox>
<textbox id="info"></textbox>
</vbox>
<vbox>
<textinput id="textinput" align="bottom" height="1">hello</textinput>
<log id="msg" align="bottom" height="20%%">
Welcome to asciifarm
</log>
<field id="field" char-size="1"></field>
</vbox>
</hbox>

View File

@ -0,0 +1,49 @@
from asciifarm.common import utils
class ListSelector:
def __init__(self, widget):
self.widget = widget
self.items = []
self.selector = 0
def getSelected(self):
return self.selector
def select(self, value, relative=False, modular=False):
invLen = len(self.items)
if relative:
value += self.selector
if modular and invLen:
value %= invLen
if value < 0:
value = 0
if value >= invLen:
value = invLen-1
if value in range(invLen):
self.doSelect(value)
def doSelect(self, value):
self.selector = value
self.widget.select(value)
def setItems(self, items):
self.items = items
self.selector = utils.clamp(self.selector, 0, len(items)-1)
self.widget.set_items([self.itemName(item) for item in self.items])
self.widget.select(self.selector)
def getItem(self, num):
return self.items[num]
def getSelectedItem(self):
return self.getItem(self.getSelected())
def getNumItems(self):
return len(self.items)
def itemName(self, item):
return item

View File

@ -61,8 +61,6 @@ def loadCharmap(name):
writable = {} writable = {}
default = None default = None
charwidth = 1 charwidth = 1
healthfull = None
healthempty = None
alphabet = "" alphabet = ""
msgcolours = {} msgcolours = {}
@ -71,8 +69,6 @@ def loadCharmap(name):
writable.update(template.get("writable", {})) writable.update(template.get("writable", {}))
default = template.get("default", default) default = template.get("default", default)
charwidth = template.get("charwidth", charwidth) charwidth = template.get("charwidth", charwidth)
healthfull = template.get("healthfull", healthfull)
healthempty = template.get("healthempty", healthempty)
alphabet = template.get("alphabet", alphabet) alphabet = template.get("alphabet", alphabet)
msgcolours.update(template.get("msgcolours", {})) msgcolours.update(template.get("msgcolours", {}))
return { return {
@ -80,8 +76,6 @@ def loadCharmap(name):
"writable": writable, "writable": writable,
"default": default, "default": default,
"charwidth": charwidth, "charwidth": charwidth,
"healthfull": healthfull,
"healthempty": healthempty,
"alphabet": alphabet, "alphabet": alphabet,
"msgcolours": msgcolours "msgcolours": msgcolours
} }

View File

@ -1,13 +1,18 @@
#! /usr/bin/python3 #! /usr/bin/python3
import curses
import json import json
import os
import getpass
import sys import sys
import termios
import tty
import signal
#import os
from .connection import Connection from .connection import Connection
from .gameclient import Client from .gameclient import Client
from .display.display import Display from .display import Display
from .parseargs import parse_args from .parseargs import parse_args
from ratuil.screen import Screen
def main(argv=None): def main(argv=None):
@ -24,33 +29,19 @@ def main(argv=None):
error = None error = None
closeMessage = None closeMessage = None
os.environ.setdefault("ESCDELAY", "25") #os.environ.setdefault("ESCDELAY", "25")
fd = sys.stdin.fileno()
oldterm = termios.tcgetattr(fd)
try: try:
# Initialize curses
stdscr = curses.initscr()
# Turn off echoing of keys, and enter cbreak mode, tty.setraw(sys.stdin)
# where no buffering is performed on keyboard input Screen.default.hide_cursor()
curses.noecho()
curses.cbreak()
# In keypad mode, escape sequences for special keys display = Display(characters)
# (like the cursor keys) will be interpreted and client = Client(display, name, connection, keybindings, logfile)
# a special value like curses.KEY_LEFT will be returned signal.signal(signal.SIGWINCH, client.onSigwinch)
stdscr.keypad(1)
# Start color, too. Harmless if the terminal doesn't have
# color; user can test with has_color() later on. The try/catch
# works around a minor bit of over-conscientiousness in the curses
# module -- the error return from C start_color() is ignorable.
try:
curses.start_color()
except:
pass
display = Display(stdscr, characters, colours)
client = Client(stdscr, display, name, connection, keybindings, logfile)
try: try:
client.start() client.start()
except KeyboardInterrupt: except KeyboardInterrupt:
@ -61,12 +52,9 @@ def main(argv=None):
error = e error = e
closeMessage = client.closeMessage closeMessage = client.closeMessage
finally: finally:
# Set everything back to normal ## Set everything back to normal
if 'stdscr' in locals(): termios.tcsetattr(fd, termios.TCSADRAIN, oldterm)
stdscr.keypad(0) Screen.default.finalize()
curses.echo()
curses.nocbreak()
curses.endwin()
if error is not None: if error is not None:

View File

@ -4,10 +4,10 @@
"s": ["move", "south"], "s": ["move", "south"],
"d": ["move", "east"], "d": ["move", "east"],
"a": ["move", "west"], "a": ["move", "west"],
"KEY_UP": ["move", "north"], "up": ["move", "north"],
"KEY_DOWN": ["move", "south"], "down": ["move", "south"],
"KEY_RIGHT": ["move", "east"], "right": ["move", "east"],
"KEY_LEFT": ["move", "west"], "left": ["move", "west"],
"k": ["move", "north"], "k": ["move", "north"],
"j": ["move", "south"], "j": ["move", "south"],
"l": ["move", "east"], "l": ["move", "east"],
@ -33,9 +33,9 @@
"D": ["input", ["attack", "east"]], "D": ["input", ["attack", "east"]],
"A": ["input", ["attack", "west"]], "A": ["input", ["attack", "west"]],
"t": ["runinput"], "t": ["runinput"],
"NEWLINE": ["runinput"], "enter": ["runinput"],
"KEY_PPAGE": ["scrollchat", 1], "pageup": ["scrollchat", 1],
"KEY_NPAGE": ["scrollchat", -1], "pagedown": ["scrollchat", -1],
"/": ["runinput", "/"] "/": ["runinput", "/"]
}, },
"help": "Controls:\n wasd or arrows:\n Move around\n e: Grab\n q: Drop/unequip\n selected\n r: Interact\n f: Attack\n t: Chat\n E: Use selected\n Q: Take selected\n xc: select item\n vb: select menu\n ctrl-c: close client" "help": "Controls:\n wasd or arrows:\n Move around\n e: Grab\n q: Drop/unequip\n selected\n r: Interact\n f: Attack\n t: Chat\n E: Use selected\n Q: Take selected\n xc: select item\n vb: select menu\n ctrl-c: close client"