Skip to content
This repository has been archived by the owner on Oct 11, 2023. It is now read-only.

Tutorial world, full implementation #266

Merged
merged 13 commits into from
Apr 12, 2022
3 changes: 3 additions & 0 deletions deploy/web/gameapp/src/pages/GamePage/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,9 @@ const Chat = ({
React.useEffect(() => {
const defaultEmoji = "❓";
let characterEmoji = DefaultEmojiMapper(persona.name);
if (characterEmoji === undefined) {
characterEmoji = persona.name;
}
if (persona === null || persona.name === null) return;
//const skipWords = ["a", "the", "an", "of", "with", "holding"];
const tryPickEmojis = !persona
Expand Down
35 changes: 35 additions & 0 deletions deploy/web/server/game_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from light.graph.builders.starspace_all import StarspaceBuilder
from light.graph.builders.map_json_builder import MapJsonBuilder
from light.graph.builders.tutorial_builder import TutorialWorldBuilder
from light.world.souls.repeat_soul import RepeatSoul
from light.world.souls.models.generative_heuristic_model_soul import (
GenerativeHeuristicModelSoul,
Expand Down Expand Up @@ -165,3 +166,37 @@ def run_graph_step(self):
ags = self.world.clean_corpses_and_respawn()
for ag in ags:
self.world.purgatory.fill_soul(ag)


class TutorialInstance(GameInstance):
"""
Version of the game meant to run tutorials, not for general play
"""

def __init__(self, game_id, ldb, opt=None):
_, tutorial_world = TutorialWorldBuilder(ldb, opt).get_graph()
self.db = ldb
self._created_time = time.time()
self._player_node = tutorial_world.oo_graph.find_nodes_by_name("You")[0]
self._target_destination = tutorial_world.oo_graph.find_nodes_by_name(
"Ethereal Mist"
)[0]
super().__init__(game_id, ldb, g=tutorial_world, opt=opt)
self._should_shutdown = False
self._did_complete = True

def fill_souls(self, _FLAGS, _model_resources):
"""Tutorials directly register the tutorial to the DM"""
self.world.purgatory.register_filler_soul_provider(
"tutorial", RepeatSoul, lambda: []
)
dm_agent = list(self.world.oo_graph.agents.values())[1]
assert dm_agent.name == "Dungeon Master", "Did not find DM!"
self.world.purgatory.fill_soul(dm_agent, "tutorial")

def run_graph_step(self):
super().run_graph_step()
self._did_complete = self._player_node.get_room() == self._target_destination
self._should_shutdown = (
len(self.players) == 0 and time.time() - self._created_time > 60
)
37 changes: 34 additions & 3 deletions deploy/web/server/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
Rule,
RuleRouter,
)
from deploy.web.server.game_instance import GameInstance
from deploy.web.server.game_instance import GameInstance, TutorialInstance
from deploy.web.server.tornado_server import TornadoPlayerFactory
from light.graph.builders.user_world_builder import UserWorldBuilder

Expand All @@ -34,6 +34,7 @@ class RegistryApplication(tornado.web.Application):
def __init__(self, FLAGS, ldb, model_resources, tornado_settings):
self.game_instances = {}
self.step_callbacks = {}
self.tutorial_map = {} # Player ID to game ID
self.model_resources = model_resources
self.FLAGS = FLAGS
self.ldb = ldb
Expand All @@ -43,7 +44,7 @@ def __init__(self, FLAGS, ldb, model_resources, tornado_settings):

def get_handlers(self, FLAGS, ldb, tornado_settings):
self.tornado_provider = TornadoPlayerFactory(
self.game_instances,
self,
FLAGS.hostname,
FLAGS.port,
given_tornado_settings=tornado_settings,
Expand Down Expand Up @@ -86,7 +87,6 @@ def cleanup_games(self):
del self.game_instances[game_id]

def run_new_game(self, game_id, ldb, player_id=None, world_id=None):

if world_id is not None and player_id is not None:
builder = UserWorldBuilder(ldb, player_id=player_id, world_id=world_id)
_, world = builder.get_graph()
Expand All @@ -104,6 +104,37 @@ def run_new_game(self, game_id, ldb, player_id=None, world_id=None):
self.step_callbacks[game_id].start()
return game

def run_tutorial(self, user_id, on_complete):
game_id = get_rand_id()

game = TutorialInstance(game_id, self.ldb, opt=vars(self.FLAGS))
game.fill_souls(self.FLAGS, self.model_resources)
world = game.world

def run_or_cleanup_world():
game.run_graph_step()
if game._should_shutdown or game._did_complete:
if game._did_complete:
with self.ldb as ldb:
flags = ldb.get_user_flags(user_id)
flags.completed_onboarding = True
ldb.set_user_flags(user_id, flags)
on_complete()
self.step_callbacks[game_id].stop()
del self.step_callbacks[game_id]
# TODO delete this game instance
del self.game_instances[game_id]
del self.tutorial_map[user_id]

self.game_instances[game_id] = game
self.tutorial_map[user_id] = game_id
game.register_provider(self.tornado_provider)
self.step_callbacks[game_id] = tornado.ioloop.PeriodicCallback(
run_or_cleanup_world, 125
)
self.step_callbacks[game_id].start()
return game_id


# Default BaseHandler - should be extracted to some util?
class BaseHandler(tornado.web.RequestHandler):
Expand Down
74 changes: 53 additions & 21 deletions deploy/web/server/tornado_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from light.world.player_provider import PlayerProvider
from light.world.quest_loader import QuestLoader
from light.graph.events.graph_events import init_safety_classifier, RewardEvent
from light.world.souls.tutorial_player_soul import TutorialPlayerSoul

import argparse
import inspect
Expand Down Expand Up @@ -56,6 +57,7 @@
quest_loader = QuestLoader(QUESTS_LOCATION)
else:
quest_loader = None
TRANSITION_AFTER_TUTORIAL = 8
here = os.path.abspath(os.path.dirname(__file__))

_seen_warnings = set()
Expand Down Expand Up @@ -210,6 +212,28 @@ def check_origin(self, origin):
def set_player(self, player):
self.player = player

def user_should_do_tutorial(self, user_id):
with self.db as ldb:
flags = ldb.get_user_flags(user_id)
return not flags.completed_onboarding

def launch_game_for_user(self, user_id, game_id):
# Check for custom game world
if game_id not in self.app.registry.game_instances:
self.close()
# TODO: Have an error page about game deleted
self.redirect("/game/")
graph_purgatory = self.app.registry.game_instances[game_id].world.purgatory
if self.alive:
new_player = TornadoPlayerProvider(
self,
graph_purgatory,
db=self.db,
user=user_id,
)
new_player.init_soul()
self.app.registry.game_instances[game_id].players.append(new_player)

def open(self, game_id):
"""
Open a websocket, validated either by a valid user cookie or
Expand Down Expand Up @@ -239,21 +263,21 @@ def open(self, game_id):
if user is not None:
logging.info("Opened new socket from ip: {}".format(self.request.remote_ip))
logging.info("For game: {}".format(game_id))
if game_id not in self.app.graphs:
self.close()
# TODO: Have an error page about game deleted
self.redirect("/game/")
graph_purgatory = self.app.graphs[game_id].world.purgatory
if self.alive:
new_player = TornadoPlayerProvider(
self,
graph_purgatory,
db=self.db,
user=user,
context=preauth_context,
)
new_player.init_soul()
self.app.graphs[game_id].players.append(new_player)
# First check for tutorials
if self.user_should_do_tutorial(user):
# Spawn a tutorial world for this user, or inject them into
# their existing world
if user in self.app.registry.tutorial_map:
game_id = self.app.registry.tutorial_map[user]
else:
orig_game_id = game_id

def on_complete():
time.sleep(TRANSITION_AFTER_TUTORIAL)
self.launch_game_for_user(user, orig_game_id)

game_id = self.app.registry.run_tutorial(user, on_complete)
self.launch_game_for_user(user, game_id)
else:
self.close()
self.redirect("/#/login")
Expand Down Expand Up @@ -616,7 +640,11 @@ def set_current_user(self, user):
with self.db as ldb:
_ = ldb.create_user(user)
self.set_secure_cookie(
"user", tornado.escape.json_encode(user), domain=self.hostname
"user",
tornado.escape.json_encode(user),
domain=self.hostname,
secure=True,
httponly=True,
)
else:
self.clear_cookie("user")
Expand Down Expand Up @@ -653,7 +681,11 @@ def post(self):
def set_current_user(self, user):
if user:
self.set_secure_cookie(
"user", tornado.escape.json_encode(user), domain=self.hostname
"user",
tornado.escape.json_encode(user),
domain=self.hostname,
# secure=True, login handler is for local testing
httponly=True,
)
else:
self.clear_cookie("user")
Expand Down Expand Up @@ -780,7 +812,7 @@ def on_reap_soul(self, soul):
json.dumps({"command": "actions", "data": [dat]})
)
if self.user is not None:
if self.db is not None:
if self.db is not None and not isinstance(soul, TutorialPlayerSoul):
with self.db as ldb:
ldb.store_agent_score(soul.target_node, self.user)
self.app.user_node_map[self.user] = None
Expand All @@ -795,14 +827,14 @@ class TornadoPlayerFactory:

def __init__(
self,
graphs,
registry,
hostname=DEFAULT_HOSTNAME,
port=DEFAULT_PORT,
db=None,
listening=False,
given_tornado_settings=None,
):
self.graphs = graphs
self.registry = registry
self.app = None
self.db = db

Expand All @@ -815,7 +847,7 @@ def _run_server():
self.app = Application(
given_tornado_settings=given_tornado_settings, db=self.db
)
self.app.graphs = self.graphs
self.app.registry = self.registry
if listening:
self.app.listen(port, max_buffer_size=1024 ** 3)
print(
Expand Down
47 changes: 46 additions & 1 deletion light/data_model/light_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from light.data_model.conversation_checkpoint_parser import ConversationCheckpointParser
from light.data_model.environment_checkpoint_parser import EnvironmentCheckpointParser
from light.graph.utils import get_article
from light.data_model.onboarding_flags import OnboardingFlags
import sys
import parlai.utils.misc as parlai_utils
import json
Expand Down Expand Up @@ -277,6 +278,7 @@ def __init__(self, dbpath, read_only=False):
self.init_game_tables()
self.create_triggers()
self.check_custom_tags_objects_tables()
self.check_user_onboarding_tags()

# Dictionaries to convert between previous pickle IDs and current
# database IDs
Expand Down Expand Up @@ -1425,7 +1427,8 @@ def init_user_tables(self):
CREATE TABLE IF NOT EXISTS user_table (
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
extern_id text UNIQUE NOT NULL,
username text);
username text,
onboarding_flags integer DEFAULT 0);
"""
)

Expand Down Expand Up @@ -2206,6 +2209,24 @@ def check_custom_tags_objects_tables(self):
self.c.execute("ALTER TABLE objects_table ADD COLUMN shape;")
self.c.execute("ALTER TABLE objects_table ADD COLUMN value;")

def check_user_onboarding_tags(self):
"""
Check if the tables has the attrs related to the custom tagged attributes. If not, add them.
"""

# Check the table for size, contain_size, shape, value columns and add if nonexistent
# this should be deprecated soon when a legacy table is opened
has_flags_column = dict(
self.c.execute(
" SELECT COUNT(*) AS CNTREC FROM pragma_table_info('user_table') WHERE name='onboarding_flags' "
).fetchone()
)["CNTREC"]

if not has_flags_column:
self.c.execute(
"ALTER TABLE user_table ADD COLUMN onboarding_flags INTEGER DEFAULT 0;"
)

def add_single_conversation(self, room, participants, turns):
"""
Adds a single conversation to the database
Expand Down Expand Up @@ -3582,6 +3603,30 @@ def get_user_id(self, extern_id):
id = int(result[0][0])
return id

def get_user_flags(self, extern_id):
self.c.execute(
"""
SELECT onboarding_flags from user_table WHERE extern_id = ?
""",
(extern_id,),
)
result = self.c.fetchall()
assert len(result) == 1
flags = int(result[0][0])
return OnboardingFlags(flags)

def set_user_flags(self, user, flags: "OnboardingFlags"):
if flags.flag_did_update():
new_flags = flags.get_flag()
self.c.execute(
"""
UPDATE user_table
SET onboarding_flags = ?
WHERE extern_id = ?
""",
(new_flags, user),
)

def initialize_agent_score(self, target_node, user):
"""
Initialize this agent with the current scores written in the database for the given user
Expand Down
Loading