Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First-class movie creation mode #542

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions pelita/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@


class TkViewer:
def __init__(self, *, address, controller, geometry=None, delay=None, stop_after=None):
self.proc = self._run_external_viewer(address, controller, geometry=geometry, delay=delay, stop_after=stop_after)
def __init__(self, *, address, controller, geometry=None, delay=None, stop_after=None, snapshot_folder=None):
self.proc = self._run_external_viewer(address, controller, geometry=geometry, delay=delay, stop_after=stop_after, snapshot_folder=snapshot_folder)

def _run_external_viewer(self, subscribe_sock, controller, geometry, delay, stop_after):
def _run_external_viewer(self, subscribe_sock, controller, geometry, delay, stop_after, snapshot_folder):
# Something on OS X prevents Tk from running in a forked process.
# Therefore we cannot use multiprocessing here. subprocess works, though.
viewer_args = [ str(subscribe_sock) ]
Expand All @@ -51,6 +51,8 @@ def _run_external_viewer(self, subscribe_sock, controller, geometry, delay, stop
viewer_args += ["--delay", str(delay)]
if stop_after is not None:
viewer_args += ["--stop-after", str(stop_after)]
if snapshot_folder is not None:
viewer_args += ["--snapshot-folder", str(snapshot_folder)]

tkviewer = 'pelita.scripts.pelita_tkviewer'
external_call = [sys.executable,
Expand Down Expand Up @@ -234,12 +236,14 @@ def setup_viewers(viewers=None, options=None, print_result=True):
proc = TkViewer(address=zmq_publisher.socket_addr, controller=viewer_state['controller'].socket_addr,
stop_after=options.get('stop_at'),
geometry=options.get('geometry'),
delay=options.get('delay'))
delay=options.get('delay'),
snapshot_folder=options.get('snapshot_folder'))
else:
proc = TkViewer(address=zmq_publisher.socket_addr, controller=None,
stop_after=options.get('stop_at'),
geometry=options.get('geometry'),
delay=options.get('delay'))
delay=options.get('delay'),
snapshot_folder=options.get('snapshot_folder'))

else:
raise ValueError(f"Unknown viewer {viewer}.")
Expand Down
4 changes: 4 additions & 0 deletions pelita/scripts/pelita_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ def long_help(s):
metavar='REPLAYFILE', dest='replayfile', const='pelita.dump', nargs='?')
parser.add_argument('--store-output', help=long_help('Write all player’s stdout/stderr to the given folder (must exist)'),
metavar='FOLDER')
parser.add_argument('--snapshot-folder', help=long_help('Store the replay in this folder'),
metavar='FOLDER', dest='snapshot_folder')
parser.add_argument('--list-layouts', action='store_true',
help='List all available built-in layouts.')
parser.add_argument('--check-team', action="store_true",
Expand Down Expand Up @@ -216,6 +218,8 @@ def main():
viewers.append(('write-replay-to', args.write_replay))

if args.replayfile:
if args.snapshot_folder:
viewer_options['snapshot_folder'] = args.snapshot_folder
viewer_state = game.setup_viewers(viewers, options=viewer_options)
if game.controller_exit(viewer_state, await_action='set_initial'):
sys.exit(0)
Expand Down
5 changes: 4 additions & 1 deletion pelita/scripts/pelita_tkviewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ def geometry_string(s):
help='delay')
parser.add_argument('--stop-after', type=int, metavar="N",
help='Stop after N rounds.')
parser.add_argument('--snapshot-folder', help='Store thw replay in this folder',
metavar='FOLDER', dest='snapshot_folder')
parser._optionals = parser.add_argument_group('Options')
parser.add_argument('--version', help='show the version number and exit',
action='store_const', const=True)
Expand All @@ -56,7 +58,8 @@ def main():
'controller_address': args.controller_address,
'geometry': args.geometry,
'delay': args.delay,
'stop_after': args.stop_after
'stop_after': args.stop_after,
'snapshot_folder': args.snapshot_folder
}
v = TkViewer(**{k: v for k, v in list(tkargs.items()) if v is not None})
v.run()
Expand Down
66 changes: 65 additions & 1 deletion pelita/ui/tk_canvas.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import logging
from pathlib import Path
import time

import zmq
Expand All @@ -15,6 +16,16 @@

_logger = logging.getLogger(__name__)

NINJA_TEMPLATE = r"""
rule convert
command = convert -alpha off -density 800 -resize 25% -format png $in -append $out

rule ffmpeg
command = ffmpeg -y -framerate 10 -i snap-%04d.png -start_number 2 -s:v 1424x778 -c:v libx264 -profile:v high -crf 20 -pix_fmt yuv420p $out

build movie.mp4: ffmpeg {pngs}
{converts}
"""

def guess_size(display_string, bounding_width, bounding_height, rel_size=0):
no_lines = display_string.count("\n") + 1
Expand Down Expand Up @@ -130,7 +141,7 @@ class UI:

class TkApplication:
def __init__(self, master, controller_address=None,
geometry=None, delay=1, stop_after=None):
geometry=None, delay=1, stop_after=None, snapshot_folder=None):
self.master = master
self.master.configure(background="white")

Expand Down Expand Up @@ -178,6 +189,21 @@ def __init__(self, master, controller_address=None,
self.ui.status_canvas = tkinter.Frame(master, height=25)
self.ui.status_canvas.config(background="white")

if snapshot_folder:
self.snapshot_mode = True
self.snapshot_folder = Path(snapshot_folder)
try:
self.snapshot_folder.mkdir()
except FileExistsError:
pass
#self.quit()
#raise RuntimeError("Folder ‘{}’ already exists. Exiting.".format(snapshot_folder)) from None
self.snapshot_count = 0
else:
self.snapshot_mode = False
self.snapshot_folder = None
self.snapshot_count = 0

self.ui.game_canvas = tkinter.Canvas(master)
self.ui.game_canvas.config(background="white", bd=0, highlightthickness=0, relief='ridge')
self.ui.game_canvas.bind('<Configure>', lambda e: master.after_idle(self.update))
Expand Down Expand Up @@ -335,6 +361,8 @@ def __init__(self, master, controller_address=None,
if self.controller_socket:
self.master.after_idle(self.request_initial)

if self.snapshot_mode:
self.ui.status_canvas.pack_forget()

def init_mesh(self, game_state):
width, height = game_state['shape']
Expand Down Expand Up @@ -391,6 +419,12 @@ def update(self, game_state=None):
self.mesh_graph.screen_width = self.ui.game_canvas.winfo_width()
self.mesh_graph.screen_height = self.ui.game_canvas.winfo_height()

if self.snapshot_mode:
# Ideal scaling should be 24
width = game_state['shape'][0] * 24
height = game_state['shape'][1] * 24 + self.ui.header_canvas.winfo_height()
self.master.geometry('{width}x{height}'.format(width=width, height=height))

if self.mesh_graph.screen_width < 600:
if self._default_font.cget('size') != 8:
self._default_font.configure(size=8)
Expand Down Expand Up @@ -419,6 +453,25 @@ def update(self, game_state=None):

self.size_changed = False

if self.snapshot_mode:
# we need to ensure that the geometry is correct before taking a snapshot
if (not game_state['shape'][0] * 24 == self.ui.game_canvas.winfo_width() and
not game_state['shape'][1] * 24 == self.ui.game_canvas.winfo_height()):
self.master.after_idle(self.update)
return

if game_state['team_names'][0] is None and game_state['team_names'][1] is None:
return

header = self.snapshot_folder / 'snap-{:04}.header.ps'.format(self.snapshot_count)
canvas = self.snapshot_folder / 'snap-{:04}.canvas.ps'.format(self.snapshot_count)
state = self.snapshot_folder / 'snap-{:04}.state.json'.format(self.snapshot_count)
ninja = self.snapshot_folder / 'build.ninja'
canvas.write_text(self.ui.game_canvas.postscript(colormode='color'))
header.write_text(self.ui.header_canvas.postscript(colormode='color'))
state.write_text(json.dumps(game_state))
self.snapshot_count += 1

def draw_universe(self, game_state):
self.mesh_graph.num_x = game_state['shape'][0]
self.mesh_graph.num_y = game_state['shape'][1]
Expand Down Expand Up @@ -957,6 +1010,17 @@ def on_quit(self):
""" override for things which must be done when we exit.
"""
self.running = False

if self.snapshot_mode:
snap_ids = ['{:04}'.format(snap_id) for snap_id in range(self.snapshot_count)]
headers = map('snap-{}.header.ps'.format, snap_ids)
canvas = map('snap-{}.canvas.ps'.format, snap_ids)
pngs = list(map('snap-{}.png'.format, snap_ids))
ninja = self.snapshot_folder / 'build.ninja'
converts = "\n".join(map(lambda phc: "build {}: convert {} {}".format(*phc), zip(pngs, headers, canvas)))
ninja_str = NINJA_TEMPLATE.format(pngs=" ".join(pngs), converts=converts)
ninja.write_text(ninja_str)

if self.controller_socket:
_logger.debug('---> exit')
self.controller_socket.send_json({"__action__": "exit"})
Expand Down
6 changes: 4 additions & 2 deletions pelita/ui/tk_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,13 @@ class TkViewer:
app : The TkApplication class

"""
def __init__(self, address, controller_address=None, geometry=None, delay=1, stop_after=None):
def __init__(self, address, controller_address=None, geometry=None, delay=1, stop_after=None, snapshot_folder=None):
self.address = address
self.controller_address = controller_address
self.delay = delay
self.geometry = geometry if geometry else (900, 580)
self.stop_after = stop_after
self.snapshot_folder = snapshot_folder

self.context = zmq.Context()
self.socket = self.context.socket(zmq.SUB)
Expand Down Expand Up @@ -111,7 +112,8 @@ def run(self):
controller_address=self.controller_address,
geometry=self.geometry,
delay=self.delay,
stop_after=self.stop_after)
stop_after=self.stop_after,
snapshot_folder=self.snapshot_folder)
# schedule next read
self.root.after_idle(self.read_queue)
try:
Expand Down