diff --git a/apps/oxofocus/README.md b/apps/oxofocus/README.md new file mode 100644 index 000000000..f65d0f9ca --- /dev/null +++ b/apps/oxofocus/README.md @@ -0,0 +1,24 @@ +# Oxofocus + +A Naughts and Crosses game that learns as it goes + +To start with the computer will play random moves and slowly increase +its skill based on a set of logic rules. During your first few games +the playing logic will apply a new logic rule after each time you +win. Once you reach eight wins the banner area will turn green +indicating that the computer is now playing at maximum strength. +However it is not as easy as you think and if you make a mistake and +loose a game your score goes back to zero. The more you play against +a weaker algorithm the more likely you are to loose concentration. + +Have you got the focus and concentration to get to maximum playing +strength. Do you know all the winning moves to out fox the algorithm +? + +Written by: [Hugh Barney](https://github.com/hughbarney) For support +and discussion please post in the [Bangle JS +Forum](http://forum.espruino.com/microcosms/1424/) + +Credit to `MissionMake` for +[tictactoe](https://banglejs.com/apps/?id=tictactoe) where I have +borrowed the grid drawing code diff --git a/apps/oxofocus/app-icon.js b/apps/oxofocus/app-icon.js new file mode 100644 index 000000000..c238afa61 --- /dev/null +++ b/apps/oxofocus/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwIJGv//AAX+oEAwEBgECApuD4IFBocww1hAoXwxkxAoNB8GIiIFC4AFDofgCIYdFoEQFIZBRLLoFBHYkAI4YFBKYYFHCIodFLO8M4RHDh4FCKYMHwQFELIkPBYQdFFIVCLK8AAAg=")) diff --git a/apps/oxofocus/app.js b/apps/oxofocus/app.js new file mode 100644 index 000000000..5b02116e6 --- /dev/null +++ b/apps/oxofocus/app.js @@ -0,0 +1,464 @@ + +const YOUR_MOVE = 0; +const SHOW_SELECTION = 1; +const DISPLAY_MOVE = 2; +const THINKING = 3; +const GAME_OVER = 4; + +var move_count; +var win_count = 0; +var game_state; +var msg; +var board; + +const wins = [ [1,2,3], [4,5,6], [7,8,9], [1,4,7], [2,5,8], [3,6,9], [1,5,9], [3,5,7] ]; +const rowcol = [ [-1,-1], [1,1], [1,2], [1,3], [2,1], [2,2], [2,3], [3,1], [3,2], [3,3] ]; +const x_img = require("heatshrink").decompress(atob("mEwwI63jACEngCEvwCEv4CB/wCBn+AgP8AoMf4ED/AFBh/gg/wAoIDBA4IFBB4ITBAoIbBD4I8C/wrCGAQuCGAQuCGAQuCGAQuCAo4RFDoopFGohBFJopZFMopxFPoqJFSoqhFVooA0A")); +const o_img = require("heatshrink").decompress(atob("mEwwIdah/wAof//4ECgYFB4AFBg4FB8AFBj/wh/4AoM/wEB/gFBvwCB/wCBBAU/AQIUCj8AgIzCh+AgYmCg/AgYyCAYIHBAoXgg+AAoMBApkPLgZKBAtBBRLIprDMoJxFPoqJFSoyhCAQStFXIrFFaIrdFdIwAVA")); + + +function debug(o) { + //console.log(o); +} + + +// 1 2 3 +// 4 5 6 +// 7 8 9 + +function draw(){ + debug("draw()"); + g.clear(); + message(msg); + + //drawboard + g.setColor(g.theme.fg); + g.drawLine(62,24,62,176); + g.drawLine(112,24,112,176); + g.drawLine(12,74,164,74); + g.drawLine(12,124,164,124); + + for (let cell = 1; cell < 10; cell++) { + let row = rowcol[cell][0]; + let col = rowcol[cell][1]; + + if (board[cell] == "X") { + g.drawImage(x_img, (col - 1)*50+12, (row - 1)*50+24); + } else if (board[cell] == "O") { + g.drawImage(o_img, (col - 1)*50+12, (row - 1)*50+24); + } + } +} + +function message(m) { + g.reset(); + // if all rules are operating show a green background + debug('win count=' + win_count); + + if (win_count == 0) { + g.setColor('#f00'); // red, no wins + } else if (win_count < 8) { + g.setColor('#00f'); // blue, some wins, not all rules active + } else { + g.setColor('#0f0'); // green all rules active + } + + g.fillRect(0, 0, 176, 23); + g.setColor('#fff'); + g.setFont('6x8',2); + g.setFontAlign(0, 0); + g.drawString("" + win_count + " " + m, g.getWidth()/2, 12); +} + + +// Square locations +//12,24;62,24,112,24 +//12,74;62,74,112,74 +//12,124;62,124,112,124 + +function get_move() { + var col; + var row; + + if (game_state != YOUR_MOVE) + return; + + // work out which row/col was selected + if (x <= 62) { + col= 0; + } else if (x <= 112){ + col= 1; + } else { + col= 2; + } + + if (y <= 74) { + row = 0; + } else if (y <= 124){ + row = 1; + } else { + row = 2; + } + + // convert row / col to a cell + let cell = 3*row + col + 1; + debug("select:" + cell); + + if (cell_is_free(cell)) { + set_cell(cell,'X'); + move_count++; + game_state = SHOW_SELECTION; + if (check_for_win()) { + draw(); + return; + } + next_state(); + } else { + message('try again'); + } +} + +function new_game() { + game_state = YOUR_MOVE; + move_count = 0; + msg = 'your move'; + board = [ "-", "1", "2", "3", "4", "5", "6", "7", "8", "9" ]; + draw(); +} + +function next_state() { + debug("state=" + game_state); + + // show humans selected move with a selection circle + if (game_state == SHOW_SELECTION) { + game_state = DISPLAY_MOVE; + //message('selection..'); + g.fillCircle(x, y, 10); + setTimeout(next_state,300); + } else if (game_state == DISPLAY_MOVE) { + game_state = THINKING; + msg = 'thinking..'; + draw(); + setTimeout(next_state,1800); + } else if (game_state == THINKING) { + game_state = YOUR_MOVE; + msg = 'your move'; + computer_move(); + move_count++; + check_for_win(); + draw(); + } + +} + + +function computer_move() { + var mvs; + var mv; + + if (win_count > 0) { + if (first_move_was_a_corner()) { + make_my_move(5); + debug("RULE 1: you played corner, computer played centre"); + return; + } + } + + if (win_count > 1) { + if (first_move_was_the_centre()) { + mv = get_a_corner_move(); + make_my_move(mv); + debug("RULE 2: you played center, computer played corner"); + return; + } + } + + if (win_count > 6) { + if (first_move_was_a_side()) { + make_my_move(5); + debug("RULE 3: you played side, computer played centre"); + return; + } + } + + if (win_count > 2) { + mvs = get_winning_moves("O"); + if (mvs.length > 0) { + mv = select_random_move_from(mvs); + make_my_move(mv); + debug("RULE 4: computer played a winning move"); + return; + } + } + + if (win_count > 3) { + mvs = get_winning_moves("X"); + if (mvs.length > 0) { + mv = select_random_move_from(mvs); + make_my_move(mv); + debug("RULE 5: computer played a blocking move"); + return; + } + } + + /*** + Adjacent Sides, play in appropriate corner (.) + + . | X + ---|--- + X | + + ***/ + + // not covered by rule 3 + if (win_count > 4) { + if (player_adjacent_sides("X")) { + mv = get_adjacent_corner("X"); + if (mv != -1) { + make_my_move(mv); + debug("RULE 6: compluter played adjacent corner"); + return; + } + } + } + + if (win_count > 7) { + if (player_has_corner_and_centre("X")) { + mv = get_a_corner_move(); + if (mv != -1) { + make_my_move(mv); + debug("RULE 7: compluter played a corner"); + return; + } + } + } + + if (win_count > 5) { + mvs = get_free_sides(); + if (mvs.length > 0) { + mv = select_random_move_from(mvs); + make_my_move(mv); + debug("RULE 8: compluter played a side"); + return; + } + } + + // default rule + mvs = get_free_cells(); + mv = select_random_move_from(mvs); + debug("RULE 8: computer played a random cell"); + make_my_move(mv); +} + +// check the move and make it for "O" +function make_my_move(mv) { + if (valid_move(mv)) { + set_cell(mv, "O"); + } else { + debug("make_my_move(): Invalid move was generated " + mv); + } +} + +function check_for_win() { + if (player_has_won("X")) { + msg = 'you win'; + game_state = GAME_OVER; + win_count++; + return true; + } else if (player_has_won("O")) { + msg = 'I win'; + win_count = 0; + game_state = GAME_OVER; + return true; + } else if (check_for_draw()) { + msg = 'draw'; + game_state = GAME_OVER; + return true; + } + + return false; +} + +function player_has_won(player) { + for (var r in wins) + if (row_is_won(wins[r], player)) + return true; + return false; +} + +function check_for_draw() { + var v = get_free_cells(); + return (v.length == 0); +} + +function row_is_won(rw, pl) { + if (board[rw[0]] == pl && board[rw[1]] == pl && board[rw[2]] == pl) + return true; + return false; +} + + +function get_winning_moves(player) { + var win_moves = new Array(); + + for (var r in wins) { + var ind = winning_move_for_row(wins[r], player); + + if (ind > -1) { + win_moves.push(wins[r][ind]); + } + } + + return win_moves; +} + + +function winning_move_for_row(rw, pl) { + if (board[rw[1]] == pl && board[rw[2]] == pl && cell_is_free(rw[0])) return 0; + if (board[rw[2]] == pl && board[rw[0]] == pl && cell_is_free(rw[1])) return 1; + if (board[rw[0]] == pl && board[rw[1]] == pl && cell_is_free(rw[2])) return 2; + + return -1; +} + +function first_move_was_a_corner() { + if (move_count != 1) + return false; + + if (board[1] == "X" || board[3] == "X" || board[7] == "X" || board[9] == "X") + return true; + + return false; +} + +function first_move_was_a_side() { + if (move_count != 1) + return false; + + if (board[2] == "X" || board[4] == "X" || board[6] == "X" || board[8] == "X") + return true; + + return false; +} + +function player_adjacent_sides(pl) { + if (move_count > 3) return false; + if (board[2] == pl && board[4] == pl) return true; + if (board[2] == pl && board[6] == pl) return true; + if (board[8] == pl && board[2] == pl) return true; + if (board[8] == pl && board[6] == pl) return true; + + return false; +} + +function get_adjacent_corner(pl) { + if (board[2] == pl && board[4] == pl) return 1; + if (board[2] == pl && board[6] == pl) return 3; + if (board[8] == pl && board[2] == pl) return 7; + if (board[8] == pl && board[6] == pl) return 9; + + return -1; +} + +function player_has_corner_and_centre(pl) { + if (board[1] == pl && board[5] == pl) return true; + if (board[3] == pl && board[5] == pl) return true; + if (board[7] == pl && board[5] == pl) return true; + if (board[9] == pl && board[5] == pl) return true; + + return false; +} + +function first_move_was_the_centre() { + if (move_count == 1 && board[5] == "X") + return true; + return false; +} + +function get_a_side_move() { + return select_random_move_from([2,4,6,8]); +} + +function get_a_corner_move() { + return select_random_move_from([1,3,7,9]); +} + +function select_random_move_from(mvs) { + var len = mvs.length; + var rnd = random(len) - 1; + return mvs[rnd]; +} + +function random(n) { + try { + return Math.floor((Math.random() * n) + 1); + } catch ( e ) { debug("Error: " + this + e.description); } +} + +function get_free_cells() { + var frees = new Array(); + + for (var i in board) { + if (i > 0 && cell_is_free(i)) + frees.push(i); + } + return frees; +} + +function get_free_sides() { + var frees = new Array(); + var sides = [2,4,6,8]; + + for (var i in sides) { + if (cell_is_free(sides[i])) + frees.push(sides[i]); + } + return frees; +} + +function get_free_corner() { + var frees = new Array(); + var sides = [1,3,7,9]; + + for (var i in sides) { + if (cell_is_free(sides[i])) + frees.push(sides[i]); + } + return frees; +} + +function cell_is_free(i) { + if (board[i] == "X" || board[i] == "O") return false; + return true; +} + +function valid_move(id) { + if (cell_is_free(id) == false) { + debug("Invalid move, try another cell"); + return false; + } + return true; +} + +function set_cell(n, player) { + if (player == "X") { + board[n] = "X"; + } else { + board[n] = "O"; + } +} + +Bangle.on('touch', function(zone,e) { + x = Object.values(e)[0]; + y = Object.values(e)[1]; + + if (game_state == GAME_OVER) { + new_game(); + return(); + } + + get_move(); +}); + + +new_game(); diff --git a/apps/oxofocus/app.png b/apps/oxofocus/app.png new file mode 100644 index 000000000..1e47fdc7b Binary files /dev/null and b/apps/oxofocus/app.png differ diff --git a/apps/oxofocus/metadata.json b/apps/oxofocus/metadata.json new file mode 100644 index 000000000..60c84fab5 --- /dev/null +++ b/apps/oxofocus/metadata.json @@ -0,0 +1,18 @@ +{ "id": "oxofocus", + "name": "oxofocus", + "shortName":"Oxo Focus", + "icon": "app.png", + "version":"0.01", + "description": "Play the computer while it learns to play Naughts and Crosses!", + "tags": "game", + "storage": [ + {"name":"oxofocus.app.js","url":"app.js"}, + {"name":"oxofocus.img","url":"app-icon.js","evaluate":true} + ], + "screenshots" : [ + { "url":"screenshot.png" }, + { "url":"screenshot1.png" }, + { "url":"screenshot2.png" } + ], + "supports": ["BANGLEJS2"] +} diff --git a/apps/oxofocus/screenshot.png b/apps/oxofocus/screenshot.png new file mode 100644 index 000000000..4f05fe2e9 Binary files /dev/null and b/apps/oxofocus/screenshot.png differ diff --git a/apps/oxofocus/screenshot1.png b/apps/oxofocus/screenshot1.png new file mode 100644 index 000000000..6f7986d0a Binary files /dev/null and b/apps/oxofocus/screenshot1.png differ diff --git a/apps/oxofocus/screenshot2.png b/apps/oxofocus/screenshot2.png new file mode 100644 index 000000000..c1ef445f7 Binary files /dev/null and b/apps/oxofocus/screenshot2.png differ