Skip to content

Commit

Permalink
Refactoring of the rendering stuff.
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcCote committed Feb 12, 2020
1 parent d8eb94e commit 1362665
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 108 deletions.
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,19 @@ Or, after cloning the repo, go inside the root folder of the project (i.e. along

pip install .

#### Extras
#### Visualization

In order to use the `take_screenshot` or `visualize` functions in `textworld.render`, you'll need to install either the [Chrome](https://sites.google.com/a/chromium.org/chromedriver/) or [Firefox](https://github.com/mozilla/geckodriver) webdriver (depending on which browser you have installed).
TextWorld comes with some tools to visualize game states. Make sure all dependencies are installed by running

pip install textworld[vis]

Then, you will need to install either the [Chrome](https://sites.google.com/a/chromium.org/chromedriver/) or [Firefox](https://github.com/mozilla/geckodriver) webdriver (depending on which browser you have currently installed).
If you have Chrome already installed you can use the following command to install chromedriver

pip install chromedriver_installer

Current visualization tools include: `take_screenshot`, `visualize` and `show_graph` from [`textworld.render`](https://textworld.readthedocs.io/en/latest/textworld.render.html).

## Usage

### Generating a game
Expand All @@ -47,7 +53,6 @@ TextWorld provides an easy way of generating simple text-based games via the `tw

where `custom` indicates we want to customize the game using the following options: `--world-size` controls the number of rooms in the world, `--nb-objects` controls the number of objects that can be interacted with (excluding doors) and `--quest-length` controls the minimum number of commands that is required to type in order to win the game. Once done, the game `custom_game.ulx` will be saved in the `tw_games/` folder.


### Playing a game (terminal)

To play a game, one can use the `tw-play` script. For instance, the command to play the game generated in the previous section would be
Expand All @@ -56,6 +61,12 @@ To play a game, one can use the `tw-play` script. For instance, the command to p

> **Note:** Only Z-machine's games (*.z1 through *.z8) and Glulx's games (*.ulx) are supported.
To visualize the game state while playing, use the `--viewer [port]` option.

tw-play tw_games/custom_game.ulx --viewer

A new browser tab should open and track your progress in the game.

### Playing a game (Python + [Gym](https://github.com/openai/gym))

Here's how you can interact with a text-based game from within Python using OpenAI's Gym framework.
Expand Down
3 changes: 2 additions & 1 deletion textworld/envs/wrappers/tests/test_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@

import textworld

from textworld.utils import make_temp_directory, get_webdriver
from textworld.utils import make_temp_directory
from textworld.generator import compile_game
from textworld.envs.wrappers import HtmlViewer
from textworld.render import get_webdriver


def test_html_viewer():
Expand Down
7 changes: 4 additions & 3 deletions textworld/envs/wrappers/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from typing import Tuple

from textworld.core import Environment, GameState, Wrapper
from textworld.render.serve import VisualizationService
from textworld.render import WebdriverNotFoundError


class HtmlViewer(Wrapper):
Expand Down Expand Up @@ -85,11 +87,10 @@ def reset(self) -> GameState:

self._stop_server() # In case it is still running.
try:
from textworld.render.serve import VisualizationService
self._server = VisualizationService(game_state, self.open_automatically)
self._server.start(threading.current_thread(), port=self._port)
except ModuleNotFoundError as e:
print("Importing HtmlViewer without installed dependencies. Try re-installing textworld.")
except WebdriverNotFoundError as e:
print("Missing dependencies for using HtmlViewer. See 'Visualization' section of TextWorld's README.md")
raise e

return game_state
Expand Down
3 changes: 3 additions & 0 deletions textworld/render/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT license.


from textworld.render.render import WebdriverNotFoundError
from textworld.render.render import get_webdriver
from textworld.render.render import load_state, load_state_from_game_state, visualize
115 changes: 105 additions & 10 deletions textworld/render/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@


import io
import os
import time
import json
import tempfile
from os.path import join as pjoin
Expand All @@ -15,11 +17,30 @@
from textworld.logic import Proposition, Action
from textworld.logic import State
from textworld.generator import World, Game
from textworld.utils import maybe_mkdir, get_webdriver
from textworld.utils import maybe_mkdir, check_modules

from textworld.generator.game import EntityInfo
from textworld.generator.data import KnowledgeBase

from textworld.render.serve import get_html_template

# Try importing optional libraries.
missing_modules = []
try:
import webbrowser
except ImportError:
missing_modules.append("webbrowser")

try:
from PIL import Image
except ImportError:
missing_modules.append("pillow")

try:
from selenium import webdriver
except ImportError:
missing_modules.append("selenium")


XSCALE, YSCALE = 6, 3

Expand Down Expand Up @@ -98,7 +119,7 @@ def load_state_from_game_state(game_state: GameState, format: str = 'png', limit
:return: The graph generated from this World
"""
game_infos = game_state.game.infos
game_infos["objective"] = game_state.objective
game_infos["objective"] = game_state.objective # TODO: should not modify game.infos inplace!
last_action = game_state.last_action
# Create a world from the current state's facts.
world = World.from_facts(game_state._facts)
Expand Down Expand Up @@ -234,6 +255,7 @@ def used_pos():
# Objective
if "objective" in game_infos:
result["objective"] = game_infos["objective"]
del game_infos["objective"] # TODO: objective should not be part of game_infos in the first place.

# Objects
all_items = {}
Expand Down Expand Up @@ -319,8 +341,7 @@ def take_screenshot(url: str, id: str = 'world'):
:param id: ID of DOM element.
:return: Image object.
"""
from PIL import Image

check_modules(["pillow"], missing_modules)
driver = get_webdriver()

driver.get(url)
Expand All @@ -340,7 +361,8 @@ def take_screenshot(url: str, id: str = 'world'):


def concat_images(*images):
from PIL import Image
check_modules(["pillow"], missing_modules)

widths, heights = zip(*(i.size for i in images))
total_width = sum(widths)
max_height = max(heights)
Expand All @@ -355,6 +377,83 @@ def concat_images(*images):
return new_im


class WebdriverNotFoundError(Exception):
pass


def which(program):
"""
helper to see if a program is in PATH
:param program: name of program
:return: path of program or None
"""
def is_exe(fpath):
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)

fpath, _ = os.path.split(program)
if fpath:
if is_exe(program):
return program
else:
for path in os.environ["PATH"].split(os.pathsep):
exe_file = os.path.join(path, program)
if is_exe(exe_file):
return exe_file

return None


def get_webdriver(path=None):
"""
Get the driver and options objects.
:param path: path to browser binary.
:return: driver
"""
check_modules(["selenium", "webdriver"], missing_modules)

def chrome_driver(path=None):
import urllib3
from selenium.webdriver.chrome.options import Options
options = Options()
options.add_argument('headless')
options.add_argument('ignore-certificate-errors')
options.add_argument("test-type")
options.add_argument("no-sandbox")
options.add_argument("disable-gpu")
if path is not None:
options.binary_location = path

SELENIUM_RETRIES = 10
SELENIUM_DELAY = 3 # seconds
for _ in range(SELENIUM_RETRIES):
try:
return webdriver.Chrome(chrome_options=options)
except urllib3.exceptions.ProtocolError: # https://github.com/SeleniumHQ/selenium/issues/5296
time.sleep(SELENIUM_DELAY)

raise ConnectionResetError('Cannot connect to Chrome, giving up after {SELENIUM_RETRIES} attempts.')

def firefox_driver(path=None):
from selenium.webdriver.firefox.options import Options
options = Options()
options.add_argument('headless')
driver = webdriver.Firefox(firefox_binary=path, options=options)
return driver

driver_mapping = {
'geckodriver': firefox_driver,
'chromedriver': chrome_driver,
'chromium-driver': chrome_driver
}

for driver in driver_mapping.keys():
found = which(driver)
if found is not None:
return driver_mapping.get(driver, None)(path)

raise WebdriverNotFoundError("Chrome/Chromium/FireFox Webdriver not found.")


def visualize(world: Union[Game, State, GameState, World],
interactive: bool = False):
"""
Expand All @@ -363,11 +462,7 @@ def visualize(world: Union[Game, State, GameState, World],
:param interactive: Whether or not to visualize the state in the browser.
:return: Image object of the visualization.
"""
try:
import webbrowser
from textworld.render.serve import get_html_template
except ImportError:
raise ImportError('Visualization dependencies not installed. Try running `pip install textworld[vis]`')
check_modules(["webbrowser"], missing_modules)

if isinstance(world, Game):
game = world
Expand Down
Loading

0 comments on commit 1362665

Please sign in to comment.