Fix bugs, correct one-line format_grid function

This commit is contained in:
Griatch 2021-05-12 09:32:08 +02:00
parent 770fac275d
commit 055bbcfee3
7 changed files with 162 additions and 113 deletions

View file

@ -51,14 +51,13 @@ _DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
class CmdChannel(COMMAND_DEFAULT_CLASS):
"""
Talk on and manage in-game channels.
Use and manage in-game channels.
Usage:
channel
channel channelname <msg>
channel channel name [= <msg>]
channel/list
channel/all
channel channel name = <msg>
channel (show all subscription)
channel/all (show available channels)
channel/alias channelname = alias[;alias...]
channel/unalias alias
channel/who channelname
@ -73,10 +72,10 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
channel/desc channelname = description
channel/lock channelname = lockstring
channel/unlock channelname = lockstring
channel/boot[/quiet] channelname[,channelname,...] = subscribername [: reason]
channel/ban channelname (list bans)
channe/ban[/quiet] channelname[, channelname, ...] = subscribername [: reason]
channel/ban[/quiet] channelname[, channelname, ...] = subscribername [: logmessage]
channel/unban[/quiet] channelname[, channelname, ...] = subscribername
channel/boot[/quiet] channelname[,channelname,...] = subscribername [: reason]
# subtopics
@ -85,8 +84,8 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
Usage: channel channelname msg
channel channel name = msg (with space in channel name)
This sends a message to the channel. Note that you will rarely use this command
like this; instead you can use the alias
This sends a message to the channel. Note that you will rarely use this
command like this; instead you can use the alias
channelname <msg>
channelalias <msg>
@ -117,8 +116,8 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
wguild Hello
warchannel Hello
Note that this will not work if the alias has a space in it. So the 'warrior guild'
alias must be used with the `channel` command:
Note that this will not work if the alias has a space in it. So the
'warrior guild' alias must be used with the `channel` command:
channel warrior guild = Hello
@ -138,6 +137,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
This will display the last |c20|n lines of channel history. By supplying an
index number, you will step that many lines back before viewing those 20 lines.
For example:
channel/history public = 35
@ -182,14 +182,39 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
listen - who may listen or join the channel.
send - who may send messages to the channel
control - who controls the channel. This is usually the one creating
the channel.
the channel.
Common lockfuncs are all() and perm(). To make a channel everyone can listen
to but only builders can talk on, use this:
Common lockfuncs are all() and perm(). To make a channel everyone can
listen to but only builders can talk on, use this:
listen:all()
send: perm(Builders)
## boot and ban
Usage: channel/ban channelname (list bans)
channel/ban channelname[, channelname, ...] = subscribername [: logmessage]
channel/unban channelname[, channelname, ...] = subscribername
channel/unban channelname
channel/boot[/quiet] channelname[,channelname,...] = subscribername [: reason]
Booting will kick a named subscriber from a channel temporarily. It will
also remove all their aliases. They are still able to re-connect unless
they are also banned. The 'reason' given will be echoed to the user and
channel.
Banning will block a given subscriber's ability to connect to a channel. It
will not automatically boot them. The 'logmessage' will be stored on the
channel and shown when you list your bans (so you can remember why they
were banned).
So to permanently get rid of a user, the way to do so is to first ban them
and then boot them.
Example:
ban mychannel1,mychannel2 = EvilUser : Was banned for spamming.
boot mychannel1,mychannel2 = EvilUser : No more spamming!
"""
key = "channel"
aliases = ["chan", "channels"]
@ -505,7 +530,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
new_chan = create.create_channel(
name, aliases=aliases, desc=description, locks=lockstring, typeclass=typeclass)
new_chan.connect(caller)
self.sub_to_channel(new_chan)
return new_chan, ""
def destroy_channel(self, channel, message=None):
@ -831,12 +856,12 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
table = self.display_all_channels(subscribed, available)
self.msg(
"\n|wAvailable channels|n (use /list to "
f"only show subscriptions)\n{table}")
"\n|wAvailable channels|n (use no argument to "
f"only show your subscriptions)\n{table}")
return
if not channel_names:
# (empty or /list) show only subscribed channels
# empty arg show only subscribed channels
subscribed, _ = self.list_channels()
table = self.display_subbed_channels(subscribed)

View file

@ -31,8 +31,6 @@ DEFAULT_HELP_CATEGORY = settings.DEFAULT_HELP_CATEGORY
# limit symbol import for API
__all__ = ("CmdHelp", "CmdSetHelp")
_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
_SEP = "|C" + "-" * _DEFAULT_WIDTH + "|n"
@dataclass
@ -59,16 +57,16 @@ class HelpCategory:
class CmdHelp(COMMAND_DEFAULT_CLASS):
"""
View help or a list of topics
Get help.
Usage:
help
help <topic, command or category>
help <topic> / <subtopic>
help <topic> / <subtopic> / <subsubtopic> ...
help <topic>/<subtopic>
help <topic>/<subtopic>/<subsubtopic> ...
Use the help command alone to see an index of all help topics, organized by
category. Some long topics may offer additional sub-topics.
Use the 'help' command alone to see an index of all help topics, organized
by category.eSome big topics may offer additional sub-topics.
"""
@ -123,8 +121,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
self.msg(text=(text, {"type": "help"}))
@staticmethod
def format_help_entry(topic="", help_text="", aliases=None, suggested=None,
def format_help_entry(self, topic="", help_text="", aliases=None, suggested=None,
subtopics=None):
"""
This visually formats the help entry.
@ -142,7 +139,8 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
Returns the formatted string, ready to be sent.
"""
start = f"{_SEP}\n"
separator = "|C" + "-" * self.client_width() + "|n"
start = f"{separator}\n"
title = f"|CHelp for |w{topic}|n" if topic else "|rNo help found|n"
@ -156,26 +154,27 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
help_text = "\n\n" + dedent(help_text.strip('\n')) + "\n" if help_text else ""
if subtopics:
subtopics = [f"|w{topic}/{subtop}|n" for subtop in subtopics]
subtopics = (
"\n|CSubtopics:|n\n {}".format(
"\n ".join(f"|w{topic}/{subtop}|n" for subtop in subtopics))
"\n ".join(format_grid(subtopics, width=self.client_width())))
)
else:
subtopics = ''
if suggested:
suggested = [f"|w{sug}|n" for sug in suggested]
suggested = (
"\n|CSuggestions:|n\n{}".format(
fill("|C,|n ".join(f"|w{sug}|n" for sug in suggested), indent=2))
"\n ".join(format_grid(suggested, width=self.client_width())))
)
else:
suggested = ''
end = f"\n{_SEP}"
end = f"\n{separator}"
return "".join((start, title, aliases, help_text, subtopics, suggested, end))
def format_help_index(self, cmd_help_dict=None, db_help_dict=None, title_lone_category=False):
"""
Output a category-ordered g for displaying the main help, grouped by

View file

@ -439,8 +439,8 @@ class TestHelp(CommandTest):
"Help for test\n\n"
"Main help text\n\n"
"Subtopics:\n"
" test/creating extra stuff\n"
" test/something else\n"
" test/creating extra stuff"
" test/something else"
" test/more"
),
("test/creating extra stuff", # subtopic, full match
@ -483,14 +483,14 @@ class TestHelp(CommandTest):
"Help for test/more/second-more\n\n"
"The Second More text.\n\n"
"Subtopics:\n"
" test/more/second-more/more again\n"
" test/more/second-more/more again"
" test/more/second-more/third more"
),
("test/More/-more", # partial match
"Help for test/more/second-more\n\n"
"The Second More text.\n\n"
"Subtopics:\n"
" test/more/second-more/more again\n"
" test/more/second-more/more again"
" test/more/second-more/third more"
),
("test/more/second/more again",

View file

@ -59,6 +59,8 @@ things you want from here into your game folder and change them there.
time to pass depending on if you are walking/running etc.
* Talking NPC (Griatch 2011) - A talking NPC object that offers a
menu-driven conversation tree.
* Traits (Whitenoise, Griatch 2021) - Properties for handling and tracking
changing RPG skill and stat values.
* Tree Select (FlutterSprite 2017) - A simple system for creating a
branching EvMenu with selection options sourced from a single
multi-line string.

View file

@ -173,16 +173,9 @@ def parse_entry_for_subcategories(entry):
}
}
Apart from making
sub-categories at the bottom of the entry.
This will be applied both to command docstrings and database-based help
entries.
"""
topic, *subtopics = _RE_HELP_SUBTOPICS_START.split(entry, maxsplit=1)
structure = {None: topic.strip()}
structure = {None: topic.strip('\n')}
if subtopics:
subtopics = subtopics[0]

View file

@ -321,7 +321,7 @@ class TestFormatGrid(TestCase):
"""Grid with small variations"""
elements = self._generate_elements(3, 1, 30)
rows = utils.format_grid(elements, width=78)
self.assertEqual(len(rows), 3)
self.assertEqual(len(rows), 4)
self.assertTrue(all(len(row) == 78 for row in rows))
def test_disparate_grid(self):
@ -356,7 +356,7 @@ class TestFormatGrid(TestCase):
"lock",
)
rows = utils.format_grid(elements, width=78)
self.assertEqual(len(rows), 2)
self.assertEqual(len(rows), 3)
for element in elements:
self.assertTrue(element in "\n".join(rows), f"element {element} is missing.")

View file

@ -1846,7 +1846,7 @@ def percentile(iterable, percent, key=lambda x: x):
return d0 + d1
def format_grid(elements, width=78, sep=" ", verbatim_elements=None):
def format_grid(elements, width=78, sep=" ", verbatim_elements=None):
"""
This helper function makes a 'grid' output, where it distributes the given
string-elements as evenly as possible to fill out the given width.
@ -1869,6 +1869,97 @@ def format_grid(elements, width=78, sep=" ", verbatim_elements=None):
like this to make it easier to insert decorations between rows, such
as horizontal bars.
"""
def _minimal_rows(elements):
"""
Minimalistic distribution with minimal spacing, good for single-line
grids but will look messy over many lines.
"""
rows = [""]
for element in elements:
rowlen = len(rows[-1])
elen = len(element)
if rowlen + elen <= width:
rows[-1] += element
else:
rows.append(element)
return rows
def _weighted_rows(elements):
"""
Dynamic-space, good for making even columns in a multi-line grid but
will look strange for a single line.
"""
wls = [len(elem) for elem in elements]
wls_percentile = [wl for iw, wl in enumerate(wls) if iw not in verbatim_elements]
if wls_percentile:
# get the nth percentile as a good representation of average width
averlen = int(percentile(sorted(wls_percentile), 0.9)) + 2 # include extra space
aver_per_row = width // averlen + 1
else:
# no adjustable rows, just keep all as-is
aver_per_row = 1
if aver_per_row == 1:
# one line per row, output directly since this is trivial
# we use rstrip here to remove extra spaces added by sep
return [
crop(element.rstrip(), width) + " " * max(0, width - len(element.rstrip()))
for iel, element in enumerate(elements)
]
indices = [averlen * ind for ind in range(aver_per_row - 1)]
rows = []
ic = 0
row = ""
for ie, element in enumerate(elements):
wl = wls[ie]
lrow = len(row)
debug = row.replace(" ", ".")
if lrow + wl > width:
# this slot extends outside grid, move to next line
row += " " * (width - lrow)
rows.append(row)
if wl >= width:
# remove sep if this fills the entire line
element = element.rstrip()
row = crop(element, width)
ic = 0
elif ic >= aver_per_row - 1:
# no more slots available on this line
row += " " * max(0, (width - lrow))
rows.append(row)
row = crop(element, width)
ic = 0
else:
try:
while lrow > max(0, indices[ic]):
# slot too wide, extend into adjacent slot
ic += 1
row += " " * max(0, indices[ic] - lrow)
except IndexError:
# we extended past edge of grid, crop or move to next line
if ic == 0:
row = crop(element, width)
else:
row += " " * max(0, width - lrow)
rows.append(row)
ic = 0
else:
# add a new slot
row += element + " " * max(0, averlen - wl)
ic += 1
if ie >= nelements - 1:
# last element, make sure to store
row += " " * max(0, width - len(row))
rows.append(row)
return rows
if not elements:
return []
if not verbatim_elements:
@ -1877,76 +1968,15 @@ def format_grid(elements, width=78, sep=" ", verbatim_elements=None):
nelements = len(elements)
# add sep to all but the very last element
elements = [elements[ie] + sep for ie in range(nelements - 1)] + [elements[-1]]
wls = [len(elem) for elem in elements]
wls_percentile = [wl for iw, wl in enumerate(wls) if iw not in verbatim_elements]
if wls_percentile:
# get the nth percentile as a good representation of average width
averlen = int(percentile(sorted(wls_percentile), 0.9)) + 2 # include extra space
aver_per_row = width // averlen + 1
if sum(len(element) for element in elements) <= width:
# grid fits in one line
return _minimal_rows(elements)
else:
# no adjustable rows, just keep all as-is
aver_per_row = 1
# full multi-line grid
return _weighted_rows(elements)
if aver_per_row == 1:
# one line per row, output directly since this is trivial
# we use rstrip here to remove extra spaces added by sep
return [
crop(element.rstrip(), width) + " " * max(0, width - len(element.rstrip()))
for iel, element in enumerate(elements)
]
indices = [averlen * ind for ind in range(aver_per_row - 1)]
rows = []
ic = 0
row = ""
for ie, element in enumerate(elements):
wl = wls[ie]
lrow = len(row)
debug = row.replace(" ", ".")
if lrow + wl > width:
# this slot extends outside grid, move to next line
row += " " * (width - lrow)
rows.append(row)
if wl >= width:
# remove sep if this fills the entire line
element = element.rstrip()
row = crop(element, width)
ic = 0
elif ic >= aver_per_row - 1:
# no more slots available on this line
row += " " * max(0, (width - lrow))
rows.append(row)
row = crop(element, width)
ic = 0
else:
try:
while lrow > max(0, indices[ic]):
# slot too wide, extend into adjacent slot
ic += 1
row += " " * max(0, indices[ic] - lrow)
except IndexError:
# we extended past edge of grid, crop or move to next line
if ic == 0:
row = crop(element, width)
else:
row += " " * max(0, width - lrow)
rows.append(row)
ic = 0
else:
# add a new slot
row += element + " " * max(0, averlen - wl)
ic += 1
if ie >= nelements - 1:
# last element, make sure to store
row += " " * max(0, width - len(row))
rows.append(row)
return rows
def get_evennia_pids():