I think you may be getting a little bogged down in the English meaning of state, when compared to the State Pattern (or Finite-State Machine, which is really a diagrammatic representation of a State Pattern). Both are appropriate here, but they shouldn't be confused.
The State Pattern is something which should, given various common stimuli, operate on the game state differently at different times in the game. So, as you've rightly concluded, the lobby stage (which I'm assuming to be while people are joining a game) is a State in that context.
The game state is a list of players, pieces, cards, dice, whatever else, and all the information required to simulate them. You might want to call that the context.
Each State should receive a UserClicked message from the application which receives the context and coordinates or area clicked and should operate on those accordingly. The information that you are trying to give ownership of to the State object does not belong there, it belongs in the game itself, alongside the State.
The state may also have a UserPressedKey method, a TimerTicked method or any other kind of stimulus on which it should act. But each of these should act on the game state rather than being the game state.
One important method of the State will be ScreenRefresh, which will draw the context for the user.
Here is a very rough example:
abstract class ApplicationState {
void Begin(GameContext context);
void UserClicked(GameContext context, int x, int y);
void UserPressedKey(GameContext context, char key);
void TimerTicked(GameContext context);
void ScreenRefresh(GameContext context);
protected void OnStateChanged(ApplicationState newState) {
// inform interested parties that state has changed,
// using the observer pattern
}
}
class LobbyState : ApplicationState
{
void Begin(GameContext context) {
context.Players = new User[context.NoOfPlayers];
}
void UserClicked(GameContext context, int x, int y) {
// Find a displayed player box which contains click coords
int index = -1;
for (i=0; i < context.Players.Length; i++) {
if (PlayerBox[i].Contains(x, y)) {
index = i;
}
}
if (index > -1) {
if (context.Player[index] == null) {
context.Player[index] = context.ActivePlayer;
} else if (context.Player[index] == context.ActivePlayer()) {
context.Player[index] = null;
} else {
ErrorSound.Play();
}
} else if (CloseIcon.Contains(x, y)) {
OnStateChanged(new ExittingState());
} else {
ErrorSound.Play();
}
}
void UserPressedKey(GameContext context, char key) {
if (key = 'J' or key = 'j') {
if (! context.AddPlayerRandomly(context.ActiveUser)) {
ErrorSound.Play();
}
if (context.GameIsNowFull()) {
OnStateChanged(new InitializingState());
}
} else if key = Esc {
if (!context.RemovePlayer(context.ActiveUser)) {
ErrorSound.Play();
}
} else {
ErrorSound.Play();
}
}
void TimerTicked(GameContext context) {
// there is no use for a game timer while
// we're trying to fill the game, but you
// want one to tick anyway, because the
// application doesn't know which state
// it is in.
}
void ScreenRefresh(GameContext context) {
DrawPlayerBoxes(context.Screen, context.Players);
DrawExitIcon(context.Screen);
}
}
class InitializingState : ApplicationState {
private double waitSpinnerAngle = 0;
void Begin(GameContext context) {
context.RandomizePlayOrder();
context.ShuffleCards();
// all other game initialization rules here
OnContextChanged(new PlayerUpState(context.Players[0]));
}
void UserClicked(GameContext context, int x, int y) {
// Not responding to user input for a moment
ErrorSound.Play();
}
void UserPressedKey(GameContext context, char key) {
// Not responding to user input for a moment
ErrorSound.Play();
}
void TimerTicked(GameContext context) {
waitSpinnerAngle += 0.05;
if (waitSpinnerAngle > 1) waitSpinnerAngle = 0;
}
void ScreenRefresh(GameContext context) {
DrawWaitSpinner(waitSpinnerAngle);
}
}
See where I'm going with this? The Application itself is then as simple as setting the initial State, hooking a listener into the OnStateChanged event and calling Begin. When the listener hears that the event is called, unhook your old State, hook in the new one, and call Begin again.
Everything else is triggered by events from the mouse, keyboard or timer, passed directly to the current State without knowledge of which State the game is currently in. If you are running this game across a network then you will also need an event for changes of state received from other players.
Everything in your State and Context is now very unit-testable and separation of concerns are observed. Although, you may want to refactor that LobbyState#UserClicked method a bit, among other things.
Remember, this is just a free example and you get what you paid for it. Don't try to apply it directly to your game. Just use it to understand how the State Pattern should work in the context of a game.
I would be very loath to call a data structure "immutable" unless, once it was exposed to the outside world, unless all changes that are made to its internal state continue at all times to leave the object in the same observable state, and unless the state of the object would be valid with any arbitrary combination of those changes either happening or not happening.
An example of a reasonably-good "immutable" object which obeys this principle is the Java string
type. It includes a hash-code field which is initially zero, but which is used to memoize the result of querying the hash code. The state of a string
with a zero hash-code field is semantically the same as that of a string where the hash-code field is filled in. If two threads simultaneously attempt to query the hash code, it's possible that both may end up performing the computation and storing the result, but it doesn't matter because neither store will affect the observable state of the object. If a third thread comes along and queries the hash code, it might or might not see the stores from the first two, but the returned hash code will be the same regardless.
(BTW, my one quibble with Java's string-hashing method is that it's possible for the hash function to return zero for a non-null string. I would have thought it better to have the hash function test for zero and substitute something else. For example, if the hashing step is such that adding a single character to a string whose hash is zero will always yield a non-zero hash, simply return the hash of the string minus the last character. Otherwise the worst-case time to hash a long string thousands of times may be much worse than the normal-case time.)
The big things to beware of are (1) sequences of operations which change the state of an object and then change it back, or (2) substitution of objects that appear to have the same state, but don't. Ironically, Microsoft's fixing of what it regarded as bug in its default Struct.Equals
method makes #2 harder to protect against. If one has a number of immutable objects which hold references to what appear to be identical immutable objects, replacing all those references with references to one of those immutable objects should be safe. Unfortunately, the Equals
overrides for system types Decimal
, Double
, and Float
will sometimes report true
even when comparing slightly-different values. It used to be that wrapping one of those types in a struct and calling Equals
on that struct would test for true equivalence, but Microsoft changed things so a struct will report Equals
if its members do, even if those members hold non-equivalent values of the aforementioned types.
Best Answer
There is no benefit from from having
addItemToCart
rather thanICart.AddItem
. They are essentially identical. Both of those necessarily have a runtime check, because you want to be able to have a variable with any kind ofCart
in it, and you won't know at compile time which one will get passed into the function.Where the benefit comes is when you first implement
addItemToCart
, or later when you add another operation at the same level of abstraction, sayoneClickAddAndPay
. The union type will give you a compile error if you try toAdd
an item to aPaidFor
cart, or if you completely forget to account forPaidFor
carts at all. TheICart
interface can't catch that kind of programming error at compile time.In other words, the union type can't move all kinds of errors to compile time, but it does move some. If you don't end up adding a lot of functions like
oneClickAddAndPay
that reuse the existingAdd
functionality, it won't buy you much.