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

Retire title. Add minimal story and menu #2

Merged
merged 2 commits into from Nov 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 11 additions & 5 deletions src/lib/engine/actions.ts
@@ -1,9 +1,15 @@
import { Passage, PromptAction, TellAction, TitleAction } from "./types";
import { Id, Passage } from "./types";

export function* title(passage: Passage): Generator<TitleAction, void, void> {
yield {
kind: "title",
passage,
export interface TellAction {
kind: "tell";
passage: Passage;
}

export interface PromptAction<ChoiceId extends Id> {
kind: "prompt";
passage: Passage;
choices: {
[id in ChoiceId]?: Passage;
};
}

Expand Down
@@ -1,5 +1,4 @@
import { title } from "../actions";
import { ActionSequence, Id, Passage } from "../types";
import { Action, ActionSequence, Id, Passage } from "../types";

/** Types and utilities to create an ActionSequence structured around multiple
* rooms in a world with global state.
Expand Down Expand Up @@ -34,30 +33,44 @@ type RoomStoryOptions<
rooms: RoomLookup<RoomId, WorldState>;
};

/** Modifies the passage in a tell or prompt, adding a header. */
export function addRoomHeader<RoomId extends string>(
action: Action,
roomWorldState: RoomWorldState<RoomId>
): Action {
const { roomTitles, currentRoomId } = roomWorldState;
const title = roomTitles[currentRoomId];
return {
...action,
passage: (
<>
<h3>{title}</h3>
{action.passage}
</>
),
};
}

/** ActionSequence delegating story sequences to rooms */
export function* roomStory<
RoomId extends Id,
WorldState extends RoomWorldState<RoomId>
>(options: RoomStoryOptions<RoomId, WorldState>): ActionSequence<void> {
const { rooms, worldState } = options;

let destination: RoomId | typeof END = worldState.currentRoomId;
let room = rooms[worldState.currentRoomId];

// keep visiting destinations until you reach the end
for (;;) {
// room sequence returns destination or END
const roomIdOrEnd = yield* room(worldState);

// check if story is complete
if (destination === END) {
yield* title(<>Game Over</>);
if (roomIdOrEnd === END) {
return;
}

worldState.currentRoomId = destination;
yield* title(worldState.roomTitles[destination]);

// retrieve the next room
const room = rooms[worldState.currentRoomId];

// complete room tell/prompt sequence and get next room
destination = yield* room(worldState);
// else prepare for next room sequence
room = rooms[roomIdOrEnd];
worldState.currentRoomId = roomIdOrEnd;
}
}
33 changes: 8 additions & 25 deletions src/lib/engine/types.ts
@@ -1,35 +1,18 @@
import { DelegatingGenerator } from "../util";
import { tell, title, prompt } from "./actions";
import { DelegatingGenerator, GYielded } from "../util";
import { tell, prompt } from "./actions";

export type Id = string;

export type Passage = JSX.Element;

export interface TitleAction {
kind: "title";
passage: Passage;
}

export interface TellAction {
kind: "tell";
passage: Passage;
}
export type ActionDelegator =
| ReturnType<typeof tell>
| ReturnType<typeof prompt>;

export interface PromptAction<ChoiceId extends Id> {
kind: "prompt";
passage: Passage;
choices: {
[id in ChoiceId]?: Passage;
};
}
export type Action = GYielded<ActionDelegator>;

/** An ActionSequence is a sequence of calls to title, tell, prompt, which
* finally results in some value that serves the logic of its calling generator. */
export type ActionSequence<Ret> = DelegatingGenerator<
| ReturnType<typeof title>
| ReturnType<typeof tell>
| ReturnType<typeof prompt>,
Ret
>;
* eventually returns a Ret to its yieldee. */
export type ActionSequence<Ret> = DelegatingGenerator<ActionDelegator, Ret>;

export type Story = () => ActionSequence<void>;
24 changes: 0 additions & 24 deletions src/lib/frontend/components/End.tsx

This file was deleted.

7 changes: 2 additions & 5 deletions src/lib/frontend/components/Reader.tsx
Expand Up @@ -3,7 +3,6 @@ import { useSelected } from "@lauf/store-react";
import { unhandled } from "../../util";
import { ReaderStoreContext } from "../context";
import { ReaderState } from "../types";
import { End } from "./End";
import { Prompt } from "./Prompt";
import { Tell } from "./Tell";

Expand All @@ -16,10 +15,8 @@ function renderKind(kind: PageKind) {
return <Tell />;
} else if (kind === "prompt") {
return <Prompt />;
} else if (kind === "end") {
return <End />;
}
unhandled(kind);
}
kind satisfies never;
}

export function Reader(props: { readerStore: Store<ReaderState> }) {
Expand Down
19 changes: 0 additions & 19 deletions src/lib/frontend/read.tsx
Expand Up @@ -12,29 +12,10 @@ export async function readStory(story: Story, store: Store<ReaderState>) {
// get the next action
const { done, value } = sequence.next(nextValue);
if (done) {
// render end page, await restart callback
await new Promise<void>((resolve) => {
store.write({
...store.read(),
page: {
kind: "end",
restart: resolve,
},
});
});
// sequence has ended with `value`
return value;
}

if (value.kind === "title") {
// render title
store.write({
...store.read(),
title: value.passage,
});
continue;
}

if (value.kind === "tell") {
// render tell page, await turnPage callback
await new Promise<void>((resolve) => {
Expand Down
10 changes: 3 additions & 7 deletions src/lib/frontend/types.ts
@@ -1,4 +1,5 @@
import { Id, Passage, PromptAction, TellAction } from "../engine/types";
import { PromptAction, TellAction } from "../engine/actions";
import { Id, Passage } from "../engine/types";

/** Compose a state combining the last action from the story with a callback
* which progresses to the next page or choice . */
Expand All @@ -7,11 +8,6 @@ interface EmptyState {
kind: "empty";
}

interface EndState {
kind: "end";
restart: () => void;
}

type TellState = TellAction & {
turnPage: () => void;
};
Expand All @@ -20,7 +16,7 @@ type PromptState<ChoiceId extends Id> = PromptAction<ChoiceId> & {
selectChoice: (choice: ChoiceId) => void;
};

type PageState = TellState | PromptState<Id> | EmptyState | EndState;
type PageState = TellState | PromptState<Id> | EmptyState;

export type PageKind = PageState["kind"];

Expand Down
35 changes: 35 additions & 0 deletions src/lib/util.ts
@@ -1,3 +1,38 @@
export function decorateSequence<
G extends Generator<Yielded>,
Yielded,
Decorated
>(
generator: G,
decorate: (yielded: Yielded) => Decorated
): Generator<Decorated, GReturned<G>, GNexted<G>> {
function mapResult(result: IteratorResult<Yielded, GReturned<G>>) {
const { value, done } = result;
if (!done) {
return {
done,
value: decorate(value),
};
}
return result;
}

return {
[Symbol.iterator]() {
return this;
},
next(...args) {
return mapResult(generator.next(...args));
},
return(value) {
return mapResult(generator.return(value));
},
throw(e) {
return mapResult(generator.throw(e));
},
};
}

export function createSatisfies<Base>() {
return <Actual extends Base>(value: Actual) => value;
}
Expand Down
4 changes: 2 additions & 2 deletions src/main.tsx
Expand Up @@ -6,15 +6,15 @@ import { initialiseReaderState } from "./lib/frontend/context";
import { Reader } from "./lib/frontend/components/Reader";

// Load the story
import { story } from "./stories/cloak-of-darkness";
import { menu } from "./stories/menu";

// create the watchable ReaderState
const readerStore = createStore(initialiseReaderState());

async function readForever() {
for (;;) {
// perform ActionSequence, updating the ReaderState
await readStory(story, readerStore);
await readStory(menu, readerStore);
}
}

Expand Down
23 changes: 17 additions & 6 deletions src/stories/cloak-of-darkness.tsx
@@ -1,7 +1,8 @@
import type { ActionSequence, Story } from "../lib/engine/types";
import type { RoomWorldState } from "../lib/engine/formats/room";
import { roomStory, END } from "../lib/engine/formats/room";
import type { Action, ActionSequence, Story } from "../lib/engine/types";
import { addRoomHeader, RoomWorldState } from "../lib/engine/extensions/room";
import { roomStory, END } from "../lib/engine/extensions/room";
import { tell, prompt } from "../lib/engine/actions";
import { decorateSequence } from "../lib/util";

/** Locations in the story. */
type RoomId = "outside" | "lobby" | "cloakroom" | "bar";
Expand Down Expand Up @@ -212,16 +213,18 @@ export const lightBar: Room = function* (state) {
return END;
};

/** Delegates to roomStory() which yields title/tell/prompt actions, combining the
* rooms into a navigable world having shared global state. */
/** Delegates to a room based sequence to yield title/tell/prompt actions. These
* combine the rooms into a navigable world having shared global state. */
export const story: Story = function* () {
// populate the rooms (a lookup used for navigation)
const rooms = {
outside,
lobby,
cloakroom,
bar,
};

// initial world (state that can drive story logic)
const worldState: WorldState = {
turnsInBar: 0,
hasCloak: true,
Expand All @@ -234,8 +237,16 @@ export const story: Story = function* () {
},
};

yield* roomStory({
// roomStory is a navigation-based interaction for all room-based stories
const sequence = roomStory({
rooms,
worldState,
});

//decorate each tell or prompt with a header naming the room
const headedSequence = decorateSequence(sequence, (action: Action) =>
addRoomHeader(action, worldState)
);

yield* headedSequence;
};
54 changes: 54 additions & 0 deletions src/stories/goodbye-world.tsx
@@ -0,0 +1,54 @@
import type { Story } from "../lib/engine/types";
import { prompt, tell } from "../lib/engine/actions";
import { unhandled } from "../lib/util";

export const story: Story = function* () {
yield* tell(
<>
<h1>Goodbye World</h1>
<>By Cefn Hoile</>
</>
);
yield* tell(
<>
You wake bruised in a darkened, musty room with the freshness of the sound
of running water
</>
);
const choice = yield* prompt(
<>
You've no idea how you arrived here, but you are so groggy and exhausted
you're not sure if you can even stand up.
</>,
{
run: <>Run away</>,
sleep: <>Go back to sleep</>,
}
);
if (choice === "sleep") {
yield* tell(
<>
<h1>You win!</h1>
<>
You go back to sleep and die peacefully from Carbon Monoxide
poisoning, thanks to a poorly maintained gas fire. Not a bad way to
go.
</>
</>
);
return;
}
if (choice === "run") {
yield* tell(
<>
<h1>You lose!</h1>
<>
You run away and die violently in an accident with agricultural
machinery. What a horrible way to go.
</>
</>
);
return;
}
choice satisfies never
};