Implement standard debuggers.
These debug actions are added to all players by default.
This commit is contained in:
		| @@ -101,6 +101,54 @@ func (b *BasicCard[C]) Drawn(_ *Player[C]) bool { | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| // A PanelCard is a Card that takes its title and text from an InfoPanel, | ||||
| // while options, urgency, and the post-option callback are specified | ||||
| // (like a BasicCard). It never does anything in particular when drawn. | ||||
| // | ||||
| // Omitting all options yields an inactionable card, which can be displayed | ||||
| // but not played. This can be useful for adding an info panel as a debug action. | ||||
| type PanelCard[C StatsCollection] struct { | ||||
| 	Panel       InfoPanel[C] | ||||
| 	IsUrgent    bool | ||||
| 	CardOptions []CardOption[C] | ||||
| 	// AfterOption is given the card itself as its first argument. | ||||
| 	AfterOption func(c Card[C], p *Player[C], option CardOption[C]) error | ||||
| } | ||||
|  | ||||
| // Title implements Card. | ||||
| func (c *PanelCard[C]) Title(p *Player[C]) Message { | ||||
| 	return c.Panel.Title(p) | ||||
| } | ||||
|  | ||||
| // Urgent implements Card. | ||||
| func (c *PanelCard[C]) Urgent(_ *Player[C]) bool { | ||||
| 	return c.IsUrgent | ||||
| } | ||||
|  | ||||
| // EventText implements Card. | ||||
| func (c *PanelCard[C]) EventText(p *Player[C]) (Message, error) { | ||||
| 	msgs, err := c.Panel.Info(p) | ||||
| 	return MultiMessage(msgs), err | ||||
| } | ||||
|  | ||||
| // Options implements Card. | ||||
| func (c *PanelCard[C]) Options(_ *Player[C]) ([]CardOption[C], error) { | ||||
| 	return c.CardOptions, nil | ||||
| } | ||||
|  | ||||
| // Then implements Card. | ||||
| func (c *PanelCard[C]) Then(p *Player[C], option CardOption[C]) error { | ||||
| 	if c.AfterOption == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return c.AfterOption(c, p, option) | ||||
| } | ||||
|  | ||||
| // Drawn implements Card. | ||||
| func (c *PanelCard[C]) Drawn(_ *Player[C]) bool { | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| // A BasicOption is a CardOption with fixed text, effects, and output. | ||||
| // It's always enabled. | ||||
| type BasicOption[C StatsCollection] struct { | ||||
| @@ -151,3 +199,17 @@ func (o *optionFunc[C]) Enact(p *Player[C]) (Message, error) { | ||||
| func (o *optionFunc[C]) Enabled(p *Player[C]) bool { | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| // OnlyDiscardFree returns a []CardOption[C] providing a single option, which | ||||
| // returns the action point. It does not shuffle the card back into the deck | ||||
| // or draw a replacement (consider the AfterFunc for that if needed). This | ||||
| // can be used for cards that are displayable but not actionable, but show up | ||||
| // as cards rather than permanent or debug actions for some reason. | ||||
| func OnlyDiscardFree[C StatsCollection](msg Message) []CardOption[C] { | ||||
| 	return []CardOption[C]{ | ||||
| 		OptionFunc(msg, func(p *Player[C]) (Message, error) { | ||||
| 			p.ActionsRemaining++ | ||||
| 			return MsgStr("Okay."), nil | ||||
| 		}), | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										142
									
								
								cardsim/deck.go
									
									
									
									
									
								
							
							
						
						
									
										142
									
								
								cardsim/deck.go
									
									
									
									
									
								
							| @@ -279,3 +279,145 @@ func (d *Deck[C]) Strip(shouldRemove func(idx int, c Card[C]) bool) int { | ||||
| 	d.cards = Strip(d.cards, shouldRemove) | ||||
| 	return origLen - d.Len() | ||||
| } | ||||
|  | ||||
| // DeckDebugger is a Card[C], intended for use only as a debug action, that | ||||
| // lists the top 10 cards of the deck (without checking if they are drawable) | ||||
| // and allows various sorts of deck manipulation for free. It can't be drawn. | ||||
| type DeckDebugger[C StatsCollection] struct{} | ||||
|  | ||||
| // Title implements Card[C]. | ||||
| func (DeckDebugger[C]) Title(p *Player[C]) Message { | ||||
| 	return MsgStr("Debug Mode Deck Controls") | ||||
| } | ||||
|  | ||||
| // Urgent implements Card[C] as used in permanent actions. It's always valid | ||||
| // to use the deck debugger. Debug actions do not check urgency flags, but this | ||||
| // marks itself as urgent-compatible just in case. | ||||
| func (DeckDebugger[C]) Urgent(p *Player[C]) bool { | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| // Drawn implements Card[C]. It can't be drawn. | ||||
| func (DeckDebugger[C]) Drawn(p *Player[C]) bool { | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // EventText implements Card[C]. It lists the top ten cards of the deck and | ||||
| // a few deck-related and hand-related stats. | ||||
| func (DeckDebugger[C]) EventText(p *Player[C]) (Message, error) { | ||||
| 	var msgs []Message | ||||
|  | ||||
| 	msgs = append(msgs, Msgf("The Deck contains %d cards.", p.Deck.Len())) | ||||
| 	if p.Deck.Len() > 0 { | ||||
| 		portion := p.Deck.cards | ||||
| 		msgs = append(msgs, nil) | ||||
| 		topness := "All" | ||||
| 		if p.Deck.Len() > 10 { | ||||
| 			portion = p.Deck.cards[:10] | ||||
| 			topness = "Top 10" | ||||
| 		} | ||||
| 		msgs = append(msgs, Msgf("%s cards in the Deck:", topness)) | ||||
| 		for i, c := range portion { | ||||
| 			urgency := " " | ||||
| 			if c.Urgent(p) { | ||||
| 				urgency = "!" | ||||
| 			} | ||||
| 			msgs = append(msgs, Msgf(" %s %2d) %v", urgency, i+1, c.Title(p))) | ||||
| 		} | ||||
| 	} | ||||
| 	msgs = append(msgs, nil) | ||||
| 	msgs = append(msgs, Msgf("At the start of each turn, the Player draws to %d cards. The player has %d cards in hand.", p.HandLimit, len(p.Hand))) | ||||
| 	return MultiMessage(msgs), nil | ||||
| } | ||||
|  | ||||
| // Options implements Card[C]. It offers many possible actions. | ||||
| func (DeckDebugger[C]) Options(p *Player[C]) ([]CardOption[C], error) { | ||||
| 	ret := []CardOption[C]{ | ||||
| 		&BasicOption[C]{ | ||||
| 			Text: MsgStr("Draw a card."), | ||||
| 			Effect: func(p *Player[C]) error { | ||||
| 				return p.Draw() | ||||
| 			}, | ||||
| 			Output: MsgStr("Done."), | ||||
| 		}, | ||||
| 		&BasicOption[C]{ | ||||
| 			Text: MsgStr("Shuffle the deck."), | ||||
| 			Effect: func(p *Player[C]) error { | ||||
| 				return p.Deck.Shuffle() | ||||
| 			}, | ||||
| 			Output: MsgStr("Done."), | ||||
| 		}, | ||||
| 		&BasicOption[C]{ | ||||
| 			Text: MsgStr("Shuffle top half."), | ||||
| 			Effect: func(p *Player[C]) error { | ||||
| 				return p.Deck.ShuffleTop(0.5) | ||||
| 			}, | ||||
| 			Output: MsgStr("Done."), | ||||
| 		}, | ||||
| 		&BasicOption[C]{ | ||||
| 			Text: MsgStr("Shuffle bottom half."), | ||||
| 			Effect: func(p *Player[C]) error { | ||||
| 				return p.Deck.ShuffleBottom(0.5) | ||||
| 			}, | ||||
| 			Output: MsgStr("Done."), | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, n := range []int{1, 3, 5, 10} { | ||||
| 		if p.Deck.Len() <= n { | ||||
| 			break | ||||
| 		} | ||||
|  | ||||
| 		// We don't want the functions we're creating to all share the same "n" | ||||
| 		// field -- we want to create distinct functions that move distinct | ||||
| 		// numbers of cards. For more information on what's going on here, see | ||||
| 		// https://eli.thegreenplace.net/2019/go-internals-capturing-loop-variables-in-closures/ | ||||
| 		// | ||||
| 		// For the curious, the Go developers don't like the gotcha that | ||||
| 		// this "shadow variable with itself" workaround patches over either. | ||||
| 		// Here's the guy who implemented apologizing for it and discussing | ||||
| 		// changing it: https://github.com/golang/go/discussions/56010 | ||||
| 		// | ||||
| 		// "Loop variables being per-loop instead of per-iteration is the only | ||||
| 		//  design decision I know of in Go that makes programs incorrect more | ||||
| 		//  often than it makes them correct." -- Russ Cox (rsc) | ||||
| 		n := n | ||||
| 		invN := p.Deck.Len() - n | ||||
| 		ret = append(ret, | ||||
| 			&BasicOption[C]{ | ||||
| 				Text: Msgf("Move the top %d card(s) to the bottom of the deck, in order.", n), | ||||
| 				Effect: func(p *Player[C]) error { | ||||
| 					p.Deck.cards = append(p.Deck.cards, p.Deck.cards[:n]...) | ||||
| 					p.Deck.cards = p.Deck.cards[n:] | ||||
| 					return nil | ||||
| 				}, | ||||
| 				Output: MsgStr("Done."), | ||||
| 			}, | ||||
| 			&BasicOption[C]{ | ||||
| 				Text: Msgf("Move the bottom %d card(s) to the top of the deck, in order.", n), | ||||
| 				Effect: func(p *Player[C]) error { | ||||
| 					p.Deck.cards = append(p.Deck.cards, p.Deck.cards[:invN]...) | ||||
| 					p.Deck.cards = p.Deck.cards[invN:] | ||||
| 					return nil | ||||
| 				}, | ||||
| 				Output: MsgStr("Done."), | ||||
| 			}, | ||||
| 		) | ||||
|  | ||||
| 		if n > 1 { | ||||
| 			ret = append(ret, &BasicOption[C]{ | ||||
| 				Text: Msgf("Shuffle the top %d card(s) of the deck.", n), | ||||
| 				Effect: func(p *Player[C]) error { | ||||
| 					return p.Deck.ShufflePart(0, n) | ||||
| 				}, | ||||
| 				Output: MsgStr("Done."), | ||||
| 			}) | ||||
| 		} | ||||
| 	} | ||||
| 	return ret, nil | ||||
| } | ||||
|  | ||||
| // Then implements Card[C]. It refunds the action point. | ||||
| func (DeckDebugger[C]) Then(p *Player[C], o CardOption[C]) error { | ||||
| 	p.ActionsRemaining++ | ||||
| 	return nil | ||||
| } | ||||
|   | ||||
| @@ -106,7 +106,7 @@ type Player[C StatsCollection] struct { | ||||
| 	PermanentActions []Card[C] | ||||
|  | ||||
| 	// DebugActions are PermanentActions only available when the player is in | ||||
| 	// debug mode. | ||||
| 	// debug mode. InitPlayer adds some standard debugging actions by default. | ||||
| 	DebugActions []Card[C] | ||||
|  | ||||
| 	// InfoPanels lists informational views available to the player. The Prompt | ||||
| @@ -175,6 +175,10 @@ func InitPlayer[C StatsCollection](stats C) *Player[C] { | ||||
| 		HandLimit:      1, | ||||
| 		ActionsPerTurn: 1, | ||||
| 		Rules:          NewRuleCollection[C](), | ||||
| 		DebugActions: []Card[C]{ | ||||
| 			&DeckDebugger[C]{}, | ||||
| 			&PanelCard[C]{Panel: RuleDumper[C]{}}, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -4,6 +4,8 @@ import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"sort" | ||||
|  | ||||
| 	"github.com/kr/pretty" | ||||
| ) | ||||
|  | ||||
| // A Rule implements an operation run on every game turn. | ||||
| @@ -402,3 +404,14 @@ func (r *RuleCollection[C]) applyDelayedUpdates() { | ||||
| 		r.RemoveID(id) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // RuleDumper is an InfoPanel[C] that dumps all rules in P. | ||||
| type RuleDumper[C StatsCollection] struct{} | ||||
|  | ||||
| func (RuleDumper[C]) Title(p *Player[C]) Message { | ||||
| 	return MsgStr("Rule Dumper") | ||||
| } | ||||
|  | ||||
| func (RuleDumper[C]) Info(p *Player[C]) ([]Message, error) { | ||||
| 	return []Message{Msgf("%# v", pretty.Formatter(p.Rules))}, nil | ||||
| } | ||||
|   | ||||
| @@ -145,3 +145,21 @@ func Strip[T any](slice []T, removeWhen func(idx int, t T) bool) []T { | ||||
| 	} | ||||
| 	return slice[:to] | ||||
| } | ||||
|  | ||||
| // EnsureCapacity checks if `cap(slice)` is at least req. If so, it returns | ||||
| // slice unchanged. Otherwise, it copies `slice` to a new slice that is at least | ||||
| // capacity `req` (but may be larger) and returns the copy. | ||||
| // | ||||
| // It is reasonably efficient to use EnsureCapacity consecutively without | ||||
| // regard for the final overall capacity that a specific slice will need to be. | ||||
| func EnsureCapacity[T any](slice []T, req int) []T { | ||||
| 	if cap(slice) >= req { | ||||
| 		return slice | ||||
| 	} | ||||
| 	if req < 2*cap(slice) { | ||||
| 		req = 2 * cap(slice) | ||||
| 	} | ||||
| 	ret := make([]T, len(slice), req) | ||||
| 	copy(ret, slice) | ||||
| 	return ret | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user