Skip to content

Commit de4c3db

Browse files
authored
Merge pull request #934 from otizonaizit/overlays
Add `paint_background` method to `Bot` object to paint cells in the maze with user-defined colors and show it in TK viewer in debug mode
2 parents c0539a0 + cad1299 commit de4c3db

5 files changed

Lines changed: 263 additions & 14 deletions

File tree

pelita/game.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,15 @@ def setup_game(team_specs, *, layout_dict, max_rounds=300, rng=None,
445445
#: Messages the bots say. Keeps only the recent one at the respective bot’s index.
446446
say=[""] * 4,
447447

448+
#: List of 4 lists to store the cell overlays for each bot
449+
# Items of each list are dictionaries in the form:
450+
# { 'pos' : (x, y), 'color' : "#AABBCC', ... }
451+
# where (x, y) are coordinates on the maze and color is HTML-encoded
452+
# At the moment only property 'color' is used by the TK-viewer,
453+
# but more can be added without modifying the network protocol or
454+
# this list
455+
overlays=[[], [], [], []],
456+
448457
### Internal
449458
#: Internal team representation
450459
teams=[None] * 2,
@@ -865,6 +874,14 @@ def play_turn(game_state, raise_bot_exceptions=False):
865874
else:
866875
game_state['say'][game_state['turn']] = ""
867876

877+
# reset the overlays so that they are painted new at every turn
878+
game_state['overlays'] = [[], [], [], []]
879+
880+
if position_dict.get('overlay'):
881+
game_state['overlays'][game_state['turn']] = position_dict['overlay']
882+
else:
883+
game_state['overlays'][game_state['turn']] = []
884+
868885
# If the returned move looks okay, we add it to the list of requested moves
869886
old_position = game_state['bots'][turn]
870887
game_state['requested_moves'][turn] = {

pelita/team.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,36 @@ def sanitize_say(string):
122122
return ''.join(sane)
123123

124124

125+
def convert_overlay_to_json(overlay):
126+
"""Convert the overlay dictionary to a list of dictionaries for JSON compatibility
127+
128+
Parameters
129+
----------
130+
overlay : dict
131+
A dictionary { pos : props } where pos are coordinates in the maze, like
132+
for example ( 10, 12) and props is a dictionary of properties, for example:
133+
134+
{
135+
'color' : '#AABBCC',
136+
'text' : 'special text',
137+
...
138+
}
139+
140+
Returns
141+
-------
142+
A list of dictionaries in the form
143+
[ { 'pos' : pos0, 'color' : '#AABBCC', 'text' : 'special text'},
144+
{ 'pos' : pos1, 'color' : '#BBCCDD', 'text' : 'other text'},
145+
...
146+
]
147+
148+
Note
149+
----
150+
The conversion is necessary because JSON does not support dictionaries
151+
with tuples/lists as keys.
152+
"""
153+
return [dict(pos=pos, **props) for pos, props in overlay.items()]
154+
125155
class Team:
126156
"""
127157
Wraps a move function and forwards it the `set_initial`
@@ -255,6 +285,7 @@ def get_move(self, game_state):
255285
move = self.apply_move_fn(self._team_move, team[me._bot_turn], self._state)
256286
if "error" not in move:
257287
move["say"] = me._say
288+
move["overlay"] = convert_overlay_to_json(me._overlay)
258289
return move
259290

260291
@staticmethod
@@ -649,6 +680,7 @@ def __init__(self, *, bot_index,
649680
bot_turn=None):
650681
self._bots = None
651682
self._say = None
683+
self._overlay = {}
652684

653685
#: The previous positions of this bot including the current one.
654686
self.track = []
@@ -728,6 +760,59 @@ def say(self, text):
728760
# sanitize text so that funny users can't break the GUI
729761
self._say = sanitize_say(str(text))
730762

763+
def paint_background(self, pos, color='#96FF96'):
764+
""" Color background of cell at position pos in the maze with the specified color
765+
766+
Parameters
767+
----------
768+
pos : 2-tuple
769+
770+
where pos = (x, y) are the coordinates of the cell in the maze that you want
771+
to set the background of, and "color" is a string representing an
772+
HTML-encoded color for that cell.
773+
774+
For example:
775+
>>> bot.paint_background((10, 10))
776+
777+
To paint the cell at (10, 10) with the default light yellow background, or:
778+
779+
>>> bot.paint_background((10, 10), color="#FFFFA8")
780+
781+
To paint it instead with a light green background
782+
783+
Note
784+
----
785+
Painting the background only works with viewers that support it, like the built-in
786+
Tk-viewer in debug mode.
787+
"""
788+
789+
width, height = self.shape
790+
# first verify that position is list/tuple of length two
791+
try:
792+
x, y = pos
793+
x = int(x)
794+
y = int(y)
795+
except (TypeError, ValueError):
796+
msg = f'Position "{pos}" is not a valid coordinate (x, y).'
797+
raise ValueError(msg)
798+
799+
# check: coordinates fit in the maze
800+
if x < 0 or x >= width or y < 0 or y >= height:
801+
# just ignore this coordinate
802+
return
803+
804+
# check: color is a string that can be interpreted as a 6 digit hexadecimal number
805+
if (not color.startswith('#')) or len(color) != 7:
806+
msg = f'Background color "{color}" is not a valid color.'
807+
raise ValueError(msg)
808+
try:
809+
int(color[1:], base=16)
810+
except (TypeError, ValueError):
811+
msg = f'Background color "{color}" is not a valid color.'
812+
raise ValueError(msg)
813+
814+
self._overlay.setdefault((x, y), {}).update({'color' : color})
815+
731816
# def get_direction(self, position):
732817
# """ Return the direction needed to get to the given position.
733818

pelita/ui/tk_canvas.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ def update(self, game_state=None, redraw=False):
483483
self.draw_end_of_game(None)
484484

485485
def draw_universe(self, game_state, redraw):
486-
self.draw_overlay(game_state.get('overlays', []))
486+
self.draw_overlays(game_state.get('overlays', []))
487487
self.draw_grid(redraw=redraw)
488488
self.draw_selected(game_state)
489489
self.draw_line_of_sight(game_state)
@@ -563,25 +563,25 @@ def draw_line(x0, y0, x1, y1):
563563
y_pos = self.mesh_graph.mesh_to_screen_y(0, -0.7)
564564
self.ui_game_canvas.create_text(x_pos, y_pos, text="y", **label_style)
565565

566-
def draw_overlay(self, overlays):
567-
""" Draws a light grid on the background.
566+
def draw_overlays(self, overlays):
567+
""" Draws overlays on top of cells at given coordinates.
568568
"""
569-
self.ui_game_canvas.delete("overlay")
569+
self.ui_game_canvas.delete("overlays")
570570
if not self._grid_enabled:
571571
return
572572

573-
def draw_box(pos, fill_col):
573+
def draw_color_box(pos, fill_col):
574574
ul = self.mesh_graph.mesh_to_screen(pos, (-1, -1))
575575
lr = self.mesh_graph.mesh_to_screen(pos, (1, 1))
576+
self.ui_game_canvas.create_rectangle(*ul, *lr, width=0, fill=fill_col, tags=("overlays",))
576577

577-
self.ui_game_canvas.create_rectangle(*ul, *lr, width=0, fill=fill_col, tags=("overlay",))
578-
579-
for overlay in overlays:
580-
if fill_col := overlay.get("fill"):
581-
for pos in overlay.get("pos", []):
582-
draw_box(pos, fill_col)
578+
for bot_overlays in overlays:
579+
for prop in bot_overlays:
580+
# paint background if color is specified in the bot overlay
581+
if 'color' in prop:
582+
draw_color_box(prop['pos'], prop['color'])
583583

584-
self.ui_game_canvas.tag_lower("overlay")
584+
self.ui_game_canvas.tag_lower("overlays")
585585
self.ui_game_canvas.tag_raise("wall")
586586

587587
def draw_line_of_sight(self, game_state):

test/test_network.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ def stopping(bot, state):
123123
'__uuid__': _uuid,
124124
'__return__': {
125125
"move": [1, 1],
126-
"say": None
126+
"say": None,
127+
"overlay" : [],
127128
}
128129
}
129130

test/test_team.py

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pelita.gamestate_filters import manhattan_dist
77
from pelita.layout import initial_positions, parse_layout
88
from pelita.maze_generator import generate_maze
9-
9+
from pelita.exceptions import PelitaBotError
1010

1111
def stopping(bot, state):
1212
return bot.position
@@ -767,3 +767,149 @@ def speaking_team(bot, state):
767767
# check that we finished without problem
768768
assert state['round'] == 1
769769
assert state['turn'] == 3
770+
771+
772+
def test_valid_paint_background():
773+
test_layout = """
774+
##################
775+
#.#... .##. y#
776+
# # # . .### #x#
777+
# ####. . #
778+
# . .#### #
779+
#a# ###. . # # #
780+
#b .##. ...#.#
781+
##################
782+
"""
783+
overlay = [
784+
((0,0) , "#AABBCC"),
785+
((1,1) , "#BBCCDD"),
786+
((100, 200) , "#DDCCAA"), # silently ignore coords out of the maze
787+
]
788+
789+
parsed = parse_layout(test_layout)
790+
791+
def overlay_team(bot, state):
792+
for pos, color in overlay:
793+
bot.paint_background(pos, color=color)
794+
return bot.position
795+
796+
state = setup_game([overlay_team, overlay_team], max_rounds=2, layout_dict=parsed, raise_bot_exceptions=True)
797+
798+
overlay_in_maze = [{'pos':(0,0), 'color':"#AABBCC"}, {'pos':(1,1), 'color':"#BBCCDD"}]
799+
for idx in range(4):
800+
state = play_turn(state)
801+
gs_overlay = state['overlays'][idx]
802+
assert gs_overlay == overlay_in_maze
803+
804+
805+
@pytest.mark.parametrize('pos, color, match', [
806+
((0,1,2), '#AABBCC', 'Position.+(0, 1, 2).+not a valid'), # position has too many values
807+
('#AABBCC', (0, 1), 'Position.+#AABBCC.+not a valid'), # swapped arguments
808+
((1, 2), 'white', 'Background color "white".+'), # color is not HTML encoded
809+
((1, 2),'#AABBCCDDEE', 'Background color.+AABBCCDDEE.+'), # color uses alpha channel
810+
((1, 2),'#GGGGGG', 'Background color.+GGGGGG.+'), # color out of range
811+
])
812+
def test_paint_background_exceptions(pos, color, match):
813+
test_layout = """
814+
##################
815+
#.#... .##. y#
816+
# # # . .### #x#
817+
# ####. . #
818+
# . .#### #
819+
#a# ###. . # # #
820+
#b .##. ...#.#
821+
##################
822+
"""
823+
parsed = parse_layout(test_layout)
824+
825+
def overlay_team(bot, state):
826+
bot.paint_background(pos, color)
827+
return bot.position
828+
829+
state = setup_game([overlay_team, overlay_team], max_rounds=2, layout_dict=parsed, raise_bot_exceptions=True)
830+
with pytest.raises(PelitaBotError, match=match):
831+
state = play_turn(state, raise_bot_exceptions=True)
832+
833+
834+
def test_paint_background_doesnt_overwrite():
835+
# check that we do override color for a coordinate if already set and
836+
# that we don't touch other properties that may be defined on that coordinate
837+
test_layout = """
838+
##################
839+
#.#... .##. y#
840+
# # # . .### #x#
841+
# ####. . #
842+
# . .#### #
843+
#a# ###. . # # #
844+
#b .##. ...#.#
845+
##################
846+
"""
847+
parsed = parse_layout(test_layout)
848+
849+
pos = (0, 1)
850+
color = '#FFFFFF'
851+
def overlay_team(bot, state):
852+
bot.paint_background(pos, '#AABBCC')
853+
bot._overlay[pos].update({'text' : 'something'})
854+
bot.paint_background(pos, color)
855+
return bot.position
856+
857+
state = setup_game([overlay_team, overlay_team], max_rounds=2, layout_dict=parsed, raise_bot_exceptions=True)
858+
859+
for idx in range(4):
860+
state = play_turn(state)
861+
gs_overlay = state['overlays'][idx]
862+
assert gs_overlay == [{'pos':(0,1), 'color':'#FFFFFF', 'text':'something'}]
863+
864+
865+
def test_paint_background_different_bots():
866+
test_layout = """
867+
##################
868+
#.#... .##. y#
869+
# # # . .### #x#
870+
# ####. . #
871+
# . .#### #
872+
#a# ###. . # # #
873+
#b .##. ...#.#
874+
##################
875+
"""
876+
parsed = parse_layout(test_layout)
877+
878+
# blue team
879+
pos0 = (0, 0)
880+
color0 = '#FFFFF0'
881+
pos2 = (0, 2)
882+
color2 = '#FFFFF2'
883+
def overlay_team_blue(bot, state):
884+
if bot.turn == 0:
885+
bot.paint_background(pos0, color0)
886+
else:
887+
bot.paint_background(pos2, color2)
888+
return bot.position
889+
890+
# red team
891+
pos1 = (0, 1)
892+
color1 = '#FFFFF1'
893+
pos3 = (0, 3)
894+
color3 = '#FFFFF3'
895+
def overlay_team_red(bot, state):
896+
if bot.turn == 0:
897+
bot.paint_background(pos1, color1)
898+
else:
899+
bot.paint_background(pos3, color3)
900+
return bot.position
901+
902+
state = setup_game([overlay_team_blue, overlay_team_red], max_rounds=2, layout_dict=parsed, raise_bot_exceptions=True)
903+
904+
pos_color = [(pos0, color0), (pos1, color1), (pos2, color2), (pos3, color3)]
905+
for idx in range(4):
906+
pos, color = pos_color[idx]
907+
state = play_turn(state)
908+
gs_overlay = state['overlays'][idx]
909+
rest_left = state['overlays'][:idx]
910+
rest_right = state['overlays'][idx+1:]
911+
assert gs_overlay == [{'pos' : pos, 'color': color}]
912+
# verify that the overlays get overriden at every turn
913+
for overlay in rest_left+rest_right:
914+
assert overlay == []
915+

0 commit comments

Comments
 (0)