Implement drawing and running a turn.
This commit is contained in:
		| @@ -52,6 +52,12 @@ func Warningf(f string, args ...any) *Warning { | ||||
| 	return &Warning{fmt.Errorf(f, args...)} | ||||
| } | ||||
|  | ||||
| // IsSeriousError returns whether e is a non-warning error. If e is nil, this | ||||
| // returns false. | ||||
| func IsSeriousError(e error) bool { | ||||
| 	return !errors.Is(e, AnyWarning) | ||||
| } | ||||
|  | ||||
| // A Failure is an error that is definitely not a Warning. If a Warning is | ||||
| // wrapped in a Failure, it stops being a Warning. | ||||
| type Failure struct { | ||||
| @@ -144,8 +150,11 @@ func (f Failure) As(target any) bool { | ||||
| //   - Otherwise, it returns an error that combines all the errors it collected. | ||||
| //     The aggregated error is a Warning if and only if all collected errors | ||||
| //     were also warnings. | ||||
| // | ||||
| // An ErrorCollector's Errs should not be written by anything other than Add, | ||||
| // or HasFailure may get out of sync. | ||||
| type ErrorCollector struct { | ||||
| 	errs       []error | ||||
| 	Errs       []error | ||||
| 	hasFailure bool | ||||
| } | ||||
|  | ||||
| @@ -157,12 +166,12 @@ func (ec *ErrorCollector) Add(e error) { | ||||
| 	if !errors.Is(e, AnyWarning) { | ||||
| 		ec.hasFailure = true | ||||
| 	} | ||||
| 	ec.errs = append(ec.errs, e) | ||||
| 	ec.Errs = append(ec.Errs, e) | ||||
| } | ||||
|  | ||||
| // IsEmpty reports whether ec has zero accumulated errors/warnings. | ||||
| func (ec *ErrorCollector) IsEmpty() bool { | ||||
| 	return len(ec.errs) == 0 | ||||
| 	return len(ec.Errs) == 0 | ||||
| } | ||||
|  | ||||
| // HasFailure reports whether ec has at least one non-warning error. | ||||
| @@ -181,16 +190,16 @@ func (ec *ErrorCollector) HasFailure() bool { | ||||
| // errors.Is does not erroneously represent a failure as a warning because it | ||||
| // contains a warning as a subcomponent. | ||||
| func (ec *ErrorCollector) Emit() error { | ||||
| 	if len(ec.errs) == 0 { | ||||
| 	if len(ec.Errs) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
| 	if len(ec.errs) == 1 { | ||||
| 		return ec.errs[0] | ||||
| 	if len(ec.Errs) == 1 { | ||||
| 		return ec.Errs[0] | ||||
| 	} | ||||
| 	if ec.HasFailure() { | ||||
| 		return aggregateFailure(ec.errs) | ||||
| 		return aggregateFailure(ec.Errs) | ||||
| 	} | ||||
| 	return &aggregateError{ec.errs} // all these are recognizable warnings | ||||
| 	return &aggregateError{ec.Errs} // all these are recognizable warnings | ||||
| } | ||||
|  | ||||
| // An aggregateError is a collection of errors that is itself an error. | ||||
|   | ||||
| @@ -25,3 +25,34 @@ func MsgStr(s string) Message { | ||||
| func Msgf(f string, args ...any) Message { | ||||
| 	return stringMessage(fmt.Sprintf(f, args...)) | ||||
| } | ||||
|  | ||||
| // A SpecialMessage is a specific, uniquely identifiable message. | ||||
| type SpecialMessage struct { | ||||
| 	msg Message | ||||
| } | ||||
|  | ||||
| // String implements Message. | ||||
| func (s *SpecialMessage) String() string { | ||||
| 	if s == nil { | ||||
| 		return "" | ||||
| 	} | ||||
| 	return s.msg.String() | ||||
| } | ||||
|  | ||||
| // Messages that various display surfaces or other components may have a special interpretation of | ||||
| // and identify specifically. These are largely sentinel values. Nil is a paragraph break. | ||||
| var ( | ||||
| 	SectionBreak = &SpecialMessage{MsgStr("      --------------------------------------------------------------------      ")} | ||||
| 	ChapterBreak = &SpecialMessage{MsgStr("      ====================================================================      ")} | ||||
| ) | ||||
|  | ||||
| // IsSpecialMessage returns whether a provided Message is a specific SpecialMessage. | ||||
| func IsSpecialMessage(m Message, s *SpecialMessage) bool { | ||||
| 	if m == nil { | ||||
| 		return s == nil | ||||
| 	} | ||||
| 	if s2, ok := m.(*SpecialMessage); ok { | ||||
| 		return s == s2 | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,15 @@ | ||||
| package cardsim | ||||
|  | ||||
| import "math/rand" | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"math/rand" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	ErrUncooperativeCards = errors.New("a milion cards refused to join the hand") | ||||
|  | ||||
| 	WarningStalemate = errors.New("no actions can be taken") | ||||
| ) | ||||
|  | ||||
| // Player stores all gameplay state for one player at a specific point in time. | ||||
| // Game-specific data is stored in Stats. | ||||
| @@ -138,3 +147,111 @@ const ( | ||||
| func (g GameState) Over() bool { | ||||
| 	return g == GameLost || g == GameWon || g == GameCrashed || g == GameStalled | ||||
| } | ||||
|  | ||||
| // ChapterBreak apends a chapter break to p.TemporaryMessages, unless it is | ||||
| // empty or the most recent non-nil message is already a chapter break. | ||||
| func (p *Player[C]) ChapterBreak() { | ||||
| 	for i := len(p.TemporaryMessages) - 1; i >= 0; i-- { | ||||
| 		m := p.TemporaryMessages[i] | ||||
| 		if IsSpecialMessage(m, ChapterBreak) { | ||||
| 			return | ||||
| 		} | ||||
| 		if p.TemporaryMessages[i] != nil { | ||||
| 			p.TemporaryMessages = append(p.TemporaryMessages, ChapterBreak) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 	// No non-nil messages -- nothing to do. | ||||
| } | ||||
|  | ||||
| // SectionBreak apends a section break to p.TemporaryMessages, unless it is | ||||
| // empty or the most recent non-nil message is already a section/chapter break. | ||||
| func (p *Player[C]) SectionBreak() { | ||||
| 	for i := len(p.TemporaryMessages) - 1; i >= 0; i-- { | ||||
| 		m := p.TemporaryMessages[i] | ||||
| 		if IsSpecialMessage(m, ChapterBreak) || IsSpecialMessage(m, SectionBreak) { | ||||
| 			return | ||||
| 		} | ||||
| 		if m != nil { | ||||
| 			p.TemporaryMessages = append(p.TemporaryMessages, SectionBreak) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 	// No non-nil messages -- nothing to do. | ||||
| } | ||||
|  | ||||
| // Simulate executes the simulation up to the start of the next turn. If the | ||||
| // simulation crashes, the game state becomes GameCrashed. This returns any | ||||
| // generated errors; if the debugging mode is 0 or greater, they also become | ||||
| // temporary messages for the next turn. | ||||
| func (p *Player[C]) Simulate() error { | ||||
| 	var errs ErrorCollector | ||||
| 	p.TemporaryMessages = nil | ||||
| 	p.TemporaryPanels = nil | ||||
| 	errs.Add(p.Rules.Run(p)) | ||||
| 	errs.Add(p.StartNextTurn()) | ||||
|  | ||||
| 	if errs.HasFailure() { | ||||
| 		p.State = GameCrashed | ||||
| 	} | ||||
|  | ||||
| 	if p.DebugLevel > 0 && !errs.IsEmpty() { | ||||
| 		p.ChapterBreak() | ||||
| 		p.TemporaryMessages = append(p.TemporaryMessages, Msgf("%d errors and warnings:", len(errs.Errs))) | ||||
| 		for i, e := range errs.Errs { | ||||
| 			yikes := " " | ||||
| 			if IsSeriousError(e) { | ||||
| 				yikes = "!" | ||||
| 			} | ||||
| 			p.TemporaryMessages = append(p.TemporaryMessages, Msgf("%s[%d]:\t%v", yikes, i, e)) | ||||
| 		} | ||||
| 	} | ||||
| 	return errs.Emit() | ||||
| } | ||||
|  | ||||
| // StartNextTurn increments the turn counter, resets the action counter, | ||||
| // and draws back up to full. If the player cannot take any actions after | ||||
| // drawing is complete, the game stalls. (If a drawn card would like to | ||||
| // force the player to take no actions for a turn, the best approach is to | ||||
| // make that card Urgent and make it reset ActionsRemaining to 0 after it runs.) | ||||
| func (p *Player[C]) StartNextTurn() error { | ||||
| 	var errs ErrorCollector | ||||
| 	p.TurnNumber++ | ||||
| 	p.ActionsRemaining = p.ActionsPerTurn | ||||
| 	errs.Add(p.FillHand()) | ||||
| 	if len(p.Hand)+len(p.PermanentActions) == 0 { | ||||
| 		p.State = GameStalled | ||||
| 		errs.Add(Warningf("%w: no cards in hand, no permanent actions", WarningStalemate)) | ||||
| 	} | ||||
| 	if p.ActionsRemaining == 0 { | ||||
| 		p.State = GameStalled | ||||
| 		errs.Add(Warningf("%w: 0 actions available in the turn", WarningStalemate)) | ||||
| 	} | ||||
| 	return errs.Emit() | ||||
| } | ||||
|  | ||||
| // FillHand draws up to the hand limit, informing cards that they have been | ||||
| // drawn. If more than a million cards refuse to enter the hand, this crashes | ||||
| // with ErrUncooperativeCards. If the deck does not have enough cards, this | ||||
| // returns WarningTooFewCards. | ||||
| func (p *Player[C]) FillHand() error { | ||||
| 	failureLimit := 1000000 | ||||
| 	for failureLimit > 0 && p.Deck.Len() > 0 && len(p.Hand) < p.HandLimit { | ||||
| 		c := p.Deck.Draw() | ||||
| 		if c.Drawn(p) { | ||||
| 			p.Hand = append(p.Hand, c) | ||||
| 		} else { | ||||
| 			failureLimit-- | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if len(p.Hand) >= p.HandLimit { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	if failureLimit <= 0 { | ||||
| 		return ErrUncooperativeCards | ||||
| 	} | ||||
|  | ||||
| 	return WarningTooFewCards | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user