-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #10 from psyomn/feature/tinystory
Tinystory
- Loading branch information
Showing
14 changed files
with
599 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }} • {{ . }} {{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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
} | ||
} |
Oops, something went wrong.