package cardsim import "math/rand" // Player stores all gameplay state for one player at a specific point in time. // 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 { // Stats stores simulation-specific state. Stats C // Name stores the player's name. Name string // Rand is a source of randomness that other components can use. Rand rand.Rand Deck *Deck[C] Hand []Card[C] TurnNumber int State GameState // HandLimit is number of cards to draw to at the start of each turn. // If the player has more cards than this already, none will be drawn, // but the player will keep them all. // // If this is 0 or less and the player has no cards in hand, no permanent // actions available, and must take an action, the game ends in stalemate. HandLimit int // ActionsPerTurn is what ActionsRemaining resets to at the start of each // turn. If this is 0 or less at the start of a turn, the game ends in // stalemate. Activating a card or permanent action spends an action, but // the card or action itself can counter this by changing the player's // ActionsRemaining by giving the action back -- or force the turn to // progress immediately to simulation by setting it to 0. ActionsPerTurn int ActionsRemaining int // PermanentActions are an "extra hand" of cards that are not discarded when used. PermanentActions []Card[C] // InfoPanels lists informational views available to the player. The Prompt // is the InfoPanel shown before the main action menu. InfoPanels []InfoPanel[C] Prompt InfoPanel[C] // Rules are the simulation rules executed every turn after the player has // run out of remaining actions. See `RuleCollection`'s documentation for // more information about how rule execution works. Rules *RuleCollection[C] // Temporary messages are shown *before* the Prompt. They're cleared just // before executing rules for the turn, so rules adding to TemporaryMessages // are creating messages that will show up for the next turn. Temporary // panels are cleared out at the same time as temporary messages; when // available, they are listed separately from standard panels (before them). TemporaryMessages []Message TemporaryPanels []InfoPanel[C] // DebugLevel stores how verbose the game should be about errors. If this // is greater than 0, invisible stats will usually be shown to the player // (this is up to individual info panels, though). If this is -1 or lower, // warning messages will not be displayed. DebugLevel int } // GameState represents various states a player's Game can be in. type GameState int const ( // The game has not started. GameUninitialized = GameState(iota) // The game is ready to play. GameActive // The game is over and the player has lost. GameLost // The game is over and the player has won. GameWon // The game is over because of an error. GameCrashed // The game is over because the player cannot take any actions. GameStalled ) // Over returns whether this state represents a game that is over. func (g GameState) Over() bool { return g == GameLost || g == GameWon || g == GameCrashed || g == GameStalled }