Handle IRC formatting
This commit is contained in:
parent
396b3c0185
commit
c5a58dac4e
314
gameloop.py
314
gameloop.py
|
@ -22,6 +22,36 @@ Limit = namedtuple('Limit', ['type', 'number'])
|
|||
|
||||
Card = namedtuple('Card', ['deck', 'text'])
|
||||
|
||||
class IRCFormattingState:
|
||||
def __init__(self):
|
||||
# 99 is the "client default colour"
|
||||
self.fg_color = 99
|
||||
self.bg_color = 99
|
||||
self.bold = False
|
||||
self.italic = False
|
||||
self.underline = False
|
||||
self.reverse = False
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
self.fg_color == other.fg_color and
|
||||
self.bg_color == other.bg_color and
|
||||
self.bold == other.bold and
|
||||
self.italic == other.italic and
|
||||
self.underline == other.underline and
|
||||
self.reverse == other.reverse
|
||||
)
|
||||
|
||||
def copy(self):
|
||||
new = IRCFormattingState()
|
||||
new.fg_color = self.fg_color
|
||||
new.bg_color = self.bg_color
|
||||
new.bold = self.bold
|
||||
new.italic = self.italic
|
||||
new.underline = self.underline
|
||||
new.reverse = self.reverse
|
||||
return new
|
||||
|
||||
class Player:
|
||||
def __init__(self, nick):
|
||||
self.nick = nick
|
||||
|
@ -659,58 +689,264 @@ def game(send, notice, voice, devoice, get_event):
|
|||
|
||||
return top_of_round
|
||||
|
||||
def sanitize(text):
|
||||
return ''.join(i if ord(i) >= 32 and ord(i) != 127 else '^' + chr(ord(i) ^ 64) for i in text)
|
||||
def handle_control_codes(text, start_state):
|
||||
state = start_state.copy()
|
||||
|
||||
r = []
|
||||
index = 0
|
||||
while index < len(text):
|
||||
char = text[index]
|
||||
index += 1
|
||||
|
||||
if char == '\x02':
|
||||
# ^B - bold
|
||||
state.bold = not state.bold
|
||||
r.append(char)
|
||||
|
||||
elif char == '\x1d':
|
||||
# ^] - italic
|
||||
state.italic = not state.italic
|
||||
r.append(char)
|
||||
|
||||
elif char == '\x1f':
|
||||
# ^_ - underline
|
||||
state.underline = not state.underline
|
||||
r.append(char)
|
||||
|
||||
elif char == '\x16':
|
||||
# ^V - reverse video mode
|
||||
state.reverse = not state.reverse
|
||||
r.append(char)
|
||||
|
||||
elif char == '\x0f':
|
||||
# ^O - disable all formatting
|
||||
state = IRCFormattingState()
|
||||
r.append(char)
|
||||
|
||||
elif char == '\x03':
|
||||
# ^C - colour
|
||||
start = index - 1
|
||||
|
||||
# Find the foreground colour
|
||||
# It can be at max 2 digits forwards
|
||||
# We use <= here since we are comparing against
|
||||
# the end point of slice, which'll be one more
|
||||
# than the index of the last cell in the slice
|
||||
if index + 2 <= len(text) and text[index:index + 2].isdigit():
|
||||
state.fg_color = int(text[index:index + 2])
|
||||
index += 2
|
||||
elif index + 1 <= len(text) and text[index:index + 1].isdigit():
|
||||
state.fg_color = int(text[index:index +1])
|
||||
index += 1
|
||||
else:
|
||||
# Not a valid colour code after all
|
||||
r.append('^C')
|
||||
continue
|
||||
|
||||
r.append(text[start:index])
|
||||
|
||||
# Do we have a background colour?
|
||||
if index < len(text) and text[index] == ',':
|
||||
# Maybe
|
||||
start = index
|
||||
index += 1
|
||||
|
||||
# Find the bg colour
|
||||
# Details are the same as above
|
||||
if index + 2 <= len(text) and text[index:index + 2].isdigit():
|
||||
state.bg_color = int(text[index:index + 2])
|
||||
index += 2
|
||||
elif index + 1 <= len(text) and text[index:index + 1].isdigit():
|
||||
state.bg_color = int(text[index:index +1])
|
||||
index += 1
|
||||
else:
|
||||
# No bg colour after all
|
||||
index -= 1
|
||||
|
||||
r.append(text[start:index])
|
||||
|
||||
elif ord(char) < 32 or ord(char) == 127:
|
||||
r.append('^' + chr(ord(i) ^ 64))
|
||||
|
||||
else:
|
||||
r.append(char)
|
||||
|
||||
return (''.join(r), state)
|
||||
|
||||
def to_formatting_state(from_state, to_state):
|
||||
if to_state == from_state:
|
||||
return ''
|
||||
elif to_state == IRCFormattingState():
|
||||
return '\x0f' # ^O
|
||||
|
||||
r = ''
|
||||
|
||||
if to_state.fg_color != from_state.fg_color or to_state.bg_color != from_state.bg_color:
|
||||
# Always use the full form, if for no other reason than
|
||||
# to not screw up ,<num> or <num> following this
|
||||
r += '\x03%02i,%02i' % (to_state.fg_color, to_state.bg_color)
|
||||
|
||||
if to_state.bold != from_state.bold:
|
||||
r += '\x02' # ^B
|
||||
if to_state.italic != from_state.italic:
|
||||
r += '\x1d' # ^]
|
||||
if to_state.underline != from_state.underline:
|
||||
r += '\x1f' # ^_
|
||||
if to_state.reverse != from_state.reverse:
|
||||
r += '\x16' # ^V
|
||||
|
||||
return r
|
||||
|
||||
def sanitize(text, start_state):
|
||||
sanitized, state_after = handle_control_codes(text, start_state)
|
||||
return sanitized + to_formatting_state(state_after, start_state)
|
||||
|
||||
def send_cards(nick):
|
||||
nonlocal players
|
||||
|
||||
cards = ' | '.join('%i: [%s]' % (index, sanitize(card.text)) for index, card in enumerate(players[nick].hand))
|
||||
no_formatting = IRCFormattingState()
|
||||
cards = ' | '.join('%i: [%s]' % (index, sanitize(card.text, no_formatting)) for index, card in enumerate(players[nick].hand))
|
||||
|
||||
notice(nick, cards)
|
||||
|
||||
def combine_cards(call, responses):
|
||||
def handle_call_part(call_part):
|
||||
nonlocal responses
|
||||
# This function is really messy, in part due to the format used
|
||||
#
|
||||
# `call` is a list of strings, and here I'll refer to each of
|
||||
# them as "part". This division into parts indicates the
|
||||
# locations of the blanks. For example:
|
||||
#
|
||||
# Foo bar? _.
|
||||
#
|
||||
# is stored as
|
||||
#
|
||||
# ['Foo bar? ', '.']
|
||||
#
|
||||
# So far good, take a part from `call`, then one response
|
||||
# (assuming we still have one remaining) and repeat.
|
||||
#
|
||||
# However, CardsAgainstIRC extended the simple system of blanks
|
||||
# with backreferences that are of the form /\$[0-9]/. Since
|
||||
# Cardcast doesn't know about them, they are stored like any
|
||||
# other text. E.g.
|
||||
#
|
||||
# Foo _? Bar $0.
|
||||
# ['Foo ', '? Bar $0.']
|
||||
#
|
||||
# You could add backreference support to the earlier algorithm
|
||||
# by going over each part and replacing each $<num> with the
|
||||
# corresponding response when you add the call part. It'd give
|
||||
# this algorith:
|
||||
#
|
||||
# 1. Go over call[index] and satisfy backrefrences
|
||||
# 1. Copy text verbatim until we hit a $<num>
|
||||
# 2. Add responses[num]
|
||||
# 3. Repeat
|
||||
# 2. Add response[index]
|
||||
# 3. Repeat
|
||||
#
|
||||
# This was how I first implemented it. However, dealing with
|
||||
# backreferences has more to do with dealing a blank than it
|
||||
# does with copying the rest of the part For this reason, this
|
||||
# function deals # with the concept of a "segment". Where parts
|
||||
# are delineated by blanks, segments are delineated by both
|
||||
# blanks and backreferences. To reuse the example from earlier:
|
||||
#
|
||||
# Foo _? Bar $0.
|
||||
# ^^^^ ^^^^^^ ^
|
||||
# 1111 222222 3
|
||||
#
|
||||
# Because the storage format is still split by part instead of
|
||||
# by segment, this function has to manually split each part into
|
||||
# its segments. That's why we walk both part by part and char by
|
||||
# char.
|
||||
#
|
||||
# Our algorithm is
|
||||
# 1. Find either a blank, end of card, or a backreference,
|
||||
# whichever comes first
|
||||
# blank:
|
||||
# 1. Copy the segment before this verbatim
|
||||
# 2. Add next response
|
||||
# end:
|
||||
# 1. Copy the segment before this verbatim
|
||||
# backreference:
|
||||
# 1. Copy the segment before this verbatim
|
||||
# 2. Add the response pointed to by the
|
||||
# backreference
|
||||
# 2. Repeat
|
||||
|
||||
r = []
|
||||
after_dollar = False
|
||||
for char in call_part:
|
||||
if after_dollar and ord('0') <= ord(char) <= ord('9'):
|
||||
formatting_state = IRCFormattingState()
|
||||
no_formatting = IRCFormattingState()
|
||||
|
||||
r = []
|
||||
part_index = 0
|
||||
index = 0
|
||||
segment_start_index = 0
|
||||
while part_index < len(call):
|
||||
call_part = call[part_index]
|
||||
|
||||
if index >= len(call_part):
|
||||
# We've reached a blank or the end of the card
|
||||
|
||||
# Copy the previous segment fully to `r`
|
||||
call_segment, formatting_state = handle_control_codes(call_part[segment_start_index:], formatting_state)
|
||||
r.append(call_segment)
|
||||
|
||||
# If there is still one, add the response coming
|
||||
# after that segment
|
||||
if part_index < len(responses):
|
||||
r.append(to_formatting_state(formatting_state, no_formatting))
|
||||
r.append('[')
|
||||
r.append(sanitize(responses[part_index], no_formatting))
|
||||
r.append(']')
|
||||
r.append(to_formatting_state(no_formatting, formatting_state))
|
||||
|
||||
# Start on a new part as well as a new segment
|
||||
part_index += 1
|
||||
index = 0
|
||||
segment_start_index = 0
|
||||
|
||||
continue
|
||||
|
||||
char = call_part[index]
|
||||
index += 1
|
||||
|
||||
if char == '$':
|
||||
if index < len(call_part) and ord('0') <= ord(call_part[index]) <= ord('9'):
|
||||
# Handle $0 .. $9
|
||||
# Hopefully we won't run into more backreferences
|
||||
# in one card
|
||||
index = int(char)
|
||||
# Hopefully we won't run into more
|
||||
# backreferences in one card
|
||||
backreference_index = int(call_part[index])
|
||||
index += 1
|
||||
|
||||
if 0 <= backreference_index < len(responses):
|
||||
# Copy the previous segment fully to `r`
|
||||
call_segment, formatting_state = handle_control_codes(call_part[segment_start_index:index - 2], formatting_state)
|
||||
|
||||
# Add the response this backreference refers to
|
||||
r.append(call_segment)
|
||||
r.append(to_formatting_state(formatting_state, no_formatting))
|
||||
r.append('[')
|
||||
r.append(sanitize(responses[backreference_index], no_formatting))
|
||||
r.append(']')
|
||||
r.append(to_formatting_state(no_formatting, formatting_state))
|
||||
|
||||
# Start new segment after this char
|
||||
segment_start_index = index
|
||||
|
||||
if 0 <= index < len(responses):
|
||||
r.append('[' + responses[index] + ']')
|
||||
else:
|
||||
# Not valid backreference, copy verbatim
|
||||
r.append('$' + char)
|
||||
|
||||
after_dollar = False
|
||||
|
||||
elif after_dollar:
|
||||
# Wasn't a backreference, copy verbatim
|
||||
r.append('$' + char)
|
||||
|
||||
after_dollar = False
|
||||
|
||||
elif char == '$':
|
||||
after_dollar = True
|
||||
# A backreference, but not a
|
||||
# valid one. Copy verbatim
|
||||
r.append('$')
|
||||
r.append(call_part[index])
|
||||
|
||||
else:
|
||||
r.append(char)
|
||||
# Not a backreference
|
||||
r.append('$')
|
||||
|
||||
return sanitize(''.join(r))
|
||||
r.append(to_formatting_state(formatting_state, no_formatting))
|
||||
|
||||
combined = [handle_call_part(call[0])]
|
||||
|
||||
for i in range(len(call) - 1):
|
||||
combined.append('[' + sanitize(responses[i]) + ']')
|
||||
combined.append(handle_call_part(call[i + 1]))
|
||||
|
||||
return ''.join(combined)
|
||||
return ''.join(r)
|
||||
|
||||
def combine_played(call, player_bot, selected_cards):
|
||||
return combine_cards(call.text, [player_bot.hand[i].text for i in selected_cards])
|
||||
|
@ -720,14 +956,14 @@ def game(send, notice, voice, devoice, get_event):
|
|||
|
||||
choosers = [i for i in players.values() if i is not czar]
|
||||
|
||||
send('Round %i. %s is czar. %s choose your cards' % (round_number, czar.nick, ', '.join(i.nick for i in choosers)))
|
||||
send('[%s]' % '_'.join(sanitize(part) for part in round_call_card.text))
|
||||
|
||||
# Round call card has N parts. Between each of those parts
|
||||
# goes one response card. Therefore there should be N - 1
|
||||
# response cards
|
||||
num_blanks = len(round_call_card.text) - 1
|
||||
|
||||
send('Round %i. %s is czar. %s choose your cards' % (round_number, czar.nick, ', '.join(i.nick for i in choosers)))
|
||||
send('[%s]' % combine_cards(round_call_card.text, ['_'] * num_blanks))
|
||||
|
||||
# Have bots choose first
|
||||
for bot in bots.values():
|
||||
card_choices[bot] = bot.play(num_blanks)
|
||||
|
|
Loading…
Reference in New Issue