The company "Pop cap games" have a popular Flash game called "Bejewelled".
It's simple: a grid of coloured pieces. Each move, you swap two adjacent pieces to form a line of three or more of the same colour. The line disappears, tetris style, the pieces shift downwards, and the empty grid spaces are filled from the top.
I wanted to try out different strategies in a controlled environment, to see which was best. I decided to make something that would play Pop Cap's Bejewelled or any of the mass of similar clones that have come out, and record scores for different strategies.
The graphics in the latest version, Bejewelled 2 Deluxe 1.0 are significantly "improved" from the earlier version, in ways that I suspected would prevent ImageSearch (animated pieces, with flashy sparkles on) or PixelSearch (pieces are different shapes, have arbitrary photographic backgrounds which could contain any colour). But that version of the game has a really handy-for-testing-strategies "play forever" mode.
So I pick a pixel within each piece, and use the r/g/b proportions to try to guess what colour it's meant to be.
So far, I think I'm successful. It plays on three different versions of the game, though obviously once they use colours it can't differentiate (cream and white), it fails.
The version below is configured for, and the colour guessing function is optimised for, the locally-installed demo version of Bejewelled 2 Deluxe 1.0, though it will with tweaking work with others too.
Now I have to get some RL work done, but if I can be bothered, next I'll: 1) Make it prompt for grid size,
2) Ask for mouseclicks on the top left and bottom right of the grid
3) Ask for mouseclick on a piece in the centre of the board to get the sample pixel position within the gridsquares.
4) Grab the window title from the active window after those clicks
5) Grab the score after a fixed time (probably as an image).
For the moment, I've run it through a large number of moves using two algorithms: bottommost valid move, and topmost valid move. It doesn't rank moves by potential gain at all, and does no planning ahead (for that, I'll throw the board layout at a language that I'm more comfortable in). But already, a pattern is emerging.
Going for the lowest move on the board gives the highest average per-move score, so would be good for timed games.
Going for the highest move on the board gives lower per-move scores, but gives longer games, and hence higher overall scores, since you're less likely to run out of valid moves. Good for versions of the game where running out of moves ends the game.
This strategy difference seems to be the case on every version of the game, though the smaller the board, the less the difference is.
I'd really be interested in whether people can think of a better way of differentiating between different pieces. At the moment, it sometimes has a hard time differentiating between cyan/blue (so I removed cyan, since there were no cyan pieces), and red/orange/yellow.
Mostly, though, it's the white sparkles that cause problems: it thinks they are white pieces. I guess one option there would be to retest "white" pieces at a couple of other points, just in case.
I'd also be interested in any other feedback whatsoever, from code style to optimisation suggestions to telling me to STFU. I'm new to this language, and anything you can say will be muchly appreciated.
; Script to play a board game. ; ; Proof of concept thang to recognise pieces by colour. ; ; Assumes that all pieces are primary (rgb) or secondary (cmy) ; colors, orange, or grey. Doesn't use PixelSearch, as the ; background may be any colour. Instead, checks the color of a ; single pixel offset within each square to build a map of the ; board layout each turn, then operates on that. ; ; Detection of red/orange/yellow and cyan/blue is really a bit dodgy ; and often wrong. ; ; Copyright is explicitly released upon the public domain. ; This means you may do as you wish with it, without credit. ^!r::Reload ; pause/reload the script. ^!d:: toggleDebug() toggleDebug() { global SG_DEBUG if ( SG_DEBUG == 1) SG_DEBUG=0 else SG_DEBUG=1 } ^!z:: ; Globals. ; These should be gathered on the fly rather than predefined. ; Offset to measure within the square. SG_OFFSET_X=6 SG_OFFSET_Y=12 ; Coords within the window for the top left of the board. SG_ORIGIN_X=168 SG_ORIGIN_Y=15 ; Size of a single square. SG_SIZE_X=52 SG_SIZE_Y=52 ; Number of squares on board. SG_SQUARES_X=8 SG_SQUARES_Y=8 ; Remember last move so we're less likely to get stuck trying the same wrong move forever. ; But if there are TWO wrong moves, then... SG_LAST_X=0 SG_LAST_Y=0 ; Whether this version allows piece dragging rather than click/click. SG_DRAG_OK=1 ; Debugging level SG_DEBUG=0 Loop { getWindow() ; Loop Y then X: move across every square, down only once per line. sg_line := ("") Loop, %SG_SQUARES_Y% { sg_y := A_Index sg_py := yCoordToPixel(A_Index) sg_line = %sg_line% `n# %A_Index% : Loop, %SG_SQUARES_X% { sg_x := A_Index sg_px := xCoordToPixel(A_Index) PixelGetColor, sg_color, %sg_px%, %sg_py%, RGB sg_c := hex2Color(sg_color) SG_ARR_%sg_x%_%sg_y% := sg_c col := SG_ARR_%sg_x%_%sg_y% ;MsgBox, SG_ARR_%sg_x%_%sg_y% = %col% sg_line = %sg_line% %sg_c% ( %sg_x% , %sg_y% = %sg_px% , %sg_py% ) } ;MsgBox, %sg_line% } getMove() if ( SG_DEBUG == 1 ) { MsgBox, DEBUG %SG_DEBUG% %sg_line% } ;return } getWindow() { ; Window management. This shouldn't care about the window name. WinWait, Bejeweled 2 Deluxe 1.0, IfWinNotActive, Bejeweled 2 Deluxe 1.0, , WinActivate, Bejeweled 2 Deluxe 1.0, WinWaitActive, Bejeweled 2 Deluxe 1.0, } ; Convert a board y square coord to a screen y coord. ; This means our y coords are, internally, all "backwards" - but who cares? yCoordToPixel(y) { global SG_SIZE_Y global SG_ORIGIN_Y global SG_OFFSET_Y global SG_SQUARES_Y ; SG_SQUARES_Y - y as we're working from the bottom for highest scores. return ( ( SG_SQUARES_Y - y + 1 ) * SG_SIZE_Y ) + SG_ORIGIN_Y + SG_OFFSET_Y } ; Convert a board square x coord to a screen x coord. xCoordToPixel(x) { global SG_SIZE_X global SG_ORIGIN_X global SG_OFFSET_X return ( x * SG_SIZE_X ) + SG_ORIGIN_X + SG_OFFSET_X } ; Make the mouse move a piece. movePiece(x1, y1, x2, y2) { global SG_DRAG_OK global SG_ORIGIN_X global SG_ORIGIN_Y global SG_LAST_X global SG_LAST_Y SG_LAST_X := x1 SG_LAST_Y := y1 ;MsgBox, Moving from %x1% , %y1% to %x2% , %y2%, set last = %SG_LAST_X% , %SG_LAST_Y% getWindow() if (SG_DRAG_OK == 1) { MouseClickDrag, L, xCoordToPixel(x1), yCoordToPixel(y1), xCoordToPixel(x2), yCoordToPixel(y2), 2 } else { MouseClick, L, xCoordToPixel(x1), yCoordToPixel(y1) Sleep, 100 MouseClick, L, xCoordToPixel(x2), yCoordToPixel(y2) Sleep, 100 } ; Move to home point to avoid hilighting pieces. MouseMove, SG_ORIGIN_X, SG_ORIGIN_Y ; Wait for pieces to fall. Sleep, 600 } ; Check that the piece in an array coord is not out of bounds, ; and is the same color. isSameAt(col, x, y) { ; Generic global to include array. global if (x < 1 || x > SG_SQUARES_X || y < 1 || y > SG_SQUARES_Y ) { ;MsgBox, returning false as OOB ( %x% < 1 || %x% > %SG_SQUARES_X% || %y% < 1 || %y% > %SG_SQUARES_Y% ) return false } ax := SG_ARR_%x%_%y% if ( SG_ARR_%x%_%y% == col ) { ;MsgBox, returning true as %ax% == %col% and none of ( %x% < 1 || %x% > %SG_SQUARES_X% || %y% < 1 || %y% > %SG_SQUARES_Y% ) return true } ;MsgBox, returning false as %ax% != %col% though none of ( %x% < 1 || %x% > %SG_SQUARES_X% || %y% < 1 || %y% > %SG_SQUARES_Y% ) return false } ; An extremely nasty bruteforcism: tries each possible move in turn. ; If it finds one, it does it. ; Unfortunately, for the SG_ARR stuff, need all globals defined. getMove() { global Loop, %SG_SQUARES_Y% { y = %A_Index% py := xCoordToPixel(y) y1 := ( y + 1 ) y2 := ( y + 2 ) y3 := ( y + 3 ) y_1 := ( y - 1 ) y_2 := ( y - 2 ) y_3 := ( y - 3 ) Loop, %SG_SQUARES_X% { x = %A_Index% ; Anti-stick: don't play the same piece twice in a row ; If it's the only playable square, we'll get it next time round. if ( SG_LAST_X == x && SG_LAST_Y == y ) { ; Zero them so we can get this square next time if it's the only one. SG_LAST_X = 0 SG_LAST_Y = 0 Continue } px := xCoordToPixel(px) x1 := ( x + 1 ) x2 := ( x + 2 ) x3 := ( x + 3 ) x_1 := ( x - 1 ) x_2 := ( x - 2 ) x_3 := ( x - 3 ) col := SG_ARR_%x%_%y% ;MsgBox, At %x%, %y% comparing %col% , %x2% , %y% And %col%, %x3%, %y% ; X_xx if ( isSameAt(col, x2, y) && isSameAt(col, x3, y) ) { movePiece( x, y, x1, y) return } ; xx_X else if ( isSameAt(col, x_2, y) && isSameAt(col, x_3, y) ) { movePiece( x, y, x_1, y) return } ; X ; _ ; x ; x else if ( isSameAt(col, x, y2) && isSameAt(col, x, y3) ) { movePiece( x, y, x, y1) return } ; x ; x ; _ ; X else if ( isSameAt(col, x, y_2) && isSameAt(col, x, y_3) ) { movePiece( x, y, x, y_1) return } ; x_ ; x_ ; _X else if ( isSameAt(col, x_1, y_1) && isSameAt(col, x_1, y_2) ) { movePiece( x, y, x_1, y) return } ; _x ; _x ; X_ else if ( isSameAt(col, x1, y_1) && isSameAt(col, x1, y_2) ) { movePiece( x, y, x1, y) return } ; _X ; x_ ; x_ else if ( isSameAt(col, x_1, y1) && isSameAt(col, x_1, y2) ) { movePiece( x, y, x_1, y) return } ; X_ ; _x ; _x else if ( isSameAt(col, x1, y1) && isSameAt(col, x1, y2) ) { movePiece( x, y, x1, y) return } ; X__ ; _xx else if ( isSameAt(col, x1, y1) && isSameAt(col, x2, y1) ) { movePiece( x, y, x, y1) return } ; _xx ; X__ else if ( isSameAt(col, x1, y_1) && isSameAt(col, x2, y_1) ) { movePiece( x, y, x, y_1) return } ; __X ; xx_ else if ( isSameAt(col, x_1, y1) && isSameAt(col, x_2, y1) ) { movePiece( x, y, x, y1) return } ; xx_ ; __X else if ( isSameAt(col, x_1, y_1) && isSameAt(col, x_2, y_1) ) { movePiece( x, y, x, y_1) return } ; x_ ; _X ; x_ else if ( isSameAt(col, x_1, y_1) && isSameAt(col, x_1, y1) ) { movePiece( x, y, x_1, y) return } ; _x ; X_ ; _x else if ( isSameAt(col, x1, y_1) && isSameAt(col, x1, y1) ) { movePiece( x, y, x1, y) return } ; _X_ ; x_x else if ( isSameAt(col, x_1, y1) && isSameAt(col, x1, y1) ) { movePiece( x, y, x, y1) return } ; x_x ; _X_ else if ( isSameAt(col, x_1, y_1) && isSameAt(col, x1, y_1) ) { movePiece( x, y, x, y_1) return } } } ; MsgBox, Unable to find a move } ; Convert a hex value to a colour letter, from: ; r(ed)c(yan)g(reen)y(ellow)o(range)b(lue)m(agenta)w(hite). ; Per: r rg=o gr=y g gb=c bg=c b rb=m br=m hex2Color(color) { r := ( ( color >> 16 ) & 0xFF ) g := ( ( color >> 8 ) & 0xFF ) b := ( color & 0xFF ) if (r > g && r > b ) { col = r min := ( r * 0.5 ) if (min <= g && min <= b ) col=w if (min <= g && min > b ) ; dodgy yellow test. if (r - g > 10) col=o else ; numbers too similar, still yellow. col=y if (min > g && min <= b ) col=m } if (g >= r && g >= b ) { col = g min := ( g * 0.6 ) if (min <= r && min <= b ) col=w if (min <= r && min > b ) col=y if (min > r && min <= b ) ; col=c ; commented since there ARE no cyan pieces in the test game col=b } if (b >= r && b >= g ) { col = b min := ( b * 0.6 ) if (min <= r && min <= g ) col=w if (min <= r && min > g ) col=m if (min > r && min <= g ) ; col=c ; commented since there ARE no cyan pieces in the test game col=b } ;MsgBox, Colour %color%=%col% r:%r% g:%g% b:%b% return col }