1
0
Fork 0

give each client a unique id, split up UI sections

This commit is contained in:
Sean Sube 2024-05-05 13:54:39 -05:00
parent f15390bd72
commit 580076335f
Signed by: ssube
GPG Key ID: 3EED7B957D362AF1
10 changed files with 452 additions and 240 deletions

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
adventure/custom_*.py
adventure/custom_*
worlds/
__pycache__/
.env

View File

@ -1,10 +1,10 @@
from functools import partial
from logging import getLogger
from random import random
from typing import Callable, Dict, List, Optional
from functools import partial
from rule_engine import Rule
from pydantic import Field
from rule_engine import Rule
from yaml import Loader, load
from adventure.models import Actor, Item, Room, World, dataclass
@ -65,13 +65,15 @@ def update_attributes(
if rule.rule:
# TODO: pre-compile rules
rule_impl = Rule(rule.rule)
if not rule_impl.matches({
if not rule_impl.matches(
{
"attributes": typed_attributes,
}):
}
):
logger.debug("logic rule did not match attributes: %s", rule.rule)
continue
if rule.match and not(rule.match.items() <= typed_attributes.items()):
if rule.match and not (rule.match.items() <= typed_attributes.items()):
logger.debug("logic did not match attributes: %s", rule.match)
continue
@ -95,15 +97,25 @@ def update_attributes(
return attributes
def update_logic(world: World, step: int, rules: LogicTable, triggers: TriggerTable) -> None:
def update_logic(
world: World, step: int, rules: LogicTable, triggers: TriggerTable
) -> None:
for room in world.rooms:
room.attributes = update_attributes(room, room.attributes, rules=rules, triggers=triggers)
room.attributes = update_attributes(
room, room.attributes, rules=rules, triggers=triggers
)
for actor in room.actors:
actor.attributes = update_attributes(actor, actor.attributes, rules=rules, triggers=triggers)
actor.attributes = update_attributes(
actor, actor.attributes, rules=rules, triggers=triggers
)
for item in actor.items:
item.attributes = update_attributes(item, item.attributes, rules=rules, triggers=triggers)
item.attributes = update_attributes(
item, item.attributes, rules=rules, triggers=triggers
)
for item in room.items:
item.attributes = update_attributes(item, item.attributes, rules=rules, triggers=triggers)
item.attributes = update_attributes(
item, item.attributes, rules=rules, triggers=triggers
)
logger.info("updated world attributes")
@ -138,5 +150,5 @@ def init_from_file(filename: str):
logger.info("initialized logic system")
return (
partial(update_logic, rules=logic_rules, triggers=logic_triggers),
partial(format_logic, rules=logic_rules)
partial(format_logic, rules=logic_rules),
)

View File

@ -89,7 +89,9 @@ def action_use(item: str, target: str) -> str:
"""
_, action_room, action_actor = get_current_context()
available_items = [item.name for item in action_actor.items] + [item.name for item in action_room.items]
available_items = [item.name for item in action_actor.items] + [
item.name for item in action_room.items
]
if item not in available_items:
return f"The {item} item is not available to use."

View File

@ -1,11 +1,14 @@
from json import dumps
from readline import add_history
from logging import getLogger
from queue import Queue
from readline import add_history
from typing import Any, Callable, Dict, List, Sequence
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
from packit.utils import could_be_json
logger = getLogger(__name__)
class BasePlayer:
"""
@ -95,6 +98,8 @@ class LocalPlayer(BasePlayer):
Ask the player for input.
"""
logger.info("prompting local player: {self.name}")
formatted_prompt = prompt.format(**kwargs)
self.memory.append(HumanMessage(content=formatted_prompt))
print(formatted_prompt)
@ -109,7 +114,9 @@ class RemotePlayer(BasePlayer):
input_queue: Queue[str]
send_prompt: Callable[[str, str], bool]
def __init__(self, name: str, backstory: str, send_prompt: Callable[[str, str], bool]) -> None:
def __init__(
self, name: str, backstory: str, send_prompt: Callable[[str, str], bool]
) -> None:
super().__init__(name, backstory)
self.input_queue = Queue()
self.send_prompt = send_prompt
@ -123,11 +130,12 @@ class RemotePlayer(BasePlayer):
self.memory.append(HumanMessage(content=formatted_prompt))
try:
logger.info(f"prompting remote player: {self.name}")
if self.send_prompt(self.name, formatted_prompt):
reply = self.input_queue.get(timeout=60)
logger.info(f"got reply from remote player: {reply}")
return self.parse_input(reply)
except Exception:
pass
logger.exception("error getting reply from remote player")
# logger.warning("Failed to send prompt to remote player")
return ""

View File

@ -3,11 +3,13 @@ from collections import deque
from json import dumps, loads
from logging import getLogger
from threading import Thread
from typing import Dict, Tuple
from typing import Dict
from uuid import uuid4
import websockets
from packit.agent import Agent
from adventure.context import get_actor_agent_for_name
from adventure.context import get_actor_agent_for_name, set_actor_agent_for_name
from adventure.models import Actor, Room, World
from adventure.player import RemotePlayer
from adventure.state import snapshot_world, world_json
@ -16,20 +18,28 @@ logger = getLogger(__name__)
connected = set()
characters: Dict[str, RemotePlayer] = {}
previous_agents: Dict[str, Agent] = {}
recent_events = deque(maxlen=100)
recent_world = None
async def handler(websocket):
logger.info("Client connected")
id = uuid4().hex
logger.info("Client connected, given id: %s", id)
connected.add(websocket)
async def next_turn(character: str, prompt: str) -> None:
await websocket.send(connected, dumps({
"type": "turn",
await websocket.send(
dumps(
{
"type": "prompt",
"id": id,
"character": character,
"prompt": prompt,
}))
"actions": [],
}
),
)
def sync_turn(character: str, prompt: str) -> bool:
if websocket not in characters:
@ -39,6 +49,8 @@ async def handler(websocket):
return True
try:
await websocket.send(dumps({"type": "id", "id": id}))
if recent_world:
await websocket.send(recent_world)
@ -55,26 +67,41 @@ async def handler(websocket):
try:
data = loads(message)
if "become" in data:
message_type = data.get("type", None)
if message_type == "player":
character = characters.get(websocket)
if character:
del characters[websocket]
del characters[id]
character_name = data["become"]
actor, _ = get_actor_agent_for_name(character_name)
actor, llm_agent = get_actor_agent_for_name(character_name)
if not actor:
logger.error(f"Failed to find actor {character_name}")
continue
if character_name in [player.name for player in characters.values()]:
if character_name in [
player.name for player in characters.values()
]:
logger.error(f"Character {character_name} is already in use")
continue
characters[websocket] = RemotePlayer(actor.name, actor.backstory, sync_turn)
# player_name = data["player"]
player = RemotePlayer(actor.name, actor.backstory, sync_turn)
characters[id] = player
logger.info(f"Client {websocket} is now character {character_name}")
elif websocket in characters:
player = characters[websocket]
player.input_queue.put(message)
# swap out the LLM agent
set_actor_agent_for_name(actor.name, actor, player)
previous_agents[actor.name] = llm_agent
# notify all clients that this character is now active
send_and_append(
{"type": "player", "name": character_name, "id": id}
)
elif message_type == "input" and id in characters:
player = characters[id]
logger.info("queueing input for player %s: %s", player.name, data)
player.input_queue.put(data["input"])
except Exception:
logger.exception("Failed to parse message")
@ -83,9 +110,14 @@ async def handler(websocket):
connected.remove(websocket)
# TODO: swap out the character for the original agent
# swap out the character for the original agent when they disconnect
if websocket in characters:
del characters[websocket]
player = characters[id]
del characters[id]
actor, _ = get_actor_agent_for_name(player.name)
if actor:
set_actor_agent_for_name(player.name, actor, previous_agents[player.name])
logger.info("Client disconnected")

View File

@ -1,30 +1,31 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useState, useEffect, useRef, MutableRefObject, Fragment } from 'react';
import React, { useState, useEffect, Fragment } from 'react';
import useWebSocketModule, { ReadyState } from 'react-use-websocket';
import { Maybe, doesExist } from '@apextoaster/js-utils';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import Divider from '@mui/material/Divider';
import ListItemText from '@mui/material/ListItemText';
import ListItemAvatar from '@mui/material/ListItemAvatar';
import Typography from '@mui/material/Typography';
import Avatar from '@mui/material/Avatar';
import Container from '@mui/material/Container';
import Stack from '@mui/material/Stack';
import Alert from '@mui/material/Alert';
import Switch from '@mui/material/Switch';
import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView';
import { TreeItem } from '@mui/x-tree-view/TreeItem';
import { Button, CssBaseline, Dialog, DialogContent, DialogTitle, PaletteMode, ThemeProvider, createTheme } from '@mui/material';
import {
Button,
CssBaseline,
Dialog,
DialogContent,
DialogTitle,
PaletteMode,
ThemeProvider,
createTheme,
List,
Divider,
Typography,
Container,
Stack,
Alert,
Switch,
} from '@mui/material';
import { formatters } from './format.js';
import { Room, Actor, Item, World, WorldPanel, SetDetails } from './world.js';
import { EventItem } from './events.js';
import { PlayerPanel } from './player.js';
const useWebSocket = (useWebSocketModule as any).default;
export interface EventItemProps {
event: any;
}
const statusStrings = {
[ReadyState.CONNECTING]: 'Connecting',
[ReadyState.OPEN]: 'Running',
@ -38,190 +39,10 @@ export function interleave(arr: Array<any>) {
return arr.reduce((acc, val, idx) => acc.concat(val, <Divider component='li' key={`sep-${idx}`} variant='inset' />), []).slice(0, -1);
}
export function ActionItem(props: EventItemProps) {
const { event } = props;
const { actor, room, type } = event;
const content = formatters[type](event);
return <ListItem alignItems="flex-start">
<ListItemAvatar>
<Avatar alt={actor} src="/static/images/avatar/1.jpg" />
</ListItemAvatar>
<ListItemText
primary={room}
secondary={
<React.Fragment>
<Typography
sx={{ display: 'block' }}
component="span"
variant="body2"
color="text.primary"
>
{actor}
</Typography>
{content}
</React.Fragment>
}
/>
</ListItem>;
}
export function WorldItem(props: EventItemProps) {
const { event } = props;
const { step, world } = event;
const { theme } = world;
return <ListItem alignItems="flex-start">
<ListItemAvatar>
<Avatar alt={step.toString()} src="/static/images/avatar/1.jpg" />
</ListItemAvatar>
<ListItemText
primary={theme}
secondary={
<Typography
sx={{ display: 'block' }}
component="span"
variant="body2"
color="text.primary"
>
Step {step}
</Typography>
}
/>
</ListItem>;
}
export function MessageItem(props: EventItemProps) {
const { event } = props;
const { message } = event;
return <ListItem alignItems="flex-start">
<ListItemAvatar>
<Avatar alt="System" src="/static/images/avatar/1.jpg" />
</ListItemAvatar>
<ListItemText
primary="System"
secondary={
<Typography
sx={{ display: 'block' }}
component="span"
variant="body2"
color="text.primary"
>
{message}
</Typography>
}
/>
</ListItem>;
}
export function EventItem(props: EventItemProps) {
const { event } = props;
const { type } = event;
switch (type) {
case 'action':
case 'result':
return <ActionItem event={event} />;
case 'event':
return <MessageItem event={event} />;
case 'world':
return <WorldItem event={event} />;
default:
return <ListItem>
<ListItemText primary={`Unknown event type: ${type}`} />
</ListItem>;
}
}
export interface AppProps {
socketUrl: string;
}
export interface Item {
name: string;
description: string;
}
export interface Actor {
name: string;
description: string;
items: Array<Item>;
}
export interface Room {
name: string;
description: string;
portals: Record<string, string>;
actors: Array<Actor>;
items: Array<Item>;
}
export interface World {
name: string;
order: Array<string>;
rooms: Array<Room>;
theme: string;
}
export type SetDetails = (entity: Maybe<Item | Actor | Room>) => void;
export function ItemItem(props: { item: Item; setDetails: SetDetails }) {
const { item, setDetails } = props;
return <TreeItem itemId={item.name} label={item.name}>
<TreeItem itemId={`${item.name}-details`} label="Details" onClick={() => setDetails(item)} />
</TreeItem>;
}
export function ActorItem(props: { actor: Actor; setDetails: SetDetails }) {
const { actor, setDetails } = props;
return <TreeItem itemId={actor.name} label={actor.name}>
<TreeItem itemId={`${actor.name}-details`} label="Details" onClick={() => setDetails(actor)} />
<TreeItem itemId={`${actor.name}-items`} label="Items">
{actor.items.map((item) => <ItemItem item={item} setDetails={setDetails} />)}
</TreeItem>
</TreeItem>;
}
export function RoomItem(props: { room: Room; setDetails: SetDetails }) {
const { room, setDetails } = props;
return <TreeItem itemId={room.name} label={room.name}>
<TreeItem itemId={`${room.name}-details`} label="Details" onClick={() => setDetails(room)} />
<TreeItem itemId={`${room.name}-actors`} label="Actors">
{room.actors.map((actor) => <ActorItem actor={actor} setDetails={setDetails} />)}
</TreeItem>
<TreeItem itemId={`${room.name}-items`} label="Items">
{room.items.map((item) => <ItemItem item={item} setDetails={setDetails} />)}
</TreeItem>
</TreeItem>;
}
export function WorldPanel(props: { world: Maybe<World>; setDetails: SetDetails }) {
const { world, setDetails } = props;
// eslint-disable-next-line no-restricted-syntax
if (!doesExist(world)) {
return <Typography variant="h4">
No world data available
</Typography>;
}
return <Stack direction="column" sx={{ minWidth: 600 }}>
<Typography variant="h4">
World: {world.name}
</Typography>
<Typography variant="h6">
Theme: {world.theme}
</Typography>
<SimpleTreeView>
{world.rooms.map((room) => <RoomItem room={room} setDetails={setDetails} />)}
</SimpleTreeView>
</Stack>;
}
export function EntityDetails(props: { entity: Maybe<Item | Actor | Room>; close: () => void }) {
// eslint-disable-next-line no-restricted-syntax
if (!doesExist(props.entity)) {
@ -251,11 +72,28 @@ export function DetailDialog(props: { setDetails: SetDetails; details: Maybe<Ite
}
export function App(props: AppProps) {
const [ activeTurn, setActiveTurn ] = useState<boolean>(false);
const [ detailEntity, setDetailEntity ] = useState<Maybe<Item | Actor | Room>>(undefined);
const [ character, setCharacter ] = useState<Maybe<Actor>>(undefined);
const [ clientId, setClientId ] = useState<string>('');
const [ world, setWorld ] = useState<Maybe<World>>(undefined);
const [ themeMode, setThemeMode ] = useState('light');
const [ history, setHistory ] = useState<Array<string>>([]);
const { lastMessage, readyState } = useWebSocket(props.socketUrl);
const { lastMessage, readyState, sendMessage } = useWebSocket(props.socketUrl);
function setPlayer(actor: Maybe<Actor>) {
setCharacter(actor);
if (doesExist(actor)) {
sendMessage(JSON.stringify({ type: 'player', become: actor.name }));
}
}
function sendInput(input: string) {
if (doesExist(character)) {
sendMessage(JSON.stringify({ type: 'input', input }));
}
}
const theme = createTheme({
palette: {
@ -268,12 +106,37 @@ export function App(props: AppProps) {
useEffect(() => {
if (doesExist(lastMessage)) {
const data = JSON.parse(lastMessage.data);
if (data.type === 'id') {
setClientId(data.id);
return;
}
if (data.type === 'prompt') {
if (data.id === clientId) {
// notify the player and show the prompt
setActiveTurn(true);
} else {
const message = `Waiting for ${data.character} to take their turn`;
setHistory((prev) => prev.concat(message));
}
return;
}
setHistory((prev) => prev.concat(data));
// if we get a world event, update the last world state
if (data.type === 'world') {
setWorld(data.world);
}
if (data.type === 'player' && data.id === clientId) {
// find the actor that matches the player name
const { name } = data;
// eslint-disable-next-line no-restricted-syntax
const actor = world?.rooms.flatMap((room) => room.actors).find((a) => a.name === name);
setCharacter(actor);
}
}
}, [lastMessage]);
@ -298,7 +161,10 @@ export function App(props: AppProps) {
</Stack>
</Alert>
<Stack direction="row">
<WorldPanel world={world} setDetails={setDetailEntity} />
<Stack direction="column" spacing={2} sx={{ minWidth: 600 }}>
<WorldPanel world={world} activeCharacter={character} setDetails={setDetailEntity} setPlayer={setPlayer} />
<PlayerPanel actor={character} activeTurn={activeTurn} setDetails={setDetailEntity} sendInput={sendInput} />
</Stack>
<Stack direction="column" sx={{ minWidth: 800 }}>
<List sx={{ width: '100%', bgcolor: 'background.paper' }}>
{interleave(items)}

131
client/src/events.tsx Normal file
View File

@ -0,0 +1,131 @@
import { ListItem, ListItemText, ListItemAvatar, Avatar, Typography } from '@mui/material';
import React from 'react';
import { formatters } from './format.js';
export interface EventItemProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
event: any;
}
export function ActionItem(props: EventItemProps) {
const { event } = props;
const { actor, room, type } = event;
const content = formatters[type](event);
return <ListItem alignItems="flex-start">
<ListItemAvatar>
<Avatar alt={actor} src="/static/images/avatar/1.jpg" />
</ListItemAvatar>
<ListItemText
primary={room}
secondary={
<React.Fragment>
<Typography
sx={{ display: 'block' }}
component="span"
variant="body2"
color="text.primary"
>
{actor}
</Typography>
{content}
</React.Fragment>
}
/>
</ListItem>;
}
export function WorldItem(props: EventItemProps) {
const { event } = props;
const { step, world } = event;
const { theme } = world;
return <ListItem alignItems="flex-start">
<ListItemAvatar>
<Avatar alt={step.toString()} src="/static/images/avatar/1.jpg" />
</ListItemAvatar>
<ListItemText
primary={theme}
secondary={
<Typography
sx={{ display: 'block' }}
component="span"
variant="body2"
color="text.primary"
>
Step {step}
</Typography>
}
/>
</ListItem>;
}
export function MessageItem(props: EventItemProps) {
const { event } = props;
const { message } = event;
return <ListItem alignItems="flex-start">
<ListItemAvatar>
<Avatar alt="System" src="/static/images/avatar/1.jpg" />
</ListItemAvatar>
<ListItemText
primary="System"
secondary={
<Typography
sx={{ display: 'block' }}
component="span"
variant="body2"
color="text.primary"
>
{message}
</Typography>
}
/>
</ListItem>;
}
export function PlayerItem(props: EventItemProps) {
const { event } = props;
const { name } = event;
return <ListItem alignItems="flex-start">
<ListItemAvatar>
<Avatar alt={name} src="/static/images/avatar/1.jpg" />
</ListItemAvatar>
<ListItemText
primary="New Player"
secondary={
<Typography
sx={{ display: 'block' }}
component="span"
variant="body2"
color="text.primary"
>
Someone is playing as {name}
</Typography>
}
/>
</ListItem>;
}
export function EventItem(props: EventItemProps) {
const { event } = props;
const { type } = event;
switch (type) {
case 'action':
case 'result':
return <ActionItem event={event} />;
case 'event':
return <MessageItem event={event} />;
case 'player':
return <PlayerItem event={event} />;
case 'world':
return <WorldItem event={event} />;
default:
return <ListItem>
<ListItemText primary={`Unknown event type: ${type}`} />
</ListItem>;
}
}

View File

@ -13,8 +13,12 @@ export function formatAction(data: any) {
}
export function formatInput(data: any) {
try {
const action = formatAction(JSON.parse(data.input));
return `Starting turn: ${action}`;
} catch (err) {
return `Error parsing input: ${err}`;
}
}
export function formatResult(data: any) {

40
client/src/player.tsx Normal file
View File

@ -0,0 +1,40 @@
import React, { useState } from 'react';
import { Maybe } from '@apextoaster/js-utils';
import { Alert, Button, Card, CardContent, Stack, TextField, Typography } from '@mui/material';
import { Actor, SetDetails } from './world.js';
export interface PlayerPanelProps {
actor: Maybe<Actor>;
activeTurn: boolean;
setDetails: SetDetails;
sendInput: (input: string) => void;
}
export function PlayerPanel(props: PlayerPanelProps) {
const { actor, activeTurn, sendInput } = props;
const [input, setInput] = useState<string>('');
// eslint-disable-next-line no-restricted-syntax
if (!actor) {
return <Card>
<CardContent>
<Typography variant="h6">No player character</Typography>
</CardContent>
</Card>;
}
return <Card>
<CardContent>
{activeTurn && <Alert severity="warning">It's your turn!</Alert>}
<Typography variant="h6">Playing as: {actor.name}</Typography>
<Typography variant="body1">{actor.backstory}</Typography>
<Stack direction="row" spacing={2}>
<TextField label="Input" variant="outlined" fullWidth value={input} onChange={(event) => setInput(event.target.value)} />
<Button variant="contained" onClick={() => {
setInput('');
sendInput(input);
}}>Send</Button>
</Stack>
</CardContent>
</Card>;
}

117
client/src/world.tsx Normal file
View File

@ -0,0 +1,117 @@
import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView';
import { TreeItem } from '@mui/x-tree-view/TreeItem';
import { Card, CardContent, CardHeader, Stack, Typography } from '@mui/material';
import { Maybe, doesExist } from '@apextoaster/js-utils';
import React from 'react';
export interface Item {
name: string;
description: string;
}
export interface Actor {
name: string;
backstory: string;
description: string;
items: Array<Item>;
}
export interface Room {
name: string;
description: string;
portals: Record<string, string>;
actors: Array<Actor>;
items: Array<Item>;
}
export interface World {
name: string;
order: Array<string>;
rooms: Array<Room>;
theme: string;
}
export type SetDetails = (entity: Maybe<Item | Actor | Room>) => void;
export type SetPlayer = (actor: Maybe<Actor>) => void;
export interface BaseEntityItemProps {
activeCharacter: Maybe<Actor>;
setDetails: SetDetails;
setPlayer: SetPlayer;
}
export function formatLabel(name: string, active = false): string {
if (active) {
return `${name} (!)`;
}
return name;
}
export function ItemItem(props: { item: Item } & BaseEntityItemProps) {
const { item, setDetails } = props;
return <TreeItem itemId={item.name} label={item.name}>
<TreeItem itemId={`${item.name}-details`} label="Details" onClick={() => setDetails(item)} />
</TreeItem>;
}
export function ActorItem(props: { actor: Actor } & BaseEntityItemProps) {
const { actor, activeCharacter, setDetails, setPlayer } = props;
const active = doesExist(activeCharacter) && actor === activeCharacter;
const label = formatLabel(actor.name, active);
let playButton;
if (active === false) {
playButton = <TreeItem itemId={`${actor.name}-play`} label="Play!" onClick={() => setPlayer(actor)} />;
}
return <TreeItem itemId={actor.name} label={label}>
{playButton}
<TreeItem itemId={`${actor.name}-details`} label="Details" onClick={() => setDetails(actor)} />
<TreeItem itemId={`${actor.name}-items`} label="Items">
{actor.items.map((item) => <ItemItem key={item.name} item={item} activeCharacter={activeCharacter} setDetails={setDetails} setPlayer={setPlayer} />)}
</TreeItem>
</TreeItem>;
}
export function RoomItem(props: { room: Room } & BaseEntityItemProps) {
const { room, activeCharacter, setDetails, setPlayer } = props;
const active = doesExist(activeCharacter) && room.actors.some((it) => it === activeCharacter);
const label = formatLabel(room.name, active);
return <TreeItem itemId={room.name} label={label}>
<TreeItem itemId={`${room.name}-details`} label="Details" onClick={() => setDetails(room)} />
<TreeItem itemId={`${room.name}-actors`} label="Actors">
{room.actors.map((actor) => <ActorItem key={actor.name} actor={actor} activeCharacter={activeCharacter} setDetails={setDetails} setPlayer={setPlayer} />)}
</TreeItem>
<TreeItem itemId={`${room.name}-items`} label="Items">
{room.items.map((item) => <ItemItem key={item.name} item={item} activeCharacter={activeCharacter} setDetails={setDetails} setPlayer={setPlayer} />)}
</TreeItem>
</TreeItem>;
}
export function WorldPanel(props: { world: Maybe<World> } & BaseEntityItemProps) {
const { world, activeCharacter, setDetails, setPlayer } = props;
// eslint-disable-next-line no-restricted-syntax
if (!doesExist(world)) {
return <Typography variant="h4">
No world data available
</Typography>;
}
return <Card>
<CardContent>
<Typography gutterBottom variant="h5" component="div">{world.name}</Typography>
<Typography variant="body1">
Theme: {world.theme}
</Typography>
<SimpleTreeView>
{world.rooms.map((room) => <RoomItem key={room.name} room={room} activeCharacter={activeCharacter} setDetails={setDetails} setPlayer={setPlayer} />)}
</SimpleTreeView>
</CardContent>
</Card>;
}