Baret / pltcmd

Control military units only with your radio
MIT License
4 stars 0 forks source link

Knowledge is power! #104

Closed Baret closed 2 years ago

Baret commented 3 years ago

Basic idea/big picture Factions (#62 ) and elements should have "knowledge". It contains everything they know (well, duh). Every entity should act based on what they know, not on the actual game world and its contents. Knowledge may be exact and up to date, then it covers with what is contained in the world, but it may also be inaccurate or even missing. This leads to cases where entities might make bad decisions or they may unable to do something altogether.

The basic idea is that the HQ/faction has the central knowledge of "everything". Elements that are stationed in the main base are not represented as entities but just as CommandingElements in a list of the faction. But as soon as they are deployed to be sent out a new entity is being spawned. And this new ElementEntity gets the HQ's current knowledge as a copy. From an ingame point of view the element leader has just been briefed, took notes of everything, picked the most accurate map and so on. Equipped with this knowledge the elements moves out and from this point on it learns (and forgets) on its own. New knowledge comes in via direct perception and via radio calls, i.e. it sees new, previously unknown terrain or hears about enemy elements on the radio. Especially contacts may get outdated at some point so knowledge about them might be discarded.

Copying the current state of the main base's knowledge is one important point. The other one is merging an element's knowledge back into the HQ's knowledge. As soon as an elements returns to stationed state it "gives back" everything it knows to the HQ. After that the HQ should know the most recent/relevant things of both entities. To make this progress more clear, here are two examples:

Example 1: A scout plane moves out to scout an unknown sector. It gathers information about the terrain and when it is finished it moves back to the base. It then transfers all the known terrain to the known terrain of the HQ, which simply adds one sector.

Example 2: An element returns to base. It knows of an enemy contact somewhere but that bit of knowledge is already an hour old. The HQ on the other hand just received an updated contact report about that exact enemy so when merging the knowledge it discards the element's one and keeps its own. It might also happen that the element knows one bit that the HQ didn't, for example maybe it knows the element's size while the HQ did not receive that information yet. Then this piece of information is updated in the HQ's knowledge. That's why I like to call it mergine knowledge.

Details My first idea was to have some kind of "Knowledge database". A single attribute that keeps everything an entity might know. On every occasion where and entity does something it queries this database. For example when plotting a path, the pathfinder queries for know terrain in these sectors. This would make merging knowledge easy because you simply take one attribute of an entity and give it to another entity.

But meanwhile I realized it is not nessecary. We might just add an interface to every attribute that represents knowledge. To merge it we may then simply get all attributes of that type.

Technical background As a quick mockup I put down two interface declarations:

interface KnowledgeBit<T, ID> {
  val original: T
  val id: ID
  val isObsolete: Boolean // or mayBeForgotten or somthing like that

  fun mergeWith(otherBit: KnowledgeBit<T>)
}

interface Knowlege<T> {
  val bits: List<KnowledgeBit<T>>

  fun update(bit: KowledgeBit<T>)

  fun mergeWith(otherKnowledge: Knowledge<T>)
}

Explanation: An attribute may implement Knowledge which simply means it is "a bucket of known things of the same type". Examples are KnownTerrain or Contacts. It keeps a list (or set? identified by the ID?) of knowledge bits. When merged with a different Knowledge it merges all bits that have equal IDs.

A KnowlegeBit describes possibly inaccurate knowledge about something. It has an original which is the object that this bit resolves around. When the knowledge bit is accurate it may simply delegate to this object, but when not it overrides single values. Derived from the orignial or any other source it also has an id. It is used to find it in the memory of an entity to either use or update it. Entities should have a simple behavior like Forgetting that scrolls through the memory each tick and looks for obsolete knowledge bits to remove them. When a bit may be forgotten depends on the implementation. It might simply be the age or it may be "never" or any other rule applicable. The mergeWith() method updates this bit of knowledge with the information gained from otherBit. The result is the most accurate and recent sum of both.

Example: Known terrain might be an easy example. It consists of lots of WorldTile-Bits and each bit basically has two points of information: Terrain height and type. So when the suggested interfaces hold true a bit might be defined as KnownTile: KnowledgeBit<WorldTile, Coordinate>. New bits are created by the detecting system. Depending on the visibility the type and height are guessed and may thus be incorrect until an update with a better visibility is done.

Related issues/Prerequisites The knowledge is also a part of a faction, which comes with #62 but maybe this can also be implemented before that issue is done

Related TODO: https://github.com/Baret/pltcmd/blob/b3c9a2427a6a34f4a73e8e76433674f475d424b9/model/radio/src/main/kotlin/de/gleex/pltcmd/model/radio/communication/RadioCommunicator.kt#L178

Loomie commented 3 years ago

I try to create the interface use case driven almost like test-driven. I would like to access the knowledge like this:

val knownTerrain = entity.knowledgeAbout(MapTerrain)

If the knowledge model is mutable it can be modified with the same access. In the terrain example it could be knownTerrain[coordinate] = seenTerrain.

For the management of the knowledge we need maybe something like:

entity.remember[MapTerrain] = knownTerrain

entity.forget[MapTerrain]

For the location of units which change often and individually it might be:

entity.knowledgeAbout[Elements].update(elementInfo)

Hm, but for the time component the context or tick must be provided. So the knowledge can store the date of the knowledge which can be used to clean up obsolete data. Maybe:

entity.remember[MapTerrain, context.currentTick] = knownTerrain
entity.forgetObsolete(Elements, context.currentTick)

But if it can be forgotten the return value should be a Maybe when accessing knowledge. But that disturbs the clean accessor. So the KnowledgeType should provide a default value for "no knowledge". The type must also provide the maximum age before data becomes obsolete.

interface KnowledgeType<T> {
    val maxAgeInSeconds: Long
    fun emptyKnowledge(): T
}

That is only for Knowledge itself. The knowledge transfer is missing in my use cases. If the knowledge itself is just a domain object we need to define how to merge it. I think that would also be part of KnowledgeType that gets two objects and returns the common knowledge:

    fun merge(base: T, addition: T): T

That could be called in an extension function of T so we have the same as @Baret wrote above: fun T.mergeWith(otherKnowledge: T).

Loomie commented 3 years ago

Putting my 'what' and Barets 'how' together:

Still each domain must specify its knowledge object. It may be just an model class (like WorldMap) or something more specific for knowledge like DescriptionOfSeenElements. I think it depends on if we already have a model container like the WorldMap for WorldTiles or just Element objects (which may not memorize as whole). Should the framework provide a container for such data?

Loomie commented 3 years ago

Most time we will have a Map for the knowledge container. Each stored fact is accessed by an key, so for accessing the knowledge of a single bit the ID is great. It can also implement fuzziness to roughly describe what should be remembered. So it can probably be used in all cases. So I would merge the Knowledge and KnowledgeBit together to a map based container:

class Knowledge<T, ID>(val type: KnowledgeType<T>) {
    private val _memory = mutableMapOf<ID, T>()

    operator fun get(id: ID): T { ... }
    operator fun set(id: ID, data: T) { ... }

    fun mergeWith(otherKnowledge: Knowledge<T, ID>)
}

If there is one attribute per type or a single attribute that holds all types with their knowledge object seems not so important.

Loomie commented 3 years ago

Missed the "forget" part. For simplicity I would start just with the tick of the remembrance.

data class KnowledgeBit<T>(val data: T, val tick: TickId)

That would be used as value of the map. Additionally a cleanup function needs to be added:

    fun forgetObsolete() { ... }

    protected fun isObsolete(data: KnowledgeBit<T>): Bool { ... }

Maybe we can put this concern to another class? 🤔 So we have a store and a cleanup class - sounds good. Also we can use different strategies for cleaning up our memory. 👍

PS: TickId is not in the model but the game. Currently all time related stuff is there as the time is advanced for the entities. So either we need a modeled time, don't put time in the model or even put the whole knowledge in the game without a model....

Baret commented 3 years ago

I think it is good that time/ticks is in the game only. Each model is contained for itself and should know as little as possible about other models and nothing about game mechanics. Knowledge is a game feature, it wraps around "something", i.e. a model object and exposes its details depending on how well an entity knows that thing. And it holds the tick to determine when this bit of knowledge is obsolete, so the model itself has no need to know TickID.