Saturday, 8 February 2014

The Geometrician

Guess what - I made a game at long last! Actually, this isn't really a game, since at no point it shouts "Congratulations! You won!", the score just keeps going upwards until it overflows Java's long. It's more like a tech demo - which could be interesting to port into a few different mobile platforms.

But before I babble more about it, let me give you the link to the actual description and download site:



The only real requirement is the Java7. It probably runs just by double clicking the jar in your favourite file explorer. If not, and java happens to be inside the $path, there are the scripts for both the windows and systems with usable shells.

Still, I won't guarantee this will work in Windows. JRE's WTFyness isn't that bad in unixes, and Windows's WTFyness isn't that bad with stuff that compiles to exes, but combined WTFyness of those two is so huge I can't guarantee anything. Especially if you have installed some jar-snatching program that'll wish to install this game to your 5 year old nokia phone.

Technical stuff

As everything I implement on my own time these days seems to be, this game is implemented in Clojure. It borrows some drawing and game-state-managing routines from the MERPG-codebase, and in fact began only because developing MERPG began to feel like work. I also got some inspiration from a certain game everyone around me seems to be attached to, and got interested in trying to guess how the state-structures were handled behind that game. As I said, this is really more like a tech demo than a game: the real work I spent honing the data structures (well, at at least a data structure…) and the functions that operate on them. The GUI-gamy-thing was an afterthought I hacked on top of the MERPG-code.

As you might expect, the world is represented as a two-dimensional vector whose width is screen-width/50 and whose height most of the time is screen-height/50. Every cells value is from the set #{:x :square :circle :triangle}, and initially it tries to avoid putting similiar cells next to each other, and usually fails miserably (:P). If the user selects two cells, the game checks in the first phase of the state-update-loop if the swapping of these cells is legal by first creating a coordinate-diffs matrice like this: [(- x1 x2) (- x2 x1) (- y1 y2) (- y2 y1)]. x's and y's represent the coordinates of the selected two cells. Then it makes sure you won't try to swap cells that aren't next to each other by checking that none of the values of coord-diffs are other than 1, 0 or -1. It also checks that you don't try to swap lean cells by checking the coord-diff isn't any of the following: [[-1 1 1 -1] [1 -1 1 -1] [1 -1 -1 1] [-1 1 -1 1]]. I acquired these magic matrices just by swapping cells in all four lean directions, and printing the resulting matrices in the Emacs.

After these base legality checks the game has only one more condition: the swap has to create a horizontal or vertical row of three or more cells of the same type. It is easily done: lets swap these cells and check if this condition is fulfilled. Though how this happens is interesting too: lets open the contains-horizontal-starys? - function (whose name is ridiculous, and has probably been born of typo from contains-horizontal-straights?, which too was a "it's midnight and the parts that do english in my brain have been dead for hours now" - kind of a name). The first if-not - clause in the code should probably be moved into the pre-conditions. After it the code checks if we were sent a whole world (i.e. a vector of vectors of keywords) or just a column of it (i.e. a vector of keywords).

If the former, we map our function against every column of the world AND every column of the transposed world, thus checking the both vertical and horizontal (i.e. vertical after the transpose) rows. If the latter, we partition the column with a function that just returns it's parameter, thus changing the [:x :x :triangle :circle :x :x :x :x] into ((:x :x) (:triangle) (:circle) (:x :x :x)). Then its trivial to filter seqs whose count is smaller than 3 away, and check if we have anything left.

Of course none of this applies unless there's two cells selected. If none or only one, click-handling routine returns the world unchanged.

After we've dealt with the selected cells, it's time to remove any straights of threes or more. Running the world through indexes-of-straights and flatten-index-results returns the indexes of all the cells who participate in these straights in an somewhat useable form of [[[x] [y y y y]] [[x] [y y]] [[x x x] [y]]...]. Remove-nice-rows processes through this index-seq, changing every [[x][y y y]] and [[x x x x][y]] to [[x x x] [y y y]] and [[x x x x][y y y y]]. In each pair of x and y it replaces the old value with :remove-me!, so that it doesn't have to adjust the rest of the ids to understand disappearing cells. When done with the ids, the remove-nice-rows counts how many :remove-me!s the world contains, and increments the score-out - atom ten times with that. After this, the function maps to the world a filtering, which removes the :remove-me!s from it.

Let's return to the core.clj's handle-nice-rows. When we have removed-nice-rows, its time to fill-reduced-columns. That function can't guess the required height just by finding the max of (map count the-world) (although, now that I think about it, why couldn't it?), so we need to tell the height by dividing the windows height with the cell's height. By the way: don't use the magic constants like the 50 is used throughout the code. fill-reduced-columns is a simple function: it just conses random cells on top of every column whose height<the-world's height.

After doing a round of removing nice rows and conssing random cells, we recur and do those as many times as needed. I added there a hard limit of ten recursions because I had infinite recursion in that function killing my REPL. Oh, and when there's no more to remove, we return to the :update - function, and return the new world inside the state map to the merpg.2D.make-game/make-game - thingie. We also return the score updated by the remove-nice-rows. This map is then sent to the :pre-drawqueue, which draws the selection-rects based on the lexical state (I think? Yell at me, if this isn't lexical state, please) and the score based on the state-map sent from the update-function. :post-drawqueue receives also the state-map, and calls to the Java2D-primitives the draw all the required things.

How do I handle the clicks? With bubblegum, of course. Inside the make-game I originally attached mouse-released - listener to the frame. Don't do that, for the origin will be at the top-left point of the window, se the first 10-or-so horizontal pixels will be used for window decorations. Then I moved it to the viewport-canvas, thus moving the origin to its rightful place, the most top-left point of the viewport, under the decorations. Inside this listener I get the location of the click, divide its components with the damned magic constant 50, cast them to int, and sent the to the make-game's parameter function mouse-clicked inside a vector. There I either deselect both selected cells, select the first cell, or select the second cell. The handling of the y-coordinate seems to be screwed inside the whole core.clj, which is the result of me needing to make space for the Score: %d - text and moving the whole world 50px downwards.

The future?

I think I've finished with the tech-babbling. I don't think there will be more real development by me on this project. I've had some (more than I actually expected, although the first one is a bug report from yours truly) input of this game: don't try to select cells outside the world (in other words: don't click on the Score - text), and instead of just drawing the current state of the world, the game should provide some sort of transition animations from one state to another. The resposiveness is also a problem: don't try to resize the window. I think it should be easier to just disable the resizing of the frame, and build a GUI for adjusting the frame's dimensions (or encourage people to change the values from the ~/.geometrician. It's not that hard.) than try to resize the world according to the resize events without screwing the state of the world up. If you wish to develop this thing forward, it's available in the Google Code for a reason.

No comments:

Post a comment