how do I come up with a simple architecture that is also extensible?
that's largely a matter of opinion. ask ten different devs, you'll get 10 different answers probably.
me personally, i'd probably use c++ oo syntax, without using polymorphism or inheritance, and always using composition when possible. polymorphism and inheritance were originally designed to make exiting code extensible, not as a way to design new code. by using c++ oo syntax, you have the power of inheritance and polymophism to extend the exiting code when the time comes, such as the next version. by sticking to composition in the original design, you simplify things.
Should I do something like the model view controller pattern which the book game coding complete recommends?
patterns are general terms used to describe different patterns of code design. they are not really a way to design a specific type of software.
the "pattern" for turn based games is
while !quit
{
render screen
process user input, re-rendering as needed until end turn button is pressed
update everything
}
How should I manage various game states? Would a stack of states be the way to go, with each screen (main menu, exploration screen, combat screen) be states on a stack?
many state changes can be handled automatically by the call hierarchy of the code. main menu calls exploration screen, exploration screen calls combat screen, exploration and combat both call the in-game menu. all three call the help system. no states required. states are only really required when the game operates in multiple modes at a single call level. say you had space and planetary combat for example, and they were the same except for the background you drew. you could make combat take a mode parameter and use it for both. just draw the correct background based on mode. combat would then be capable of operating in one of two possible game states or modes - IE planetary and space.
Would these constraints help to simplify the architecture as compared to a regular game?
you mean no performance, graphics or physics? hell yes! (pardon my french)
data driven (adding/changing content without a recompile), integration of a scripting language. Would using an entity-component system be a good way of achieving this?
ECS has a few different applications:
1. you need to be able to define new entity types from existing component types without recompiling because you don't have source code access. this is why engines like unity use ECS. this is also why AAA studios use ECS - level designers typically don't have source code access.
odds are this isn't your situation.
2. you need to be able to define new entity types from existing component types without recompiling because builds take too long. this is the whole "ECS speeds up development by reducing builds." argument. but odds are if you look at the time spent implementing an ECS vs the time spent recompiling due to new entity type definitions, unless you're a AAA studio working on a big game with lots of (100+) entity types - who would already use ECS due to reason 1 above - its more work to use ECS than just recompile a few times. on my current project (a FPSRPG) i'm at 125,000 lines of c++ code and three years. link times are around 2 minutes. and i really only have four types of entities, players, non-players, projectiles, and dropped objects. no ECS needless to say. Sure i add new variables to entity type definitions from time to time, but those are new component types being added to the engine, not new entity types. so a rebuild is required no matter what.
odds are this isn't your situation either.
3. you've optimized everything you can in render, and you're still too slow. so in desperation you look to update to shave off a few clock cycles. in doing so, you optimize update by re-designing it to be cache friendly, using data oriented design. the result is arrays of components, which naturally leads to an ECS.
odds are this isn't your situation either.
serializing inter-entity references
vectors and indices.
an un-ordered list memory pool:
implement an array or vector of structs or objects, each has an active field (a boolean). insert (ie copy data into) at first inactive. do not move once inserted. set active to false to remove from list. skip inactive when iterating over the list. use indices to refer to objects. you can load and save memory pool lists without the need to fixup index references. new objects in the list just once when you create the memory pool, and delete them when you shut it down. just copy data to the object when you activate it, and set active to false when you remove it. don't new and dispose an object every time you activate and deactivate a list entry.