Skip to content

Commit

Permalink
Merge pull request #10 from psyomn/feature/tinystory
Browse files Browse the repository at this point in the history
Tinystory
  • Loading branch information
psyomn authored Apr 18, 2021
2 parents aa196ee + aebdd5c commit 5709515
Show file tree
Hide file tree
Showing 14 changed files with 599 additions and 0 deletions.
37 changes: 37 additions & 0 deletions tinystory/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# tinystory

This is a very simple implementation of an interactive story teller. I
currently have two people in mind that will only be able to have minor
control over things with their hands -- so basically probably will be
able to use a phone in one hand, and limited as well (probably only
thumb control).

Hence, to provide maybe something somewhat fun, I thought of coming up
with a very cheap prototype where interactive stories could be put
together very quickly, and inserted into a service (like a web
server), where said phones can point to and run the interactive
stories.

The stories should be of this form:

```
you see a platypus. you choose to:
- greet it
- ignore it
- scream at it
```

So the interface should be very simple (buttons, that lead you to
different parts of the story).

# build / run

Run `make` in the root directory. Then you can run the binary in this
directory. You just need to pass the proper flags to the binary to
point to a story repository, and an assets directory (with the html
templates).

# todo

Since we're dealing with graphs, might be nice to have some checks to
make sure that certain nodes are not orphaned.
14 changes: 14 additions & 0 deletions tinystory/assets/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<html>
<head/>

<body>
<h1> Welcome! </h1>

<p> List of stories: </p>
<ul>
{{range .Items}}<li> [<a href="/story/{{ .Index }}/0/">read</a>] {{ .Title }} </li>{{else}}<li> no stories </li>{{end}}
</ul>

</body>

</html>
28 changes: 28 additions & 0 deletions tinystory/assets/story.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<html>
<head/>

<body>
<h1> the platypus war: part CXXII (TODO) </h1>
<p><i> authors: {{ range .Authors }} &bull; {{ . }} {{else}} no authors specified {{end}} </i></p>
{{ if .Website }}
<p><a href="{{ .Website }}">{{ .Website }}</a></p>
{{ else }}
{{ end }}

<hr/>

<p> {{ .Fragment.Content }} </p>

{{ range .Fragment.Choices }}
<form action="/story/{{ $.StoryIndex }}/{{ .Index }}">
<input type="submit" value="{{ .Description }}">
</form>
{{ else }}
<p> THE END </p>
{{ end }}

<hr/>
<p><a href="/">back to story directory</a></p>

</body>
</html>
9 changes: 9 additions & 0 deletions tinystory/lib/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package tinystory

import (
"errors"
)

var (
ErrNoTokens = errors.New("no tokens have been provided")
)
96 changes: 96 additions & 0 deletions tinystory/lib/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* A very simple parser that relies on stories that are written in
* json
*/
package tinystory

import (
"encoding/json"
"io/fs"
"path"
"path/filepath"

"github.com/psyomn/ecophagy/common"
)

type Choice struct {
Description string
Index int
}

func (s *Choice) UnmarshalJSON(data []byte) error {
elements := []interface{}{&s.Description, &s.Index}
return json.Unmarshal(data, &elements)
}

type StoryFragment struct {
Index int
Content string
Choices []Choice
}

func (s *StoryFragment) UnmarshalJSON(data []byte) error {
elements := []interface{}{&s.Index, &s.Content, &s.Choices}
return json.Unmarshal(data, &elements)
}

type Document struct {
Title string `json:"title"`
Authors []string `json:"authors"`
Website string `json:"website"`
Fragments []StoryFragment `json:"story"`
}

func Parse(bjson []byte) (*Document, error) {
doc := &Document{}

err := json.Unmarshal(bjson, &doc)
if err != nil {
return nil, err
}

return doc, nil
}

func ParseAllInDir(dirpath string) ([]Document, error) {
docs := make([]Document, 0, 256)

err := filepath.Walk(dirpath, func(currpath string, info fs.FileInfo, err error) error {
if err != nil {
return err
}

if path.Ext(currpath) != ".json" {
return nil
}

if info.IsDir() {
return nil
}

if !info.Mode().IsRegular() {
return nil
}

data, err := common.FileToBytes(currpath)
if err != nil {
//nolint
return nil
}

document, err := Parse(data)
if err != nil {
return err
}

docs = append(docs, *document)

return nil
})

if err != nil {
return nil, err
}

return docs, nil
}
26 changes: 26 additions & 0 deletions tinystory/lib/parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package tinystory

import (
"testing"

"github.com/psyomn/ecophagy/common"
)

const fixture = "../stories/simple.json"

func TestTinyStoryParserTitle(t *testing.T) {
data, err := common.FileToBytes(fixture)

if err != nil {
t.Fatalf("no such file: %s", fixture)
}

document, err := Parse(data)
if err != nil {
t.Fatalf("error: %s", err.Error())
}

if document == nil {
t.Fatalf("document must not be nil")
}
}
145 changes: 145 additions & 0 deletions tinystory/lib/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package tinystory

import (
"fmt"
"html/template"
"log"
"net/http"
"path"
"strconv"
"strings"

"github.com/psyomn/ecophagy/common"
)

type Server struct {
visitor *Visitor
httpServer *http.Server

indexTemplate *template.Template
storyTemplate *template.Template
}

const indexFilename = "index.html"
const storyFilename = "story.html"

func ServerNew(sess *Session, documents []Document) (*Server, error) {
muxer := http.NewServeMux()

var indexTemplate *template.Template
{
data, err := common.FileToBytes(path.Join(sess.Assets, indexFilename))
if err != nil {
return nil, err
}

maybeIndex, err := template.New("index").Parse(string(data))
if err != nil {
return nil, err
}
indexTemplate = maybeIndex
}

var storyTemplate *template.Template
{
data, err := common.FileToBytes(path.Join(sess.Assets, storyFilename))
if err != nil {
return nil, err
}

maybeStory, err := template.New("story").Parse(string(data))
if err != nil {
return nil, err
}
storyTemplate = maybeStory
}

server := &Server{
indexTemplate: indexTemplate,
storyTemplate: storyTemplate,
httpServer: &http.Server{
Addr: sess.Host + ":" + sess.Port,
Handler: muxer,
},
visitor: VisitorNew(documents),
}

muxer.HandleFunc("/", server.HandleRoot)
muxer.HandleFunc("/story/", server.HandleStory)

return server, nil
}

func (s *Server) Start() error {
log.Println("starting server...")
return s.httpServer.ListenAndServe()
}

func (s *Server) HandleRoot(w http.ResponseWriter, r *http.Request) {
listing := struct {
Items []IndexListing
}{
Items: s.visitor.GetIndexListing(),
}

if err := s.indexTemplate.Execute(w, listing); err != nil {
fmt.Println("error writing template: ", err)
}
}

func (s *Server) HandleStory(w http.ResponseWriter, r *http.Request) {
var storyIndex, nodeIndex int
{
thinPath := strings.TrimPrefix(r.URL.Path, "/story/")
parts := strings.Split(thinPath, "/")

if len(parts) < 2 {
renderError(w, "badly formed path")
return
}

maybeStoryIndex, err := strconv.Atoi(parts[0])
if err != nil {
renderError(w, "stories should be numeric")
return
}

maybeNodeIndex, err := strconv.Atoi(parts[1])
if err != nil {
renderError(w, "parts should be numeric")
return
}

storyIndex = maybeStoryIndex
nodeIndex = maybeNodeIndex
}

// TODO: make a safe index check here with min(len(documents), actual)

responseData := struct {
Title string
Authors []string
Website string
Fragment StoryFragment
StoryIndex int
NodeIndex int
}{
Title: s.visitor.Documents[storyIndex].Title,
Authors: s.visitor.Documents[storyIndex].Authors,
Website: s.visitor.Documents[storyIndex].Website,
Fragment: s.visitor.Documents[storyIndex].Fragments[nodeIndex],
StoryIndex: storyIndex,
}

if err := s.storyTemplate.Execute(w, responseData); err != nil {
renderError(w, "problem getting that fragment")
return
}
}

func renderError(w http.ResponseWriter, str string) {
w.WriteHeader(http.StatusBadRequest)
if _, err := w.Write([]byte(str)); err != nil {
fmt.Printf("problem writing error: %s\n", err.Error())
}
}
19 changes: 19 additions & 0 deletions tinystory/lib/session.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package tinystory

type Session struct {
Host string
Port string
Repository string
Assets string

ExperimentalParser string
}

func MakeDefaultSession() *Session {
return &Session{
Host: "127.0.0.1",
Port: "9090",
Repository: "./stories",
Assets: "./assets",
}
}
Loading

0 comments on commit 5709515

Please sign in to comment.