Created
December 19, 2014 22:26
-
-
Save jordanorelli/768a30f723656f971af3 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
package main | |
import ( | |
"fmt" | |
"time" | |
) | |
type Bomb struct { | |
profile *Connection | |
origin *System | |
target *System | |
start time.Time | |
done bool | |
fti int64 // frames to impact | |
} | |
func NewBomb(conn *Connection, from, to *System) *Bomb { | |
dist := from.DistanceTo(to) | |
fti := int64(dist / (options.lightSpeed * options.bombSpeed)) | |
eta := time.Duration(fti) * time.Second / time.Duration(options.frameRate) | |
log_info("bomb from: %v to: %v ETA: %v", from, to, eta) | |
return &Bomb{ | |
profile: conn, | |
origin: from, | |
target: to, | |
fti: fti, | |
start: time.Now(), | |
} | |
} | |
func (b *Bomb) Dead() bool { | |
return b.done | |
} | |
func (b *Bomb) Tick(frame int64) { | |
b.fti -= 1 | |
if b.fti <= 0 { | |
b.target.Bombed(b.profile, frame) | |
b.done = true | |
log_info("bomb went off on %v", b.target) | |
} | |
} | |
func (b *Bomb) String() string { | |
return fmt.Sprintf("[bomb from: %v to: %v lived: %s]", b.origin, b.target, time.Since(b.start)) | |
} | |
type MakeBombState struct { | |
CommandSuite | |
*System | |
start int64 | |
} | |
func MakeBomb(s *System) ConnectionState { | |
m := &MakeBombState{System: s} | |
m.CommandSuite = CommandSet{ | |
balCommand, | |
BroadcastCommand(s), | |
helpCommand, | |
NearbyCommand(s), | |
playersCommand, | |
} | |
return m | |
} | |
func (m *MakeBombState) Enter(c *Connection) { | |
c.Printf("Making a bomb...\n") | |
c.money -= options.bombCost | |
} | |
func (m *MakeBombState) Tick(c *Connection, frame int64) ConnectionState { | |
if m.start == 0 { | |
m.start = frame | |
} | |
if framesToDur(frame-m.start) >= options.makeBombTime { | |
return Idle(m.System) | |
} | |
return m | |
} | |
func (m *MakeBombState) Exit(c *Connection) { | |
c.bombs += 1 | |
c.Printf("Done! You now have %v bombs.\n", c.bombs) | |
} |
This file contains hidden or 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
package main | |
import ( | |
"fmt" | |
"time" | |
) | |
type broadcast struct { | |
start time.Time | |
origin *System | |
dist float64 | |
nextHitIndex int | |
message string | |
} | |
func NewBroadcast(from *System, template string, args ...interface{}) *broadcast { | |
return &broadcast{ | |
start: time.Now(), | |
origin: from, | |
message: fmt.Sprintf(template, args...), | |
} | |
} | |
func (b *broadcast) Tick(frame int64) { | |
b.dist += options.lightSpeed | |
for ; b.nextHitIndex < len(b.origin.Distances()); b.nextHitIndex += 1 { | |
candidate := b.origin.Distances()[b.nextHitIndex] | |
if b.dist < candidate.dist { | |
break | |
} | |
candidate.s.NotifyInhabitants("message received from system %v:\n\t%s\n", b.origin, b.message) | |
} | |
} | |
func (b *broadcast) Dead() bool { | |
return b.dist > b.origin.Distances()[len(b.origin.Distances())-1].dist | |
} | |
func (b *broadcast) String() string { | |
return fmt.Sprintf("[broadcast origin: %v message: %s]", b.origin, b.message) | |
} |
This file contains hidden or 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
package main | |
import () | |
func MakeColony(c *Connection, sys *System) { | |
if c.money < options.colonyCost { | |
c.Printf("Not enough money! Colonies cost %v but you only have %v space duckets. Mine more space duckets!\n", options.colonyCost, c.money) | |
return | |
} | |
if sys.colonizedBy == c { | |
c.Printf("You've already colonized this system.\n") | |
return | |
} | |
c.money -= options.colonyCost | |
m := &MakeColonyState{ | |
System: sys, | |
CommandSuite: CommandSet{ | |
balCommand, | |
BroadcastCommand(sys), | |
helpCommand, | |
NearbyCommand(sys), | |
playersCommand, | |
}, | |
} | |
c.SetState(m) | |
} | |
type MakeColonyState struct { | |
CommandSuite | |
*System | |
start int64 | |
} | |
func (m *MakeColonyState) Enter(c *Connection) { | |
c.Printf("Making colony on %v...\n", m.System) | |
} | |
func (m *MakeColonyState) Tick(c *Connection, frame int64) ConnectionState { | |
if m.start == 0 { | |
m.start = frame | |
} | |
if framesToDur(frame-m.start) >= options.makeColonyTime { | |
return Idle(m.System) | |
} | |
return m | |
} | |
func (m *MakeColonyState) Exit(c *Connection) { | |
m.System.colonizedBy = c | |
c.Printf("Established colony on %v.\n", m.System) | |
} |
This file contains hidden or 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
package main | |
import ( | |
"fmt" | |
"sort" | |
// "strconv" | |
"strings" | |
) | |
var commandRegistry map[string]*Command | |
type Command struct { | |
name string | |
help string | |
arity int | |
variadic bool | |
handler func(*Connection, ...string) | |
debug bool // marks command as a debug mode command | |
} | |
type CommandSuite interface { | |
GetCommand(name string) *Command | |
Commands() []Command | |
} | |
func (c Command) GetCommand(name string) *Command { | |
if name == c.name { | |
return &c | |
} | |
return nil | |
} | |
func (c Command) Commands() []Command { | |
return []Command{c} | |
} | |
type CommandSet []Command | |
func (c CommandSet) GetCommand(name string) *Command { | |
for _, cmd := range c { | |
if cmd.name == name { | |
return &cmd | |
} | |
} | |
return nil | |
} | |
func (c CommandSet) Commands() []Command { | |
return []Command(c) | |
} | |
var helpCommand = Command{ | |
name: "help", | |
help: "helpful things to help you", | |
handler: func(conn *Connection, args ...string) { | |
msg := ` | |
Star Dragons is a stupid name, but it's the name that Brian suggested. It has | |
nothing to do with Dragons. | |
Anyway, Star Dragons is a game of cunning text-based, real-time strategy. You | |
play as 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. If you want a pink one, it's pink, if you want a black one, it's | |
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. | |
Yeah, that's the ticket! You're the biggest baddest queen bug in space. | |
In Star Dragons, you issue your spacecraft (which is *not* called a Dragon) | |
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 | |
species. Right now that is the only win condition. | |
All of the systems present in Star Dragons are named and positioned after known | |
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 | |
systems. Star systems that are farther away take longer to communicate with. | |
` | |
msg = strings.TrimSpace(msg) | |
fmt.Fprintln(conn, msg) | |
if len(args) == 0 { | |
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.`) | |
return | |
} | |
for _, cmdName := range args { | |
cmd, ok := commandRegistry[cmdName] | |
if !ok { | |
conn.Printf("no such command: %v\n", cmdName) | |
continue | |
} | |
conn.Printf("%v: %v\n", cmdName, cmd.help) | |
} | |
}, | |
} | |
var commandsCommand = Command{ | |
name: "commands", | |
help: "gives you a handy list of commands", | |
handler: func(conn *Connection, args ...string) { | |
names := make([]string, 0, len(commandRegistry)) | |
for name, _ := range commandRegistry { | |
names = append(names, name) | |
} | |
sort.Strings(names) | |
fmt.Fprintln(conn, "--------------------------------------------------------------------------------") | |
for _, name := range names { | |
cmd := commandRegistry[name] | |
conn.Printf("%-16s %s\n", name, cmd.help) | |
} | |
fmt.Fprintln(conn, "--------------------------------------------------------------------------------") | |
}, | |
} | |
func BroadcastCommand(sys *System) Command { | |
return Command{ | |
name: "broadcast", | |
help: "broadcast a message for all systems to hear", | |
handler: func(c *Connection, args ...string) { | |
msg := strings.Join(args, " ") | |
b := NewBroadcast(sys, msg) | |
log_info("player %s send broadcast from system %v: %v\n", c.Name(), sys, msg) | |
currentGame.Register(b) | |
}, | |
} | |
} | |
func NearbyCommand(sys *System) Command { | |
handler := func(c *Connection, args ...string) { | |
neighbors, err := sys.Nearby(25) | |
if err != nil { | |
log_error("unable to get neighbors: %v", err) | |
return | |
} | |
c.Printf("--------------------------------------------------------------------------------\n") | |
c.Printf("%-4s %-20s %s\n", "id", "name", "distance") | |
c.Printf("--------------------------------------------------------------------------------\n") | |
for _, neighbor := range neighbors { | |
other := index[neighbor.id] | |
c.Printf("%-4d %-20s %-5.6v\n", other.id, other.name, neighbor.distance) | |
} | |
c.Printf("--------------------------------------------------------------------------------\n") | |
} | |
return Command{ | |
name: "nearby", | |
help: "list nearby star systems", | |
arity: 0, | |
handler: handler, | |
} | |
} | |
var winCommand = Command{ | |
name: "win", | |
help: "win the game.", | |
debug: true, | |
handler: func(conn *Connection, args ...string) { | |
conn.Win("win-command") | |
}, | |
} | |
var playersCommand = Command{ | |
name: "players", | |
help: "lists the connected players", | |
handler: func(conn *Connection, args ...string) { | |
for other, _ := range currentGame.connections { | |
conn.Printf("%v\n", other.Name()) | |
} | |
}, | |
} | |
var balCommand = Command{ | |
name: "bal", | |
help: "displays your current balance in space duckets", | |
handler: func(conn *Connection, args ...string) { | |
fmt.Fprintln(conn, conn.money) | |
}, | |
} |
This file contains hidden or 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
package main | |
import ( | |
"bufio" | |
"fmt" | |
"io" | |
"net" | |
"sort" | |
"strings" | |
"time" | |
) | |
type Connection struct { | |
*bufio.Reader | |
net.Conn | |
ConnectionState | |
bombs int | |
colonies []*System | |
kills int | |
lastBomb time.Time | |
lastScan time.Time | |
money int | |
profile *Profile | |
} | |
func NewConnection(conn net.Conn) *Connection { | |
c := &Connection{ | |
Conn: conn, | |
Reader: bufio.NewReader(conn), | |
bombs: options.startBombs, | |
money: options.startMoney, | |
} | |
c.SetState(SpawnRandomly()) | |
currentGame.Join(c) | |
return c | |
} | |
func (c *Connection) Login() { | |
for { | |
c.Printf("what is your name, adventurer?\n") | |
nick, err := c.ReadString('\n') | |
if err == nil { | |
nick = strings.TrimSpace(nick) | |
} else { | |
log_error("player failed to connect: %v", err) | |
return | |
} | |
if !ValidName(nick) { | |
c.Printf("that name is illegal.\n") | |
continue | |
} | |
log_info("player connected: %v", nick) | |
profile, err := loadProfile(nick) | |
if err != nil { | |
log_error("could not read profile: %v", err) | |
profile = &Profile{nick: nick} | |
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.nick) | |
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.nick) | |
} | |
break | |
} | |
currentGame.Register(c) | |
} | |
func (c *Connection) Dead() bool { | |
return false | |
} | |
func (c *Connection) Tick(frame int64) { | |
if c.ConnectionState == nil { | |
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.Close() | |
return | |
} | |
c.SetState(c.ConnectionState.Tick(c, frame)) | |
} | |
func (c *Connection) RunCommand(name string, args ...string) { | |
defer func() { | |
if r := recover(); r != nil { | |
c.Printf("something is broken. Log this as a ticket!\n") | |
c.Printf("recovered: %v\n", r) | |
log_error("recovered: %v", r) | |
} | |
}() | |
switch name { | |
case "commands": | |
c.Line() | |
commands := c.Commands() | |
names := make([]string, len(commands)) | |
for i := range commands { | |
names[i] = commands[i].name | |
} | |
sort.Strings(names) | |
for _, name := range names { | |
cmd := c.GetCommand(name) | |
c.Printf("%-20s%s\n", name, cmd.help) | |
} | |
c.Line() | |
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) { | |
if c.ConnectionState == s { | |
return | |
} | |
log_info("set state: %v", s) | |
if c.ConnectionState != nil { | |
log_info("exit state: %v", c.ConnectionState) | |
c.ConnectionState.Exit(c) | |
} | |
log_info("enter state: %v", s) | |
s.Enter(c) | |
c.ConnectionState = s | |
} | |
func (c *Connection) ReadLines(out chan []string) { | |
defer close(out) | |
for { | |
line, err := c.ReadString('\n') | |
switch err { | |
case io.EOF: | |
return | |
case nil: | |
break | |
default: | |
log_error("unable to read line on connection: %v", err) | |
return | |
} | |
line = strings.TrimSpace(line) | |
if line == "" { | |
continue | |
} | |
out <- strings.Split(line, " ") | |
} | |
} | |
func (c *Connection) Line() { | |
c.Printf("--------------------------------------------------------------------------------\n") | |
} | |
func (c *Connection) Printf(template string, args ...interface{}) (int, error) { | |
return fmt.Fprintf(c, template, args...) | |
} | |
func (c *Connection) Close() error { | |
log_info("player disconnecting: %s", c.Name()) | |
currentGame.Quit(c) | |
if c.Conn != nil { | |
return c.Conn.Close() | |
} | |
return nil | |
} | |
func (c *Connection) Name() string { | |
if c.profile == nil { | |
return "" | |
} | |
return c.profile.nick | |
} | |
func (c *Connection) RecordScan() { | |
c.Printf("scanning known systems for signs of life\n") | |
c.lastScan = time.Now() | |
time.AfterFunc(options.scanTime, func() { | |
c.Printf("scanner ready\n") | |
}) | |
} | |
func (c *Connection) RecordBomb() { | |
c.lastBomb = time.Now() | |
time.AfterFunc(15*time.Second, func() { | |
fmt.Fprintln(c, "bomb arsenal reloaded") | |
}) | |
} | |
func (c *Connection) CanScan() bool { | |
return time.Since(c.lastScan) > options.scanTime | |
} | |
func (c *Connection) NextScan() time.Duration { | |
return -time.Since(c.lastScan.Add(options.scanTime)) | |
} | |
func (c *Connection) NextBomb() time.Duration { | |
return -time.Since(c.lastBomb.Add(15 * time.Second)) | |
} | |
func (c *Connection) MadeKill(victim *Connection) { | |
if c == victim { | |
log_info("player %s commited suicide.", c.Name()) | |
return | |
} | |
c.kills += 1 | |
if c.kills == 3 { | |
c.Win("military") | |
} | |
} | |
func (c *Connection) Withdraw(n int) { | |
c.money -= n | |
} | |
func (c *Connection) Deposit(n int) { | |
c.money += n | |
if c.money >= options.economic { | |
c.Win("economic") | |
} | |
} | |
func (c *Connection) Win(method string) { | |
currentGame.Win(c, method) | |
} | |
func (c *Connection) Die(frame int64) { | |
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) | |
} |
This file contains hidden or 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
package main | |
import ( | |
"database/sql" | |
_ "github.com/mattn/go-sqlite3" | |
"os" | |
) | |
var ( | |
db *sql.DB | |
) | |
func dbconnect() { | |
var err error | |
db, err = sql.Open("sqlite3", "./exo.db") | |
if err != nil { | |
bail(E_No_DB, "couldn't connect to db: %v", err) | |
} | |
} | |
func planetsTable() { | |
stmnt := `create table if not exists planets ( | |
id integer not null primary key autoincrement, | |
name text, | |
x integer, | |
y integer, | |
z integer, | |
planets integer | |
);` | |
if _, err := db.Exec(stmnt); err != nil { | |
log_error("couldn't create planets table: %v", err) | |
} | |
} | |
func planetsData() { | |
n, err := countSystems() | |
if err != nil { | |
log_error("couldn't count planets: %v", err) | |
return | |
} | |
if n == 0 { | |
fi, err := os.Open(options.speckPath) | |
if err != nil { | |
bail(E_No_Data, "unable to open data path: %v", err) | |
} | |
c := make(chan System) | |
go speckStream(fi, c) | |
for planet := range c { | |
planet.Store(db) | |
} | |
} | |
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() { | |
planetsTable() | |
planetsData() | |
edgesTable() | |
profilesTable() | |
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) | |
} | |
} | |
} | |
} |
This file contains hidden or 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
package main | |
import () | |
type DeadState struct { | |
CommandSuite | |
start int64 | |
} | |
func NewDeadState(died int64) ConnectionState { | |
return &DeadState{start: died} | |
} | |
func (d *DeadState) Enter(c *Connection) { | |
c.Printf("You are dead.\n") | |
} | |
func (d *DeadState) Tick(c *Connection, frame int64) ConnectionState { | |
if frame-d.start > options.respawnFrames { | |
return SpawnRandomly() | |
} | |
return d | |
} | |
func (d *DeadState) Exit(c *Connection) { | |
c.Printf("You're alive again.\n") | |
} | |
func (d *DeadState) String() string { | |
return "dead" | |
} |
This file contains hidden or 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
package main | |
import ( | |
"fmt" | |
"strings" | |
) | |
const ( | |
E_Ok int = iota | |
E_No_Data | |
E_No_DB | |
E_No_Port | |
E_Bad_Duration | |
E_Bad_HTTP | |
E_BadPrivateKey | |
E_ListenError | |
) | |
type errorGroup []error | |
func (e errorGroup) Error() string { | |
messages := make([]string, 0, len(e)) | |
for i, _ := range e { | |
messages[i] = e[i].Error() | |
} | |
return strings.Join(messages, " && ") | |
} | |
func (g *errorGroup) AddError(err error) { | |
if err == nil { | |
return | |
} | |
if g == nil { | |
panic("fart") | |
*g = make([]error, 0, 4) | |
} | |
*g = append(*g, err) | |
} | |
// ErrorState represents a valid client state indicating that the client has | |
// hit an error. On tick, the client will be disconnected. ErrorState is both | |
// a valid ConnectionState and a valid error value. | |
type ErrorState struct { | |
CommandSuite | |
error | |
NopEnter | |
NopExit | |
} | |
func NewErrorState(e error) *ErrorState { | |
return &ErrorState{error: e} | |
} | |
func (e *ErrorState) Tick(c *Connection, frame int64) ConnectionState { | |
c.Printf("something went wrong: %v", e.error) | |
log_error("player hit error: %v", e.error) | |
c.Close() | |
return nil | |
} | |
func (e *ErrorState) String() string { | |
return fmt.Sprintf("error state: %v", e.error) | |
} | |
func (e *ErrorState) RunCommand(c *Connection, name string, args ...string) ConnectionState { | |
return e | |
} |
This file contains hidden or 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
package main | |
import ( | |
"fmt" | |
"time" | |
) | |
type Game struct { | |
id Id | |
start time.Time | |
end time.Time | |
done chan interface{} | |
winner string | |
winMethod string | |
connections map[*Connection]bool | |
frame int64 | |
elems map[GameElement]bool | |
} | |
func gamesTable() { | |
stmnt := `create table if not exists games ( | |
id text not null, | |
start text not null, | |
end text, | |
winner text, | |
win_method text | |
);` | |
if _, err := db.Exec(stmnt); err != nil { | |
log_error("couldn't create games table: %v", err) | |
} | |
} | |
func NewGame() *Game { | |
game := &Game{ | |
id: NewId(), | |
start: time.Now(), | |
done: make(chan interface{}), | |
connections: make(map[*Connection]bool, 32), | |
elems: make(map[GameElement]bool, 32), | |
} | |
if err := game.Create(); err != nil { | |
log_error("unable to create game: %v", err) | |
} | |
for _, system := range index { | |
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 | |
} | |
func (g *Game) Create() error { | |
_, err := db.Exec(` | |
insert into games | |
(id, start) | |
values | |
(?, ?) | |
;`, g.id.String(), g.start) | |
if err != nil { | |
return fmt.Errorf("error writing sqlite insert statement to create game: %v") | |
} | |
return nil | |
} | |
func (g *Game) Store() error { | |
_, err := db.Exec(` | |
update games | |
set end = ?, winner = ?, win_method = ? | |
where id = ? | |
;`, g.end, g.winner, g.winMethod, g.id) | |
return err | |
} | |
func (g *Game) Join(conn *Connection) { | |
g.connections[conn] = true | |
} | |
func (g *Game) Quit(conn *Connection) { | |
delete(g.connections, conn) | |
} | |
func (g *Game) Win(winner *Connection, method string) { | |
defer close(g.done) | |
g.end = time.Now() | |
g.winner = winner.Name() | |
g.winMethod = method | |
g.Store() | |
log_info("player %s has won by %s victory", winner.Name(), method) | |
for conn, _ := range g.connections { | |
conn.Printf("player %s has won by %s victory.\n", winner.Name(), method) | |
} | |
} | |
func (g *Game) Reset() { | |
connections := g.connections | |
fresh := NewGame() | |
*g = *fresh | |
g.connections = connections | |
} | |
func (g *Game) Run() { | |
ticker := time.Tick(time.Second / time.Duration(options.frameRate)) | |
for { | |
select { | |
case <-ticker: | |
g.tick() | |
case <-g.done: | |
for conn, _ := range g.connections { | |
conn.Close() | |
} | |
return | |
} | |
} | |
} | |
func (g *Game) Register(elem GameElement) { | |
g.elems[elem] = true | |
} | |
func (g *Game) tick() { | |
g.frame += 1 | |
for elem := range g.elems { | |
elem.Tick(g.frame) | |
} | |
for elem := range g.elems { | |
if elem.Dead() { | |
log_info("delete game object: %v", elem) | |
delete(g.elems, elem) | |
} | |
} | |
} | |
type GameElement interface { | |
Tick(frame int64) | |
Dead() bool | |
} |
This file contains hidden or 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
package main | |
import ( | |
"crypto/md5" | |
"os" | |
) | |
var global struct { | |
hostname string | |
machineId []byte | |
pid int | |
idCounter uint32 | |
} | |
func init() { | |
global.pid = os.Getpid() | |
var err error | |
global.hostname, err = os.Hostname() | |
if err != nil { | |
panic("failed to get hostname: " + err.Error()) | |
} | |
hw := md5.New() | |
if _, err := hw.Write([]byte(global.hostname)); err != nil { | |
panic("unable to md5 hostname: " + err.Error()) | |
} | |
global.machineId = make([]byte, 3) | |
copy(global.machineId, hw.Sum(nil)) | |
} |
This file contains hidden or 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
package main | |
import ( | |
"bufio" | |
"code.google.com/p/go.crypto/ssh" | |
"fmt" | |
"io" | |
"net/http" | |
"os" | |
"regexp" | |
"strings" | |
) | |
var ( | |
templates = new(templateCache) | |
httpRoot = &rootHandler{ | |
routes: make(map[*regexp.Regexp]http.Handler, 16), | |
handlers: map[string]http.Handler{ | |
"Home": http.HandlerFunc(homeHandler), | |
"Register": http.HandlerFunc(registerHandler), | |
}, | |
} | |
) | |
type rootHandler struct { | |
handlers map[string]http.Handler | |
routes map[*regexp.Regexp]http.Handler | |
} | |
func writeHTTPError(w http.ResponseWriter, code int, template string, args ...interface{}) { | |
w.WriteHeader(code) | |
fmt.Fprintf(w, template, args...) | |
} | |
func homeHandler(w http.ResponseWriter, r *http.Request) { | |
tpl, err := templates.Get("home.html.tpl") | |
if err != nil { | |
writeHTTPError(w, 505, "server error: %v", err) | |
} | |
tpl.Execute(w, nil) | |
} | |
func registerHandler(w http.ResponseWriter, r *http.Request) { | |
switch r.Method { | |
case "POST": | |
if err := r.ParseForm(); err != nil { | |
log_error("unable to parse form: %v", err) | |
} | |
email := r.FormValue("email") | |
nick := r.FormValue("nick") | |
publicKey := r.FormValue("public-key") | |
log_info("raw email: %s", email) | |
log_info("raw public key: %s", publicKey) | |
key, comment, options, rest, err := ssh.ParseAuthorizedKey([]byte(publicKey)) | |
if err != nil { | |
writeHTTPError(w, http.StatusNotAcceptable, "bad public key: %v", err) | |
return | |
} | |
log_info("email: %s key: %v comment: %v options: %v rest: %s", email, key, comment, options, rest) | |
profile := &Profile{ | |
nick: nick, | |
email: email, | |
key: publicKey, | |
} | |
log_info("creating profile %v", profile) | |
if err := profile.Create(); err != nil { | |
writeHTTPError(w, http.StatusNotAcceptable, "unable to create profile: %v", err) | |
return | |
} | |
case "GET": | |
tpl, err := templates.Get("register.html.tpl") | |
if err != nil { | |
writeHTTPError(w, 505, "server error: %v", err) | |
} | |
tpl.Execute(w, nil) | |
} | |
} | |
func (h *rootHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |
log_info("%s %s", r.Method, r.URL.String()) | |
handler := h.Route(r) | |
handler.ServeHTTP(w, r) | |
} | |
func (h *rootHandler) NotFound(w http.ResponseWriter, r *http.Request) { | |
w.WriteHeader(http.StatusNotFound) | |
fmt.Fprintln(w, "not found, sorry") | |
} | |
func (h *rootHandler) addRoute(route string, handler http.Handler) error { | |
r, err := regexp.Compile(route) | |
if err != nil { | |
return fmt.Errorf("unable to parse path: %v", err) | |
} | |
if h.routes == nil { | |
h.routes = make(map[*regexp.Regexp]http.Handler, 16) | |
} | |
h.routes[r] = handler | |
return nil | |
} | |
// registers a handler in the list of known http handlers | |
func (h *rootHandler) RegisterHandler(name string, handler http.Handler) error { | |
if h.handlers == nil { | |
h.handlers = make(map[string]http.Handler, 16) | |
} | |
h.handlers[name] = handler | |
return nil | |
} | |
func (h *rootHandler) Route(r *http.Request) http.Handler { | |
for route, handler := range h.routes { | |
if route.MatchString(r.URL.Path) { | |
log_info("path %s matches route %v", r.URL.Path, route) | |
return handler | |
} | |
} | |
return http.HandlerFunc(h.NotFound) | |
} | |
func (h *rootHandler) ReadRoutesFile(path string) error { | |
f, err := os.Open(path) | |
if err != nil { | |
return fmt.Errorf("unable to read routes file at path %s: %v", path, err) | |
} | |
defer f.Close() | |
return h.ReadRoutes(f) | |
} | |
func (h *rootHandler) ReadRoutes(f io.Reader) error { | |
r := bufio.NewReader(f) | |
for lineno, done := 1, false; !done; lineno++ { | |
line, err := r.ReadString('\n') | |
switch err { | |
case io.EOF: | |
done = true | |
case nil: | |
if err := h.parseRouteLine(lineno, line); err != nil { | |
return fmt.Errorf("unable to parse routes file: %v", err) | |
} | |
default: | |
return fmt.Errorf("unable to parse routes file: %v", err) | |
} | |
} | |
return nil | |
} | |
func (h *rootHandler) parseRouteLine(lineno int, line string) error { | |
// comment lines | |
if match, err := regexp.MatchString(`^\s+\#`, line); match { | |
return nil | |
} else if err != nil { | |
return fmt.Errorf("parse routes is fucked: %v", err) | |
} | |
// blank lines | |
if match, err := regexp.MatchString(`^\s+$`, line); match { | |
return nil | |
} else if err != nil { | |
return fmt.Errorf("parse routes is fucked: %v", err) | |
} | |
parts := strings.Split(line, " ") | |
if len(parts) != 2 { | |
return fmt.Errorf("routes parse error on line %d: expected exactly %d line parts, saw %d", lineno, 2, len(parts)) | |
} | |
path, name := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]) | |
handler, ok := h.handlers[name] | |
if !ok { | |
log_info("%v", h.handlers) | |
return fmt.Errorf("routes parse error on line %d: no such handler: %s", lineno, name) | |
} | |
if err := h.addRoute(path, handler); err != nil { | |
return fmt.Errorf("routes parse error on line %d: %v", lineno, err) | |
} | |
return nil | |
} | |
func runHTTPServer() { | |
templates.root = options.httpTemplates | |
if err := httpRoot.ReadRoutesFile(options.httpRoutes); err != nil { | |
bail(E_Bad_HTTP, "unable to start http server: %v", err) | |
} | |
log_info("starting http server on port %v", options.http) | |
if err := http.ListenAndServe(options.http, httpRoot); err != nil { | |
bail(E_Bad_HTTP, "unable to start http server: %v", err) | |
} | |
} |
This file contains hidden or 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
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]) | |
} |
This file contains hidden or 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
package main | |
import ( | |
"fmt" | |
"time" | |
) | |
type IdleState struct { | |
CommandSuite | |
NopExit | |
*System | |
} | |
func Idle(sys *System) ConnectionState { | |
i := &IdleState{System: sys} | |
i.CommandSuite = CommandSet{ | |
balCommand, | |
helpCommand, | |
playersCommand, | |
BroadcastCommand(sys), | |
NearbyCommand(sys), | |
Command{ | |
name: "goto", | |
help: "travel between star systems", | |
arity: 1, | |
handler: i.travelTo, | |
}, | |
Command{ | |
name: "bomb", | |
help: "bomb another star system", | |
arity: 1, | |
handler: i.bomb, | |
}, | |
Command{ | |
name: "mine", | |
help: "mine the current system for resources", | |
arity: 0, | |
handler: i.mine, | |
}, | |
Command{ | |
name: "info", | |
help: "gives you information about the current star system", | |
arity: 0, | |
handler: i.info, | |
}, | |
Command{ | |
name: "scan", | |
help: "scans the galaxy for signs of life", | |
arity: 0, | |
handler: i.scan, | |
}, | |
Command{ | |
name: "make", | |
help: "makes things", | |
handler: i.maek, | |
}, | |
} | |
return i | |
} | |
func (i *IdleState) Enter(c *Connection) { | |
i.System.Arrive(c) | |
} | |
func (i *IdleState) String() string { | |
return fmt.Sprintf("idle on %v", i.System) | |
} | |
func (i *IdleState) Tick(c *Connection, frame int64) ConnectionState { | |
return i | |
} | |
func (i *IdleState) travelTo(c *Connection, args ...string) { | |
dest, err := GetSystem(args[0]) | |
if err != nil { | |
c.Printf("%v\n", err) | |
return | |
} | |
c.SetState(NewTravel(c, i.System, dest)) | |
} | |
func (i *IdleState) bomb(c *Connection, args ...string) { | |
if c.bombs <= 0 { | |
c.Printf("Cannot send bomb: no bombs left! Build more bombs!\n") | |
return | |
} | |
if time.Since(c.lastBomb) < 5*time.Second { | |
c.Printf("Cannot send bomb: bombs are reloading\n") | |
return | |
} | |
target, err := GetSystem(args[0]) | |
if err != nil { | |
c.Printf("Cannot send bomb: %v\n", err) | |
return | |
} | |
c.bombs -= 1 | |
c.lastBomb = time.Now() | |
bomb := NewBomb(c, i.System, target) | |
currentGame.Register(bomb) | |
} | |
func (i *IdleState) mine(c *Connection, args ...string) { | |
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) { | |
if time.Since(c.lastScan) < 1*time.Minute { | |
return | |
} | |
c.Printf("Scanning the galaxy for signs of life...\n") | |
currentGame.Register(NewScan(i.System)) | |
} | |
// "make" is already a keyword | |
func (i *IdleState) maek(c *Connection, args ...string) { | |
switch args[0] { | |
case "bomb": | |
if c.money < options.bombCost { | |
c.Printf("Not enough money! Bombs costs %v but you only have %v space duckets. Mine more space duckets!\n", options.bombCost, c.money) | |
return | |
} | |
c.SetState(MakeBomb(i.System)) | |
case "colony": | |
MakeColony(c, i.System) | |
return | |
case "shield": | |
MakeShield(c, i.System) | |
default: | |
c.Printf("I don't know how to make a %v.\n", args[0]) | |
} | |
} |
This file contains hidden or 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
package main | |
import () |
This file contains hidden or 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
package main | |
import ( | |
"flag" | |
"fmt" | |
"log" | |
"math/rand" | |
"net" | |
"os" | |
"time" | |
) | |
var options struct { | |
bombCost int | |
bombSpeed float64 | |
debug bool | |
economic int | |
frameLength time.Duration | |
colonyCost int | |
frameRate int | |
http string | |
httpRoutes string | |
httpTemplates string | |
lightSpeed float64 | |
makeBombTime time.Duration | |
makeColonyTime time.Duration | |
makeShieldTime time.Duration | |
moneyMean float64 | |
moneySigma float64 | |
playerSpeed float64 | |
respawnFrames int64 | |
respawnTime time.Duration | |
scanTime time.Duration | |
speckPath string | |
ssh string | |
sshKey string | |
startBombs int | |
startMoney int | |
} | |
var ( | |
info_log *log.Logger | |
error_log *log.Logger | |
currentGame *Game | |
) | |
func log_error(template string, args ...interface{}) { | |
error_log.Printf(template, args...) | |
} | |
func log_info(template string, args ...interface{}) { | |
info_log.Printf(template, args...) | |
} | |
func bail(status int, template string, args ...interface{}) { | |
if status == 0 { | |
fmt.Fprintf(os.Stdout, template, args...) | |
} else { | |
fmt.Fprintf(os.Stderr, template, args...) | |
} | |
os.Exit(status) | |
} | |
func handleConnection(conn *Connection) { | |
defer conn.Close() | |
conn.Login() | |
c := make(chan []string) | |
go conn.ReadLines(c) | |
for parts := range c { | |
conn.RunCommand(parts[0], parts[1:]...) | |
} | |
} | |
// converts a duration in human time to a number of in-game frames | |
func durToFrames(dur time.Duration) int64 { | |
return int64(dur / options.frameLength) | |
} | |
func framesToDur(frames int64) time.Duration { | |
return options.frameLength * time.Duration(frames) | |
} | |
func main() { | |
flag.Parse() | |
dbconnect() | |
options.frameLength = time.Second / time.Duration(options.frameRate) | |
options.respawnFrames = durToFrames(options.respawnTime) | |
rand.Seed(time.Now().UnixNano()) | |
info_log = log.New(os.Stdout, "[INFO] ", 0) | |
error_log = log.New(os.Stderr, "[ERROR] ", 0) | |
setupDb() | |
listener, err := net.Listen("tcp", ":9219") | |
if err != nil { | |
bail(E_No_Port, "unable to start server: %v", err) | |
} | |
go runHTTPServer() | |
go runSSHServer() | |
go func() { | |
for { | |
log_info("starting new game") | |
currentGame = NewGame() | |
currentGame.Run() | |
} | |
}() | |
for { | |
conn, err := listener.Accept() | |
if err != nil { | |
log_error("error accepting connection: %v", err) | |
continue | |
} | |
go handleConnection(NewConnection(conn)) | |
} | |
} | |
func init() { | |
flag.Float64Var(&options.lightSpeed, "light-speed", 0.01, "speed of light in parsecs per frame") | |
flag.IntVar(&options.frameRate, "frame-rate", 100, "frame rate, in frames per second") | |
flag.Float64Var(&options.playerSpeed, "player-speed", 0.8, "player travel speed, relative to C, the speed of light") | |
flag.Float64Var(&options.bombSpeed, "bomb-speed", 0.9, "bomb travel speed, relattive to C, the speed of light") | |
flag.IntVar(&options.economic, "economic", 25000, "amount of money needed to win economic victory") | |
flag.Float64Var(&options.moneyMean, "money-mean", 10000, "mean amount of money on a system") | |
flag.Float64Var(&options.moneySigma, "money-sigma", 1500, "standard deviation in money per system") | |
flag.BoolVar(&options.debug, "debug", false, "puts the game in debug mode") | |
flag.StringVar(&options.speckPath, "speck-path", "/projects/exo/expl.speck", "path to exoplanet speck file") | |
flag.DurationVar(&options.respawnTime, "respawn-time", 60*time.Second, "time for player respawn") | |
flag.DurationVar(&options.makeBombTime, "bomb-time", 5*time.Second, "time it takes to make a bomb") | |
flag.IntVar(&options.bombCost, "bomb-cost", 500, "price of a bomb") | |
flag.IntVar(&options.colonyCost, "colony-cost", 2000, "price of a colony") | |
flag.DurationVar(&options.makeColonyTime, "colony-time", 15*time.Second, "time it takes to make a colony") | |
flag.IntVar(&options.startBombs, "start-bombs", 0, "number of bombs a player has at game start") | |
flag.IntVar(&options.startMoney, "start-money", 1000, "amount of money a player has to start") | |
flag.DurationVar(&options.makeShieldTime, "shield-time", 15*time.Second, "time it takes to make a shield") | |
flag.DurationVar(&options.scanTime, "scan-recharge", 1*time.Minute, "time it takes for scanners to recharge") | |
flag.StringVar(&options.http, "http", ":8000", "http hostname:port") | |
flag.StringVar(&options.httpRoutes, "http-routes", "./routes", "path to a file containing http routes") | |
flag.StringVar(&options.httpTemplates, "http-templates", "./templates", "path to a file containing http templates") | |
flag.StringVar(&options.sshKey, "ssh-key", "./exo_rsa", "path to an ssh private key for running the ssh server") | |
flag.StringVar(&options.ssh, "ssh", ":9220", "ssh host:port to open") | |
} |
This file contains hidden or 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
package main | |
import ( | |
"fmt" | |
) | |
type MiningState struct { | |
CommandSuite | |
*System | |
mined int | |
} | |
func Mine(sys *System) ConnectionState { | |
m := &MiningState{System: sys} | |
m.CommandSuite = CommandSet{ | |
balCommand, | |
helpCommand, | |
playersCommand, | |
BroadcastCommand(sys), | |
NearbyCommand(sys), | |
Command{ | |
name: "stop", | |
help: "stops mining", | |
arity: 0, | |
handler: m.stop, | |
}, | |
Command{ | |
name: "info", | |
help: "gives you information about the current mining operation", | |
arity: 0, | |
handler: m.info, | |
}, | |
} | |
return m | |
} | |
func (m *MiningState) Enter(c *Connection) { | |
c.Printf("Mining %v. %v space duckets remaining.\n", m.System, m.money) | |
} | |
func (m *MiningState) Tick(c *Connection, frame int64) ConnectionState { | |
if m.money <= 0 { | |
c.Printf("system %s is all out of space duckets.\n", m.System) | |
return Idle(m.System) | |
} else { | |
c.Deposit(1) | |
m.mined += 1 | |
m.money -= 1 | |
return m | |
} | |
} | |
func (m *MiningState) Exit(c *Connection) { | |
if m.money == 0 { | |
c.Printf("Done mining %v.\nMined %v space duckets total.\nNo space duckets remain on %v, and it can't be mined again.\n", m.System, m.mined, m.System) | |
} else { | |
c.Printf("Done mining %v.\nMined %v space duckets total.\n%v space duckets remain on %v, and it can be mined again.\n", m.System, m.mined, m.money, m.System) | |
} | |
} | |
func (m *MiningState) String() string { | |
return fmt.Sprintf("mining %v", m.System) | |
} | |
func (m *MiningState) stop(c *Connection, args ...string) { | |
c.SetState(Idle(m.System)) | |
} | |
func (m *MiningState) info(c *Connection, args ...string) { | |
c.Printf("Currently mining system %v\n", m.System) | |
c.Printf("Mined so far: %v\n", m.mined) | |
c.Printf("Remaining space duckets on %v: %v\n", m.System, m.money) | |
} |
This file contains hidden or 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
package main | |
import ( | |
"code.google.com/p/go.crypto/ssh" | |
"fmt" | |
"regexp" | |
) | |
var namePattern = regexp.MustCompile(`^[[:alpha:]][[:alnum:]-_]{0,19}$`) | |
func ValidName(name string) bool { | |
return namePattern.MatchString(name) | |
} | |
type Profile struct { | |
id int | |
nick string | |
email string | |
key string | |
} | |
func (p *Profile) Create() error { | |
_, err := db.Exec(` | |
insert into profiles | |
(nick, email, key) | |
values | |
(?, ?, ?) | |
;`, p.nick, p.email, p.key) | |
if err != nil { | |
return fmt.Errorf("unable to create profile: %v", err) | |
} | |
return nil | |
} | |
func (p *Profile) PublicKey() (ssh.PublicKey, error) { | |
key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(p.key)) | |
return key, err | |
} | |
func profilesTable() { | |
stmnt := `create table if not exists profiles ( | |
id integer not null primary key autoincrement, | |
nick text unique not null, | |
email text unique not null, | |
key text unique not null | |
);` | |
if _, err := db.Exec(stmnt); err != nil { | |
log_error("couldn't create profiles table: %v", err) | |
} | |
} | |
func loadProfile(nick string) (*Profile, error) { | |
row := db.QueryRow(`select * from profiles where nick = ?`, nick) | |
var p Profile | |
if err := row.Scan(&p.id, &p.nick, &p.email, &p.key); err != nil { | |
return nil, fmt.Errorf("unable to fetch profile from database: %v", err) | |
} | |
return &p, nil | |
} |
This file contains hidden or 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
package main | |
import ( | |
"fmt" | |
"time" | |
) | |
type scan struct { | |
start time.Time | |
origin *System | |
dist float64 | |
nextHitIndex int | |
nextEchoIndex int | |
results []scanResult | |
} | |
type scanResult struct { | |
system *System | |
dist float64 | |
players map[*Connection]bool | |
colonizedBy *Connection | |
shielded bool | |
shieldEnergy float64 | |
} | |
func (r *scanResult) Empty() bool { | |
return (r.players == nil || len(r.players) == 0) && r.colonizedBy == nil | |
} | |
func (r *scanResult) playerNames() []string { | |
if r.players == nil || len(r.players) == 0 { | |
return nil | |
} | |
names := make([]string, 0, len(r.players)) | |
for conn := range r.players { | |
names = append(names, conn.Name()) | |
} | |
return names | |
} | |
func NewScan(origin *System) *scan { | |
return &scan{ | |
origin: origin, | |
start: time.Now(), | |
results: make([]scanResult, 0, len(origin.Distances())), | |
} | |
} | |
func (s *scan) Tick(frame int64) { | |
s.dist += options.lightSpeed | |
s.hits() | |
s.echos() | |
} | |
func (s *scan) Dead() bool { | |
return s.nextEchoIndex >= len(s.origin.Distances()) | |
} | |
func (s *scan) String() string { | |
return fmt.Sprintf("[scan origin: %s start_time: %v]", s.origin.name, s.start) | |
} | |
func (s *scan) hits() { | |
for ; s.nextHitIndex < len(s.origin.Distances()); s.nextHitIndex += 1 { | |
candidate := s.origin.Distances()[s.nextHitIndex] | |
if s.dist < candidate.dist { | |
break | |
} | |
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)) | |
} | |
} | |
func (s *scan) hitSystem(sys *System, dist float64) scanResult { | |
sys.NotifyInhabitants("scan detected from %v\n", s.origin) | |
r := scanResult{ | |
system: sys, | |
colonizedBy: sys.colonizedBy, | |
dist: dist * 2.0, | |
shielded: sys.Shield != nil, | |
} | |
if sys.Shield != nil { | |
r.shieldEnergy = sys.Shield.energy | |
} | |
if sys.players != nil { | |
r.players = make(map[*Connection]bool, len(sys.players)) | |
for k, v := range sys.players { | |
r.players[k] = v | |
} | |
} | |
return r | |
} | |
func (s *scan) echos() { | |
for ; s.nextEchoIndex < len(s.results); s.nextEchoIndex += 1 { | |
res := s.results[s.nextEchoIndex] | |
if s.dist < res.dist { | |
break | |
} | |
log_info("echo from %v reached origin %v after %v", res.system.name, s.origin.name, time.Since(s.start)) | |
if res.Empty() { | |
continue | |
} | |
s.origin.NotifyInhabitants("results from scan of %v:\n", res.system) | |
s.origin.NotifyInhabitants("\tdistance: %v\n", s.origin.DistanceTo(res.system)) | |
s.origin.NotifyInhabitants("\tshielded: %v\n", res.shielded) | |
if res.shielded { | |
s.origin.NotifyInhabitants("\tshield energy: %v\n", res.shieldEnergy) | |
} | |
inhabitants := res.playerNames() | |
if inhabitants != nil { | |
s.origin.NotifyInhabitants("\tinhabitants: %v\n", inhabitants) | |
} | |
if res.colonizedBy != nil { | |
s.origin.NotifyInhabitants("\tcolonized by: %v\n", res.colonizedBy.Name()) | |
} | |
} | |
} |
This file contains hidden or 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
package main | |
import ( | |
"fmt" | |
) | |
func MakeShield(c *Connection, s *System) { | |
m := &MakeShieldState{ | |
System: s, | |
CommandSuite: CommandSet{ | |
balCommand, | |
BroadcastCommand(s), | |
helpCommand, | |
NearbyCommand(s), | |
playersCommand, | |
}, | |
} | |
c.SetState(m) | |
} | |
type MakeShieldState struct { | |
CommandSuite | |
*System | |
start int64 | |
} | |
func (m *MakeShieldState) Enter(c *Connection) { | |
c.Printf("Making shield on %v...\n", m.System) | |
} | |
func (m *MakeShieldState) Tick(c *Connection, frame int64) ConnectionState { | |
if m.start == 0 { | |
m.start = frame | |
} | |
if framesToDur(frame-m.start) >= options.makeShieldTime { | |
return Idle(m.System) | |
} | |
return m | |
} | |
func (m *MakeShieldState) Exit(c *Connection) { | |
c.Printf("Done! System %v is now shielded.\n", m.System) | |
m.System.Shield = new(Shield) | |
} | |
func (m *MakeShieldState) String() string { | |
return fmt.Sprintf("Making shield on %v", m.System) | |
} | |
type Shield struct { | |
energy float64 | |
} | |
func (s *Shield) Tick(frame int64) { | |
if s.energy < 1000 { | |
s.energy += (1000 - s.energy) * 0.0005 | |
} | |
} | |
func (s *Shield) Hit() bool { | |
if s.energy > 750 { | |
s.energy -= 750 | |
return true | |
} | |
return false | |
} | |
func (s *Shield) Dead() bool { | |
return false | |
} |
This file contains hidden or 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
package main | |
import ( | |
"bufio" | |
"io" | |
"regexp" | |
"strconv" | |
"strings" | |
) | |
func speckStream(r io.ReadCloser, c chan System) { | |
defer close(c) | |
defer r.Close() | |
keep := regexp.MustCompile(`^\s*[\d-]`) | |
br := bufio.NewReader(r) | |
for { | |
line, err := br.ReadBytes('\n') | |
switch err { | |
case io.EOF: | |
return | |
case nil: | |
break | |
default: | |
log_error("unable to stream speck file: %v", err) | |
return | |
} | |
if !keep.Match(line) { | |
continue | |
} | |
planet := parseSpeckLine(line) | |
c <- *planet | |
} | |
} | |
func parseSpeckLine(line []byte) *System { | |
parts := strings.Split(string(line), " ") | |
var err error | |
var g errorGroup | |
s := new(System) | |
s.x, err = strconv.ParseFloat(parts[0], 64) | |
g.AddError(err) | |
s.y, err = strconv.ParseFloat(parts[1], 64) | |
g.AddError(err) | |
s.z, err = strconv.ParseFloat(parts[2], 64) | |
g.AddError(err) | |
s.planets, err = strconv.Atoi(parts[3]) | |
g.AddError(err) | |
s.name = strings.TrimSpace(strings.Join(parts[7:], " ")) | |
if g != nil { | |
log_error("unable to parse speck line: %v", g) | |
} | |
return s | |
} |
This file contains hidden or 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
package main | |
import ( | |
"bufio" | |
"code.google.com/p/go.crypto/ssh" | |
"fmt" | |
"io" | |
"io/ioutil" | |
"net" | |
"time" | |
"unicode" | |
) | |
func handlePublicKey(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { | |
profile, err := loadProfile(conn.User()) | |
if err != nil { | |
log_info("client saw error when attempting to establish ssh connection: %v", err) | |
return nil, fmt.Errorf("unable to find profile: %v", err) | |
} | |
storedKey, err := profile.PublicKey() | |
if err != nil { | |
log_error("unable to parse stored public key for user %s: %v", conn.User(), err) | |
return nil, fmt.Errorf("we have a bad key: %v", err) | |
} | |
if string(storedKey.Marshal()) != string(key.Marshal()) { | |
log_info("client sent the wrong key") | |
return nil, fmt.Errorf("wrong key") | |
} | |
log_info("player %s connected", conn.User()) | |
var perms ssh.Permissions | |
return &perms, nil | |
} | |
func streamSSHRunes(c ssh.Channel, out chan rune) { | |
defer close(out) | |
r := bufio.NewReader(c) | |
for { | |
roon, _, err := r.ReadRune() | |
switch err { | |
case io.EOF: | |
return | |
case nil: | |
out <- roon | |
default: | |
log_error("error on ssh byte stream: %v", err) | |
return | |
} | |
} | |
} | |
func handleSSHSessionChan(conn *ssh.ServerConn, c ssh.Channel) { | |
t := time.Tick(250 * time.Millisecond) | |
runes := make(chan rune) | |
cmd := make([]rune, 0, 32) | |
go streamSSHRunes(c, runes) | |
for { | |
select { | |
case now := <-t: | |
fmt.Fprintf(c, "\r%v: %s", now.Unix(), string(cmd)) | |
case r, ok := <-runes: | |
if !ok { | |
return | |
} | |
switch r { | |
case 0x4: // this is what ctrl+d sends | |
c.Close() | |
return | |
case 0xc: // ctrl+l | |
fmt.Fprint(c, "c") | |
case 0xd: // enter | |
log_info("%x (enter)", r) | |
fmt.Fprintf(c, "\r\n%v: %s", time.Now().Unix(), string(cmd)) | |
case 0x41: | |
log_info("up") | |
case 0x42: | |
log_info("down") | |
case 0x43: | |
log_info("right") | |
case 0x44: | |
log_info("left") | |
case 0x7f: // backspace | |
if len(cmd) > 0 { | |
cmd = cmd[:len(cmd)-1] | |
fmt.Fprint(c, " ") | |
} | |
case 0x15: // ctrl+u | |
cmd = cmd[:0] | |
fmt.Fprintf(c, "\r%v: %s", time.Now().Unix(), string(cmd)) | |
default: | |
if unicode.IsGraphic(r) { | |
cmd = append(cmd, r) | |
fmt.Fprintf(c, "\r%v: %s", time.Now().Unix(), string(cmd)) | |
} else { | |
// a 0x61 | |
// z 0x7a | |
// 0 0x30 | |
// 9 0x39 | |
// space 0x20 | |
// esc 0x1b | |
log_info("%x %c", r, r) | |
} | |
} | |
} | |
} | |
} | |
func handleNewSSHChannel(conn *ssh.ServerConn, req ssh.NewChannel) { | |
logSSHChan(req) | |
switch req.ChannelType() { | |
case "session": | |
c, requests, err := req.Accept() | |
if err != nil { | |
log_error("unable to handle ssh session channel request: %v", err) | |
return | |
} | |
go handleSSHRequests(conn, requests, "session") | |
go handleSSHSessionChan(conn, c) | |
default: | |
log_info("refusing ssh channel request for a channel of type %s", req.ChannelType()) | |
if err := req.Reject(ssh.UnknownChannelType, "I don't know how to handle that channel type"); err != nil { | |
log_error("hit error rejecting ssh channel request: %v", err) | |
} | |
} | |
} | |
func handleSSHChannels(conn *ssh.ServerConn, c <-chan ssh.NewChannel) { | |
for req := range c { | |
handleNewSSHChannel(conn, req) | |
} | |
} | |
func logSSHChan(req ssh.NewChannel) { | |
log_info("ssh channel request:\n\ttype:%s\n\textra: %s", req.ChannelType(), req.ExtraData()) | |
} | |
func logSSHRequest(req *ssh.Request, chanType string) { | |
log_info("ssh request on %s channel:\n\ttype:%s\n\tresponse requested: %v\n\t payload: %s", chanType, req.Type, req.WantReply, req.Payload) | |
} | |
func handleSSHRequests(conn *ssh.ServerConn, c <-chan *ssh.Request, chanType string) { | |
for request := range c { | |
logSSHRequest(request, chanType) | |
} | |
} | |
func handleSSHConnection(conn net.Conn, config *ssh.ServerConfig) { | |
log_info("incoming ssh connection from %s", conn.RemoteAddr()) | |
sshConn, channels, requests, err := ssh.NewServerConn(conn, config) | |
if err != nil { | |
log_error("error in client handshake: %v", err) | |
return | |
} | |
go handleSSHRequests(sshConn, requests, "root") | |
go handleSSHChannels(sshConn, channels) | |
if err := sshConn.Wait(); err != nil { | |
log_error("server conn exited with error: %v", err) | |
} | |
} | |
func runSSHServer() { | |
// load up private key file | |
pemBytes, err := ioutil.ReadFile(options.sshKey) | |
if err != nil { | |
bail(E_BadPrivateKey, "unable to read private key file: %v", err) | |
} | |
log_info("read %d pem bytes\n", len(pemBytes)) | |
// parse private key file data | |
signer, err := ssh.ParsePrivateKey(pemBytes) | |
if err != nil { | |
bail(E_BadPrivateKey, "unable to parse private key data: %v", err) | |
} | |
log_info("we have a signer: %v", signer) | |
config := &ssh.ServerConfig{ | |
NoClientAuth: false, | |
PublicKeyCallback: handlePublicKey, | |
} | |
config.AddHostKey(signer) | |
listener, err := net.Listen("tcp", options.ssh) | |
if err != nil { | |
bail(E_ListenError, "unable to open tcp listener: %v", err) | |
} | |
log_info("opened tcp port %s for ssh", options.ssh) | |
for { | |
conn, err := listener.Accept() | |
if err != nil { | |
log_error("client failed to establish tcp connection: %v", err) | |
continue | |
} | |
go handleSSHConnection(conn, config) | |
} | |
} |
This file contains hidden or 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
package main | |
import ( | |
"database/sql" | |
"fmt" | |
"math" | |
"math/rand" | |
"strconv" | |
"time" | |
) | |
var ( | |
index map[int]*System | |
nameIndex map[string]*System | |
) | |
type System struct { | |
*Shield | |
id int | |
x, y, z float64 | |
planets int | |
name string | |
players map[*Connection]bool | |
colonizedBy *Connection | |
distances []Ray | |
money int64 | |
} | |
func GetSystem(id string) (*System, error) { | |
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 { | |
s.colonizedBy.Deposit(1) | |
s.money -= 1 | |
} | |
if s.Shield != nil { | |
s.Shield.Tick(frame) | |
} | |
} | |
func (s *System) Dead() bool { | |
return false | |
} | |
func (s *System) Reset() { | |
s.players = make(map[*Connection]bool, 32) | |
s.colonizedBy = nil | |
} | |
func (s *System) Arrive(conn *Connection) { | |
// conn.SetSystem(s) | |
if s.players[conn] { | |
return | |
} | |
log_info("player %s has arrived at system %v", conn.Name(), s) | |
if s.players == nil { | |
s.players = make(map[*Connection]bool, 8) | |
} | |
s.players[conn] = true | |
if s.planets == 1 { | |
conn.Printf("you are in the system %v. There is %d planet here.\n", s, s.planets) | |
} else { | |
conn.Printf("you are in the system %v. There are %d planets here.\n", s, s.planets) | |
} | |
} | |
func (s *System) Leave(p *Connection) { | |
delete(s.players, p) | |
// p.location = nil | |
} | |
func (s *System) NotifyInhabitants(template string, args ...interface{}) { | |
s.EachConn(func(conn *Connection) { | |
conn.Printf(template, args...) | |
}) | |
} | |
func (s *System) EachConn(fn func(*Connection)) { | |
if s.players == nil { | |
return | |
} | |
for conn, _ := range s.players { | |
fn(conn) | |
} | |
} | |
func (s *System) NumInhabitants() int { | |
if s.players == nil { | |
return 0 | |
} | |
return len(s.players) | |
} | |
func (e System) Store(db *sql.DB) { | |
_, err := db.Exec(` | |
insert into planets | |
(name, x, y, z, planets) | |
values | |
(?, ?, ?, ?, ?) | |
;`, e.name, e.x, e.y, e.z, e.planets) | |
if err != nil { | |
log_error("unable to store system: %v", err) | |
} | |
} | |
func (s *System) DistanceTo(other *System) float64 { | |
return dist3d(s.x, s.y, s.z, other.x, other.y, other.z) | |
} | |
func (s *System) LightTimeTo(other *System) time.Duration { | |
return time.Duration(int64(s.DistanceTo(other) * 100000000)) | |
} | |
func (s *System) BombTimeTo(other *System) time.Duration { | |
return time.Duration(int64(s.DistanceTo(other) * 110000000)) | |
} | |
func (s *System) TravelTimeTo(other *System) time.Duration { | |
return time.Duration(int64(s.DistanceTo(other) * 125000000)) | |
} | |
type Ray struct { | |
s *System | |
dist float64 // distance in parsecs | |
} | |
func (s *System) Distances() []Ray { | |
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.Hit() { | |
s.EachConn(func(conn *Connection) { | |
conn.Printf("A bomb has hit %v but it was stopped by the system's shield.\n", s) | |
conn.Printf("Shield remaining: %v.\n", s.energy) | |
conn.Printf("Shield is recharing....\n") | |
}) | |
return | |
} | |
} | |
s.EachConn(func(conn *Connection) { | |
conn.Die(frame) | |
s.Leave(conn) | |
bomber.MadeKill(conn) | |
}) | |
if s.colonizedBy != nil { | |
s.colonizedBy.Printf("your mining colony on %s has been destroyed!\n", s.name) | |
s.colonizedBy = nil | |
} | |
for id, _ := range index { | |
if id == s.id { | |
continue | |
} | |
delay := s.LightTimeTo(index[id]) | |
id2 := id | |
time.AfterFunc(delay, func() { | |
bombNotice(id2, s.id) | |
}) | |
} | |
} | |
func bombNotice(to_id, from_id int) { | |
to := index[to_id] | |
from := index[from_id] | |
to.EachConn(func(conn *Connection) { | |
conn.Printf("a bombing has been observed on %s\n", from.name) | |
}) | |
} | |
func (s System) String() string { | |
return fmt.Sprintf("%s (id: %v)", s.name, s.id) | |
} | |
type Neighbor struct { | |
id int | |
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) { | |
row := db.QueryRow(`select count(*) from planets`) | |
var n int | |
err := row.Scan(&n) | |
return n, err | |
} | |
func sq(x float64) float64 { | |
return x * x | |
} | |
func dist3d(x1, y1, z1, x2, y2, z2 float64) float64 { | |
return math.Sqrt(sq(x1-x2) + sq(y1-y2) + sq(z1-z2)) | |
} | |
func indexSystems() map[int]*System { | |
rows, err := db.Query(`select * from planets`) | |
if err != nil { | |
log_error("unable to select all planets: %v", err) | |
return nil | |
} | |
defer rows.Close() | |
index = make(map[int]*System, 551) | |
nameIndex = make(map[string]*System, 551) | |
for rows.Next() { | |
p := System{} | |
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) | |
continue | |
} | |
index[p.id] = &p | |
nameIndex[p.name] = &p | |
p.money = int64(rand.NormFloat64()*options.moneySigma + options.moneyMean) | |
log_info("seeded system %v with %v monies", p, p.money) | |
} | |
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 | |
} |
This file contains hidden or 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
package main | |
import ( | |
"fmt" | |
"html/template" | |
"path/filepath" | |
) | |
type templateCache struct { | |
root string | |
} | |
func (t *templateCache) Get(relpath string) (*template.Template, error) { | |
path := filepath.Join(t.root, relpath) | |
tpl, err := template.ParseFiles(path) | |
if err != nil { | |
return nil, fmt.Errorf("unable to fetch template at %s: %v", relpath, err) | |
} | |
return tpl, nil | |
} |
This file contains hidden or 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
package main | |
import ( | |
"fmt" | |
"time" | |
) | |
type TravelState struct { | |
CommandSuite | |
start *System | |
dest *System | |
travelled float64 // distance traveled so far in parsecs | |
dist float64 // distance between start and end in parsecs | |
} | |
func NewTravel(c *Connection, start, dest *System) ConnectionState { | |
t := &TravelState{ | |
start: start, | |
dest: dest, | |
dist: start.DistanceTo(dest), | |
} | |
t.CommandSuite = CommandSet{ | |
helpCommand, | |
playersCommand, | |
balCommand, | |
Command{ | |
name: "progress", | |
help: "displays how far you are along your travel", | |
arity: 0, | |
handler: t.progress, | |
}, | |
Command{ | |
name: "eta", | |
help: "displays estimated time of arrival", | |
arity: 0, | |
handler: func(c *Connection, args ...string) { | |
c.Printf("Remaining: %v\n", t.remaining()) | |
c.Printf("Current time: %v\n", time.Now()) | |
c.Printf("ETA: %v\n", t.eta()) | |
}, | |
}, | |
} | |
return t | |
} | |
func (t *TravelState) Enter(c *Connection) { | |
c.Printf("Leaving %v, bound for %v.\n", t.start, t.dest) | |
c.Printf("Trip duration: %v\n", t.tripTime()) | |
c.Printf("Current time: %v\n", time.Now()) | |
c.Printf("ETA: %v\n", t.eta()) | |
t.start.Leave(c) | |
} | |
func (t *TravelState) Tick(c *Connection, frame int64) ConnectionState { | |
t.travelled += options.playerSpeed * options.lightSpeed | |
if t.travelled >= t.dist { | |
return Idle(t.dest) | |
} | |
return t | |
} | |
func (t *TravelState) Exit(c *Connection) { | |
c.Printf("You have arrived at %v.\n", t.dest) | |
t.dest.Arrive(c) | |
} | |
func (t *TravelState) String() string { | |
return fmt.Sprintf("Traveling from %v to %v", t.start, t.dest) | |
} | |
func (t *TravelState) progress(c *Connection, args ...string) { | |
c.Printf("%v\n", t.travelled/t.dist) | |
} | |
func (t *TravelState) remaining() time.Duration { | |
remaining := t.dist - t.travelled | |
frames := remaining / (options.playerSpeed * options.lightSpeed) | |
return framesToDur(int64(frames)) | |
} | |
func (t *TravelState) eta() time.Time { | |
// distance remaining in parsecs | |
return time.Now().Add(t.remaining()) | |
} | |
func (t *TravelState) tripTime() time.Duration { | |
frames := t.dist / (options.playerSpeed * options.lightSpeed) | |
return framesToDur(int64(frames)) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment