Compare commits
No commits in common. "bcfd42970bac8676e2bfac0227344bd55e0dfee4" and "fb5735d5b94d5ea9e014e360a5f33991daaadc34" have entirely different histories.
bcfd42970b
...
fb5735d5b9
@ -1,167 +0,0 @@
|
|||||||
package cardsim
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// An InfoPanel displays some set of stats to the player. It does
|
|
||||||
// not consume an action. It must not advance the state of the game
|
|
||||||
// in any way.
|
|
||||||
type InfoPanel[C StatsCollection] interface {
|
|
||||||
// Title returns the title of this InfoPanel, which is also used as the
|
|
||||||
// label presented to the player to access this panel.
|
|
||||||
Title(p *Player[C]) (Message, error)
|
|
||||||
|
|
||||||
// Info returns the displayable contents of this InfoPanel. A nil Message
|
|
||||||
// in the output is interpreted as a paragraph break.
|
|
||||||
Info(p *Player[C]) ([]Message, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// A StatFilter decides whether to show a specific stat in a BasicStatsPanel
|
|
||||||
// (and maybe other kinds of stats panels, if they choose to support this).
|
|
||||||
type StatFilter[C StatsCollection] func(p *Player[C], s Stat) bool
|
|
||||||
|
|
||||||
// BasicStatsPanel shows some or all of the stats output by C, under
|
|
||||||
// a fixed name, introduced by a specific prompt. Stats are shown as a two
|
|
||||||
// column table with the name, then the value.
|
|
||||||
type BasicStatsPanel[C StatsCollection] struct {
|
|
||||||
// Name stores the name of this stats panel, which is also shown in the menu.
|
|
||||||
Name Message
|
|
||||||
|
|
||||||
// Intro stores a message to always display before the stats. Optional.
|
|
||||||
Intro Message
|
|
||||||
|
|
||||||
// Filter stores a function to decide what stats to show. If this is not
|
|
||||||
// provided, the BasicStatsPanel uses VisibleOrDebug by default.
|
|
||||||
Filter StatFilter[C]
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisibleOrDebug returns whether s is Visible or p is in debug mode,
|
|
||||||
// so a debug-mode player shows all stats.
|
|
||||||
func VisibleOrDebug[C StatsCollection](p *Player[C], s Stat) bool {
|
|
||||||
return p.DebugLevel > 0 || s.Visible()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Title implements `InfoPanel[C]` by returning b's `Name`.
|
|
||||||
func (b *BasicStatsPanel[C]) Title(p *Player[C]) (Message, error) {
|
|
||||||
return b.Name, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Info implements `InfoPanel[C]` by dumpiing p.Stats, showing those items for
|
|
||||||
// whch b.Filter returns true.
|
|
||||||
func (b *BasicStatsPanel[C]) Info(p *Player[C]) ([]Message, error) {
|
|
||||||
stats := p.Stats.Stats()
|
|
||||||
cached := make([]cachedStat, 0, len(stats))
|
|
||||||
longestName := 0
|
|
||||||
filter := b.Filter
|
|
||||||
if filter == nil {
|
|
||||||
filter = VisibleOrDebug[C]
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, s := range stats {
|
|
||||||
if !filter(p, s) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
name := s.StatName()
|
|
||||||
if len(name) > longestName {
|
|
||||||
longestName = len(name)
|
|
||||||
}
|
|
||||||
cached = append(cached, cachedStat{name, s.String(), s.Visible()})
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cached) == 0 {
|
|
||||||
return []Message{b.Intro, nil, MsgStr("No stats available")}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ret := make([]Message, 0, 2+len(cached))
|
|
||||||
ret = append(ret, b.Intro, nil)
|
|
||||||
for _, s := range cached {
|
|
||||||
ret = append(ret, MsgStr(s.output(longestName)))
|
|
||||||
}
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// cachedStat is an implementation detail of BasicStatsPanel.Info. It stores the
|
|
||||||
// values out of a stat so they do not need to be recalculated, in case they
|
|
||||||
// are expensive to calculate or the filter deciding which stats to output
|
|
||||||
// was expensive.
|
|
||||||
type cachedStat struct {
|
|
||||||
name string
|
|
||||||
value string
|
|
||||||
visible bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// output returns a string representing this stat, right-aligning the name in
|
|
||||||
// a nameWidth-wide field and prefixing it with a bullet: "•" for a visible
|
|
||||||
// stat and "◦" for an invisible stat. If nameWidth is 0, the name and
|
|
||||||
// colon are omitted. If it is negative, the name is emitted as-is with
|
|
||||||
// no alignment. If it is too short for the name and it is nonzero,
|
|
||||||
// it is truncated with "⋯".
|
|
||||||
func (c cachedStat) output(nameWidth int) string {
|
|
||||||
bullet := "◦"
|
|
||||||
if c.visible {
|
|
||||||
bullet = "•"
|
|
||||||
}
|
|
||||||
|
|
||||||
if nameWidth == 0 {
|
|
||||||
return fmt.Sprintf("%s %s", bullet, c.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
name := c.name
|
|
||||||
if len(name) < nameWidth {
|
|
||||||
name = strings.Repeat(" ", nameWidth-len(name)) + name
|
|
||||||
}
|
|
||||||
if len(name) > nameWidth && nameWidth > 0 {
|
|
||||||
name = name[:nameWidth-1] + "⋯"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s %s: %s", bullet, name, c.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StatsNamed returns a StatFilter[C] matching any stat with a listed name.
|
|
||||||
func StatsNamed[C StatsCollection](names ...string) StatFilter[C] {
|
|
||||||
nameSet := make(map[string]bool)
|
|
||||||
for _, n := range names {
|
|
||||||
nameSet[n] = true
|
|
||||||
}
|
|
||||||
return func(_ *Player[C], s Stat) bool {
|
|
||||||
return nameSet[s.StatName()]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisibleOrDebugStatsNamed returns a StatFilter[C] matching any visible stat
|
|
||||||
// with a listed name, or any stat with a listed name if the player is in
|
|
||||||
// debug mode.
|
|
||||||
func VisibleOrDebugStatsNamed[C StatsCollection](names ...string) StatFilter[C] {
|
|
||||||
return All(VisibleOrDebug[C], StatsNamed[C](names...))
|
|
||||||
}
|
|
||||||
|
|
||||||
// All returns a StatFilter[C] that requires a Stat to match all provided
|
|
||||||
// filters. If no filters are provided, All matches every Stat (it's very easy
|
|
||||||
// to meet every requirement when there are no requirements).
|
|
||||||
func All[C StatsCollection](ff ...StatFilter[C]) StatFilter[C] {
|
|
||||||
return func(p *Player[C], s Stat) bool {
|
|
||||||
for _, f := range ff {
|
|
||||||
if !f(p, s) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Any returns a StatFilter[C] that requires a Stat to match any one or more of
|
|
||||||
// the filters provided. If no filters are provided, Any never matches a stat
|
|
||||||
// (it's very hard to meet at least one requirement out when there are no
|
|
||||||
// requirements).
|
|
||||||
func Any[C StatsCollection](ff ...StatFilter[C]) StatFilter[C] {
|
|
||||||
return func(p *Player[C], s Stat) bool {
|
|
||||||
for _, f := range ff {
|
|
||||||
if f(p, s) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,71 +2,18 @@ package cardsim
|
|||||||
|
|
||||||
import "math/rand"
|
import "math/rand"
|
||||||
|
|
||||||
// Player stores all gameplay state for one player at a specific point in time.
|
// Player stores all gameplay state for one player.
|
||||||
// Game-specific data is stored in Stats.
|
|
||||||
//
|
|
||||||
// Player is a generic type -- see https://go.dev/blog/intro-generics for more
|
|
||||||
// information on how these work. Think of "Player" as a "type of type" --
|
|
||||||
// when you create one, you tell it what kind of data it needs to keep for
|
|
||||||
// the simulation itself, and each Player that works with a different kind of
|
|
||||||
// data is a different kind of Player and the compiler will help you with that.
|
|
||||||
// This is the same idea as "slice of something" or "map from something to
|
|
||||||
// something" -- different kinds of Players are different from each other and
|
|
||||||
// "know" what type of data they use, so the compiler can tell you if you're
|
|
||||||
// using the wrong type.
|
|
||||||
//
|
|
||||||
// Generic types have to use a placeholder to represent the type (or types --
|
|
||||||
// consider maps, which have both keys and values) that will be more specific
|
|
||||||
// when the type is actually used. They're called "type parameters", like
|
|
||||||
// function parameters, because they're the same kind of idea. A function puts
|
|
||||||
// its parameters into variables so you can write a function that works with
|
|
||||||
// whatever data it gets; a generic type takes type parameters and represents
|
|
||||||
// them with type placeholders so you can write a *type* that works with
|
|
||||||
// whatever specific other types it gets.
|
|
||||||
//
|
|
||||||
// Just like function parameters have a type that says what kind of data the
|
|
||||||
// function works with, type parameters have a "type constraint" that says what
|
|
||||||
// kind of types the generic type works with. Go already has a familiar way
|
|
||||||
// to express the idea of "what a type has to do": `interface`. In Go, type
|
|
||||||
// constraints are just interfaces.
|
|
||||||
//
|
|
||||||
// But wait, why use generics at all? Can't we just use an interface in the
|
|
||||||
// normal way instead of doing this thing? Well, yes, we could, but then the
|
|
||||||
// compiler doesn't know that the "real types" for things matching these
|
|
||||||
// interfaces all have to actually be the same type. The compiler will stop
|
|
||||||
// you from putting an `Orange` into a `[]Apple`, but it wouldn't stop you from
|
|
||||||
// putting a `Fruit` into a `[]Fruit` because, well, of course it wouldn't,
|
|
||||||
// they're the same type.
|
|
||||||
//
|
|
||||||
// Different simulation games made with `cardsim` are different. Rules made for
|
|
||||||
// simulating the economy of a kobold colony and mine wouldn't work at all with
|
|
||||||
// data for a simulation about three flocks of otter-gryphons having a
|
|
||||||
// territory conflict over a river full of fish. By using generics, the compiler
|
|
||||||
// can recognize functions and data and types intended for different simulation
|
|
||||||
// games and prevent you from using the wrong one, when it wouldn't be able to
|
|
||||||
// if all this stuff was written for "some simulation game, don't care what".
|
|
||||||
//
|
|
||||||
// Generic interfaces (like `Card[C]`, `Rule[C]`, `InfoPanel[C]`, and more)
|
|
||||||
// don't mean you have to write generics of your own. It's exactly the opposite!
|
|
||||||
// Because the interface has this extra type in it, you only need to implement
|
|
||||||
// the specific kind of interface that works with your game. There's more detail
|
|
||||||
// on this in the comment on `Rule[C]`.
|
|
||||||
type Player[C StatsCollection] struct {
|
type Player[C StatsCollection] struct {
|
||||||
Stats C
|
Stats C
|
||||||
Name string
|
Name string
|
||||||
Deck *Deck[C]
|
Deck *Deck[C]
|
||||||
Hand []Card[C]
|
Hand []Card[C]
|
||||||
HandLimit int
|
HandLimit int
|
||||||
ActionsPerTurn int
|
ActionsPerTurn int
|
||||||
ActionsRemaining int
|
ActionsRemaining int
|
||||||
PermanentActions []Card[C]
|
PermanentActions []Card[C]
|
||||||
InfoPanels []InfoPanel[C]
|
Rules *RuleCollection[C]
|
||||||
Prompt InfoPanel[C]
|
Rand rand.Rand
|
||||||
Rules *RuleCollection[C]
|
Turn int
|
||||||
Rand rand.Rand
|
PendingMessages []Message
|
||||||
Turn int
|
|
||||||
TemporaryMessages []Message
|
|
||||||
TemporaryPanels []InfoPanel[C]
|
|
||||||
|
|
||||||
DebugLevel int
|
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,8 @@ import (
|
|||||||
// A StatsCollection contains stats.
|
// A StatsCollection contains stats.
|
||||||
type StatsCollection interface {
|
type StatsCollection interface {
|
||||||
// Stats returns all the stats in this collection. It's okay for
|
// Stats returns all the stats in this collection. It's okay for
|
||||||
// these to be copies rather than pointers. BasicStatsPanel presents
|
// these to be copies rather than pointers. Stats will be presented
|
||||||
// stats to the player in this order. It's okay for this list to
|
// to the player in this order.
|
||||||
// contain nil entries; these are interpreted as line breaks,
|
|
||||||
// section breaks, etc.
|
|
||||||
Stats() []Stat
|
Stats() []Stat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user