forked from tildetown/town
		
	
		
			
				
	
	
		
			360 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			360 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package main
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"database/sql"
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"log"
 | 
						|
	"os"
 | 
						|
	"path"
 | 
						|
	"strings"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"git.tilde.town/tildetown/town/models"
 | 
						|
	"git.tilde.town/tildetown/town/signup"
 | 
						|
	"github.com/MakeNowJust/heredoc/v2"
 | 
						|
	"github.com/gdamore/tcell/v2"
 | 
						|
	_ "github.com/mattn/go-sqlite3"
 | 
						|
	"github.com/rivo/tview"
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	maxInputLength = 10000
 | 
						|
	logDir         = "/town/var/signups/log"
 | 
						|
)
 | 
						|
 | 
						|
type scene struct {
 | 
						|
	Name        string
 | 
						|
	Description string
 | 
						|
	Host        *character
 | 
						|
	SorryMsg    string
 | 
						|
	Input       *bytes.Buffer
 | 
						|
	OnAdvance   func(*scene)
 | 
						|
	OnMsg       func(*scene, *tview.TextView, string)
 | 
						|
}
 | 
						|
 | 
						|
func newScene(name, desc, sorryMsg string, host *character, onAdvance func(*scene), onMsg func(*scene, *tview.TextView, string)) *scene {
 | 
						|
	return &scene{
 | 
						|
		Name:        name,
 | 
						|
		Description: desc,
 | 
						|
		SorryMsg:    sorryMsg,
 | 
						|
		Host:        host,
 | 
						|
		Input:       bytes.NewBuffer([]byte{}),
 | 
						|
		OnAdvance:   onAdvance,
 | 
						|
		OnMsg:       onMsg,
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
type sceneManager struct {
 | 
						|
	scenes  []*scene
 | 
						|
	Current *scene
 | 
						|
	index   int
 | 
						|
	output  io.Writer
 | 
						|
	Save    func()
 | 
						|
}
 | 
						|
 | 
						|
func (m *sceneManager) Advance() bool {
 | 
						|
	if m.Current.Name == "done" {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
	if m.Current.Input.Len() == 0 {
 | 
						|
		fmt.Fprintln(m.output, m.Current.Host.Say(m.Current.SorryMsg))
 | 
						|
		return false
 | 
						|
	}
 | 
						|
	m.Current.OnAdvance(m.Current)
 | 
						|
	m.index++
 | 
						|
	m.Current = m.scenes[m.index]
 | 
						|
	if m.Current.Name == "done" {
 | 
						|
		m.Save()
 | 
						|
	}
 | 
						|
	fmt.Fprintln(m.output, heredoc.Doc(`
 | 
						|
 | 
						|
			[purple]----------[-:-:-]
 | 
						|
 | 
						|
		`))
 | 
						|
	fmt.Fprintln(m.output, m.Current.Description)
 | 
						|
	return true
 | 
						|
}
 | 
						|
 | 
						|
func newSceneManager(output io.Writer, scenes []*scene) *sceneManager {
 | 
						|
	return &sceneManager{
 | 
						|
		output:  output,
 | 
						|
		scenes:  scenes,
 | 
						|
		Current: scenes[0],
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
type character struct {
 | 
						|
	Name        string
 | 
						|
	Description string
 | 
						|
}
 | 
						|
 | 
						|
func newCharacter(name, description string) *character {
 | 
						|
	return &character{
 | 
						|
		Name:        name,
 | 
						|
		Description: description,
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (c *character) Say(msg string) string {
 | 
						|
	verb := "says"
 | 
						|
	if c.Name == "you" {
 | 
						|
		verb = "say"
 | 
						|
	}
 | 
						|
	return fmt.Sprintf(`[-:-:b]%s %s:[-:-:-]
 | 
						|
    %s
 | 
						|
`,
 | 
						|
		c.Name,
 | 
						|
		verb,
 | 
						|
		strings.TrimSpace(msg))
 | 
						|
}
 | 
						|
 | 
						|
func main() {
 | 
						|
	logFile := path.Join(logDir, fmt.Sprintf("%d", time.Now().Unix()))
 | 
						|
	logF, err := os.Create(logFile)
 | 
						|
	if err != nil {
 | 
						|
		fmt.Fprintln(os.Stderr, err)
 | 
						|
		os.Exit(1)
 | 
						|
	}
 | 
						|
 | 
						|
	logger := log.New(logF, "", log.Ldate|log.Ltime)
 | 
						|
 | 
						|
	db, err := signup.ConnectDB()
 | 
						|
	if err != nil {
 | 
						|
		fmt.Fprintln(os.Stderr, err)
 | 
						|
		os.Exit(2)
 | 
						|
	}
 | 
						|
 | 
						|
	err = _main(logger, db)
 | 
						|
	if err != nil {
 | 
						|
		fmt.Fprintln(os.Stderr, err)
 | 
						|
		os.Exit(3)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func _main(l *log.Logger, db *sql.DB) error {
 | 
						|
	l.Println("starting a session")
 | 
						|
	pages := tview.NewPages()
 | 
						|
	mainFlex := tview.NewFlex()
 | 
						|
	input := tview.NewTextArea()
 | 
						|
	input.SetBorder(true).SetBorderColor(tcell.ColorPaleTurquoise)
 | 
						|
	input.SetTitle("press enter to send")
 | 
						|
	input.SetMaxLength(2000)
 | 
						|
 | 
						|
	title := tview.NewTextView()
 | 
						|
	title.SetDynamicColors(true)
 | 
						|
	title.SetTextAlign(tview.AlignCenter)
 | 
						|
	title.SetText("[purple]the tilde town sign up portal[-]")
 | 
						|
 | 
						|
	footer := tview.NewTextView()
 | 
						|
	footer.SetText("type /help and press enter to get help. type /quit and press enter to leave.")
 | 
						|
 | 
						|
	msgScroll := tview.NewTextView()
 | 
						|
	msgScroll.SetDynamicColors(true)
 | 
						|
	msgScroll.SetBackgroundColor(tcell.ColorBlack)
 | 
						|
	msgScroll.SetTextColor(tcell.ColorWhite)
 | 
						|
 | 
						|
	mainFlex.SetDirection(tview.FlexRow)
 | 
						|
	mainFlex.AddItem(title, 1, -1, false)
 | 
						|
	mainFlex.AddItem(msgScroll, 0, 1, false)
 | 
						|
	mainFlex.AddItem(input, 4, -1, true)
 | 
						|
	mainFlex.AddItem(footer, 1, -1, false)
 | 
						|
 | 
						|
	pages.AddPage("main", mainFlex, true, true)
 | 
						|
 | 
						|
	app := tview.NewApplication()
 | 
						|
	app.SetRoot(pages, true)
 | 
						|
 | 
						|
	player := newCharacter("you", "it's you. how are you?")
 | 
						|
 | 
						|
	su := &models.TownSignup{ID: -1}
 | 
						|
 | 
						|
	save := func() {
 | 
						|
		su.Created = time.Now()
 | 
						|
		err := su.Insert(db)
 | 
						|
 | 
						|
		if err != nil {
 | 
						|
			l.Printf("failed to write to db: %s", err.Error())
 | 
						|
			l.Printf("dumping values: %#v", su)
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	scenes := []*scene{
 | 
						|
		newScene("start", heredoc.Doc(`
 | 
						|
				You open your eyes.
 | 
						|
 | 
						|
				You're in some kind of workshop. 
 | 
						|
				Wires and computers in various state of disrepair are strewn across
 | 
						|
				tables and shelves. It smells faintly of burnt cedar.
 | 
						|
 | 
						|
				The wires and components before you slowly drag 
 | 
						|
				themselves into the shape of a small humanoid.
 | 
						|
 | 
						|
				[-:-:b]wire guy says:[-:-:-]
 | 
						|
					hello, welcome to the application for membership in tilde town.
 | 
						|
					first, please let me know what a good [-:-:b]email address[-:-:-] is for you?
 | 
						|
 | 
						|
					just say it out loud. as many times as you need. to get it right.
 | 
						|
 | 
						|
					once you've told me your email you can [-:-:b]/nod[-:-:-] to move on.
 | 
						|
			`),
 | 
						|
			"i'm sorry, before going further could you share an email with me?",
 | 
						|
			newCharacter("wire guy", "a lil homonculus made of discarded computer cables"),
 | 
						|
			func(s *scene) { su.Email = string(s.Input.Bytes()) },
 | 
						|
			func(s *scene, tv *tview.TextView, msg string) {
 | 
						|
				// TODO could check and see if it's email shaped and admonish if not
 | 
						|
				trimmed := strings.TrimSpace(msg)
 | 
						|
				fmt.Fprintln(tv, s.Host.Say(fmt.Sprintf("I heard '%s'. Is that right?", trimmed)))
 | 
						|
			}),
 | 
						|
		newScene("how", heredoc.Doc(`
 | 
						|
				The workshop fades away. You hear the sound of a dial up modem 
 | 
						|
				in the distance.
 | 
						|
 | 
						|
				Trees spring up out of the ground around you: birches, oaks, maples,
 | 
						|
				firs, yews, pines, cypresses complete with tiny swamps around their trunks,
 | 
						|
				junipers, redwoods, cedars, towering palms waving gently in a breeze, eucalyptus, 
 | 
						|
				banyan. the smell is riotous like a canvas with all the colors splashed on. birds 
 | 
						|
				start to sing.
 | 
						|
 | 
						|
				a shrike alights on a branch in front of you.
 | 
						|
 | 
						|
				[-:-:b]the shrike says:[-:-:-]
 | 
						|
					phweeturpff. how did you find out about the town? did anyone refer you?
 | 
						|
 | 
						|
					just say your answer out loud. when you've said what you want, [-:-:b]/nod[-:-:-]
 | 
						|
					to continue.
 | 
						|
			`),
 | 
						|
			"phweeturpff",
 | 
						|
			newCharacter("the shrike", "a little grey bird. it has a pretty song."),
 | 
						|
			func(s *scene) { su.How = string(s.Input.Bytes()) }, nil),
 | 
						|
		newScene("what", heredoc.Doc(`
 | 
						|
				You sink down into soft mossy floor of the forest. You find yourself floating in darkness.
 | 
						|
 | 
						|
				At the far reaches of your vision you can make out a faint neon grid. Around you
 | 
						|
				float pieces of consumer electronic appliances from 1980s Earth. A VCR approaches
 | 
						|
				you and speaks, flapping its tape slot cover with each word.
 | 
						|
 | 
						|
				[-:-:b]the vcr says:[-:-:-]
 | 
						|
					welcome! thank you for coming this far. just two questions left. what about
 | 
						|
					tilde town interests you? what kind of stuff might you want to get up to here?
 | 
						|
 | 
						|
					as usual, just say your answer. when you're satisfied, please [-:-:b]/nod[-:-:-]
 | 
						|
			`),
 | 
						|
			"hmm did you say something?",
 | 
						|
			newCharacter("the vcr", "a black and grey VCR from 1991"),
 | 
						|
			func(s *scene) { su.Why = string(s.Input.Bytes()) }, nil),
 | 
						|
		newScene("link", heredoc.Doc(`
 | 
						|
				You realize your eyes have been shut. You open them and, in an instant,
 | 
						|
				the neon grid and polygons are gone. You're in a convenience store. Outside
 | 
						|
				it's dark besides a single pool of light coming from a street lamp. it's illuminating
 | 
						|
				some litter and a rusty, blue 1994 pontiac grand am.
 | 
						|
 | 
						|
				The shelves around you are stocked with products you've never heard of before like
 | 
						|
				Visible Pants, Petty Burgers, Gentle Rice, Boo Sponge, Power Banjo, Superware, Kneephones,
 | 
						|
				and Diet Coagulator. A mop is mopping the floor and turns to you.
 | 
						|
 | 
						|
				[-:-:b]the mop says:[-:-:-]
 | 
						|
					swishy slop. last question. where online can we get to know you? do you have a personal 
 | 
						|
					website or social media presence? we'll take whatever.
 | 
						|
 | 
						|
					say some links and words out loud, you know the drill.
 | 
						|
 | 
						|
					when you're happy you can submit this whole experience with a [-:-:b]/nod[-:-:-]
 | 
						|
			`),
 | 
						|
			"just the one last thing please",
 | 
						|
			newCharacter("the mop", "a greying mop with a wooden handle."),
 | 
						|
			func(s *scene) { su.Links = string(s.Input.Bytes()) }, nil),
 | 
						|
		newScene("done", heredoc.Doc(`
 | 
						|
				thank you for applying to tilde.town! 
 | 
						|
 | 
						|
				please be on the look out for an email from [-:-:b]root@tilde.town[-:-:-]. 
 | 
						|
				it's almost certain that it will end up in your spam filter, unfortunately.
 | 
						|
 | 
						|
				every application is reviewed by a human. it can take up to 30 days for
 | 
						|
				an application to be reviewed. not every application is approved.
 | 
						|
 | 
						|
				you can [-:-:b]/quit[-:-:-] now
 | 
						|
 | 
						|
				ok bye have a good one~
 | 
						|
			`),
 | 
						|
			"",
 | 
						|
			newCharacter("the æther", "the very air around you"),
 | 
						|
			nil, nil),
 | 
						|
	}
 | 
						|
 | 
						|
	sm := newSceneManager(msgScroll, scenes)
 | 
						|
	sm.Save = save
 | 
						|
 | 
						|
	handleInput := func(msg string) {
 | 
						|
		msg = strings.TrimSpace(msg)
 | 
						|
		if msg == "" {
 | 
						|
			return
 | 
						|
		}
 | 
						|
		if strings.HasPrefix(msg, "/") {
 | 
						|
			split := strings.Split(msg, " ")
 | 
						|
			if len(split) > 0 {
 | 
						|
				msg = split[0]
 | 
						|
			}
 | 
						|
			switch strings.TrimPrefix(msg, "/") {
 | 
						|
			case "help":
 | 
						|
				fmt.Fprintln(msgScroll, sm.Current.Host.Say(`some artificial beings will guide you through applying to tilde.town.
 | 
						|
    type things, then press enter. to take an action, put a "/" before a word.
 | 
						|
    for example typing:
 | 
						|
    /nod
 | 
						|
    and pressing enter will cause you to nod. some other verbs: /quit /look`))
 | 
						|
			case "quit":
 | 
						|
				l.Println("got /quit")
 | 
						|
				app.Stop()
 | 
						|
			case "look":
 | 
						|
				fmt.Fprintln(msgScroll, "")
 | 
						|
				fmt.Fprintln(msgScroll, sm.Current.Description)
 | 
						|
			case "nod":
 | 
						|
				if !sm.Advance() {
 | 
						|
					fmt.Fprintln(msgScroll, "you nod, but nothing happens.")
 | 
						|
					fmt.Fprintln(msgScroll)
 | 
						|
				} else {
 | 
						|
					l.Println("advancing scene")
 | 
						|
				}
 | 
						|
			}
 | 
						|
			return
 | 
						|
		}
 | 
						|
		if sm.Current.Input.Len() > maxInputLength {
 | 
						|
			fmt.Fprintln(msgScroll,
 | 
						|
				sm.Current.Host.Say(
 | 
						|
					"sorry I've heard more than I can remember :( maybe it's time to move on"))
 | 
						|
			return
 | 
						|
		}
 | 
						|
		fmt.Fprintln(msgScroll, player.Say(msg))
 | 
						|
		fmt.Fprintln(sm.Current.Input, msg)
 | 
						|
		if sm.Current.OnMsg != nil {
 | 
						|
			sm.Current.OnMsg(sm.Current, msgScroll, msg)
 | 
						|
		}
 | 
						|
		msgScroll.ScrollToEnd()
 | 
						|
	}
 | 
						|
 | 
						|
	defer func() {
 | 
						|
		l.Println("exiting")
 | 
						|
		db.Close()
 | 
						|
	}()
 | 
						|
 | 
						|
	app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
 | 
						|
		switch event.Key() {
 | 
						|
		case tcell.KeyEnter:
 | 
						|
			handleInput(input.GetText())
 | 
						|
			input.SetText("", false)
 | 
						|
			return nil
 | 
						|
		}
 | 
						|
 | 
						|
		return event
 | 
						|
	})
 | 
						|
 | 
						|
	app.SetAfterDrawFunc(func(_ tcell.Screen) {
 | 
						|
		fmt.Fprintln(msgScroll, sm.Current.Description)
 | 
						|
		app.SetAfterDrawFunc(nil)
 | 
						|
	})
 | 
						|
 | 
						|
	return app.Run()
 | 
						|
}
 |