5

I'm having a very difficult time determining how to translate a game state for a specific turn in a game I'm developing into a limited sequence of moves that represents the moves taken for that turn. I'd appreciate advice on how I can do this.

NOTE: the game is now live! And if anyone wants to try it, it's at: https://mystikaze.com
(I ended up implementing an algorithm for storing game moves that may not be fully efficient but I think does guarantee that the move list for any given turn cannot be unbounded/infinitely long).

The rules for the game are relatively simple. There's a hex board, with hexes belonging to 2 players. On any given turn, pieces can already exist on the board, having been purchased on a previous turn, or they can be purchased onto the board (a yellow piece represents its being purchased onto the board this turn).

Existing piece, and purchased piece

These pieces are "active", and can still be moved. Pieces can also be combined, and will still remain "active". They can be combined either by moving an existing piece onto another piece, or by purchasing a new piece onto an existing piece. When combined, an upgraded piece will exist on the target hex. Pieces can be of 3 strengths; X, Y, and Z. X combining with X gives Y, and X with Y gives Z.

Piece merge

Pieces can continue to be merged like this and remain "active". A piece can be moved to another hex in its own territory and remain "active". A piece stops being "active" when it is moved to capture the other player's hex. It cannot move after that, although it can still be combined with. Green below indicates an inactive piece.

Capturing

A piece can also be summoned directly on top of another piece, resulting in an upgraded piece (if it was already active, it stays active; if it was inactive, it stays inactive):

Summon-combine

Now, this is pretty easy to represent in game state; just update the state of the pieces and the board to reflect whatever's currently true. And it's quite easy to convert it into a sequence of moves as long as you theoretically allow for that sequence of moves to be unbounded; pieces could remain active and move to and fro ad infinitum. Of course, I want to keep the sequence of moves limited. This is where I'm having trouble. I have the following 2 moves:

  • Move piece to location
  • Summon piece to location

How can I convert the moves a player makes into a limited sequence of moves to represent what the player actually did, leading to the final state? I don't know if I'm missing something, but this seems to get almost impossibly complex to figure out. If you have pieces moving around within their own territory and remaining active, you might think you could just update the move in-place to the new coordinates instead of adding a new move to the new coordinates, but what if there is another move where a piece combines with that piece to form an upgraded piece, which relied upon the first piece moving to its first set of coordinates? Updating the move coordinates in-place now means that that second combination move becomes a regular move because it is now moving onto an empty hex, yet it should remain a combination move (the board state will in fact be the combined piece having moved to the new coordinates).

Conceptually, there should always be a limited sequence of moves that can represent any operation. However I am finding it extremely hard to figure out how to write an algorithm to do this automatically. I think an algorithm that would at least prevent the unbounded nature of the moves would be to say "a piece's most recent move is updated instead of adding the new move to the list if that most recent move is not a combine or capture operation". That should always result in the game state being correctly created by the move set, and prevent unlimited cycles. However that could still result in quite a lot of moves. For instance if you had 10 pieces in a territory, you could move all 1, capture with 1, move the remaining 9, combine one with another, move the remaining 8, etc. potentially resulting in over 60 moves from 10 pieces. It would be nice if there were an algorithm to get this down a bit, and I'm still not 100% sure that even this algorithm doesn't have some edge cases where it wouldn't work.

Am I missing a relatively straightforward way to solve this problem? The rules must stay the same but I'm open to suggestions about perhaps introducing new move types if that would help solve the problem, too.

Jez
  • 27,951
  • 32
  • 136
  • 233
  • 2
    I dont get the actual question. You make a move, you change the board state, repeat. – libik Jan 11 '23 at 15:50
  • 1
    So one turn can consist of many moves? Are you then just looking for the [difference](https://en.wikipedia.org/wiki/Data_differencing) between two states? – Berthur Jan 11 '23 at 15:50
  • 4
    Your description of the game is very clear but then your description of what you're having trouble with is completely obscure to me. – Stef Jan 11 '23 at 15:50
  • @Berthur Well yes, but I think that difference needs to be carefully expressed in the form of ordered moves in this instance, doesn't it? I can't really just diff the two states because I need to verify that one state changed to another through a series of legal moves on the server-side. – Jez Jan 11 '23 at 16:08
  • @libik As the question says, changing the board state is easy. Generating a limited list of moves that represent *how* the board state was changed is the tricky bit. – Jez Jan 11 '23 at 16:08
  • 1
    @Jez Are you saying that the client sends the statediff to the server, without telling the server which sequence of moves was made, and then the server has to figure it out on its own? Why? Can't the client just send the sequence of moves directly? Then it's trivial for the server to check that this sequence is legal and to update the boardstate accordingly. – Stef Jan 11 '23 at 16:10
  • @Jez That explains a bit more, thanks. But then what is the reason you can't just take all the moves? Are players actually expected to be moving back and forth infinitely to overload your verification system? – Berthur Jan 11 '23 at 16:12
  • @Jez You can create a state graph and whenever a duplicate state is detected, backtrack. But then you are not really keeping track of their order anymore. – Berthur Jan 11 '23 at 16:17
  • @Berthur I guess that might technically work but it would still allow for a crazy large number of moves per piece, if you had them moving around a territory you could have hundreds of moves with the pieces being in various different places until you hit a duplicate state; really I'd like to get each piece down to 1 or 2 moves, the maximum they should need to achieve the final state the player ends up submitting. For instance, moving A to 3 of its own different hexes, then combining with piece B, then moving B to capture an enemy hex is conceptually: combine A with B, move B. – Jez Jan 11 '23 at 16:21
  • 1
    But why does the player only submit the final state? Why not ask the player to submit the sequence of moves? That's much easier all around. – Stef Jan 11 '23 at 16:22
  • @Stef The client sends the moves to the server. I just don't want it to be sending tons of unnecessary moves as the user potentially moves a piece around the board multiple times. I want to shrink each piece's moves to the minimum required to achieve the end state. – Jez Jan 11 '23 at 16:23
  • Finding the true minimum smells like an NP-hard problem. But I think I'm starting to understand. You just want to find a nice algorithm that compresses this data using the constraints of the game, and presumably a "good enough" compression is fine, even if there may be even shorter encodings out there? – Berthur Jan 11 '23 at 16:25
  • @Jez The client or server can easily compress "Move piece 157 from space 12 to space 43. Move piece 157 from space 43 to space 47." into just "Move piece 157 from space 12 to space 47.", after checking that the first move did not result in a promotion – Stef Jan 11 '23 at 16:26
  • 1
    @Berthur Yes, that's pretty much what I'm looking for. – Jez Jan 11 '23 at 16:26
  • @Stef Right. The problem is when it did result in a promotion. That seems to be what makes things way more complicated. – Jez Jan 11 '23 at 16:27
  • @Jez Well, it's easy to check whether it results into a promotion or not. And if moving piece 157 from space 12 to move 43 results in a promotion, then don't compress this move with the next move. Just consider that a promotion is always the end of a move, never something that happens in the middle of a move. I don't see an issue there. – Stef Jan 11 '23 at 16:28
  • @Stef Well the issue is that the game rules do allow pieces to combine, move, combine, etc. and the piece only becomes unmovable once it captures. However I think you've got a point; compressing multiple "non-combine" moves into one, whilst leaving the combine moves in the middle, should result in a move order that remains correct whilst preventing the "theoretically infinite moves" scenario. – Jez Jan 11 '23 at 16:33
  • I think you should say "unbounded" and not "infinite" in this case. There is no way a player can submit an infinite sequence of moves; but the player might submit a potentially-very-long sequence of moves. This is probably the source of the misunderstanding and why everyone was confused by your description of infinite moves. – Stef Jan 11 '23 at 16:35
  • @Stef Yes - I've updated the original question to say "unbounded". – Jez Jan 11 '23 at 16:37
  • 1
    @Jez But then there is further complexity when if player claims to move from one island of their own territory to another, if they are not connected. This would be an illegal move, I suppose. So you would need to run some pathfinding, and cannot just merge a start with end location. It sounds like the verifier is going to be doing more work than if it just received every move individually. – Berthur Jan 11 '23 at 16:38
  • 1
    I would also recommend talking about "sequence of moves" rather than "set of moves". Sets are unordered, and "set of moves" evokes the set of available possible moves; whereas sequences are ordered, so "sequence of moves" evokes something chronological and will make the question much easier to understand. – Stef Jan 11 '23 at 16:39
  • @Berthur I already have it verifying whether a move is illegal for things like moving between invalid territories, etc. In fact, right now it works without issue - as long as the series of moves can be unbounded. What I'm looking to do is restrict the unboundedness. – Jez Jan 11 '23 at 16:39
  • Maybe you can restrict the unboundedness simply by short-circuiting "loops" inside sequences of moves, but otherwise not compressing moves? So if I move a piece from A to B to C to D to B to E, you can replace that by a move from A to B to E, but if I move a piece from A to B to C to D to E, then you keep it that way without compressing it, even though there might exists "more efficient" paths from A to E. – Stef Jan 11 '23 at 16:41
  • @Stef Yeah but it can be very tricky to ensure the state isn't screwed up. If you have piece A moving to location 1, then piece B moves to location 2, then piece C joins onto location 2, then piece B moves to location 3, if you then move piece A to location 2 and update its coordinates in the previous move, the sequence of moves is going to end up with piece B combining with piece A on location 2. – Jez Jan 11 '23 at 16:49
  • @Jez I don't see what's tricky about that. You said piece A disappears when merged with piece B. So when the next move in the sequence is "move piece A" it should be obvious that that's not a legal move, since there is no longer a piece A. – Stef Jan 11 '23 at 19:00
  • @Stef Huh? I didn't say at any point in the example that you merged piece A, then moved piece A. – Jez Jan 11 '23 at 19:11
  • Then I really don't understand what you think is the issue or what is tricky. – Stef Jan 11 '23 at 19:13
  • @Jez - so player has "infinite" turns each round for each piece? They can take one piece and move it 10 000x times to hex that is actually 10 000x hexes away? You should describe properly what is actually your game state, what are the basic rules, etc. – libik Jan 11 '23 at 22:54
  • @libik Yes, from a UI perspective they have "infinite" turns in the sense that they can keep moving an "active" piece around as many times as they want - I do say that active pieces can still be moved in my question so I'm not sure why that's not clear. They can continue moving it to other hexes in their current territory as long as they don't capture. Once they capture they can't move it again that turn. – Jez Jan 12 '23 at 00:10
  • It's possible to find a minimum sequence of moves that transitions between two game states, but not to find the specific sequence that the user actually used since multiple sequences of moves can wind up at the same state. – Dave Jan 17 '23 at 14:17
  • Could you post a comment here once your game becomes publicly available? – Stef Jan 20 '23 at 11:43
  • 1
    Certainly will if you'd like! – Jez Jan 20 '23 at 11:45
  • 1
    *Am I missing a relatively straightforward way to solve this problem?* - I think you are. The path doesn't matter, only that the new location has to be "floodfill-accessible" from the starting location. It also doesn't matter when it combined with another piece, just the fact that it happened, either with a pre-existing one (has to be on the same patch again), or a purchased one. And then there is the single act what matters, stepping from an own hex to an enemy one, but that's just 2 locations to store. – tevemadar Jan 25 '23 at 10:59
  • I did actually start going down that road and adding separate combine operations and I can't remember exactly why, but I abandoned it because it didn't seem to solve the underlying problem. I think I identified scenarios where there would still be the need for multiple piece moves of the same piece in one turn and so figured that I might as well stick to just having moved for combinations. – Jez Jan 26 '23 at 11:19
  • @Stef and anyone else who was interested, please check the question above, as I've now put the game live and added a link to it there! Always appreciate play-testers joining the Telegram group if interested ;-) – Jez Apr 03 '23 at 00:27

3 Answers3

1

From what I understand from your question and the comments, the problem seems to be the large amount of possible moves to be made in a single turn, including combinations. I couldn't think of a complete algorithm, but I think I can give you a pretty good starting point. In the end of the answer you will find the edge cases not contemplated here and possible solutions to them.


My suggestion is not to think in terms of movements, but in terms of events, and to check that each event is possible, according to the rules.

Previous assumptions and definitions.

  • The size of the board, the number of pieces and the number of purchasable pieces are finite.

  • Each piece is labelled with an ID of the form T_s, where T is the piece type (X, Y, Z) and s is a counter (1, 2, 3, 4...).

  • You have access to the information from the game state before the game turn and after it.

  • You can only purchase pieces of type X.

  • Possible events:

    • Piece moved
    • Piece purchased
  • No event of combination because it can always be translated to one or more events of movement. In other words: we can assume that if a piece is moved after being combined with another, there is an equivalent sequence of movements where both pieces just move to the final destination and get combined there. X_1 -> X_2 = Y_1; Y_1 -> NEWCELL equals to X_1 -> NEWCELL; X_2 -> NEWCELL

The algorithm

  1. My algorithm is based on the fact that you can represent the game state as disjoint sets of labelled pieces. So the first thing to do is to use an inundation algorithm to compute the disjoint areas (area = set of cells) of the board in the initial state that belong to the player. Let's call these areas A_1, A_2, etc. It is not necessary to have this representation for the final state.

  2. You also need to compute the difference of the initial pieces set and the final one, to determine the number of combinations and purchases made.

    • A. Let's call N_T_s to the number of pieces of type T in the starting state and N_T_f to the number of pieces of type T in the final state. You can calculate the number of purchased pieces like this: (N_X_f + 2*N_Y_f + 3*N_Z_f) - (N_X_s + 2*N_Y_s + 3*N_Z_s).
    • B. As the pieces are labelled, you can build a set NEW of pieces that were not present in the initial state. We will assume these are either the result of a purchase or a combination.
  3. Prepare some variables:

    • Set a counter of PURCHASED with the initial value computed in the step 2A.

    For each area A_i

    • Create a set of COMBINED_i with the pieces from A_i that are not present in the final state (as the only way for a piece to disappear is to be combined).
    • Define an area M_i as the union of A_i and all its adjacent cells.
  4. For each area A_i and for each piece inside it:

    • A. If the piece is not inside NEW: its final position must be inside M_i. Given that a deactivated cell cannot move, it shouldn't move any further.
  5. For each piece inside NEW: The piece must be inside one of the M_i areas (otherwise the turn is invalid). Let's call it M_p.

    • A. If the piece is of type X: decrease the value of the counter PURCHASED. If the value is negative, the turn is not valid.

    • B. If the piece is of type Y:

      • If there exist two pieces X inside COMBINED_p, remove them from the set.
      • Else, if there exists a single X inside COMBINED_p remove it from the set and decrease the value of the counter PURCHASED.
      • Else, decrease twice the value of the counter PURCHASED.
    • C. If the piece is of type Z:

      • If there exist an X and a Y inside COMBINED_p, remove them from the set.
      • Else, if there exists an Y but no X inside COMBINED_p remove it from the set and decrease the value of the counter PURCHASED.
      • Else if there exist two X inside COMBINED_p remove them from the set and decrease the value of the counter PURCHASED.
      • Else if there exists a single X inside COMBINED_p remove it from the set and decrease twice the value of the counter PURCHASED.
      • Else, decrease thrice the value of the counter PURCHASED.

How to store events

  • In step 4A you can store the movement of the piece.
  • In step 5A you can store the purchase of the piece.
  • In steps 5B and 5C you can store the movements of the pieces you remove from the COMBINED_p (movement from their initial position to the position of the final combined piece) sets and the purchase of pieces each time you decrease the counter.

Observations

  • I didn't mention it but I guess you should remove from the areas M_i all the cells occupied by enemy pieces.
  • The order of if-elses in step 5 is very important. It is the greedy approach to the Change-making problem. Keep it in mind in case you added more combinations to the game.
  • You haven't told us where the currency for purchasing pieces comes from, but you can quickly check the validity of the purchases made in step 2A.
  • In case that the board was not finite (maybe it is automatically generated), then you could apply this algorithm to the finite surface defined by the presence of the pieces in both states.

Edge cases

  1. The disjoint areas might not represent the true space where a piece can move, given that pieces are solid and can't be trespassed by other pieces without combining.

    For example: I have an area that is just a row like this: #XY##. My algorithm would approve a final state ##YX#. A possible approach would be to check the existence of disjoint sub-areas and treat them separately.

  2. In the final state, two initially disjoint areas could become joined thanks to a piece (let's call it a bridge) that moves into an intermediate cell that is adjacent to both areas at the same time. Precisely thanks to the first edge case, we don't need to worry about other existing pieces moving from one area to another through the bridge... but if we find that the bridge is a recently combined piece (in other words, it belongs to NEW), then we have to make extra checks and even backtracking to determine where did the combined pieces came from, because we can no longer assume they came from a single area plus possible purchases.


As you see, this is not a complete algorithm, but it may settle some working ground. The main contribution I attempt to make is the approach of the disjoint areas and the event focused analysis. If you want, I could try to elaborate further on the edge cases and try to find solutions and incorporate them in the whole algorithm.

Miguel
  • 2,130
  • 1
  • 11
  • 26
  • Thanks for the time spent on this answer. Are you suggesting that the client simply transmit the entire state to the server at the end of the turn instead of a list of moves made? Also, you say that pieces must be inside an `M_i` area, but what if one piece captures an enemy hex adjacent to the territory, then another piece is allowed to capture a hex next to that one that wasn't previously adjacent to the territory? – Jez Jan 20 '23 at 10:25
  • Also, I guess I didn't mention it, but the edge case 1) does not prevent the edge case 2). Pieces can travel anywhere within their territory unhindered by other pieces in the territory, and are only blocked by enemy hexes. This means that halfway through a move, 2 territories can be joined and pieces from either territory will now be able to travel into each territory. I suspect this makes things incredibly complex for being able to calculate the moves made by anlayzing the before/after states, which is why I was assuming that a set of ordered moves would need to be used to represent the turn. – Jez Jan 20 '23 at 10:29
  • As for where the money for purchasing a piece comes from, each territory has its own money store. Each turn, one hex earns 1, and each piece costs a certain wage (higher for the higher value pieces). There isn't a "global" money store. – Jez Jan 20 '23 at 10:31
  • I'm not suggesting anything related to the client-server communication, but I understood you wanted an answer to extract a valid delta between game states, to say so. My whole answer is then misinformed, because I thought that pieces could not move through each other... When I have some time I will update my answer to incorporate the information you've given in these comments and the one from your answer. Sorry for the late response! Very interesting question btw. Is there anywhere I can learn more from this project of yours? – Miguel Jan 24 '23 at 17:01
  • If you want I can post a comment when I put the game online. As I mentioned in my answer, I came to a reasonable compromise to stop the moves being unbounded, but ensure that moves/combinations/captures still happened in the right order. – Jez Jan 25 '23 at 00:20
  • 1
    Miguel, you seemed to express interest in learning more from the project - please check the question above, as I've now put the game live and added a link to it there! – Jez Apr 03 '23 at 00:25
1

In the end, given the flexibility of the ruleset, I had to come up with a different algorithm from the ones suggested. I came at the problem from the other end; instead of considering how to absolutely minimize the number of moves, what is it that allows the unbounded potential for number of moves? It's the fact that active pieces can move an unbounded number of times between empty hexes. Everything else is bounded quite nicely; a piece can move, then combine, but that's it; the combination removes it from the board and upgrades the underlying piece. It can move, then capture, but that ends its turn. So, to eliminate the unbounded nature of the moves list, I ended up with the following algorithm for validating a moves list:

  • Only one summon operation for a piece is allowed
  • For a move operation, any moves for a piece after it has captured are forbidden
  • Multiple non-combine/non-capture moves in a row for a piece are forbidden, if there are no moves between that piece's two moves that are combine/capture moves
  • When a piece is combined, it is removed from the board and the underlying piece is upgraded, meaning that that piece then can't move after the combine operation as it no longer exists on the board

This does mean that you can get a piece moving before a combine or capture operation, and a piece can even have multiple non-combine/non-capture moves in a turn if they are separated by other pieces being combined - which I think is somewhat necessary in case another piece is then moved on top of it for combination - but multiple such moves in a row are illegal, nicely limiting the bounds of how many moves may be submitted for a turn.

On the client side, it detects if a piece move is the piece moving twice in a row within its own territory. If it isn't, it just adds the move. If it is, it checks whether there is a move since the existing one with the same target coords. If there isn't, it just updates the existing move in-place. If there is, it splices that move out as well as the existing move for this piece, and inserts it at the end of the moves list, followed by this piece's move; this is needed to reflect the fact that this piece is now being combined with that piece, in the correct order.

This seems to work, but it's rather complicated and confusing. Seems to be the nature of the problem, though!

Jez
  • 27,951
  • 32
  • 136
  • 233
-2

This is actually similar task as i.e. find what moves you need to do in chess to change from one state to another. It can even happen that the solution does not exist.

By default its O(x^n) issue, where x is possible moves and n number of moves. Yes, its growing quite rapidly. In general you have to check all possible moves in BFS way each turn until you find the solution.

Actually if you are successful with it, you are very close to implementing AI, you just add some scores to your moves and thats it.

To optimize it, you can use some heuristics (to i.e. prefer movement in correct direction or not expanding branches that does not make sense - i.e. when piece get combined and you know it is not possible). However there can be situations it will not be sufficient (i.e. piece needs to travel really far away around some enemy position to not be catched).

To get you further - google how to create AI for five-in-a-row or chess, your task will be very similar. Unless you want to create AI, I would strongly recommend to get all movements from client - even if you make a bit more movements than necessary.

libik
  • 22,239
  • 9
  • 44
  • 87