Compare commits

...

21 Commits

Author SHA1 Message Date
Jordan Orelli ae409fd065 list games command 5 years ago
Jordan Orelli d603c74a18 dying should be more of a ceremony 5 years ago
Jordan Orelli 4e1ade8e12 global index is gone! :party_parrot: 5 years ago
Jordan Orelli 209635083d remove old edges table 5 years ago
Jordan Orelli 25d142fb8a get rid of system distances function 5 years ago
Jordan Orelli ce94d3cdd2 scan and broadcast both work without global index 5 years ago
Jordan Orelli dbb2c86f5a slowly getting rid of global index 5 years ago
Jordan Orelli 5ce9a530f8 player name selection no longer blocks new connections 5 years ago
Jordan Orelli 495f4c3fa2 fix nil panic when lobby connections leave 5 years ago
Jordan Orelli 34a984a397 move connection state to its own file 5 years ago
Jordan Orelli 6cd419d013 needs more explanation 5 years ago
Jordan Orelli bdc2783838 uniform status command for all states 5 years ago
Jordan Orelli 15015a9d5b rename command help to summary 5 years ago
Jordan Orelli aa6bd1ca9e travel progress indicator 5 years ago
Jordan Orelli 8cdd7bdaa6 cleaning up enter travel text 5 years ago
Jordan Orelli 2914275282 adding a status command for every player state 5 years ago
Jordan Orelli 50128a64a6 display travel time in output of "nearby" command 5 years ago
Jordan Orelli b0aaa046ad fix broken help command 5 years ago
Jordan Orelli a330d87b4c refactor lobby state 5 years ago
Jordan Orelli 938348ce26 run multiple games at once 5 years ago
Jordan Orelli e629c0b6b6 get rid of crazy id scheme
that was so unecessary wow
5 years ago

@ -1,24 +1,5 @@
space-dragons-in-outer-space space-dragons-in-outer-space
============================ ============================
there's no url because it's not a web app This is a [real-time strategy
game](https://en.wikipedia.org/wiki/Real-time_strategy) in the style of a [MUD](https://en.wikipedia.org/wiki/MUD).
it's just a tcp server
you go
like this
`nc 104.236.57.163 9220`
and that is how
you be a dragon
in space
in space
in space

@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"strings"
"time" "time"
) )
@ -32,10 +33,10 @@ func (b *Bomb) Dead() bool {
return b.done return b.done
} }
func (b *Bomb) Tick(frame int64) { func (b *Bomb) Tick(game *Game) {
b.fti -= 1 b.fti -= 1
if b.fti <= 0 { if b.fti <= 0 {
b.target.Bombed(b.profile, frame) b.target.Bombed(b.profile, game)
b.done = true b.done = true
log_info("bomb went off on %v", b.target) log_info("bomb went off on %v", b.target)
} }
@ -56,7 +57,6 @@ func MakeBomb(s *System) ConnectionState {
m.CommandSuite = CommandSet{ m.CommandSuite = CommandSet{
balCommand, balCommand,
BroadcastCommand(s), BroadcastCommand(s),
helpCommand,
NearbyCommand(s), NearbyCommand(s),
playersCommand, playersCommand,
} }
@ -78,7 +78,23 @@ func (m *MakeBombState) Tick(c *Connection, frame int64) ConnectionState {
return m return m
} }
func (MakeBombState) String() string { return "Making a Bomb" }
func (m *MakeBombState) Exit(c *Connection) { func (m *MakeBombState) Exit(c *Connection) {
c.bombs += 1 c.bombs += 1
c.Printf("Done! You now have %v bombs.\n", c.bombs) c.Printf("Done! You now have %v bombs.\n", c.bombs)
} }
func (m *MakeBombState) FillStatus(c *Connection, s *status) {
elapsedFrames := c.game.frame - m.start
elapsedDur := framesToDur(elapsedFrames)
desc := fmt.Sprintf(`
Currently making a bomb!
Build time elapsed: %v
Build time remaining: %v
`, elapsedDur, options.makeBombTime-elapsedDur)
s.Description = strings.TrimSpace(desc)
s.Location = m.System.String()
}

@ -9,8 +9,8 @@ type broadcast struct {
start time.Time start time.Time
origin *System origin *System
dist float64 dist float64
nextHitIndex int
message string message string
neighborhood Neighborhood
} }
func NewBroadcast(from *System, template string, args ...interface{}) *broadcast { func NewBroadcast(from *System, template string, args ...interface{}) *broadcast {
@ -21,20 +21,27 @@ func NewBroadcast(from *System, template string, args ...interface{}) *broadcast
} }
} }
func (b *broadcast) Tick(frame int64) { func (b *broadcast) Tick(game *Game) {
if b.neighborhood == nil {
log_info("setting up neighborhood for broadcast: %s", b.message)
b.neighborhood = game.galaxy.Neighborhood(b.origin)
log_info("nearest neighbor: %v", b.neighborhood[0])
}
b.dist += options.lightSpeed b.dist += options.lightSpeed
for ; b.nextHitIndex < len(b.origin.Distances()); b.nextHitIndex += 1 { for len(b.neighborhood) > 0 && b.neighborhood[0].distance <= b.dist {
candidate := b.origin.Distances()[b.nextHitIndex] s := game.galaxy.GetSystemByID(b.neighborhood[0].id)
if b.dist < candidate.dist { log_info("broadcast %s has reached %s from %s", b.message, s, b.origin)
break s.NotifyInhabitants("message received from system %v:\n\t%s\n", b.origin, b.message)
if len(b.neighborhood) > 1 {
b.neighborhood = b.neighborhood[1:]
} else {
b.neighborhood = nil
} }
candidate.s.NotifyInhabitants("message received from system %v:\n\t%s\n", b.origin, b.message)
} }
} }
func (b *broadcast) Dead() bool { func (b *broadcast) Dead() bool { return b.neighborhood == nil }
return b.dist > b.origin.Distances()[len(b.origin.Distances())-1].dist
}
func (b *broadcast) String() string { func (b *broadcast) String() string {
return fmt.Sprintf("[broadcast origin: %v message: %s]", b.origin, b.message) return fmt.Sprintf("[broadcast origin: %v message: %s]", b.origin, b.message)

@ -17,7 +17,6 @@ func MakeColony(c *Connection, sys *System) {
CommandSuite: CommandSet{ CommandSuite: CommandSet{
balCommand, balCommand,
BroadcastCommand(sys), BroadcastCommand(sys),
helpCommand,
NearbyCommand(sys), NearbyCommand(sys),
playersCommand, playersCommand,
}, },
@ -49,3 +48,7 @@ func (m *MakeColonyState) Exit(c *Connection) {
m.System.colonizedBy = c m.System.colonizedBy = c
c.Printf("Established colony on %v.\n", m.System) c.Printf("Established colony on %v.\n", m.System)
} }
func (m *MakeColonyState) FillStatus(c *Connection, s *status) {
s.Location = m.System.String()
}

@ -2,15 +2,45 @@ package main
import ( import (
"fmt" "fmt"
"sort"
// "strconv"
"strings" "strings"
"text/template"
) )
var helpTemplate = template.Must(template.New("help").Parse(`
{{.Name}} Command Reference
Summary: {{.Summary}}
{{- if .Usage}}
Usage: {{.Usage}}
{{end}}
{{- if .Description}}
Details:
{{.Description}}
{{end}}
`))
func printHelp(conn *Connection, cmd *Command) {
desc := strings.ReplaceAll(strings.TrimSpace(cmd.help), "\n", "\n ")
helpTemplate.Execute(conn, struct {
Name string
Summary string
Usage string
Description string
}{
Name: cmd.name,
Summary: cmd.summary,
Usage: cmd.usage,
Description: desc,
})
}
var commandRegistry map[string]*Command var commandRegistry map[string]*Command
type Command struct { type Command struct {
name string name string
summary string
usage string
help string help string
arity int arity int
variadic bool variadic bool
@ -37,6 +67,14 @@ func (c Command) Commands() []Command {
type CommandSet []Command type CommandSet []Command
func (c CommandSet) GetCommand(name string) *Command { func (c CommandSet) GetCommand(name string) *Command {
switch name {
case "help":
return &helpCommand
case "commands":
return &commandsCommand
case "status":
return &statusCommand
}
for _, cmd := range c { for _, cmd := range c {
if cmd.name == name { if cmd.name == name {
return &cmd return &cmd
@ -46,104 +84,147 @@ func (c CommandSet) GetCommand(name string) *Command {
} }
func (c CommandSet) Commands() []Command { func (c CommandSet) Commands() []Command {
return []Command(c) return append([]Command(c), statusCommand, helpCommand, commandsCommand)
} }
var helpCommand = Command{ var helpCommand = Command{
name: "help", name: "help",
help: "helpful things to help you", summary: "explains how to play the game",
usage: "help [command-name]",
help: `
help explains the usage of various commands in Exocolonus. On its own, the help
command displays some basic info about how the game is played. If given an
argument of a command name, the help command displays the detailed usage of the
specified command.
`,
handler: func(conn *Connection, args ...string) { handler: func(conn *Connection, args ...string) {
msg := ` msg := `
Star Dragons is a stupid name, but it's the name that Brian suggested. It has Exocolonus is a game of cunning text-based, real-time strategy. You play as
nothing to do with Dragons. some kind of space-faring entity, faring space in your inspecific space-faring
vessel. If you want a big one, it's big; if you want a small one, it's small.
Anyway, Star Dragons is a game of cunning text-based, real-time strategy. You If you want a pink one, it's pink, if you want a black one, it's black. And so
play as some kind of space-faring entity, faring space in your inspecific on, and so forth. It is the space craft of your dreams. Or perhaps you are
space-faring vessel. If you want a big one, it's big; if you want a small one, one of those insect-like alien races and you play as the queen. Yeah, that's
it's small. If you want a pink one, it's pink, if you want a black one, it's the ticket! You're the biggest baddest queen bug in space.
black. And so on, and so forth. It is the space craft of your dreams. Or
perhaps you are one of those insect-like alien races and you play as the queen. In Exocolonus, you issue your spacecraft textual commands to control it. The
Yeah, that's the ticket! You're the biggest baddest queen bug in space. objective of the game is to be the first person or alien or bug or magical
space ponycorn to eradicate three enemy species. Right now that is the only
In Star Dragons, you issue your spacecraft (which is *not* called a Dragon) win condition.
textual commands to control it. The objective of the game is to be the first
person or alien or bug or magical space ponycorn to eradicate three enemy Exocolonus deals with relativity with respect to observation. When an effect
species. Right now that is the only win condition. takes place, knowledge of that effect travels throughougt the galaxy at the
speed of light. If a system is bombed, the closest systems to it will know
All of the systems present in Star Dragons are named and positioned after known first. When you broadcast messages, they travel at the speed of light.
exoplanet systems. When attempting to communicate from one star system to
another, it takes time for the light of your message to reach the other star All of the systems present in Exocolonus are named and positioned after known
systems. Star systems that are farther away take longer to communicate with. exoplanet systems. Each star system in Exocolonus is a real star system that
has been researched by astronomers, and the number of planets in each system
corresponds to the number of known exoplanets in those systems. When
attempting to communicate from one star system to another, it takes time for
the light of your message to reach the other star systems. Star systems that
are farther away take longer to communicate with.
` `
if len(args) == 0 {
msg = strings.TrimSpace(msg) msg = strings.TrimSpace(msg)
fmt.Fprintln(conn, msg) fmt.Fprintln(conn, msg)
fmt.Fprint(conn, "\n")
if len(args) == 0 { conn.Line()
fmt.Fprint(conn, "\n")
fmt.Fprintln(conn, `use the "commands" command for a list of commands.`) fmt.Fprintln(conn, `use the "commands" command for a list of commands.`)
fmt.Fprintln(conn, `use "help [command-name]" to get info for a specific command.`) fmt.Fprintln(conn, `use "help [command-name]" to get info for a specific command.`)
return return
} }
for _, cmdName := range args { for _, cmdName := range args {
cmd, ok := commandRegistry[cmdName] cmd := conn.GetCommand(cmdName)
if !ok { if cmd == nil {
conn.Printf("no such command: %v\n", cmdName) conn.Printf("no such command: %v\n", cmdName)
continue continue
} }
conn.Printf("%v: %v\n", cmdName, cmd.help) printHelp(conn, cmd)
} }
}, },
} }
var commandsCommand = Command{ type status struct {
name: "commands", State string
help: "gives you a handy list of commands", GameCode string
Balance int
Bombs int
Kills int
Location string
Description string
}
var statusTemplate = template.Must(template.New("status").Parse(`
Current State: {{.State}}
--------------------------------------------------------------------------------
{{- if .GameCode}}
Current Game: {{.GameCode}}
Balance: {{.Balance}}
Bombs: {{.Bombs}}
Kills: {{.Kills}}
Location: {{.Location}}
{{end}}
{{.Description}}
`))
var statusCommand = Command{
name: "status",
summary: "display your current status",
handler: func(conn *Connection, args ...string) { handler: func(conn *Connection, args ...string) {
names := make([]string, 0, len(commandRegistry)) s := status{
for name, _ := range commandRegistry { State: conn.ConnectionState.String(),
names = append(names, name)
} }
sort.Strings(names) conn.ConnectionState.FillStatus(conn, &s)
fmt.Fprintln(conn, "--------------------------------------------------------------------------------") if conn.game != nil {
for _, name := range names { s.GameCode = conn.game.id
cmd := commandRegistry[name] s.Balance = conn.money
conn.Printf("%-16s %s\n", name, cmd.help) s.Bombs = conn.bombs
s.Kills = conn.kills
} }
fmt.Fprintln(conn, "--------------------------------------------------------------------------------") statusTemplate.Execute(conn, s)
}, },
} }
// this isn't a real command it just puts command in the list of commands, this
// is weird and circular, this is a special case.
var commandsCommand = Command{
name: "commands",
summary: "lists currently available commands",
}
func BroadcastCommand(sys *System) Command { func BroadcastCommand(sys *System) Command {
return Command{ return Command{
name: "broadcast", name: "broadcast",
help: "broadcast a message for all systems to hear", summary: "broadcast a message for all systems to hear",
handler: func(c *Connection, args ...string) { handler: func(c *Connection, args ...string) {
msg := strings.Join(args, " ") msg := strings.Join(args, " ")
b := NewBroadcast(sys, msg) b := NewBroadcast(sys, msg)
log_info("player %s send broadcast from system %v: %v\n", c.Name(), sys, msg) log_info("player %s send broadcast from system %v: %v\n", c.Name(), sys, msg)
currentGame.Register(b) c.game.Register(b)
}, },
} }
} }
func NearbyCommand(sys *System) Command { func NearbyCommand(sys *System) Command {
handler := func(c *Connection, args ...string) { handler := func(c *Connection, args ...string) {
neighbors, err := sys.Nearby(25) neighbors := c.game.galaxy.Neighborhood(sys)
if err != nil { c.Line()
log_error("unable to get neighbors: %v", err) c.Printf("%-4s %-20s %-12s %s\n", "id", "name", "distance", "trip time")
return c.Line()
} for _, neighbor := range neighbors[:25] {
c.Printf("--------------------------------------------------------------------------------\n") other := c.game.galaxy.GetSystemByID(neighbor.id)
c.Printf("%-4s %-20s %s\n", "id", "name", "distance") dur := NewTravel(c, sys, other).(*TravelState).tripTime()
c.Printf("--------------------------------------------------------------------------------\n") c.Printf("%-4d %-20s %-12.6vpc %v\n", other.id, other.name, neighbor.distance, dur)
for _, neighbor := range neighbors {
other := index[neighbor.id]
c.Printf("%-4d %-20s %-5.6v\n", other.id, other.name, neighbor.distance)
} }
c.Printf("--------------------------------------------------------------------------------\n") c.Line()
} }
return Command{ return Command{
name: "nearby", name: "nearby",
help: "list nearby star systems", summary: "list nearby star systems",
arity: 0, arity: 0,
handler: handler, handler: handler,
} }
@ -151,7 +232,7 @@ func NearbyCommand(sys *System) Command {
var winCommand = Command{ var winCommand = Command{
name: "win", name: "win",
help: "win the game.", summary: "win the game.",
debug: true, debug: true,
handler: func(conn *Connection, args ...string) { handler: func(conn *Connection, args ...string) {
conn.Win("win-command") conn.Win("win-command")
@ -160,9 +241,9 @@ var winCommand = Command{
var playersCommand = Command{ var playersCommand = Command{
name: "players", name: "players",
help: "lists the connected players", summary: "lists the connected players",
handler: func(conn *Connection, args ...string) { handler: func(conn *Connection, args ...string) {
for other, _ := range currentGame.connections { for other, _ := range conn.game.connections {
conn.Printf("%v\n", other.Name()) conn.Printf("%v\n", other.Name())
} }
}, },
@ -170,7 +251,7 @@ var playersCommand = Command{
var balCommand = Command{ var balCommand = Command{
name: "bal", name: "bal",
help: "displays your current balance in space duckets", summary: "displays your current balance in space duckets",
handler: func(conn *Connection, args ...string) { handler: func(conn *Connection, args ...string) {
fmt.Fprintln(conn, conn.money) fmt.Fprintln(conn, conn.money)
}, },

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"io" "io"
"net" "net"
"runtime"
"sort" "sort"
"strings" "strings"
"time" "time"
@ -12,6 +13,7 @@ import (
type Connection struct { type Connection struct {
*bufio.Reader *bufio.Reader
game *Game
net.Conn net.Conn
ConnectionState ConnectionState
bombs int bombs int
@ -30,69 +32,63 @@ func NewConnection(conn net.Conn) *Connection {
bombs: options.startBombs, bombs: options.startBombs,
money: options.startMoney, money: options.startMoney,
} }
c.SetState(SpawnRandomly()) c.SetState(EnterLobby())
currentGame.Join(c)
return c return c
} }
func (c *Connection) Login() {
for {
c.Printf("what is your name, adventurer?\n")
name, err := c.ReadString('\n')
if err == nil {
name = strings.TrimSpace(name)
} else {
log_error("player failed to connect: %v", err)
return
}
if !ValidName(name) {
c.Printf("that name is illegal.\n")
continue
}
log_info("player connected: %v", name)
profile, err := loadProfile(name)
if err != nil {
log_error("could not read profile: %v", err)
profile = &Profile{name: name}
if err := profile.Create(); err != nil {
log_error("unable to create profile record: %v", err)
}
c.Printf("you look new around these parts, %s.\n", profile.name)
c.Printf(`if you'd like a description of how to play, type the "help" command\n`)
c.profile = profile
} else {
c.profile = profile
c.Printf("welcome back, %s.\n", profile.name)
}
break
}
currentGame.Register(c)
}
func (c *Connection) Dead() bool { func (c *Connection) Dead() bool {
return false return false
} }
func (c *Connection) Tick(frame int64) { func (c *Connection) Tick(game *Game) {
if c.ConnectionState == nil { if c.ConnectionState == nil {
log_error("connected client has nil state.") log_error("connected client has nil state.")
c.Printf("somehow you have a nil state. I don't know what to do so I'm going to kick you off.") c.Printf("somehow you have a nil state. I don't know what to do so I'm going to kick you off.")
c.Close() c.Close()
return return
} }
c.SetState(c.ConnectionState.Tick(c, frame)) c.SetState(c.ConnectionState.Tick(c, game.frame))
} }
func (c *Connection) RunCommand(name string, args ...string) { func (c *Connection) RunCommand(name string, args ...string) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
c.Printf("something is broken. Log this as a ticket!\n") c.Printf("(something is broken)")
c.Printf("recovered: %v\n", r) c.Printf("ERROR: %v\n", r)
callers := make([]uintptr, 40)
n := runtime.Callers(5, callers)
callers = callers[:n]
frames := runtime.CallersFrames(callers)
log_error("recovered: %v", r) log_error("recovered: %v", r)
for {
frame, more := frames.Next()
if !more {
break
}
log_error(" %s +%d (%s)\n", frame.File, frame.Line, frame.Function)
}
} }
}() }()
switch name { switch name {
case "commands": case "commands":
c.ListCommands()
return
}
cmd := c.GetCommand(name)
if cmd == nil {
c.Printf("No such command: %v\n", name)
return
}
cmd.handler(c, args...)
}
func (c *Connection) ListCommands() {
c.Printf("\n")
c.Line()
c.Printf("- Available Commands in state: %s\n", c.ConnectionState.String())
c.Line() c.Line()
commands := c.Commands() commands := c.Commands()
names := make([]string, len(commands)) names := make([]string, len(commands))
@ -102,18 +98,9 @@ func (c *Connection) RunCommand(name string, args ...string) {
sort.Strings(names) sort.Strings(names)
for _, name := range names { for _, name := range names {
cmd := c.GetCommand(name) cmd := c.GetCommand(name)
c.Printf("%-20s%s\n", name, cmd.help) c.Printf("%-20s%s\n", name, cmd.summary)
} }
c.Line() c.Printf("\n")
return
}
cmd := c.GetCommand(name)
if cmd == nil {
c.Printf("No such command: %v\n", name)
return
}
cmd.handler(c, args...)
} }
func (c *Connection) SetState(s ConnectionState) { func (c *Connection) SetState(s ConnectionState) {
@ -126,8 +113,8 @@ func (c *Connection) SetState(s ConnectionState) {
c.ConnectionState.Exit(c) c.ConnectionState.Exit(c)
} }
log_info("enter state: %v", s) log_info("enter state: %v", s)
s.Enter(c)
c.ConnectionState = s c.ConnectionState = s
s.Enter(c)
} }
func (c *Connection) ReadLines(out chan []string) { func (c *Connection) ReadLines(out chan []string) {
@ -162,7 +149,9 @@ func (c *Connection) Printf(template string, args ...interface{}) (int, error) {
func (c *Connection) Close() error { func (c *Connection) Close() error {
log_info("player disconnecting: %s", c.Name()) log_info("player disconnecting: %s", c.Name())
currentGame.Quit(c) if c.game != nil {
c.game.Quit(c)
}
if c.Conn != nil { if c.Conn != nil {
return c.Conn.Close() return c.Conn.Close()
} }
@ -177,17 +166,17 @@ func (c *Connection) Name() string {
} }
func (c *Connection) RecordScan() { func (c *Connection) RecordScan() {
c.Printf("scanning known systems for signs of life\n") c.Printf("Scanning known systems for signs of life\n")
c.lastScan = time.Now() c.lastScan = time.Now()
time.AfterFunc(options.scanTime, func() { time.AfterFunc(options.scanTime, func() {
c.Printf("scanner ready\n") c.Printf("Scanner ready\n")
}) })
} }
func (c *Connection) RecordBomb() { func (c *Connection) RecordBomb() {
c.lastBomb = time.Now() c.lastBomb = time.Now()
time.AfterFunc(15*time.Second, func() { time.AfterFunc(15*time.Second, func() {
fmt.Fprintln(c, "bomb arsenal reloaded") fmt.Fprintln(c, "Bomb arsenal reloaded")
}) })
} }
@ -226,37 +215,9 @@ func (c *Connection) Deposit(n int) {
} }
func (c *Connection) Win(method string) { func (c *Connection) Win(method string) {
currentGame.Win(c, method) c.game.Win(c, method)
} }
func (c *Connection) Die(frame int64) { func (c *Connection) Die(frame int64) {
c.SetState(NewDeadState(frame)) c.SetState(NewDeadState(frame))
} }
type ConnectionState interface {
CommandSuite
String() string
Enter(c *Connection)
Tick(c *Connection, frame int64) ConnectionState
Exit(c *Connection)
}
// No-op enter struct, for composing connection states that have no interesitng
// Enter mechanic.
type NopEnter struct{}
func (n NopEnter) Enter(c *Connection) {}
// No-op exit struct, for composing connection states that have no interesting
// Exit mechanic.
type NopExit struct{}
func (n NopExit) Exit(c *Connection) {}
func SpawnRandomly() ConnectionState {
sys, err := randomSystem()
if err != nil {
return NewErrorState(fmt.Errorf("unable to create idle state: %v", err))
}
return Idle(sys)
}

@ -0,0 +1,39 @@
package main
type ConnectionState interface {
// commands available while in this state
CommandSuite
// human-readable description of the state
String() string
// fills a status struct to be printed by the status command. The
// ConnectionState only needs to fill in things that are unique to the
// state itself, the common things on the connection are filled in
// automatically
FillStatus(*Connection, *status)
// Triggered once when the state is entered
Enter(c *Connection)
// Triggered every frame in which the state is the connection's current
// state. Returning a different ConnectionState transitions between states.
Tick(c *Connection, frame int64) ConnectionState
// Triggered once when this state has finished for that connection
Exit(c *Connection)
}
// No-op enter struct, for composing connection states that have no interesitng
// Enter mechanic.
type NopEnter struct{}
func (n NopEnter) Enter(c *Connection) {}
// No-op exit struct, for composing connection states that have no interesting
// Exit mechanic.
type NopExit struct{}
func (n NopExit) Exit(c *Connection) {}

53
db.go

@ -49,63 +49,12 @@ func planetsData() {
planet.Store(db) planet.Store(db)
} }
} }
indexSystems() // indexSystems()
}
func edgesTable() {
stmnt := `create table if not exists edges (
id_1 integer,
id_2 integer,
distance real
);`
if _, err := db.Exec(stmnt); err != nil {
log_error("couldn't create distance table: %v", err)
}
} }
func setupDb() { func setupDb() {
planetsTable() planetsTable()
planetsData() planetsData()
edgesTable()
profilesTable() profilesTable()
gamesTable() gamesTable()
fillEdges()
}
func fillEdges() {
row := db.QueryRow(`select count(*) from edges;`)
var n int
if err := row.Scan(&n); err != nil {
log_error("couldn't get number of edges: %v", err)
return
}
if n > 0 {
return
}
for i := 0; i < len(index); i++ {
for j := 0; j < len(index); j++ {
if i == j {
continue
}
if index[i] == nil {
log_error("wtf there's nil shit in here for id %d", i)
continue
}
if index[j] == nil {
log_error("wtf there's nil shit in here 2 for id %d", j)
continue
}
dist := index[i].DistanceTo(index[j])
log_info("distance from %s to %s: %v", index[i].name, index[j].name, dist)
_, err := db.Exec(`
insert into edges
(id_1, id_2, distance)
values
(?, ?, ?)
;`, i, j, dist)
if err != nil {
log_error("unable to write edge to db: %v", err)
}
}
}
} }

@ -1,6 +1,9 @@
package main package main
import () import (
"strings"
"time"
)
type DeadState struct { type DeadState struct {
CommandSuite CommandSuite
@ -8,16 +11,71 @@ type DeadState struct {
} }
func NewDeadState(died int64) ConnectionState { func NewDeadState(died int64) ConnectionState {
return &DeadState{start: died} return &DeadState{
start: died,
CommandSuite: CommandSet{},
}
} }
func (d *DeadState) Enter(c *Connection) { func (d *DeadState) Enter(c *Connection) {
c.Printf("You are dead.\n") msg := `
Y88b d88P d8888
Y88b d88P d88888
Y88o88P d88P888
Y888P .d88b. 888 888 d88P 888 888d888 .d88b.
888 d88""88b 888 888 d88P 888 888P" d8P Y8b
888 888 888 888 888 d88P 888 888 88888888
888 Y88..88P Y88b 888 d8888888888 888 Y8b.
888 "Y88P" "Y88888 d88P 888 888 "Y8888
____
__,---' '--.__
,-' ; '.
,' '--.'--.
,' '._ '-.
; ; '-- ;
,-'-_ _,-~~-. ,-- '.
;; '-,; ,'~'.__ ,;;; ; ;
;; ;,' ,;; ', ;;; '. ;
': ,' ':; __/ '.; ; ;
;~~^. '. '---'~~ ;; ; ;
',' '. '. .;;; ;'
,',^. '. '._ __ ':; ,'
'-' '--' ~'--'~~'--. ~ ,'
/;'-;_ ; ;. /. / ; ~~'-. ;
-._ ; ; ; ',;'-;__;---; '----'
'--.__ ''-'-;__;: ; ;__;
... '--.__ '-- '-'
'--.:::... '--.__ ____
'--:::::--. '--.__ __,--' '.
'--:::';.... '--' ___ '.
'--'-:::... __ ) ;
~'-:::... '---. ( ,'
~'-:::::::::'--. '-.
~'-::::::::'. ;
~'--:::,' ,'
~~'--'~
8888888b. 8888888888 d8888 8888888b.
888 "Y88b 888 d88888 888 "Y88b
888 888 888 d88P888 888 888
888 888 8888888 d88P 888 888 888
888 888 888 d88P 888 888 888
888 888 888 d88P 888 888 888
888 .d88P 888 d8888888888 888 .d88P
8888888P" 8888888888 d88P 888 8888888P"
`
lines := strings.Split(msg, "\n")
for _, line := range lines {
c.Write([]byte(line + "\n"))
time.Sleep(20 * time.Millisecond)
}
} }
func (d *DeadState) Tick(c *Connection, frame int64) ConnectionState { func (d *DeadState) Tick(c *Connection, frame int64) ConnectionState {
if frame-d.start > options.respawnFrames { if frame-d.start > options.respawnFrames {
return SpawnRandomly() return c.game.SpawnPlayer()
} }
return d return d
} }
@ -26,6 +84,55 @@ func (d *DeadState) Exit(c *Connection) {
c.Printf("You're alive again.\n") c.Printf("You're alive again.\n")
} }
func (d *DeadState) String() string { func (d *DeadState) String() string { return "dead" }
return "dead"
func (d *DeadState) FillStatus(c *Connection, s *status) {
s.Description = `
Y88b d88P d8888
Y88b d88P d88888
Y88o88P d88P888
Y888P .d88b. 888 888 d88P 888 888d888 .d88b.
888 d88""88b 888 888 d88P 888 888P" d8P Y8b
888 888 888 888 888 d88P 888 888 88888888
888 Y88..88P Y88b 888 d8888888888 888 Y8b.
888 "Y88P" "Y88888 d88P 888 888 "Y8888
____
__,---' '--.__
,-' ; '.
,' '--.'--.
,' '._ '-.
; ; '-- ;
,-'-_ _,-~~-. ,-- '.
;; '-,; ,'~'.__ ,;;; ; ;
;; ;,' ,;; ', ;;; '. ;
': ,' ':; __/ '.; ; ;
;~~^. '. '---'~~ ;; ; ;
',' '. '. .;;; ;'
,',^. '. '._ __ ':; ,'
'-' '--' ~'--'~~'--. ~ ,'
/;'-;_ ; ;. /. / ; ~~'-. ;
-._ ; ; ; ',;'-;__;---; '----'
'--.__ ''-'-;__;: ; ;__;
... '--.__ '-- '-'
'--.:::... '--.__ ____
'--:::::--. '--.__ __,--' '.
'--:::';.... '--' ___ '.
'--'-:::... __ ) ;
~'-:::... '---. ( ,'
~'-:::::::::'--. '-.
~'-::::::::'. ;
~'--:::,' ,'
~~'--'~
8888888b. 8888888888 d8888 8888888b.
888 "Y88b 888 d88888 888 "Y88b
888 888 888 d88P888 888 888
888 888 8888888 d88P 888 888 888
888 888 888 d88P 888 888 888
888 888 888 d88P 888 888 888
888 .d88P 888 d8888888888 888 .d88P
8888888P" 8888888888 d88P 888 8888888P"
`
} }

@ -62,3 +62,5 @@ func (e *ErrorState) String() string {
func (e *ErrorState) RunCommand(c *Connection, name string, args ...string) ConnectionState { func (e *ErrorState) RunCommand(c *Connection, name string, args ...string) ConnectionState {
return e return e
} }
func (e *ErrorState) FillStatus(c *Connection, s *status) {}

@ -0,0 +1,85 @@
package main
import (
"math/rand"
"sort"
"strconv"
)
// Galaxy is a collection of systems
type Galaxy struct {
systems map[int]*System
names map[string]int
}
func NewGalaxy() *Galaxy {
g := &Galaxy{
systems: make(map[int]*System),
names: make(map[string]int),
}
g.indexSystems()
return g
}
func (g *Galaxy) indexSystems() {
rows, err := db.Query(`select * from planets`)
if err != nil {
log_error("unable to select all planets: %v", err)
return
}
defer rows.Close()
for rows.Next() {
s := System{}
if err := rows.Scan(&s.id, &s.name, &s.x, &s.y, &s.z, &s.planets); err != nil {
log_info("unable to scan planet row: %v", err)
continue
}
g.systems[s.id] = &s
g.names[s.name] = s.id
s.money = int64(rand.NormFloat64()*options.moneySigma + options.moneyMean)
}
}
// GetSystem gets a system by either ID or name. If the provided string
// contains an integer, we assume the lookup is intended to be by ID.
func (g *Galaxy) GetSystem(s string) *System {
id, err := strconv.Atoi(s)
if err == nil {
return g.GetSystemByID(id)
}
return g.GetSystemByName(s)
}
func (g *Galaxy) GetSystemByID(id int) *System {
return g.systems[id]
}
func (g *Galaxy) GetSystemByName(name string) *System {
id := g.SystemID(name)
if id == 0 {
return nil
}
return g.GetSystemByID(id)
}
func (g *Galaxy) SystemID(name string) int { return g.names[name] }
// Neighborhood generates the neighborhood for a given system.
func (g *Galaxy) Neighborhood(sys *System) Neighborhood {
neighbors := make(Neighborhood, 0, len(g.systems))
for id, sys2 := range g.systems {
if id == sys.id {
continue
}
neighbors = append(neighbors, Neighbor{id: id, distance: sys.DistanceTo(sys2)})
}
sort.Sort(neighbors)
return neighbors
}
func (g *Galaxy) randomSystem() *System {
id := rand.Intn(len(g.systems))
return g.GetSystemByID(id)
}

@ -2,11 +2,12 @@ package main
import ( import (
"fmt" "fmt"
"math/rand"
"time" "time"
) )
type Game struct { type Game struct {
id Id id string
start time.Time start time.Time
end time.Time end time.Time
done chan interface{} done chan interface{}
@ -15,6 +16,7 @@ type Game struct {
connections map[*Connection]bool connections map[*Connection]bool
frame int64 frame int64
elems map[GameElement]bool elems map[GameElement]bool
galaxy *Galaxy
} }
func gamesTable() { func gamesTable() {
@ -30,28 +32,32 @@ func gamesTable() {
} }
} }
func init() { rand.Seed(time.Now().UnixNano()) }
func newID() string {
chars := []rune("ABCDEEEEEEEEFGHJJJJJJJKMNPQQQQQQQRTUVWXXXXXYZZZZZ234677777789")
id := make([]rune, 0, 4)
for i := 0; i < cap(id); i++ {
id = append(id, chars[rand.Intn(len(chars))])
}
return string(id)
}
func NewGame() *Game { func NewGame() *Game {
game := &Game{ game := &Game{
id: NewId(), id: newID(),
start: time.Now(), start: time.Now(),
done: make(chan interface{}), done: make(chan interface{}),
connections: make(map[*Connection]bool, 32), connections: make(map[*Connection]bool, 32),
elems: make(map[GameElement]bool, 32), elems: make(map[GameElement]bool, 32),
galaxy: NewGalaxy(),
} }
if err := game.Create(); err != nil { if err := game.Create(); err != nil {
log_error("unable to create game: %v", err) log_error("unable to create game: %v", err)
} }
for _, system := range index { for _, system := range game.galaxy.systems {
game.Register(system) game.Register(system)
} }
if currentGame != nil {
log_info("passing %d connections...", len(currentGame.connections))
for conn, _ := range currentGame.connections {
log_info("moving player %s to new game", conn.Name())
currentGame.Quit(conn)
game.Join(conn)
}
}
return game return game
} }
@ -61,7 +67,7 @@ func (g *Game) Create() error {
(id, start) (id, start)
values values
(?, ?) (?, ?)
;`, g.id.String(), g.start) ;`, g.id, g.start)
if err != nil { if err != nil {
return fmt.Errorf("error writing sqlite insert statement to create game: %v") return fmt.Errorf("error writing sqlite insert statement to create game: %v")
} }
@ -78,7 +84,12 @@ func (g *Game) Store() error {
} }
func (g *Game) Join(conn *Connection) { func (g *Game) Join(conn *Connection) {
log_info("Player %s has joined game %s", conn.Name(), g.id)
for there, _ := range g.connections {
there.Printf("Player %s has joined the game", conn.Name())
}
g.connections[conn] = true g.connections[conn] = true
g.Register(conn)
} }
func (g *Game) Quit(conn *Connection) { func (g *Game) Quit(conn *Connection) {
@ -97,6 +108,8 @@ func (g *Game) Win(winner *Connection, method string) {
for conn, _ := range g.connections { for conn, _ := range g.connections {
conn.Printf("player %s has won by %s victory.\n", winner.Name(), method) conn.Printf("player %s has won by %s victory.\n", winner.Name(), method)
} }
gm.Remove(g)
} }
func (g *Game) Reset() { func (g *Game) Reset() {
@ -128,7 +141,7 @@ func (g *Game) Register(elem GameElement) {
func (g *Game) tick() { func (g *Game) tick() {
g.frame += 1 g.frame += 1
for elem := range g.elems { for elem := range g.elems {
elem.Tick(g.frame) elem.Tick(g)
} }
for elem := range g.elems { for elem := range g.elems {
if elem.Dead() { if elem.Dead() {
@ -138,7 +151,11 @@ func (g *Game) tick() {
} }
} }
func (g *Game) SpawnPlayer() ConnectionState {
return Idle(g.galaxy.randomSystem())
}
type GameElement interface { type GameElement interface {
Tick(frame int64) Tick(*Game)
Dead() bool Dead() bool
} }

@ -0,0 +1,41 @@
package main
import (
"sync"
)
var gm *GameManager
func init() {
gm = &GameManager{
games: make(map[string]*Game, 32),
}
}
type GameManager struct {
games map[string]*Game
sync.Mutex
}
func (g *GameManager) NewGame() *Game {
g.Lock()
defer g.Unlock()
game := NewGame()
g.games[game.id] = game
return game
}
func (g *GameManager) Get(id string) *Game {
g.Lock()
defer g.Unlock()
return g.games[id]
}
func (g *GameManager) Remove(game *Game) {
g.Lock()
defer g.Unlock()
delete(g.games, game.id)
}

55
id.go

@ -1,55 +0,0 @@
package main
import (
"encoding/binary"
"fmt"
"sync/atomic"
"time"
)
// NewObjectId returns a new unique ObjectId.
// This function causes a runtime error if it fails to get the hostname
// of the current machine.
func NewId() Id {
b := make([]byte, 12)
// Timestamp, 4 bytes, big endian
binary.BigEndian.PutUint32(b, uint32(time.Now().Unix()))
b[4] = global.machineId[0]
b[5] = global.machineId[1]
b[6] = global.machineId[2]
// Pid, 2 bytes, specs don't specify endianness, but we use big endian.
b[7] = byte(global.pid >> 8)
b[8] = byte(global.pid)
// Increment, 3 bytes, big endian
i := atomic.AddUint32(&global.idCounter, 1)
b[9] = byte(i >> 16)
b[10] = byte(i >> 8)
b[11] = byte(i)
return Id(b)
}
// Id is used for tagging each incoming http request for logging
// purposes. The actual implementation is just the ObjectId implementation
// found in launchpad.net/mgo/bson. This will most likely change and evolve
// into its own format.
type Id string
func (id Id) String() string {
return fmt.Sprintf("%x", string(id))
}
// Time returns the timestamp part of the id.
// It's a runtime error to call this method with an invalid id.
func (id Id) Time() time.Time {
secs := int64(binary.BigEndian.Uint32(id.byteSlice(0, 4)))
return time.Unix(secs, 0)
}
// byteSlice returns byte slice of id from start to end.
// Calling this function with an invalid id will cause a runtime panic.
func (id Id) byteSlice(start, end int) []byte {
if len(id) != 12 {
panic(fmt.Sprintf("Invalid Id: %q", string(id)))
}
return []byte(string(id)[start:end])
}

@ -15,43 +15,37 @@ func Idle(sys *System) ConnectionState {
i := &IdleState{System: sys} i := &IdleState{System: sys}
i.CommandSuite = CommandSet{ i.CommandSuite = CommandSet{
balCommand, balCommand,
helpCommand,
playersCommand, playersCommand,
BroadcastCommand(sys), BroadcastCommand(sys),
NearbyCommand(sys), NearbyCommand(sys),
Command{ Command{
name: "goto", name: "goto",
help: "travel between star systems", summary: "travel between star systems",
arity: 1, arity: 1,
handler: i.travelTo, handler: i.travelTo,
}, },
Command{ Command{
name: "bomb", name: "bomb",
help: "bomb another star system", summary: "bomb another star system",
arity: 1, arity: 1,
usage: "bomb [system-name or system-id]",
handler: i.bomb, handler: i.bomb,
}, },
Command{ Command{
name: "mine", name: "mine",
help: "mine the current system for resources", summary: "mine the current system for resources",
arity: 0, arity: 0,
handler: i.mine, handler: i.mine,
}, },
Command{
name: "info",
help: "gives you information about the current star system",
arity: 0,
handler: i.info,
},
Command{ Command{
name: "scan", name: "scan",
help: "scans the galaxy for signs of life", summary: "scans the galaxy for signs of life",
arity: 0, arity: 0,
handler: i.scan, handler: i.scan,
}, },
Command{ Command{
name: "make", name: "make",
help: "makes things", summary: "makes things",
handler: i.maek, handler: i.maek,
}, },
} }
@ -71,9 +65,9 @@ func (i *IdleState) Tick(c *Connection, frame int64) ConnectionState {
} }
func (i *IdleState) travelTo(c *Connection, args ...string) { func (i *IdleState) travelTo(c *Connection, args ...string) {
dest, err := GetSystem(args[0]) dest := c.game.galaxy.GetSystem(args[0])
if err != nil { if dest == nil {
c.Printf("%v\n", err) c.Printf("no such system: %s", args[0])
return return
} }
c.SetState(NewTravel(c, i.System, dest)) c.SetState(NewTravel(c, i.System, dest))
@ -89,37 +83,36 @@ func (i *IdleState) bomb(c *Connection, args ...string) {
return return
} }
target, err := GetSystem(args[0]) target := c.game.galaxy.GetSystem(args[0])
if err != nil { if target == nil {
c.Printf("Cannot send bomb: %v\n", err) c.Printf("Cannot send bomb: no such system: %v\n", args[0])
return return
} }
c.bombs -= 1 c.bombs -= 1
c.lastBomb = time.Now() c.lastBomb = time.Now()
bomb := NewBomb(c, i.System, target) bomb := NewBomb(c, i.System, target)
currentGame.Register(bomb) c.game.Register(bomb)
} }
func (i *IdleState) mine(c *Connection, args ...string) { func (i *IdleState) mine(c *Connection, args ...string) {
c.SetState(Mine(i.System)) c.SetState(Mine(i.System))
} }
func (i *IdleState) info(c *Connection, args ...string) {
c.Printf("Currently idle on system %v\n", i.System)
c.Printf("Space duckets available: %v\n", i.money)
}
func (i *IdleState) scan(c *Connection, args ...string) { func (i *IdleState) scan(c *Connection, args ...string) {
if time.Since(c.lastScan) < 1*time.Minute { if time.Since(c.lastScan) < 1*time.Minute {
return return
} }
c.Printf("Scanning the galaxy for signs of life...\n") c.Printf("Scanning the galaxy for signs of life...\n")
currentGame.Register(NewScan(i.System)) c.game.Register(NewScan(i.System, c.game.galaxy.Neighborhood(i.System)))
} }
// "make" is already a keyword // "make" is already a keyword
func (i *IdleState) maek(c *Connection, args ...string) { func (i *IdleState) maek(c *Connection, args ...string) {
if len(args) != 1 {
c.Printf("not sure what to do! Expecting a command like this: make [thing]\ne.g.:\nmake bomb\nmake colony")
return
}
switch args[0] { switch args[0] {
case "bomb": case "bomb":
if c.money < options.bombCost { if c.money < options.bombCost {
@ -136,3 +129,8 @@ func (i *IdleState) maek(c *Connection, args ...string) {
c.Printf("I don't know how to make a %v.\n", args[0]) c.Printf("I don't know how to make a %v.\n", args[0])
} }
} }
func (i *IdleState) FillStatus(c *Connection, s *status) {
s.Location = i.System.String()
s.Description = "Just hanging out, enjoying outer space."
}

@ -0,0 +1,185 @@
package main
import (
"strings"
"time"
)
var banner = `
##############################################################################################
/$$$$$$$$ /$$
| $$_____/ | $$
| $$ /$$ /$$ /$$$$$$ /$$$$$$$ /$$$$$$ | $$ /$$$$$$ /$$$$$$$ /$$ /$$ /$$$$$$$
| $$$$$ | $$ /$$/ /$$__ $$ /$$_____/ /$$__ $$| $$ /$$__ $$| $$__ $$| $$ | $$ /$$_____/
| $$__/ \ $$$$/ | $$ \ $$| $$ | $$ \ $$| $$| $$ \ $$| $$ \ $$| $$ | $$| $$$$$$
| $$ >$$ $$ | $$ | $$| $$ | $$ | $$| $$| $$ | $$| $$ | $$| $$ | $$ \____ $$
| $$$$$$$$ /$$/\ $$| $$$$$$/| $$$$$$$| $$$$$$/| $$| $$$$$$/| $$ | $$| $$$$$$/ /$$$$$$$/
|________/|__/ \__/ \______/ \_______/ \______/ |__/ \______/ |__/ |__/ \______/ |_______/
~+
* +
' |
() .-.,="''"=. - o -
'=/_ \ |
* | '=._ |
\ '=./', '
. '=.__.=' '=' *
+ +
O * ' .
A game of dark cunning in the vast unknown of space by Jordan Orelli.
##############################################################################################
`
type LobbyState struct {
CommandSuite
NopExit
}
func EnterLobby() ConnectionState {
return &LobbyState{
CommandSuite: CommandSet{
newGameCommand,
joinGameCommand,
listGamesCommand,
},
}
}
func (st *LobbyState) String() string { return "Lobby" }
func (st *LobbyState) Enter(c *Connection) {
c.Printf(strings.TrimSpace(banner))
time.Sleep(1 * time.Second)
for {
c.Printf("\n\nWhat is your name, adventurer?\n")
name, err := c.ReadString('\n')
if err == nil {
name = strings.TrimSpace(name)
} else {
log_error("player failed to connect: %v", err)
return
}
if !ValidName(name) {
c.Printf("that name is illegal.\n")
continue
}
log_info("player connected: %v", name)
profile, err := loadProfile(name)
if err != nil {
log_error("could not read profile: %v", err)
profile = &Profile{name: name}
if err := profile.Create(); err != nil {
log_error("unable to create profile record: %v", err)
}
c.Printf("you look new around these parts, %s.\n", profile.name)
c.Printf(`if you'd like a description of how to play, type the "help" command\n`)
c.profile = profile
} else {
c.profile = profile
c.Printf("Welcome back, %s.\n", profile.name)
}
break
}
c.ListCommands()
}
func (st *LobbyState) Tick(c *Connection, frame int64) ConnectionState { return st }
func (st *LobbyState) FillStatus(c *Connection, s *status) {
s.Description = strings.TrimSpace(`
Currently in the Lobby, waiting for you to issue a "new" command to start a new
game, or a "join" command to join an existing game.
`)
}
var newGameCommand = Command{
name: "new",
summary: "starts a new game",
arity: 0,
variadic: false,
handler: func(c *Connection, args ...string) {
c.Printf("Starting a new game...\n")
game := gm.NewGame()
log_info("%s Created game: %s", c.profile.name, game.id)
go game.Run()
c.game = game
c.Printf("Now playing in game: %s\n\n", game.id)
c.Line()
c.game.Join(c)
c.SetState(game.SpawnPlayer())
},
debug: false,
}
var joinGameCommand = Command{
name: "join",
summary: "joins an existing game",
usage: "join [game-code]",
arity: 1,
variadic: false,
handler: func(c *Connection, args ...string) {
if len(args) == 0 {
gm.Lock()
defer gm.Unlock()
if len(gm.games) == 1 {
for _, game := range gm.games {
c.game = game
log_info("%s Joining game: %s", c.profile.name, c.game.id)
c.Printf("You have joined game %s\n", game.id)
c.SetState(game.SpawnPlayer())
c.game.Join(c)
return
}
}
c.Printf(strings.TrimLeft(`
Missing game code! When a player starts a game, they will be given a code to
identify their game. Use this game to join the other player's game.
Usage: join [game-code]`, " \n\t"))
return
}
id := args[0]
game := gm.Get(id)
c.game = game
log_info("%s Joining game: %s", c.profile.name, c.game.id)
c.Printf("You have joined game %s\n", game.id)
c.SetState(game.SpawnPlayer())
c.game.Join(c)
},
debug: false,
}
var listGamesCommand = Command{
name: "list",
summary: "lists game lobbies that can be joined",
usage: "list",
arity: 0,
variadic: false,
handler: func(c *Connection, args ...string) {
gm.Lock()
defer gm.Unlock()
c.Line()
c.Printf("%-8s %-20s\n", "Game", "Player")
c.Line()
for id, game := range gm.games {
c.Printf("%-8s %-20s\n", id, "")
for conn, _ := range game.connections {
if conn.profile != nil {
c.Printf("%-8s %-20s\n", "", conn.profile.name)
}
}
c.Printf("--------------------\n")
}
},
}

@ -18,7 +18,7 @@ var options struct {
frameLength time.Duration frameLength time.Duration
colonyCost int colonyCost int
frameRate int frameRate int
lightSpeed float64 lightSpeed float64 // the distance that light travels in one tick
makeBombTime time.Duration makeBombTime time.Duration
makeColonyTime time.Duration makeColonyTime time.Duration
makeShieldTime time.Duration makeShieldTime time.Duration
@ -36,7 +36,6 @@ var options struct {
var ( var (
info_log *log.Logger info_log *log.Logger
error_log *log.Logger error_log *log.Logger
currentGame *Game
) )
func log_error(template string, args ...interface{}) { func log_error(template string, args ...interface{}) {
@ -56,9 +55,9 @@ func bail(status int, template string, args ...interface{}) {
os.Exit(status) os.Exit(status)
} }
func handleConnection(conn *Connection) { func handleConnection(sock net.Conn) {
conn := NewConnection(sock)
defer conn.Close() defer conn.Close()
conn.Login()
c := make(chan []string) c := make(chan []string)
go conn.ReadLines(c) go conn.ReadLines(c)
@ -88,18 +87,12 @@ func main() {
error_log = log.New(os.Stderr, "[ERROR] ", 0) error_log = log.New(os.Stderr, "[ERROR] ", 0)
setupDb() setupDb()
listener, err := net.Listen("tcp", ":9220") addr := ":9220"
listener, err := net.Listen("tcp", addr)
if err != nil { if err != nil {
bail(E_No_Port, "unable to start server: %v", err) bail(E_No_Port, "unable to start server: %v", err)
} }
log_info("listening on %s", addr)
go func() {
for {
log_info("starting new game")
currentGame = NewGame()
currentGame.Run()
}
}()
for { for {
conn, err := listener.Accept() conn, err := listener.Accept()
@ -107,7 +100,7 @@ func main() {
log_error("error accepting connection: %v", err) log_error("error accepting connection: %v", err)
continue continue
} }
go handleConnection(NewConnection(conn)) go handleConnection(conn)
} }
} }

@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"strings"
) )
type MiningState struct { type MiningState struct {
@ -14,22 +15,15 @@ func Mine(sys *System) ConnectionState {
m := &MiningState{System: sys} m := &MiningState{System: sys}
m.CommandSuite = CommandSet{ m.CommandSuite = CommandSet{
balCommand, balCommand,
helpCommand,
playersCommand, playersCommand,
BroadcastCommand(sys), BroadcastCommand(sys),
NearbyCommand(sys), NearbyCommand(sys),
Command{ Command{
name: "stop", name: "stop",
help: "stops mining", summary: "stops mining",
arity: 0, arity: 0,
handler: m.stop, handler: m.stop,
}, },
Command{
name: "info",
help: "gives you information about the current mining operation",
arity: 0,
handler: m.info,
},
} }
return m return m
} }
@ -66,8 +60,11 @@ func (m *MiningState) stop(c *Connection, args ...string) {
c.SetState(Idle(m.System)) c.SetState(Idle(m.System))
} }
func (m *MiningState) info(c *Connection, args ...string) { func (m *MiningState) FillStatus(c *Connection, s *status) {
c.Printf("Currently mining system %v\n", m.System) s.Location = m.System.String()
c.Printf("Mined so far: %v\n", m.mined) s.Description = strings.TrimSpace(fmt.Sprintf(`
c.Printf("Remaining space duckets on %v: %v\n", m.System, m.money) Currently mining on system: %s
Mined so far: %d
Available space duckets: %d
`, m.System.String(), m.mined, m.money))
} }

@ -12,6 +12,7 @@ type scan struct {
nextHitIndex int nextHitIndex int
nextEchoIndex int nextEchoIndex int
results []scanResult results []scanResult
neighborhood Neighborhood
} }
type scanResult struct { type scanResult struct {
@ -38,36 +39,40 @@ func (r *scanResult) playerNames() []string {
return names return names
} }
func NewScan(origin *System) *scan { func NewScan(origin *System, n Neighborhood) *scan {
return &scan{ return &scan{
origin: origin, origin: origin,
start: time.Now(), start: time.Now(),
results: make([]scanResult, 0, len(origin.Distances())), results: make([]scanResult, 0, len(n)),
neighborhood: n,
} }
} }
func (s *scan) Tick(frame int64) { func (s *scan) Tick(game *Game) {
s.dist += options.lightSpeed s.dist += options.lightSpeed
s.hits() s.hits(game)
s.echos() s.echos()
} }
func (s *scan) Dead() bool { func (s *scan) Dead() bool {
return s.nextEchoIndex >= len(s.origin.Distances()) return s.neighborhood == nil
} }
func (s *scan) String() string { func (s *scan) String() string {
return fmt.Sprintf("[scan origin: %s start_time: %v]", s.origin.name, s.start) return fmt.Sprintf("[scan origin: %s start_time: %v]", s.origin.name, s.start)
} }
func (s *scan) hits() { func (s *scan) hits(game *Game) {
for ; s.nextHitIndex < len(s.origin.Distances()); s.nextHitIndex += 1 { for len(s.neighborhood) > 0 && s.neighborhood[0].distance <= s.dist {
candidate := s.origin.Distances()[s.nextHitIndex] sys := game.galaxy.GetSystemByID(s.neighborhood[0].id)
if s.dist < candidate.dist { s.results = append(s.results, s.hitSystem(sys, s.neighborhood[0].distance))
break log_info("scan hit %v. Traveled %v in %v", sys.name, s.neighborhood[0].distance, time.Since(s.start))
if len(s.neighborhood) > 1 {
s.neighborhood = s.neighborhood[1:]
} else {
s.neighborhood = nil
} }
s.results = append(s.results, s.hitSystem(candidate.s, candidate.dist))
log_info("scan hit %v. Traveled %v in %v", candidate.s.name, candidate.dist, time.Since(s.start))
} }
} }

@ -10,7 +10,6 @@ func MakeShield(c *Connection, s *System) {
CommandSuite: CommandSet{ CommandSuite: CommandSet{
balCommand, balCommand,
BroadcastCommand(s), BroadcastCommand(s),
helpCommand,
NearbyCommand(s), NearbyCommand(s),
playersCommand, playersCommand,
}, },
@ -47,11 +46,15 @@ func (m *MakeShieldState) String() string {
return fmt.Sprintf("Making shield on %v", m.System) return fmt.Sprintf("Making shield on %v", m.System)
} }
func (m *MakeShieldState) FillStatus(c *Connection, s *status) {
s.Location = m.System.String()
}
type Shield struct { type Shield struct {
energy float64 energy float64
} }
func (s *Shield) Tick(frame int64) { func (s *Shield) Tick() {
if s.energy < 1000 { if s.energy < 1000 {
s.energy += (1000 - s.energy) * 0.0005 s.energy += (1000 - s.energy) * 0.0005
} }

@ -4,16 +4,9 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"math" "math"
"math/rand"
"strconv"
"time" "time"
) )
var (
index map[int]*System
nameIndex map[string]*System
)
type System struct { type System struct {
*Shield *Shield
id int id int
@ -26,29 +19,13 @@ type System struct {
money int64 money int64
} }
func GetSystem(id string) (*System, error) { func (s *System) Tick(game *Game) {
idNum, err := strconv.Atoi(id)
if err == nil {
sys, ok := index[idNum]
if !ok {
return nil, fmt.Errorf("No such system: %v", idNum)
}
return sys, nil
}
sys, ok := nameIndex[id]
if !ok {
return nil, fmt.Errorf("No such system: %v", id)
}
return sys, nil
}
func (s *System) Tick(frame int64) {
if s.colonizedBy != nil && s.money > 0 { if s.colonizedBy != nil && s.money > 0 {
s.colonizedBy.Deposit(1) s.colonizedBy.Deposit(1)
s.money -= 1 s.money -= 1
} }
if s.Shield != nil { if s.Shield != nil {
s.Shield.Tick(frame) s.Shield.Tick()
} }
} }
@ -62,7 +39,6 @@ func (s *System) Reset() {
} }
func (s *System) Arrive(conn *Connection) { func (s *System) Arrive(conn *Connection) {
// conn.SetSystem(s)
if s.players[conn] { if s.players[conn] {
return return
} }
@ -80,7 +56,6 @@ func (s *System) Arrive(conn *Connection) {
func (s *System) Leave(p *Connection) { func (s *System) Leave(p *Connection) {
delete(s.players, p) delete(s.players, p)
// p.location = nil
} }
func (s *System) NotifyInhabitants(template string, args ...interface{}) { func (s *System) NotifyInhabitants(template string, args ...interface{}) {
@ -138,38 +113,7 @@ type Ray struct {
dist float64 // distance in parsecs dist float64 // distance in parsecs
} }
func (s *System) Distances() []Ray { func (s *System) Bombed(bomber *Connection, game *Game) {
if s.distances == nil {
s.distances = make([]Ray, 0, 551)
rows, err := db.Query(`
select edges.id_2, edges.distance
from edges
where edges.id_1 = ?
order by distance
;`, s.id)
if err != nil {
log_error("unable to query for system distances: %v", err)
return nil
}
for rows.Next() {
var (
r Ray
id int
dist float64
)
if err := rows.Scan(&id, &dist); err != nil {
log_error("unable to unpack Ray from sql result: %v", err)
continue
}
r.s = index[id]
r.dist = dist
s.distances = append(s.distances, r)
}
}
return s.distances
}
func (s *System) Bombed(bomber *Connection, frame int64) {
if s.Shield != nil { if s.Shield != nil {
if s.Shield.Hit() { if s.Shield.Hit() {
s.EachConn(func(conn *Connection) { s.EachConn(func(conn *Connection) {
@ -182,7 +126,7 @@ func (s *System) Bombed(bomber *Connection, frame int64) {
} }
s.EachConn(func(conn *Connection) { s.EachConn(func(conn *Connection) {
conn.Die(frame) conn.Die(game.frame)
s.Leave(conn) s.Leave(conn)
bomber.MadeKill(conn) bomber.MadeKill(conn)
}) })
@ -191,21 +135,20 @@ func (s *System) Bombed(bomber *Connection, frame int64) {
s.colonizedBy = nil s.colonizedBy = nil
} }
for id, _ := range index { for id, other := range game.galaxy.systems {
if id == s.id { if id == s.id {
continue continue
} }
delay := s.LightTimeTo(index[id]) delay := s.LightTimeTo(game.galaxy.systems[id])
id2 := id from := s
to := other
time.AfterFunc(delay, func() { time.AfterFunc(delay, func() {
bombNotice(id2, s.id) bombNotice(to, from)
}) })
} }
} }
func bombNotice(to_id, from_id int) { func bombNotice(to, from *System) {
to := index[to_id]
from := index[from_id]
to.EachConn(func(conn *Connection) { to.EachConn(func(conn *Connection) {
conn.Printf("a bombing has been observed on %s\n", from.name) conn.Printf("a bombing has been observed on %s\n", from.name)
}) })
@ -215,36 +158,17 @@ func (s System) String() string {
return fmt.Sprintf("%s (id: %v)", s.name, s.id) return fmt.Sprintf("%s (id: %v)", s.name, s.id)
} }
type Neighborhood []Neighbor
func (n Neighborhood) Len() int { return len(n) }
func (n Neighborhood) Less(i, j int) bool { return n[i].distance < n[j].distance }
func (n Neighborhood) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
type Neighbor struct { type Neighbor struct {
id int id int
distance float64 distance float64
} }
func (e *System) Nearby(n int) ([]Neighbor, error) {
rows, err := db.Query(`
select planets.id, edges.distance
from edges
join planets on edges.id_2 = planets.id
where edges.id_1 = ?
order by distance
limit ?
;`, e.id, n)
if err != nil {
log_error("unable to get nearby systems for %s: %v", e.name, err)
return nil, err
}
neighbors := make([]Neighbor, 0, n)
for rows.Next() {
var neighbor Neighbor
if err := rows.Scan(&neighbor.id, &neighbor.distance); err != nil {
log_error("error unpacking row from nearby neighbors query: %v", err)
continue
}
neighbors = append(neighbors, neighbor)
}
return neighbors, nil
}
func countSystems() (int, error) { func countSystems() (int, error) {
row := db.QueryRow(`select count(*) from planets`) row := db.QueryRow(`select count(*) from planets`)
@ -261,35 +185,25 @@ func dist3d(x1, y1, z1, x2, y2, z2 float64) float64 {
return math.Sqrt(sq(x1-x2) + sq(y1-y2) + sq(z1-z2)) return math.Sqrt(sq(x1-x2) + sq(y1-y2) + sq(z1-z2))
} }
func indexSystems() map[int]*System { // func indexSystems() map[int]*System {
rows, err := db.Query(`select * from planets`) // rows, err := db.Query(`select * from planets`)
if err != nil { // if err != nil {
log_error("unable to select all planets: %v", err) // log_error("unable to select all planets: %v", err)
return nil // return nil
} // }
defer rows.Close() // defer rows.Close()
index = make(map[int]*System, 551) // index = make(map[int]*System, 551)
nameIndex = make(map[string]*System, 551) // nameIndex = make(map[string]*System, 551)
for rows.Next() { // for rows.Next() {
p := System{} // p := System{}
if err := rows.Scan(&p.id, &p.name, &p.x, &p.y, &p.z, &p.planets); err != nil { // if err := rows.Scan(&p.id, &p.name, &p.x, &p.y, &p.z, &p.planets); err != nil {
log_info("unable to scan planet row: %v", err) // log_info("unable to scan planet row: %v", err)
continue // continue
} // }
index[p.id] = &p // index[p.id] = &p
nameIndex[p.name] = &p // nameIndex[p.name] = &p
p.money = int64(rand.NormFloat64()*options.moneySigma + options.moneyMean) // p.money = int64(rand.NormFloat64()*options.moneySigma + options.moneyMean)
log_info("seeded system %v with %v monies", p, p.money) // // log_info("seeded system %v with %v monies", p, p.money)
} // }
return index // return index
} // }
func randomSystem() (*System, error) {
n := len(index)
if n == 0 {
return nil, fmt.Errorf("no planets are known to exist")
}
pick := rand.Intn(n)
sys := index[pick]
return sys, nil
}

@ -1,7 +1,9 @@
package main package main
import ( import (
"bytes"
"fmt" "fmt"
"text/template"
"time" "time"
) )
@ -20,39 +22,76 @@ func NewTravel(c *Connection, start, dest *System) ConnectionState {
dist: start.DistanceTo(dest), dist: start.DistanceTo(dest),
} }
t.CommandSuite = CommandSet{ t.CommandSuite = CommandSet{
helpCommand,
playersCommand, playersCommand,
balCommand, balCommand,
Command{ Command{
name: "progress", name: "progress",
help: "displays how far you are along your travel", summary: "displays how far you are along your travel",
arity: 0, arity: 0,
handler: t.progress, handler: t.progress,
}, },
Command{ Command{
name: "eta", name: "eta",
help: "displays estimated time of arrival", summary: "displays estimated time of arrival",
arity: 0, arity: 0,
handler: func(c *Connection, args ...string) { handler: func(c *Connection, args ...string) {
c.Printf("Remaining: %v\n", t.remaining()) c.Printf("%v\n", t.remaining())
c.Printf("Current time: %v\n", time.Now())
c.Printf("ETA: %v\n", t.eta())
}, },
}, },
} }
return t return t
} }
var enterTravelTemplate = template.Must(template.New("enter-travel").Parse(`
Departing: {{.Departing}}
Destination: {{.Destination}}
Total Trip Time: {{.Duration}}
`))
func (t *TravelState) Enter(c *Connection) { func (t *TravelState) Enter(c *Connection) {
c.Printf("Leaving %v, bound for %v.\n", t.start, t.dest) enterTravelTemplate.Execute(c, struct {
c.Printf("Trip duration: %v\n", t.tripTime()) Departing *System
c.Printf("Current time: %v\n", time.Now()) Destination *System
c.Printf("ETA: %v\n", t.eta()) Duration time.Duration
}{
t.start,
t.dest,
t.tripTime(),
})
t.start.Leave(c) t.start.Leave(c)
} }
func (t *TravelState) Tick(c *Connection, frame int64) ConnectionState { func (t *TravelState) Tick(c *Connection, frame int64) ConnectionState {
t.travelled += options.playerSpeed * options.lightSpeed dt := options.playerSpeed * options.lightSpeed
segmentLength := t.dist / 18
x := t.travelled
for x > segmentLength {
x -= segmentLength
}
if x < dt {
c.Printf("%v", t.start.name)
var buf bytes.Buffer
segment := int(t.travelled / t.dist * 18)
buf.WriteRune('|')
for i := 0; i < 18; i++ {
switch {
case i == segment:
buf.WriteRune('>')
case i == segment-1:
buf.WriteRune('=')
case i < segment:
buf.WriteRune('-')
default:
buf.WriteRune(' ')
}
}
buf.WriteRune('|')
c.Write(buf.Bytes())
c.Printf("at %v in %v\n", t.dest.name, t.remaining())
}
t.travelled += dt
if t.travelled >= t.dist { if t.travelled >= t.dist {
return Idle(t.dest) return Idle(t.dest)
} }
@ -68,6 +107,10 @@ func (t *TravelState) String() string {
return fmt.Sprintf("Traveling from %v to %v", t.start, t.dest) return fmt.Sprintf("Traveling from %v to %v", t.start, t.dest)
} }
func (t *TravelState) FillStatus(c *Connection, s *status) {
s.Location = fmt.Sprintf("between %s and %s", t.start, t.dest)
}
func (t *TravelState) progress(c *Connection, args ...string) { func (t *TravelState) progress(c *Connection, args ...string) {
c.Printf("%v\n", t.travelled/t.dist) c.Printf("%v\n", t.travelled/t.dist)
} }

Loading…
Cancel
Save